From 1779591b7a81b86fa7b4a936e24a8e6fb694fda0 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Mon, 25 May 2026 14:49:45 +0530 Subject: [PATCH] feat: sat design for current account, FD, RD deposit/withdrawal pages - Rebuilt current account deposit/withdrawal with sat two-column layout - Applied same sat design to FD cash transaction and RD deposit/withdrawal - Added Deposit action to FD: button config, actions dict, doAction routing - Both FD Deposit and Withdrawal now render FixedDepositsCashTransaction Co-Authored-By: Claude Sonnet 4.6 --- ...urrent-account-transactions.component.html | 431 +++++++-- ...urrent-account-transactions.component.scss | 838 +++++++++++++++++- .../current-account-transactions.component.ts | 261 +++++- .../fixed-deposit-account-view.component.ts | 2 + .../fixed-deposits-buttons.config.ts | 8 + ...ed-deposits-account-actions.component.html | 2 +- ...ixed-deposits-account-actions.component.ts | 4 +- ...d-deposits-cash-transaction.component.html | 348 ++++++-- ...d-deposits-cash-transaction.component.scss | 750 +++++++++++++++- ...xed-deposits-cash-transaction.component.ts | 216 ++++- ...-recurring-deposits-account.component.html | 396 +++++++-- ...-recurring-deposits-account.component.scss | 751 +++++++++++++++- ...it-recurring-deposits-account.component.ts | 216 ++++- 13 files changed, 3843 insertions(+), 380 deletions(-) diff --git a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html index 7db86b12c..eb87f5fa6 100644 --- a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html +++ b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html @@ -6,127 +6,364 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- - @if (transactionType.withdrawal) { - - {{ 'labels.heading.Withdraw Money From Current Account' | translate }} - - } - @if (transactionType.deposit) { - - {{ 'labels.heading.Deposit Money To Current Account' | translate }} - - } +
+ +
+
+ +
+ +
+
+

+ {{ + transactionType.withdrawal + ? ('labels.heading.Withdraw Money From Current Account' | translate) + : ('labels.heading.Deposit Money To Current Account' | translate) + }} +

+

+ {{ + transactionType.deposit + ? 'Count the cash by denomination, then confirm the payment details.' + : 'Disburse cash by denomination and confirm the payment details.' + }} +

+
+ @if (transactionType.withdrawal && clientId) { + + } +
+ Counted Total + ₹{{ denominationTotal | number: '1.2-2' }} + @if (totalPieces > 0) { + {{ totalPieces }} piece(s) + } +
+
-
- -
- - {{ 'labels.inputs.Transaction Date' | translate }} - - - - @if (currentAccountTransactionForm.controls.transactionDate.hasError('required')) { - - {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - + +
+
+ +
+ Client + {{ clientName || '—' }} +
+
-
+ +
+ +
+ Account No + {{ accountNo || '—' }} +
+
+ +
+ +
+ +
+ Available Balance + + ₹{{ availableBalance | number: '1.2-2' }} + +
+
+ +
+ +
+ +
+ KYC + - + + {{ kycStatus }} + +
+
- - {{ 'labels.inputs.Payment Type' | translate }} - - @for (paymentType of paymentTypeOptions; track paymentType) { - - {{ paymentType.name }} - - } - - @if (currentAccountTransactionForm.controls.paymentTypeId.hasError('required')) { - - {{ 'labels.inputs.Payment Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - +
-
- - -
- {{ 'labels.inputs.Show Payment Details' | translate }} -
-
+
+ +
+ Today's NEFT/RTGS + ₹{{ sameDayNeftRtgs | number: '1.2-2' }} +
+
+ +
+ +
+ +
+ Cheque In Clearing + ₹{{ sameDayChequeClearing | number: '1.2-2' }} +
+
+
+
+ + + +
+
+ +

Cash Denomination Breakdown

+ + +
+ + +
+
+ Banknotes + ₹{{ notesTotal | number: '1.2-2' }} +
+
+ @for (d of notes; track d.value) { +
+
+ {{ d.label }} + ₹{{ subtotalFor(d) | number: '1.2-2' }} +
+
+ + + +
+
+ } +
+
+ + +
+
+ Coins + ₹{{ coinsTotal | number: '1.2-2' }} +
+
+ @for (d of coins; track d.value) { +
+
+ {{ d.label }} + ₹{{ subtotalFor(d) | number: '1.2-2' }} +
+
+ + + +
+
+ } +
+
+ + +
+
+ Banknotes + ₹{{ notesTotal | number: '1.2-2' }} +
+
+ Coins + ₹{{ coinsTotal | number: '1.2-2' }} +
+
+ Grand Total + ₹{{ denominationTotal | number: '1.2-2' }} +
+ +
+
+ + +
+
+ +

Transaction Details

+
+ +
+ + {{ 'labels.inputs.Transaction Date' | translate }} + + + + @if (currentAccountTransactionForm.controls.transactionDate.hasError('required')) { + + {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + + + @if (denominationTotal > 0 && !denominationMatchesForm) { +
+ + + Counted cash ₹{{ denominationTotal | number: '1.2-2' }} doesn't match the transaction + amount. Click Use ₹{{ denominationTotal | number: '1.2-2' }} to sync.
+ } + @if (denominationMatchesForm) { +
+ + Cash count matches transaction amount. +
+ } - @if (addPaymentDetailsFlag) { - + + {{ 'labels.inputs.Payment Type' | translate }} + + @for (paymentType of paymentTypeOptions; track paymentType) { + {{ paymentType.name }} + } + + @if (currentAccountTransactionForm.controls.paymentTypeId.hasError('required')) { + + {{ 'labels.inputs.Payment Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + +
+ + {{ 'labels.inputs.Show Payment Details' | translate }} + +
+ + @if (addPaymentDetailsFlag) { +
+ {{ 'labels.inputs.Account Number' | translate }} - } - - @if (addPaymentDetailsFlag) { - + {{ 'labels.inputs.Cheque' | translate }} - } - - @if (addPaymentDetailsFlag) { - + {{ 'labels.inputs.Routing Code' | translate }} - } - - @if (addPaymentDetailsFlag) { - + {{ 'labels.inputs.Receipt Number' | translate }} - } - - @if (addPaymentDetailsFlag) { - + {{ 'labels.inputs.Bank' | translate }} - } +
+ } - - {{ 'labels.inputs.Notes' | translate }} - - + + {{ 'labels.inputs.Notes' | translate }} + + - - - - +
+ +
- - - +
+
+
diff --git a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss index b21a3cf9c..8d1c6a0cf 100644 --- a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss +++ b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss @@ -6,24 +6,842 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.expandcollapsebutton { - margin-top: -7px; +/* Banking-grade transaction-entry screen. Two-column layout: cash + denomination breakdown on the left (the cashier's primary workspace), + transaction & payment details on the right. Hero header is blue for + deposits / teal for withdrawals. */ + +$navy: #0d1b2a; +$slate: #475569; +$muted: #64748b; +$border: #e2e8f0; +$soft: #f8fafc; +$blue: #1e40af; +$green: #059669; +$green-d: #047857; +$amber: #d97706; +$amber-d: #b45309; +$red: #b91c1c; + +.sat { + width: 100%; + padding: 10px 16px 18px; + display: flex; + flex-direction: column; + gap: 10px; } -.container { - max-width: 37rem; +/* ─── Hero ─────────────────────────────────────────────────────────── */ + +.sat__hero { + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px 16px 12px; + border-radius: 12px; + color: #fff; + background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 45%, #1e40af 100%); + box-shadow: 0 6px 18px -10px rgb(15 23 42 / 50%); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at top right, rgb(255 255 255 / 12%), transparent 55%); + pointer-events: none; + } } -.right-input { +.sat__hero-top { + display: flex; + align-items: center; + gap: 12px; + position: relative; + z-index: 1; +} + +.sat__hero-back { + height: 30px !important; + border-radius: 8px !important; + padding: 0 11px !important; + font-weight: 700 !important; + font-size: 12px !important; + letter-spacing: 0.01em; + background: rgb(255 255 255 / 14%) !important; + color: #fff !important; + border: 1px solid rgb(255 255 255 / 28%) !important; + flex-shrink: 0; + transition: + background 0.15s ease, + border-color 0.15s ease; + + &:hover { + background: rgb(255 255 255 / 26%) !important; + border-color: rgb(255 255 255 / 42%) !important; + } +} + +.sat__hero-title-wrap { + flex: 1 1 auto; + min-width: 0; +} + +.sat--withdrawal .sat__hero { + background: linear-gradient(135deg, #134e4a 0%, #115e59 45%, #0f766e 100%); + box-shadow: 0 6px 18px -10px rgb(15 118 110 / 50%); +} + +.sat__hero-ico { + width: 36px; + height: 36px; + border-radius: 9px; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 28%); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 15px; + flex-shrink: 0; +} + +.sat__title { + margin: 0; + font-size: 14.5px; + font-weight: 800; + letter-spacing: 0.01em; + line-height: 1.15; +} + +.sat__sub { + margin: 1px 0 0; + font-size: 11.5px; + color: rgb(255 255 255 / 80%); +} + +.sat__hero-verify { + position: relative; + height: 30px !important; + border-radius: 8px !important; + padding: 0 12px !important; + font-weight: 700 !important; + font-size: 12px !important; + letter-spacing: 0.01em; + color: #fff !important; + background: rgb(255 255 255 / 16%) !important; + border: 1px solid rgb(255 255 255 / 30%) !important; + flex-shrink: 0; + transition: + background 0.15s ease, + border-color 0.15s ease; + overflow: visible !important; + + &:hover { + background: rgb(255 255 255 / 28%) !important; + border-color: rgb(255 255 255 / 42%) !important; + } + + &::before { + content: ''; + position: absolute; + inset: -2px; + border-radius: 10px; + border: 2px solid rgb(255 255 255 / 70%); + opacity: 0; + pointer-events: none; + will-change: transform, opacity; + } + + &--pulse::before { + animation: sat-verify-pulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite; + } + + &--pulse:hover::before { + animation-play-state: paused; + opacity: 0; + } +} + +@keyframes sat-verify-pulse { + 0% { + transform: scale(1); + opacity: 0.85; + } + + 70% { + transform: scale(1.18); + opacity: 0; + } + + 100% { + transform: scale(1.18); + opacity: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .sat__hero-verify--pulse::before { + animation: none; + opacity: 0.5; + } +} + +.sat__hero-meta { + display: inline-flex; + flex-direction: column; + align-items: flex-end; + padding: 5px 12px; + border-radius: 8px; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 30%); + backdrop-filter: blur(8px); + min-width: 130px; + flex-shrink: 0; +} + +.sat__hero-meta-label { + font-size: 9.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: rgb(255 255 255 / 78%); +} + +.sat__hero-meta-value { + font-size: 15px; + font-weight: 800; + color: #fff; + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; +} + +.sat__hero-meta-sub { + font-size: 10px; + color: rgb(255 255 255 / 75%); + margin-top: 0; +} + +/* ─── Context strip ─────────────────────────────────────────────────── */ +.sat__ctx { + display: flex; + align-items: stretch; + flex-wrap: wrap; + gap: 0; + padding: 5px 8px; + border-radius: 8px; + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 22%); + backdrop-filter: blur(8px); + position: relative; + z-index: 1; +} + +.sat__ctx-item { + display: flex; + align-items: center; + gap: 6px; + flex: 1 1 110px; + padding: 2px 8px; + min-width: 0; +} + +.sat__ctx-ico { + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 6px; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 25%); + color: #fff; + font-size: 10px; + flex-shrink: 0; +} + +.sat__ctx-text { + display: flex; + flex-direction: column; + min-width: 0; + gap: 0; +} + +.sat__ctx-label { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: rgb(255 255 255 / 70%); + font-weight: 700; +} + +.sat__ctx-value { + font-size: 11.5px; + font-weight: 700; + color: #fff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &--mono { + font-family: 'JetBrains Mono', 'Courier New', monospace; + font-size: 10.5px; + letter-spacing: 0.02em; + } + + &--amount { + font-variant-numeric: tabular-nums; + font-size: 12px; + } + + &--lg { + font-size: 13px; + } +} + +.sat__ctx-item--balance { + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 22%); + border-radius: 7px; + padding: 3px 8px; +} + +.sat__ctx-divider { + width: 1px; + background: rgb(255 255 255 / 18%); + margin: 4px 0; + flex-shrink: 0; + + @media (width <= 720px) { + display: none; + } +} + +.sat__ctx-badge { + display: inline-flex; + align-items: center; + gap: 5px; + height: 22px; + padding: 0 9px; + border-radius: 999px; + font-size: 11.5px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.04em; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 30%); + color: #fff; + align-self: flex-start; + width: fit-content; + + fa-icon { + font-size: 10px; + } + + &--ok { + background: rgb(16 185 129 / 36%); + border-color: rgb(110 231 183 / 60%); + color: #d1fae5; + } + + &--warn { + background: rgb(245 158 11 / 36%); + border-color: rgb(252 211 77 / 60%); + color: #fef3c7; + } + + &--danger { + background: rgb(239 68 68 / 36%); + border-color: rgb(252 165 165 / 60%); + color: #fee2e2; + } +} + +/* ─── Two-column layout ───────────────────────────────────────────── */ +.sat__form { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); + gap: 14px; + align-items: start; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.sat__panel { + background: #fff; + border: 1px solid $border; + border-radius: 14px; + box-shadow: 0 1px 3px rgb(15 23 42 / 4%); + overflow: hidden; +} + +.sat__panel-head { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + background: $soft; + border-bottom: 1px solid $border; +} + +.sat__panel-ico { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 7px; + background: linear-gradient(135deg, $green 0%, $green-d 100%); + color: #fff; + font-size: 11px; +} + +.sat--withdrawal .sat__panel-ico { + background: linear-gradient(135deg, $amber 0%, $amber-d 100%); +} + +.sat__panel-title { + margin: 0; + font-size: 12.5px; + font-weight: 700; + color: $navy; + letter-spacing: 0.01em; +} + +.sat__panel-spacer { + flex: 1; +} + +.sat__panel-action { + height: 26px !important; + border-radius: 7px !important; + font-size: 11.5px !important; + font-weight: 600 !important; + padding: 0 10px !important; +} + +.sat__panel-body { + padding: 12px 14px 14px; + display: flex; + flex-direction: column; + gap: 3px; +} + +/* ─── Denomination groups ─────────────────────────────────────────── */ +.sat__denom-group { + padding: 8px 14px 4px; + + & + & { + border-top: 1px solid #f1f5f9; + } +} + +.sat__denom-group-head { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 5px; +} + +.sat__denom-group-title { + font-size: 10px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.07em; + color: $slate; +} + +.sat__denom-group-total { + font-size: 12px; + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; +} + +.sat__denom-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 6px; +} + +.sat__denom { + display: flex; + flex-direction: column; + gap: 5px; + padding: 6px 9px; + border-radius: 8px; + background: #fff; + border: 1px solid $border; + transition: + border-color 0.15s ease, + background 0.15s ease, + box-shadow 0.15s ease; + min-width: 0; + + &--active { + border-color: #86efac; + background: #f0fdf4; + box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%); + } +} + +.sat__denom-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + min-width: 0; +} + +.sat__denom-chip { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 48px; + height: 22px; + padding: 0 9px; + border-radius: 5px; + font-size: 11.5px; + font-weight: 800; + letter-spacing: 0.02em; + font-variant-numeric: tabular-nums; + flex-shrink: 0; + + &--note { + background: linear-gradient(135deg, #ecfeff 0%, #cffafe 100%); + color: #155e75; + border: 1px solid #a5f3fc; + } + + &--coin { + background: linear-gradient(135deg, #fef9c3 0%, #fde68a 100%); + color: #854d0e; + border: 1px solid #fcd34d; + } +} + +.sat__denom-amount { + font-size: 11.5px; + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; } -.right-label { - padding-right: 25px !important; +.sat__denom-stepper { + display: flex; + align-items: stretch; + height: 28px; + border-radius: 6px; + border: 1px solid $border; + background: #fff; + overflow: hidden; } -.form-heading { - font-size: 1.25rem; +.sat__denom-count { + flex: 1 1 auto; + width: 100%; + min-width: 0; + border: none; + outline: none; + background: transparent; + text-align: center; + font-size: 12.5px; + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; + padding: 0 4px; + appearance: textfield; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + appearance: none; + margin: 0; + } +} + +.sat__denom-step { + flex: 0 0 auto; + width: 28px; + background: $soft; + border: none; + border-right: 1px solid $border; + font-size: 15px; + font-weight: 700; + color: $slate; + cursor: pointer; + transition: + background 0.12s ease, + color 0.12s ease; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + padding: 0; + + &:last-child { + border-right: none; + border-left: 1px solid $border; + } + + &:hover:not(:disabled) { + background: #e2e8f0; + color: $navy; + } + + &:active:not(:disabled) { + background: #cbd5e1; + } + + &:disabled { + opacity: 0.35; + cursor: not-allowed; + } +} + +/* ─── Tally / apply ───────────────────────────────────────────────── */ +.sat__tally { + margin: 8px 14px 14px; + padding: 10px 12px; + border-radius: 10px; + background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%); + border: 1px solid $border; +} + +.sat__tally-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 3px 0; + font-size: 12px; + color: $slate; + + &--grand { + margin-top: 4px; + padding-top: 8px; + border-top: 1px dashed #cbd5e1; + + .sat__tally-label { + font-weight: 800; + color: $navy; + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 11px; + } + + .sat__tally-value { + font-size: 15.5px; + font-weight: 800; + color: $green-d; + } + } +} + +.sat--withdrawal .sat__tally-row--grand .sat__tally-value { + color: $amber-d; +} + +.sat__tally-value { + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; +} + +.sat__apply { + width: 100%; + margin-top: 8px; + height: 34px !important; + border-radius: 8px !important; + background: linear-gradient(135deg, $green 0%, $green-d 100%) !important; + color: #fff !important; + font-weight: 700 !important; + font-size: 12.5px !important; + letter-spacing: 0.01em; + box-shadow: 0 3px 10px -2px rgb(4 120 87 / 35%) !important; + + &:disabled { + opacity: 0.5; + filter: grayscale(0.4); + box-shadow: none !important; + } +} + +.sat--withdrawal .sat__apply { + background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important; + box-shadow: 0 4px 12px -2px rgb(180 83 9 / 35%) !important; +} + +/* ─── Form fields (right panel) ───────────────────────────────────── */ +.sat__field { + width: 100%; + + ::ng-deep .mat-mdc-text-field-wrapper { + background: #fff; + border-radius: 10px; + } + + ::ng-deep .mat-mdc-form-field-infix { + min-height: 48px; + padding-top: 12px !important; + padding-bottom: 12px !important; + } + + ::ng-deep .mdc-notched-outline__leading, + ::ng-deep .mdc-notched-outline__notch, + ::ng-deep .mdc-notched-outline__trailing { + border-color: #cbd5e1 !important; + } +} + +.sat__sub-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px 12px; + + .sat__field--wide { + grid-column: 1 / -1; + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.sat__toggle-row { + display: flex; + align-items: center; + padding: 6px 4px 10px; + cursor: pointer; + user-select: none; + + .sat__toggle-label { + margin-left: 8px; + font-size: 13px; + font-weight: 600; + color: $slate; + } +} + +/* Match / mismatch banners */ +.sat__mismatch, +.sat__match { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 10px; + font-size: 12.5px; font-weight: 500; - margin-bottom: 0.5rem; + margin: 2px 0 8px; +} + +.sat__mismatch { + background: #fffbeb; + border: 1px solid #fde68a; + color: #92400e; + + &-ico { + color: $amber; + flex-shrink: 0; + } + + strong { + font-weight: 800; + } +} + +.sat__match { + background: #ecfdf5; + border: 1px solid #a7f3d0; + color: #065f46; + + &-ico { + color: $green; + flex-shrink: 0; + } +} + +/* ─── Actions ─────────────────────────────────────────────────────── */ +.sat__actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 10px; + padding-top: 12px; + border-top: 1px solid #f1f5f9; + + button { + height: 42px !important; + border-radius: 10px !important; + font-weight: 700 !important; + padding: 0 22px !important; + letter-spacing: 0.01em; + } +} + +.sat__cancel { + color: $slate !important; + border-color: $border !important; +} + +.sat__submit { + background: linear-gradient(135deg, $green 0%, $green-d 100%) !important; + color: #fff !important; + box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important; + + &:disabled { + opacity: 0.55; + filter: grayscale(0.3); + background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important; + box-shadow: none !important; + } +} + +.sat--withdrawal .sat__submit { + background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important; + box-shadow: 0 4px 12px -2px rgb(180 83 9 / 35%) !important; + + &:disabled { + background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important; + } +} + +.m-r-6 { + margin-right: 6px; +} + +/* ─── Responsive ──────────────────────────────────────────────────── */ +@media (width <= 720px) { + .sat__hero-top { + flex-wrap: wrap; + } + + .sat__hero-meta { + width: 100%; + } + + .sat__ctx-item { + flex: 1 1 calc(50% - 14px); + } +} + +@media (width <= 480px) { + .sat__ctx-item { + flex: 1 1 100%; + } + + .sat__actions { + flex-direction: column-reverse; + + button { + width: 100%; + } + } } diff --git a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts index c1ea4cee7..09cad551f 100644 --- a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts +++ b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts @@ -19,16 +19,28 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router'; /** Custom Services */ import { CurrentAccountsService } from '../../current-accounts.service'; +import { ClientsService } from 'app/clients/clients.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; import { Currency } from 'app/shared/models/general.model'; import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component'; import { MatSlideToggle } from '@angular/material/slide-toggle'; import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatDialog } from '@angular/material/dialog'; +import { TransactionConfirmDialogComponent } from '../../../savings/saving-account-actions/savings-account-transactions/transaction-confirm-dialog/transaction-confirm-dialog.component'; +import { IdentityVerifyDialogComponent } from '../../../savings/saving-account-actions/savings-account-transactions/identity-verify-dialog/identity-verify-dialog.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +/** Single Indian denomination row (note or coin). */ +interface Denomination { + value: number; + kind: 'note' | 'coin'; + label: string; +} + /** - * Create current account transactions component. + * Create current account transactions component (Deposit / Withdraw). */ @Component({ selector: 'mifosx-current-account-transactions', @@ -38,7 +50,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ...STANDALONE_SHARED_IMPORTS, InputAmountComponent, MatSlideToggle, - CdkTextareaAutosize + CdkTextareaAutosize, + FaIconComponent ] }) export class CurrentAccountTransactionsComponent implements OnInit { @@ -47,7 +60,9 @@ export class CurrentAccountTransactionsComponent implements OnInit { private router = inject(Router); private dateUtils = inject(Dates); private currentAccountsService = inject(CurrentAccountsService); + private clientsService = inject(ClientsService); private settingsService = inject(SettingsService); + private dialog = inject(MatDialog); /** Minimum Due Date allowed. */ minDate = new Date(2000, 0, 1); @@ -73,15 +88,38 @@ export class CurrentAccountTransactionsComponent implements OnInit { currentAccountId: string; currency: Currency | null = null; - /** - * Retrieves the Current Account transaction template data from `resolve`. - * @param {FormBuilder} formBuilder Form Builder. - * @param {CurrentAccountsService} currentAccountsService Current Accounts Service. - * @param {ActivatedRoute} route Activated Route. - * @param {Dates} dateUtils Date Utils. - * @param {Router} router Router for navigation. - * @param {SettingsService} settingsService Settings Service - */ + /** Indian banknote denominations (descending). */ + readonly notes: Denomination[] = [ + { value: 500, kind: 'note', label: '₹500' }, + { value: 200, kind: 'note', label: '₹200' }, + { value: 100, kind: 'note', label: '₹100' }, + { value: 50, kind: 'note', label: '₹50' }, + { value: 20, kind: 'note', label: '₹20' }, + { value: 10, kind: 'note', label: '₹10' } + ]; + + /** Indian coin denominations (descending). */ + readonly coins: Denomination[] = [ + { value: 20, kind: 'coin', label: '₹20' }, + { value: 10, kind: 'coin', label: '₹10' }, + { value: 5, kind: 'coin', label: '₹5' }, + { value: 2, kind: 'coin', label: '₹2' }, + { value: 1, kind: 'coin', label: 'Re 1' } + ]; + + /** Quantity per denomination — keyed by `${kind}:${value}`. */ + denominationCounts: Record = {}; + + // ── Context strip data ────────────────────────────────────────────── + accountNo: string | null = null; + clientName: string | null = null; + clientId: number | null = null; + kycStatus: 'Verified' | 'Partial' | 'Pending' = 'Pending'; + sameDayNeftRtgs = 0; + sameDayChequeClearing = 0; + availableBalance = 0; + identityVerified = false; + constructor() { this.route.data.subscribe((data: { currentAccountActionData: any }) => { this.paymentTypeOptions = data.currentAccountActionData.paymentTypeOptions; @@ -94,17 +132,93 @@ export class CurrentAccountTransactionsComponent implements OnInit { this.currentAccountId = this.route.snapshot.params['currentAccountId']; } - /** - * Creates the Current Account account transaction form when component loads. - */ ngOnInit() { this.maxDate = this.settingsService.businessDate; this.createCurrentAccountTransactionForm(); + this.loadContextStrip(); + } + + private loadContextStrip(): void { + if (!this.currentAccountId) return; + this.currentAccountsService.getCurrentAccountData(this.currentAccountId).subscribe({ + next: (account: any) => { + this.accountNo = account?.accountNo ?? null; + this.clientName = account?.clientName ?? account?.groupName ?? null; + this.clientId = account?.clientId ?? null; + if (!this.currency && account?.currency) this.currency = account.currency; + this.availableBalance = Number(account?.summary?.availableBalance ?? account?.summary?.accountBalance ?? 0); + this.computeSameDayNeftRtgs(account?.transactions); + this.computeSameDayChequeClearing(account?.transactions); + if (account?.clientId) { + this.fetchClientKyc(String(account.clientId)); + } + }, + error: () => {} + }); + } + + private fetchClientKyc(clientId: string): void { + this.clientsService.getClientKyc(clientId).subscribe({ + next: (kyc: any) => { + const pan = !!kyc?.panVerified; + const aadhaar = !!kyc?.aadhaarVerified; + this.kycStatus = pan && aadhaar ? 'Verified' : pan || aadhaar ? 'Partial' : 'Pending'; + }, + error: () => {} + }); + } + + private computeSameDayNeftRtgs(transactions: any[] | undefined): void { + if (!Array.isArray(transactions) || transactions.length === 0) { + this.sameDayNeftRtgs = 0; + return; + } + const today = this.settingsService.businessDate ?? 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() + ); + }; + const isNeftRtgs = (t: any) => { + const txnType = (t?.transactionType?.value ?? t?.transactionType?.code ?? '').toUpperCase(); + const payType = (t?.paymentDetailData?.paymentType?.name ?? '').toUpperCase(); + return /NEFT|RTGS/.test(txnType) || /NEFT|RTGS/.test(payType); + }; + this.sameDayNeftRtgs = transactions + .filter((t: any) => !t.reversed && sameDay(t.date) && isNeftRtgs(t)) + .reduce((sum, t: any) => sum + (Number(t.amount) || 0), 0); + } + + private computeSameDayChequeClearing(transactions: any[] | undefined): void { + if (!Array.isArray(transactions) || transactions.length === 0) { + this.sameDayChequeClearing = 0; + return; + } + const today = this.settingsService.businessDate ?? 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() + ); + }; + const isCheque = (t: any) => { + const txnType = (t?.transactionType?.value ?? t?.transactionType?.code ?? '').toUpperCase(); + const payType = (t?.paymentDetailData?.paymentType?.name ?? '').toUpperCase(); + const hasCheckNo = !!t?.paymentDetailData?.checkNumber; + return hasCheckNo || /CHEQUE|CHECK/.test(txnType) || /CHEQUE|CHECK/.test(payType); + }; + const isPending = (t: any) => { + const cleared = t?.transactionType?.bankTransfer === true || t?.transactionType?.deposit === true; + return !cleared; + }; + this.sameDayChequeClearing = transactions + .filter((t: any) => !t.reversed && sameDay(t.date) && isCheque(t) && isPending(t)) + .reduce((sum, t: any) => sum + (Number(t.amount) || 0), 0); } - /** - * Method to create the Current Account Transaction Form. - */ createCurrentAccountTransactionForm() { this.currentAccountTransactionForm = this.formBuilder.group({ transactionDate: [ @@ -123,6 +237,75 @@ export class CurrentAccountTransactionsComponent implements OnInit { }); } + // ── Denomination helpers ──────────────────────────────────────────── + denomKey(d: Denomination): string { + return `${d.kind}:${d.value}`; + } + + countFor(d: Denomination): number { + return this.denominationCounts[this.denomKey(d)] || 0; + } + + subtotalFor(d: Denomination): number { + return this.countFor(d) * d.value; + } + + onDenomChange(d: Denomination, raw: any): void { + const n = Math.max(0, Math.floor(Number(raw) || 0)); + this.denominationCounts[this.denomKey(d)] = n; + } + + bumpDenom(d: Denomination, delta: number): void { + const next = Math.max(0, this.countFor(d) + delta); + this.denominationCounts[this.denomKey(d)] = next; + } + + get notesTotal(): number { + return this.notes.reduce((s, d) => s + this.subtotalFor(d), 0); + } + + get coinsTotal(): number { + return this.coins.reduce((s, d) => s + this.subtotalFor(d), 0); + } + + get denominationTotal(): number { + return this.notesTotal + this.coinsTotal; + } + + get totalPieces(): number { + return Object.values(this.denominationCounts).reduce((s, n) => s + (n || 0), 0); + } + + get denominationMatchesForm(): boolean { + const formAmt = Number(this.currentAccountTransactionForm?.controls?.['transactionAmount']?.value || 0); + return this.denominationTotal > 0 && formAmt === this.denominationTotal; + } + + applyDenominationTotal(): void { + this.currentAccountTransactionForm.controls['transactionAmount'].setValue(this.denominationTotal); + this.currentAccountTransactionForm.controls['transactionAmount'].markAsDirty(); + } + + clearDenominations(): void { + this.denominationCounts = {}; + } + + openIdentityVerify(): void { + if (!this.clientId) return; + this.identityVerified = true; + this.dialog.open(IdentityVerifyDialogComponent, { + width: '500px', + maxWidth: '92vw', + panelClass: 'idv-panel', + autoFocus: false, + data: { + clientId: this.clientId, + clientName: this.clientName, + accountNo: this.accountNo + } + }); + } + /** * Method to add payment detail fields to the UI. */ @@ -144,9 +327,35 @@ export class CurrentAccountTransactionsComponent implements OnInit { } /** - * Method to submit the transaction details. + * Opens a confirmation dialog then posts the transaction. */ submit() { + const formAmount = Number(this.currentAccountTransactionForm.value.transactionAmount) || 0; + const paymentTypeId = this.currentAccountTransactionForm.value.paymentTypeId; + const paymentTypeLabel = this.paymentTypeOptions?.find((p) => p.id === paymentTypeId)?.name ?? null; + + const ref = this.dialog.open(TransactionConfirmDialogComponent, { + width: '480px', + maxWidth: '92vw', + panelClass: 'tcd-panel', + autoFocus: false, + data: { + kind: this.transactionType.deposit ? 'deposit' : 'withdrawal', + clientName: this.clientName, + accountNo: this.accountNo, + currentBalance: this.availableBalance, + transactionAmount: formAmount, + paymentTypeLabel + } + }); + ref.afterClosed().subscribe((result: any) => { + if (result?.confirm) { + this.postTransaction(); + } + }); + } + + private postTransaction(): void { const currentAccountTransactionFormData = this.currentAccountTransactionForm.value; const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; @@ -160,9 +369,23 @@ export class CurrentAccountTransactionsComponent implements OnInit { locale }; data['transactionAmount'] = data['transactionAmount'] * 1; + if (this.denominationTotal > 0) { + const breakdown = [ + ...this.notes, + ...this.coins + ] + .map((d) => ({ + denomination: d.value, + kind: d.kind, + count: this.countFor(d), + subtotal: this.subtotalFor(d) + })) + .filter((row) => row.count > 0); + data['denominationBreakdown'] = breakdown; + } this.currentAccountsService .executeCurrentAccountTransactionsCommand(this.currentAccountId, this.transactionCommand, data) - .subscribe((res) => { + .subscribe(() => { this.router.navigate(['../../transactions'], { relativeTo: this.route }); }); } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts index e7ec51bda..9365357c0 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts @@ -215,6 +215,8 @@ export class FixedDepositAccountViewComponent implements OnInit { case 'Add Charge': case 'Withdrawn by Client': case 'Premature Close': + case 'Deposit': + case 'Withdrawal': this.router.navigate([`actions/${name}`], { relativeTo: this.route }); break; case 'Modify Application': diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts index 79e315a0e..ca1b56aee 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts @@ -34,6 +34,14 @@ export class FixedDepositsButtonsConfiguration { switch (status) { case 'Active': this.buttonsArray = [ + { + name: 'Deposit', + icon: 'fa fa-arrow-down' + }, + { + name: 'Withdrawal', + icon: 'fa fa-arrow-up' + }, { name: 'Premature Close', icon: 'fa fa-arrow-left' diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html index 6ccb3ba3b..f20e46af8 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html @@ -30,6 +30,6 @@ @if (actions['Close']) { } -@if (actions['Withdrawal']) { +@if (actions['Withdrawal'] || actions['Deposit']) { } diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts index 6b856bcad..e0b47a2f7 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts @@ -55,6 +55,7 @@ export class FixedDepositsAccountActionsComponent { 'Premature Close': boolean; 'Withdrawn by Client': boolean; Withdrawal: boolean; + Deposit: boolean; } = { Approve: false, Reject: false, @@ -65,7 +66,8 @@ export class FixedDepositsAccountActionsComponent { 'Add Charge': false, 'Premature Close': false, 'Withdrawn by Client': false, - Withdrawal: false + Withdrawal: false, + Deposit: false }; /** diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.html b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.html index 08f39216c..c9074136e 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.html @@ -6,28 +6,240 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- - @if (transactionType.withdrawal) { -

- {{ 'labels.menus.' + actionName | translate }} {{ 'labels.heading.Fixed Deposit Transactions' | translate }} -

- } +
+ +
+
+ +
+ +
+
+

+ {{ + transactionType.withdrawal + ? ('labels.heading.Withdraw Money From Fixed Deposit' | translate) + : ('labels.heading.Deposit Money To Fixed Deposit' | translate) + }} +

+

+ {{ + transactionType.deposit + ? 'Count the cash by denomination, then confirm the payment details.' + : 'Disburse cash by denomination and confirm the payment details.' + }} +

+
+ @if (transactionType.withdrawal && clientId) { + + } +
+ Counted Total + ₹{{ denominationTotal | number: '1.2-2' }} + @if (totalPieces > 0) { + {{ totalPieces }} piece(s) + } +
+
-
- - + +
+
+ +
+ Client + {{ clientName || '—' }} +
+
+
+
+ +
+ Account No + {{ accountNo || '—' }} +
+
+
+
+ +
+ Current Balance + + ₹{{ availableBalance | number: '1.2-2' }} + +
+
+
+
+ +
+ KYC + + + {{ kycStatus }} + +
+
+
+
+ + + +
+
+ +

Cash Denomination Breakdown

+ + +
+ +
+
+ Banknotes + ₹{{ notesTotal | number: '1.2-2' }} +
+
+ @for (d of notes; track d.value) { +
+
+ {{ d.label }} + ₹{{ subtotalFor(d) | number: '1.2-2' }} +
+
+ + + +
+
+ } +
+
+ +
+
+ Coins + ₹{{ coinsTotal | number: '1.2-2' }} +
+
+ @for (d of coins; track d.value) { +
+
+ {{ d.label }} + ₹{{ subtotalFor(d) | number: '1.2-2' }} +
+
+ + + +
+
+ } +
+
+ +
+
+ Banknotes + ₹{{ notesTotal | number: '1.2-2' }} +
+
+ Coins + ₹{{ coinsTotal | number: '1.2-2' }} +
+
+ Grand Total + ₹{{ denominationTotal | number: '1.2-2' }} +
+ +
+
+ + +
+
+ +

Transaction Details

+
+
+ {{ 'labels.inputs.Transaction Date' | translate }} - - + + @if (accountTransactionForm.controls.transactionDate.hasError('required')) { {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} @@ -44,76 +256,78 @@ > - + @if (denominationTotal > 0 && !denominationMatchesForm) { +
+ + + Counted cash ₹{{ denominationTotal | number: '1.2-2' }} doesn't match. Click + Use ₹{{ denominationTotal | number: '1.2-2' }} to sync. + +
+ } + @if (denominationMatchesForm) { +
+ + Cash count matches transaction amount. +
+ } + + {{ 'labels.inputs.Payment Type' | translate }} - + @for (paymentType of paymentTypeOptions; track paymentType) { - - {{ paymentType.name }} - + {{ paymentType.name }} } -
- - -
- {{ 'labels.inputs.Show Payment Details' | translate }} -
-
-
+
+ + {{ 'labels.inputs.Show Payment Details' | translate }} +
@if (addPaymentDetailsFlag) { - - {{ 'labels.inputs.Account Number' | translate }} - - +
+ + {{ 'labels.inputs.Account Number' | translate }} + + + + {{ 'labels.inputs.Cheque' | translate }} + + + + {{ 'labels.inputs.Routing Code' | translate }} + + + + {{ 'labels.inputs.Receipt Number' | translate }} + + + + {{ 'labels.inputs.Bank' | translate }} + + +
} - @if (addPaymentDetailsFlag) { - - {{ 'labels.inputs.Cheque' | translate }} - - - } - - @if (addPaymentDetailsFlag) { - - {{ 'labels.inputs.Routing Code' | translate }} - - - } - - @if (addPaymentDetailsFlag) { - - {{ 'labels.inputs.Receipt Number' | translate }} - - - } - - @if (addPaymentDetailsFlag) { - - {{ 'labels.inputs.Bank' | translate }} - - - } - - + {{ 'labels.inputs.Notes' | translate }} - - - - - - - +
+
+
+
diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.scss b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.scss index 3795a54c2..d7810f153 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.scss +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.scss @@ -6,18 +6,754 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.expandcollapsebutton { - margin-top: -7px; +$navy: #0d1b2a; +$slate: #475569; +$border: #e2e8f0; +$soft: #f8fafc; +$green: #059669; +$green-d: #047857; +$amber: #d97706; +$amber-d: #b45309; + +.sat { + width: 100%; + padding: 10px 16px 18px; + display: flex; + flex-direction: column; + gap: 10px; } -.container { - max-width: 37rem; +.sat__hero { + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px 16px 12px; + border-radius: 12px; + color: #fff; + background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 45%, #1e40af 100%); + box-shadow: 0 6px 18px -10px rgb(15 23 42 / 50%); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at top right, rgb(255 255 255 / 12%), transparent 55%); + pointer-events: none; + } } -.right-input { +.sat--withdrawal .sat__hero { + background: linear-gradient(135deg, #134e4a 0%, #115e59 45%, #0f766e 100%); + box-shadow: 0 6px 18px -10px rgb(15 118 110 / 50%); +} + +.sat__hero-top { + display: flex; + align-items: center; + gap: 12px; + position: relative; + z-index: 1; +} + +.sat__hero-back { + height: 30px !important; + border-radius: 8px !important; + padding: 0 11px !important; + font-weight: 700 !important; + font-size: 12px !important; + background: rgb(255 255 255 / 14%) !important; + color: #fff !important; + border: 1px solid rgb(255 255 255 / 28%) !important; + flex-shrink: 0; + + &:hover { + background: rgb(255 255 255 / 26%) !important; + } +} + +.sat__hero-ico { + width: 36px; + height: 36px; + border-radius: 9px; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 28%); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 15px; + flex-shrink: 0; +} + +.sat__hero-title-wrap { + flex: 1 1 auto; + min-width: 0; +} + +.sat__title { + margin: 0; + font-size: 14.5px; + font-weight: 800; + letter-spacing: 0.01em; + line-height: 1.15; +} + +.sat__sub { + margin: 1px 0 0; + font-size: 11.5px; + color: rgb(255 255 255 / 80%); +} + +.sat__hero-verify { + position: relative; + height: 30px !important; + border-radius: 8px !important; + padding: 0 12px !important; + font-weight: 700 !important; + font-size: 12px !important; + color: #fff !important; + background: rgb(255 255 255 / 16%) !important; + border: 1px solid rgb(255 255 255 / 30%) !important; + flex-shrink: 0; + overflow: visible !important; + + &::before { + content: ''; + position: absolute; + inset: -2px; + border-radius: 10px; + border: 2px solid rgb(255 255 255 / 70%); + opacity: 0; + pointer-events: none; + will-change: transform, opacity; + } + + &--pulse::before { + animation: sat-verify-pulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite; + } +} + +@keyframes sat-verify-pulse { + 0% { + transform: scale(1); + opacity: 0.85; + } + + 70% { + transform: scale(1.18); + opacity: 0; + } + + 100% { + transform: scale(1.18); + opacity: 0; + } +} + +.sat__hero-meta { + display: inline-flex; + flex-direction: column; + align-items: flex-end; + padding: 5px 12px; + border-radius: 8px; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 30%); + min-width: 130px; + flex-shrink: 0; +} + +.sat__hero-meta-label { + font-size: 9.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: rgb(255 255 255 / 78%); +} + +.sat__hero-meta-value { + font-size: 15px; + font-weight: 800; + color: #fff; + font-variant-numeric: tabular-nums; +} + +.sat__hero-meta-sub { + font-size: 10px; + color: rgb(255 255 255 / 75%); +} + +.sat__ctx { + display: flex; + align-items: stretch; + flex-wrap: wrap; + padding: 5px 8px; + border-radius: 8px; + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 22%); + position: relative; + z-index: 1; +} + +.sat__ctx-item { + display: flex; + align-items: center; + gap: 6px; + flex: 1 1 110px; + padding: 2px 8px; + min-width: 0; +} + +.sat__ctx-ico { + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 6px; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 25%); + color: #fff; + font-size: 10px; + flex-shrink: 0; +} + +.sat__ctx-text { + display: flex; + flex-direction: column; + min-width: 0; +} + +.sat__ctx-label { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: rgb(255 255 255 / 70%); + font-weight: 700; +} + +.sat__ctx-value { + font-size: 11.5px; + font-weight: 700; + color: #fff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &--mono { + font-family: 'Courier New', monospace; + font-size: 10.5px; + } + + &--amount { + font-variant-numeric: tabular-nums; + font-size: 12px; + } + + &--lg { + font-size: 13px; + } +} + +.sat__ctx-item--balance { + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 22%); + border-radius: 7px; + padding: 3px 8px; +} + +.sat__ctx-divider { + width: 1px; + background: rgb(255 255 255 / 18%); + margin: 4px 0; + flex-shrink: 0; + + @media (width <= 720px) { + display: none; + } +} + +.sat__ctx-badge { + display: inline-flex; + align-items: center; + gap: 5px; + height: 22px; + padding: 0 9px; + border-radius: 999px; + font-size: 11.5px; + font-weight: 800; + text-transform: uppercase; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 30%); + color: #fff; + align-self: flex-start; + width: fit-content; + + fa-icon { + font-size: 10px; + } + + &--ok { + background: rgb(16 185 129 / 36%); + border-color: rgb(110 231 183 / 60%); + color: #d1fae5; + } + + &--warn { + background: rgb(245 158 11 / 36%); + border-color: rgb(252 211 77 / 60%); + color: #fef3c7; + } + + &--danger { + background: rgb(239 68 68 / 36%); + border-color: rgb(252 165 165 / 60%); + color: #fee2e2; + } +} + +.sat__form { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); + gap: 14px; + align-items: start; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.sat__panel { + background: #fff; + border: 1px solid $border; + border-radius: 14px; + box-shadow: 0 1px 3px rgb(15 23 42 / 4%); + overflow: hidden; +} + +.sat__panel-head { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + background: $soft; + border-bottom: 1px solid $border; +} + +.sat__panel-ico { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 7px; + background: linear-gradient(135deg, $green 0%, $green-d 100%); + color: #fff; + font-size: 11px; +} + +.sat--withdrawal .sat__panel-ico { + background: linear-gradient(135deg, $amber 0%, $amber-d 100%); +} + +.sat__panel-title { + margin: 0; + font-size: 12.5px; + font-weight: 700; + color: $navy; +} + +.sat__panel-spacer { + flex: 1; +} + +.sat__panel-action { + height: 26px !important; + border-radius: 7px !important; + font-size: 11.5px !important; + font-weight: 600 !important; + padding: 0 10px !important; +} + +.sat__panel-body { + padding: 12px 14px 14px; + display: flex; + flex-direction: column; + gap: 3px; +} + +.sat__denom-group { + padding: 8px 14px 4px; + + & + & { + border-top: 1px solid #f1f5f9; + } +} + +.sat__denom-group-head { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 5px; +} + +.sat__denom-group-title { + font-size: 10px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.07em; + color: $slate; +} + +.sat__denom-group-total { + font-size: 12px; + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; +} + +.sat__denom-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 6px; +} + +.sat__denom { + display: flex; + flex-direction: column; + gap: 5px; + padding: 6px 9px; + border-radius: 8px; + background: #fff; + border: 1px solid $border; + transition: + border-color 0.15s ease, + background 0.15s ease; + min-width: 0; + + &--active { + border-color: #86efac; + background: #f0fdf4; + box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%); + } +} + +.sat__denom-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + min-width: 0; +} + +.sat__denom-chip { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 48px; + height: 22px; + padding: 0 9px; + border-radius: 5px; + font-size: 11.5px; + font-weight: 800; + flex-shrink: 0; + + &--note { + background: linear-gradient(135deg, #ecfeff 0%, #cffafe 100%); + color: #155e75; + border: 1px solid #a5f3fc; + } + + &--coin { + background: linear-gradient(135deg, #fef9c3 0%, #fde68a 100%); + color: #854d0e; + border: 1px solid #fcd34d; + } +} + +.sat__denom-amount { + font-size: 11.5px; + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; } -.right-label { - padding-right: 25px !important; +.sat__denom-stepper { + display: flex; + align-items: stretch; + height: 28px; + border-radius: 6px; + border: 1px solid $border; + overflow: hidden; +} + +.sat__denom-count { + flex: 1 1 auto; + width: 100%; + min-width: 0; + border: none; + outline: none; + background: transparent; + text-align: center; + font-size: 12.5px; + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; + padding: 0 4px; + appearance: textfield; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + appearance: none; + margin: 0; + } +} + +.sat__denom-step { + flex: 0 0 auto; + width: 28px; + background: $soft; + border: none; + border-right: 1px solid $border; + font-size: 15px; + font-weight: 700; + color: $slate; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + + &:last-child { + border-right: none; + border-left: 1px solid $border; + } + + &:hover:not(:disabled) { + background: #e2e8f0; + color: $navy; + } + + &:disabled { + opacity: 0.35; + cursor: not-allowed; + } +} + +.sat__tally { + margin: 8px 14px 14px; + padding: 10px 12px; + border-radius: 10px; + background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%); + border: 1px solid $border; +} + +.sat__tally-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 3px 0; + font-size: 12px; + color: $slate; + + &--grand { + margin-top: 4px; + padding-top: 8px; + border-top: 1px dashed #cbd5e1; + + .sat__tally-label { + font-weight: 800; + color: $navy; + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 11px; + } + + .sat__tally-value { + font-size: 15.5px; + font-weight: 800; + color: $green-d; + } + } +} + +.sat--withdrawal .sat__tally-row--grand .sat__tally-value { + color: $amber-d; +} + +.sat__tally-value { + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; +} + +.sat__apply { + width: 100%; + margin-top: 8px; + height: 34px !important; + border-radius: 8px !important; + background: linear-gradient(135deg, $green 0%, $green-d 100%) !important; + color: #fff !important; + font-weight: 700 !important; + font-size: 12.5px !important; + box-shadow: 0 3px 10px -2px rgb(4 120 87 / 35%) !important; + + &:disabled { + opacity: 0.5; + filter: grayscale(0.4); + box-shadow: none !important; + } +} + +.sat--withdrawal .sat__apply { + background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important; +} + +.sat__field { + width: 100%; + + ::ng-deep .mat-mdc-text-field-wrapper { + background: #fff; + border-radius: 10px; + } + + ::ng-deep .mat-mdc-form-field-infix { + min-height: 48px; + padding-top: 12px !important; + padding-bottom: 12px !important; + } + + ::ng-deep .mdc-notched-outline__leading, + ::ng-deep .mdc-notched-outline__notch, + ::ng-deep .mdc-notched-outline__trailing { + border-color: #cbd5e1 !important; + } +} + +.sat__sub-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px 12px; + + .sat__field--wide { + grid-column: 1 / -1; + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.sat__toggle-row { + display: flex; + align-items: center; + padding: 6px 4px 10px; + cursor: pointer; + user-select: none; + + .sat__toggle-label { + margin-left: 8px; + font-size: 13px; + font-weight: 600; + color: $slate; + } +} + +.sat__mismatch, +.sat__match { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 10px; + font-size: 12.5px; + margin: 2px 0 8px; +} + +.sat__mismatch { + background: #fffbeb; + border: 1px solid #fde68a; + color: #92400e; + + &-ico { + color: $amber; + flex-shrink: 0; + } + + strong { + font-weight: 800; + } +} + +.sat__match { + background: #ecfdf5; + border: 1px solid #a7f3d0; + color: #065f46; + + &-ico { + color: $green; + flex-shrink: 0; + } +} + +.sat__actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 10px; + padding-top: 12px; + border-top: 1px solid #f1f5f9; + + button { + height: 42px !important; + border-radius: 10px !important; + font-weight: 700 !important; + padding: 0 22px !important; + } +} + +.sat__cancel { + color: $slate !important; + border-color: $border !important; +} + +.sat__submit { + background: linear-gradient(135deg, $green 0%, $green-d 100%) !important; + color: #fff !important; + box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important; + + &:disabled { + opacity: 0.55; + background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important; + box-shadow: none !important; + } +} + +.sat--withdrawal .sat__submit { + background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important; + box-shadow: 0 4px 12px -2px rgb(180 83 9 / 35%) !important; +} + +.m-r-6 { + margin-right: 6px; +} + +@media (width <= 720px) { + .sat__hero-top { + flex-wrap: wrap; + } + + .sat__hero-meta { + width: 100%; + } + + .sat__ctx-item { + flex: 1 1 calc(50% - 14px); + } } diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.ts b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.ts index 607b26037..77a72c092 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.ts @@ -15,6 +15,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { FixedDepositsService } from '../../fixed-deposits.service'; +import { ClientsService } from 'app/clients/clients.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; @@ -23,8 +24,18 @@ import { TransactionCommand, TransactionTypeFlags } from '../../../transaction.m import { InputAmountComponent } from '../../../../shared/input-amount/input-amount.component'; import { MatSlideToggle } from '@angular/material/slide-toggle'; import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatDialog } from '@angular/material/dialog'; +import { TransactionConfirmDialogComponent } from '../../../../savings/saving-account-actions/savings-account-transactions/transaction-confirm-dialog/transaction-confirm-dialog.component'; +import { IdentityVerifyDialogComponent } from '../../../../savings/saving-account-actions/savings-account-transactions/identity-verify-dialog/identity-verify-dialog.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +interface Denomination { + value: number; + kind: 'note' | 'coin'; + label: string; +} + @Component({ selector: 'mifosx-fixed-deposits-cash-transaction', templateUrl: './fixed-deposits-cash-transaction.component.html', @@ -33,7 +44,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ...STANDALONE_SHARED_IMPORTS, InputAmountComponent, MatSlideToggle, - CdkTextareaAutosize + CdkTextareaAutosize, + FaIconComponent ] }) export class FixedDepositsCashTransactionComponent implements OnInit { @@ -42,36 +54,47 @@ export class FixedDepositsCashTransactionComponent implements OnInit { private router = inject(Router); private dateUtils = inject(Dates); private fixedDepositsService = inject(FixedDepositsService); + private clientsService = inject(ClientsService); private settingsService = inject(SettingsService); + private dialog = inject(MatDialog); - /** Minimum Due Date allowed. */ minDate = new Date(2000, 0, 1); - /** Maximum Due Date allowed. */ maxDate = new Date(); - /** Savings account transaction form. */ accountTransactionForm: UntypedFormGroup; - /** savings account transaction payment options. */ paymentTypeOptions: PaymentType[] = []; - /** Flag to enable payment details fields. */ addPaymentDetailsFlag: Boolean = false; - /** transaction type flag to render required UI */ transactionType: TransactionTypeFlags = { deposit: false, withdrawal: false }; - /** transaction command for submit request */ transactionCommand: TransactionCommand; actionName: string; - /** saving account's Id */ accountId: string; currency: Currency; - /** - * Retrieves the Saving Account transaction template data from `resolve`. - * @param {FormBuilder} formBuilder Form Builder. - * @param {FixedDepositsService} fixedDepositsService Fixed Deposit Service. - * @param {ActivatedRoute} route Activated Route. - * @param {Dates} dateUtils Date Utils. - * @param {Router} router Router for navigation. - * @param {SettingsService} settingsService Settings Service - */ + readonly notes: Denomination[] = [ + { value: 500, kind: 'note', label: '₹500' }, + { value: 200, kind: 'note', label: '₹200' }, + { value: 100, kind: 'note', label: '₹100' }, + { value: 50, kind: 'note', label: '₹50' }, + { value: 20, kind: 'note', label: '₹20' }, + { value: 10, kind: 'note', label: '₹10' } + ]; + + readonly coins: Denomination[] = [ + { value: 20, kind: 'coin', label: '₹20' }, + { value: 10, kind: 'coin', label: '₹10' }, + { value: 5, kind: 'coin', label: '₹5' }, + { value: 2, kind: 'coin', label: '₹2' }, + { value: 1, kind: 'coin', label: 'Re 1' } + ]; + + denominationCounts: Record = {}; + + accountNo: string | null = null; + clientName: string | null = null; + clientId: number | null = null; + kycStatus: 'Verified' | 'Partial' | 'Pending' = 'Pending'; + availableBalance = 0; + identityVerified = false; + constructor() { this.route.data.subscribe((data: { fixedDepositsAccountActionData: any }) => { this.currency = data.fixedDepositsAccountActionData.currency; @@ -85,21 +108,43 @@ export class FixedDepositsCashTransactionComponent implements OnInit { } else { throw new Error(`Invalid transaction action: ${this.actionName}`); } - this.accountId = this.route.parent.snapshot.params['fixedDepositAccountId']; } - /** - * Creates the Saving account transaction form when component loads. - */ ngOnInit() { this.maxDate = this.settingsService.businessDate; this.createSavingAccountTransactionForm(); + this.loadContextStrip(); + } + + private loadContextStrip(): void { + if (!this.accountId) return; + this.fixedDepositsService.getFixedDepositsAccountData(this.accountId).subscribe({ + next: (account: any) => { + this.accountNo = account?.accountNo ?? null; + this.clientName = account?.clientName ?? account?.groupName ?? null; + this.clientId = account?.clientId ?? null; + if (!this.currency && account?.currency) this.currency = account.currency; + this.availableBalance = Number(account?.summary?.accountBalance ?? account?.summary?.availableBalance ?? 0); + if (account?.clientId) { + this.fetchClientKyc(String(account.clientId)); + } + }, + error: () => {} + }); + } + + private fetchClientKyc(clientId: string): void { + this.clientsService.getClientKyc(clientId).subscribe({ + next: (kyc: any) => { + const pan = !!kyc?.panVerified; + const aadhaar = !!kyc?.aadhaarVerified; + this.kycStatus = pan && aadhaar ? 'Verified' : pan || aadhaar ? 'Partial' : 'Pending'; + }, + error: () => {} + }); } - /** - * Method to create the Saving Account Transaction Form. - */ createSavingAccountTransactionForm() { this.accountTransactionForm = this.formBuilder.group({ transactionDate: [ @@ -115,9 +160,71 @@ export class FixedDepositsCashTransactionComponent implements OnInit { }); } - /** - * Method to add payment detail fields to the UI. - */ + // ── Denomination helpers ──────────────────────────────────────────── + denomKey(d: Denomination): string { + return `${d.kind}:${d.value}`; + } + + countFor(d: Denomination): number { + return this.denominationCounts[this.denomKey(d)] || 0; + } + + subtotalFor(d: Denomination): number { + return this.countFor(d) * d.value; + } + + onDenomChange(d: Denomination, raw: any): void { + const n = Math.max(0, Math.floor(Number(raw) || 0)); + this.denominationCounts[this.denomKey(d)] = n; + } + + bumpDenom(d: Denomination, delta: number): void { + const next = Math.max(0, this.countFor(d) + delta); + this.denominationCounts[this.denomKey(d)] = next; + } + + get notesTotal(): number { + return this.notes.reduce((s, d) => s + this.subtotalFor(d), 0); + } + + get coinsTotal(): number { + return this.coins.reduce((s, d) => s + this.subtotalFor(d), 0); + } + + get denominationTotal(): number { + return this.notesTotal + this.coinsTotal; + } + + get totalPieces(): number { + return Object.values(this.denominationCounts).reduce((s, n) => s + (n || 0), 0); + } + + get denominationMatchesForm(): boolean { + const formAmt = Number(this.accountTransactionForm?.controls?.['transactionAmount']?.value || 0); + return this.denominationTotal > 0 && formAmt === this.denominationTotal; + } + + applyDenominationTotal(): void { + this.accountTransactionForm.controls['transactionAmount'].setValue(this.denominationTotal); + this.accountTransactionForm.controls['transactionAmount'].markAsDirty(); + } + + clearDenominations(): void { + this.denominationCounts = {}; + } + + openIdentityVerify(): void { + if (!this.clientId) return; + this.identityVerified = true; + this.dialog.open(IdentityVerifyDialogComponent, { + width: '500px', + maxWidth: '92vw', + panelClass: 'idv-panel', + autoFocus: false, + data: { clientId: this.clientId, clientName: this.clientName, accountNo: this.accountNo } + }); + } + addPaymentDetails() { this.addPaymentDetailsFlag = !this.addPaymentDetailsFlag; if (this.addPaymentDetailsFlag) { @@ -135,27 +242,54 @@ export class FixedDepositsCashTransactionComponent implements OnInit { } } - /** - * Method to submit the transaction details. - */ submit() { - const savingAccountTransactionFormData = this.accountTransactionForm.value; + const formAmount = Number(this.accountTransactionForm.value.transactionAmount) || 0; + const paymentTypeId = this.accountTransactionForm.value.paymentTypeId; + const paymentTypeLabel = this.paymentTypeOptions?.find((p: any) => p.id === paymentTypeId)?.name ?? null; + + const ref = this.dialog.open(TransactionConfirmDialogComponent, { + width: '480px', + maxWidth: '92vw', + panelClass: 'tcd-panel', + autoFocus: false, + data: { + kind: this.transactionType.deposit ? 'deposit' : 'withdrawal', + clientName: this.clientName, + accountNo: this.accountNo, + currentBalance: this.availableBalance, + transactionAmount: formAmount, + paymentTypeLabel + } + }); + ref.afterClosed().subscribe((result: any) => { + if (result?.confirm) { + this.postTransaction(); + } + }); + } + + private postTransaction(): void { + const formData = this.accountTransactionForm.value; const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; - const prevTransactionDate: Date = this.accountTransactionForm.value.transactionDate; - if (savingAccountTransactionFormData.transactionDate instanceof Date) { - savingAccountTransactionFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat); + const prevDate: Date = this.accountTransactionForm.value.transactionDate; + if (formData.transactionDate instanceof Date) { + formData.transactionDate = this.dateUtils.formatDate(prevDate, dateFormat); } - const data = { - ...savingAccountTransactionFormData, - dateFormat, - locale - }; + const data = { ...formData, dateFormat, locale }; delete data.note; data['transactionAmount'] = data['transactionAmount'] * 1; + if (this.denominationTotal > 0) { + data['denominationBreakdown'] = [ + ...this.notes, + ...this.coins + ] + .map((d) => ({ denomination: d.value, kind: d.kind, count: this.countFor(d), subtotal: this.subtotalFor(d) })) + .filter((row) => row.count > 0); + } this.fixedDepositsService .executeFixedDepositsAccountTransactionsCommand(this.accountId, this.transactionCommand, data) - .subscribe((res) => { + .subscribe(() => { this.router.navigate(['../../transactions'], { relativeTo: this.route }); }); } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.html index b10752412..7f167a47d 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.html @@ -6,110 +6,334 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- - @if (transactionType.withdrawal) { -

- {{ 'labels.menus.' + actionName | translate }} {{ 'labels.heading.Recurring Deposit Transactions' | translate }} -

- } +
+ +
+
+ +
+ +
+
+

+ {{ + transactionType.withdrawal + ? ('labels.heading.Withdraw Money From Recurring Deposit' | translate) + : ('labels.heading.Deposit Money To Recurring Deposit' | translate) + }} +

+

+ {{ + transactionType.deposit + ? 'Count the cash by denomination, then confirm the payment details.' + : 'Disburse cash by denomination and confirm the payment details.' + }} +

+
+ @if (transactionType.withdrawal && clientId) { + + } +
+ Counted Total + ₹{{ denominationTotal | number: '1.2-2' }} + @if (totalPieces > 0) { + {{ totalPieces }} piece(s) + } +
+
-
- -
- - {{ 'labels.inputs.Transaction Date' | translate }} - - - - @if (depositRecurringDepositForm.controls.transactionDate.hasError('required')) { - - {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - +
+
+ +
+ Client + {{ clientName || '—' }} +
+
+
+
+ +
+ Account No + {{ accountNo || '—' }} +
+
+
+
+ +
+ Current Balance + + ₹{{ availableBalance | number: '1.2-2' }} + +
+
+
+
+ +
+ KYC + - + + {{ kycStatus }} + +
+
+
+
- - {{ 'labels.inputs.Payment Type Id' | translate }} - - @for (paymentType of paymentTypes; track paymentType) { - - {{ paymentType.name }} - - } - - @if (depositRecurringDepositForm.controls.paymentTypeId.hasError('required')) { - - {{ 'labels.inputs.Payment Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - + + +
+
+ +

Cash Denomination Breakdown

+ + +
-
- - -
- {{ 'labels.inputs.Show Payment Details' | translate }} -
-
+
+
+ Banknotes + ₹{{ notesTotal | number: '1.2-2' }} +
+
+ @for (d of notes; track d.value) { +
+
+ {{ d.label }} + ₹{{ subtotalFor(d) | number: '1.2-2' }} +
+
+ + + +
+
+ } +
+
+ +
+
+ Coins + ₹{{ coinsTotal | number: '1.2-2' }} +
+
+ @for (d of coins; track d.value) { +
+
+ {{ d.label }} + ₹{{ subtotalFor(d) | number: '1.2-2' }} +
+
+ + + +
+
+ } +
+
+ +
+
+ Banknotes + ₹{{ notesTotal | number: '1.2-2' }} +
+
+ Coins + ₹{{ coinsTotal | number: '1.2-2' }} +
+
+ Grand Total + ₹{{ denominationTotal | number: '1.2-2' }} +
+ +
+
+ + +
+
+ +

Transaction Details

+
+
+ + {{ 'labels.inputs.Transaction Date' | translate }} + + + + @if (depositRecurringDepositForm.controls.transactionDate.hasError('required')) { + + {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + + + @if (denominationTotal > 0 && !denominationMatchesForm) { +
+ + + Counted cash ₹{{ denominationTotal | number: '1.2-2' }} doesn't match. Click + Use ₹{{ denominationTotal | number: '1.2-2' }} to sync.
+ } + @if (denominationMatchesForm) { +
+ + Cash count matches transaction amount. +
+ } - @if (showPaymentDetails) { - - {{ 'labels.inputs.Account' | translate }} # + + {{ 'labels.inputs.Payment Type Id' | translate }} + + @for (paymentType of paymentTypes; track paymentType) { + {{ paymentType.name }} + } + + @if (depositRecurringDepositForm.controls.paymentTypeId.hasError('required')) { + + {{ 'labels.inputs.Payment Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + +
+ + {{ 'labels.inputs.Show Payment Details' | translate }} + +
+ + @if (showPaymentDetails) { +
+ + {{ 'labels.inputs.Account Number' | translate }} - - {{ 'labels.inputs.Cheque' | translate }} # + + {{ 'labels.inputs.Cheque' | translate }} - + {{ 'labels.inputs.Routing Code' | translate }} - - {{ 'labels.inputs.Reciept' | translate }} # + + {{ 'labels.inputs.Receipt Number' | translate }} - - {{ 'labels.inputs.Bank' | translate }} # + + {{ 'labels.inputs.Bank' | translate }} - } +
+ } - - {{ 'labels.inputs.Note' | translate }} - - + + {{ 'labels.inputs.Note' | translate }} + + + +
+ +
- - - - - - - - +
+
+
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.scss index d286d171f..d7810f153 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.scss @@ -6,19 +6,754 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.container { - max-width: 37rem; +$navy: #0d1b2a; +$slate: #475569; +$border: #e2e8f0; +$soft: #f8fafc; +$green: #059669; +$green-d: #047857; +$amber: #d97706; +$amber-d: #b45309; + +.sat { + width: 100%; + padding: 10px 16px 18px; + display: flex; + flex-direction: column; + gap: 10px; } -.expandcollapsebutton { - margin-top: -7px; - margin-left: 2%; +.sat__hero { + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px 16px 12px; + border-radius: 12px; + color: #fff; + background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 45%, #1e40af 100%); + box-shadow: 0 6px 18px -10px rgb(15 23 42 / 50%); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at top right, rgb(255 255 255 / 12%), transparent 55%); + pointer-events: none; + } } -.right-input { +.sat--withdrawal .sat__hero { + background: linear-gradient(135deg, #134e4a 0%, #115e59 45%, #0f766e 100%); + box-shadow: 0 6px 18px -10px rgb(15 118 110 / 50%); +} + +.sat__hero-top { + display: flex; + align-items: center; + gap: 12px; + position: relative; + z-index: 1; +} + +.sat__hero-back { + height: 30px !important; + border-radius: 8px !important; + padding: 0 11px !important; + font-weight: 700 !important; + font-size: 12px !important; + background: rgb(255 255 255 / 14%) !important; + color: #fff !important; + border: 1px solid rgb(255 255 255 / 28%) !important; + flex-shrink: 0; + + &:hover { + background: rgb(255 255 255 / 26%) !important; + } +} + +.sat__hero-ico { + width: 36px; + height: 36px; + border-radius: 9px; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 28%); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 15px; + flex-shrink: 0; +} + +.sat__hero-title-wrap { + flex: 1 1 auto; + min-width: 0; +} + +.sat__title { + margin: 0; + font-size: 14.5px; + font-weight: 800; + letter-spacing: 0.01em; + line-height: 1.15; +} + +.sat__sub { + margin: 1px 0 0; + font-size: 11.5px; + color: rgb(255 255 255 / 80%); +} + +.sat__hero-verify { + position: relative; + height: 30px !important; + border-radius: 8px !important; + padding: 0 12px !important; + font-weight: 700 !important; + font-size: 12px !important; + color: #fff !important; + background: rgb(255 255 255 / 16%) !important; + border: 1px solid rgb(255 255 255 / 30%) !important; + flex-shrink: 0; + overflow: visible !important; + + &::before { + content: ''; + position: absolute; + inset: -2px; + border-radius: 10px; + border: 2px solid rgb(255 255 255 / 70%); + opacity: 0; + pointer-events: none; + will-change: transform, opacity; + } + + &--pulse::before { + animation: sat-verify-pulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite; + } +} + +@keyframes sat-verify-pulse { + 0% { + transform: scale(1); + opacity: 0.85; + } + + 70% { + transform: scale(1.18); + opacity: 0; + } + + 100% { + transform: scale(1.18); + opacity: 0; + } +} + +.sat__hero-meta { + display: inline-flex; + flex-direction: column; + align-items: flex-end; + padding: 5px 12px; + border-radius: 8px; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 30%); + min-width: 130px; + flex-shrink: 0; +} + +.sat__hero-meta-label { + font-size: 9.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: rgb(255 255 255 / 78%); +} + +.sat__hero-meta-value { + font-size: 15px; + font-weight: 800; + color: #fff; + font-variant-numeric: tabular-nums; +} + +.sat__hero-meta-sub { + font-size: 10px; + color: rgb(255 255 255 / 75%); +} + +.sat__ctx { + display: flex; + align-items: stretch; + flex-wrap: wrap; + padding: 5px 8px; + border-radius: 8px; + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 22%); + position: relative; + z-index: 1; +} + +.sat__ctx-item { + display: flex; + align-items: center; + gap: 6px; + flex: 1 1 110px; + padding: 2px 8px; + min-width: 0; +} + +.sat__ctx-ico { + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 6px; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 25%); + color: #fff; + font-size: 10px; + flex-shrink: 0; +} + +.sat__ctx-text { + display: flex; + flex-direction: column; + min-width: 0; +} + +.sat__ctx-label { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: rgb(255 255 255 / 70%); + font-weight: 700; +} + +.sat__ctx-value { + font-size: 11.5px; + font-weight: 700; + color: #fff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &--mono { + font-family: 'Courier New', monospace; + font-size: 10.5px; + } + + &--amount { + font-variant-numeric: tabular-nums; + font-size: 12px; + } + + &--lg { + font-size: 13px; + } +} + +.sat__ctx-item--balance { + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 22%); + border-radius: 7px; + padding: 3px 8px; +} + +.sat__ctx-divider { + width: 1px; + background: rgb(255 255 255 / 18%); + margin: 4px 0; + flex-shrink: 0; + + @media (width <= 720px) { + display: none; + } +} + +.sat__ctx-badge { + display: inline-flex; + align-items: center; + gap: 5px; + height: 22px; + padding: 0 9px; + border-radius: 999px; + font-size: 11.5px; + font-weight: 800; + text-transform: uppercase; + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 30%); + color: #fff; + align-self: flex-start; + width: fit-content; + + fa-icon { + font-size: 10px; + } + + &--ok { + background: rgb(16 185 129 / 36%); + border-color: rgb(110 231 183 / 60%); + color: #d1fae5; + } + + &--warn { + background: rgb(245 158 11 / 36%); + border-color: rgb(252 211 77 / 60%); + color: #fef3c7; + } + + &--danger { + background: rgb(239 68 68 / 36%); + border-color: rgb(252 165 165 / 60%); + color: #fee2e2; + } +} + +.sat__form { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); + gap: 14px; + align-items: start; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.sat__panel { + background: #fff; + border: 1px solid $border; + border-radius: 14px; + box-shadow: 0 1px 3px rgb(15 23 42 / 4%); + overflow: hidden; +} + +.sat__panel-head { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + background: $soft; + border-bottom: 1px solid $border; +} + +.sat__panel-ico { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 7px; + background: linear-gradient(135deg, $green 0%, $green-d 100%); + color: #fff; + font-size: 11px; +} + +.sat--withdrawal .sat__panel-ico { + background: linear-gradient(135deg, $amber 0%, $amber-d 100%); +} + +.sat__panel-title { + margin: 0; + font-size: 12.5px; + font-weight: 700; + color: $navy; +} + +.sat__panel-spacer { + flex: 1; +} + +.sat__panel-action { + height: 26px !important; + border-radius: 7px !important; + font-size: 11.5px !important; + font-weight: 600 !important; + padding: 0 10px !important; +} + +.sat__panel-body { + padding: 12px 14px 14px; + display: flex; + flex-direction: column; + gap: 3px; +} + +.sat__denom-group { + padding: 8px 14px 4px; + + & + & { + border-top: 1px solid #f1f5f9; + } +} + +.sat__denom-group-head { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 5px; +} + +.sat__denom-group-title { + font-size: 10px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.07em; + color: $slate; +} + +.sat__denom-group-total { + font-size: 12px; + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; +} + +.sat__denom-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 6px; +} + +.sat__denom { + display: flex; + flex-direction: column; + gap: 5px; + padding: 6px 9px; + border-radius: 8px; + background: #fff; + border: 1px solid $border; + transition: + border-color 0.15s ease, + background 0.15s ease; + min-width: 0; + + &--active { + border-color: #86efac; + background: #f0fdf4; + box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%); + } +} + +.sat__denom-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + min-width: 0; +} + +.sat__denom-chip { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 48px; + height: 22px; + padding: 0 9px; + border-radius: 5px; + font-size: 11.5px; + font-weight: 800; + flex-shrink: 0; + + &--note { + background: linear-gradient(135deg, #ecfeff 0%, #cffafe 100%); + color: #155e75; + border: 1px solid #a5f3fc; + } + + &--coin { + background: linear-gradient(135deg, #fef9c3 0%, #fde68a 100%); + color: #854d0e; + border: 1px solid #fcd34d; + } +} + +.sat__denom-amount { + font-size: 11.5px; + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; } -.right-label { - padding-right: 25px !important; +.sat__denom-stepper { + display: flex; + align-items: stretch; + height: 28px; + border-radius: 6px; + border: 1px solid $border; + overflow: hidden; +} + +.sat__denom-count { + flex: 1 1 auto; + width: 100%; + min-width: 0; + border: none; + outline: none; + background: transparent; + text-align: center; + font-size: 12.5px; + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; + padding: 0 4px; + appearance: textfield; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + appearance: none; + margin: 0; + } +} + +.sat__denom-step { + flex: 0 0 auto; + width: 28px; + background: $soft; + border: none; + border-right: 1px solid $border; + font-size: 15px; + font-weight: 700; + color: $slate; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + + &:last-child { + border-right: none; + border-left: 1px solid $border; + } + + &:hover:not(:disabled) { + background: #e2e8f0; + color: $navy; + } + + &:disabled { + opacity: 0.35; + cursor: not-allowed; + } +} + +.sat__tally { + margin: 8px 14px 14px; + padding: 10px 12px; + border-radius: 10px; + background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%); + border: 1px solid $border; +} + +.sat__tally-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 3px 0; + font-size: 12px; + color: $slate; + + &--grand { + margin-top: 4px; + padding-top: 8px; + border-top: 1px dashed #cbd5e1; + + .sat__tally-label { + font-weight: 800; + color: $navy; + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 11px; + } + + .sat__tally-value { + font-size: 15.5px; + font-weight: 800; + color: $green-d; + } + } +} + +.sat--withdrawal .sat__tally-row--grand .sat__tally-value { + color: $amber-d; +} + +.sat__tally-value { + font-weight: 700; + color: $navy; + font-variant-numeric: tabular-nums; +} + +.sat__apply { + width: 100%; + margin-top: 8px; + height: 34px !important; + border-radius: 8px !important; + background: linear-gradient(135deg, $green 0%, $green-d 100%) !important; + color: #fff !important; + font-weight: 700 !important; + font-size: 12.5px !important; + box-shadow: 0 3px 10px -2px rgb(4 120 87 / 35%) !important; + + &:disabled { + opacity: 0.5; + filter: grayscale(0.4); + box-shadow: none !important; + } +} + +.sat--withdrawal .sat__apply { + background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important; +} + +.sat__field { + width: 100%; + + ::ng-deep .mat-mdc-text-field-wrapper { + background: #fff; + border-radius: 10px; + } + + ::ng-deep .mat-mdc-form-field-infix { + min-height: 48px; + padding-top: 12px !important; + padding-bottom: 12px !important; + } + + ::ng-deep .mdc-notched-outline__leading, + ::ng-deep .mdc-notched-outline__notch, + ::ng-deep .mdc-notched-outline__trailing { + border-color: #cbd5e1 !important; + } +} + +.sat__sub-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px 12px; + + .sat__field--wide { + grid-column: 1 / -1; + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.sat__toggle-row { + display: flex; + align-items: center; + padding: 6px 4px 10px; + cursor: pointer; + user-select: none; + + .sat__toggle-label { + margin-left: 8px; + font-size: 13px; + font-weight: 600; + color: $slate; + } +} + +.sat__mismatch, +.sat__match { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 10px; + font-size: 12.5px; + margin: 2px 0 8px; +} + +.sat__mismatch { + background: #fffbeb; + border: 1px solid #fde68a; + color: #92400e; + + &-ico { + color: $amber; + flex-shrink: 0; + } + + strong { + font-weight: 800; + } +} + +.sat__match { + background: #ecfdf5; + border: 1px solid #a7f3d0; + color: #065f46; + + &-ico { + color: $green; + flex-shrink: 0; + } +} + +.sat__actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 10px; + padding-top: 12px; + border-top: 1px solid #f1f5f9; + + button { + height: 42px !important; + border-radius: 10px !important; + font-weight: 700 !important; + padding: 0 22px !important; + } +} + +.sat__cancel { + color: $slate !important; + border-color: $border !important; +} + +.sat__submit { + background: linear-gradient(135deg, $green 0%, $green-d 100%) !important; + color: #fff !important; + box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important; + + &:disabled { + opacity: 0.55; + background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important; + box-shadow: none !important; + } +} + +.sat--withdrawal .sat__submit { + background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important; + box-shadow: 0 4px 12px -2px rgb(180 83 9 / 35%) !important; +} + +.m-r-6 { + margin-right: 6px; +} + +@media (width <= 720px) { + .sat__hero-top { + flex-wrap: wrap; + } + + .sat__hero-meta { + width: 100%; + } + + .sat__ctx-item { + flex: 1 1 calc(50% - 14px); + } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.ts index 6c69d8628..3afdc3cd1 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.ts @@ -19,6 +19,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router'; /** Custom Services */ import { RecurringDepositsService } from '../../recurring-deposits.service'; +import { ClientsService } from 'app/clients/clients.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; import { Currency } from 'app/shared/models/general.model'; @@ -26,8 +27,18 @@ import { TransactionCommand, TransactionTypeFlags } from '../../../transaction.m import { InputAmountComponent } from '../../../../shared/input-amount/input-amount.component'; import { MatSlideToggle } from '@angular/material/slide-toggle'; import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatDialog } from '@angular/material/dialog'; +import { TransactionConfirmDialogComponent } from '../../../../savings/saving-account-actions/savings-account-transactions/transaction-confirm-dialog/transaction-confirm-dialog.component'; +import { IdentityVerifyDialogComponent } from '../../../../savings/saving-account-actions/savings-account-transactions/identity-verify-dialog/identity-verify-dialog.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +interface Denomination { + value: number; + kind: 'note' | 'coin'; + label: string; +} + /** * Deposits Recurring Deposits Account Component */ @@ -39,7 +50,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ...STANDALONE_SHARED_IMPORTS, InputAmountComponent, MatSlideToggle, - CdkTextareaAutosize + CdkTextareaAutosize, + FaIconComponent ] }) export class DepositRecurringDepositsAccountComponent implements OnInit { @@ -48,25 +60,19 @@ export class DepositRecurringDepositsAccountComponent implements OnInit { private router = inject(Router); private dateUtils = inject(Dates); private recurringDepositsService = inject(RecurringDepositsService); + private clientsService = inject(ClientsService); private settingsService = inject(SettingsService); + private dialog = inject(MatDialog); @Input() currency: Currency; - /** Transactions Amount */ transactionAmount: any; - /** Outstanding Charge Amount */ outstandingChargeAmount: any; - /** Payment Types */ paymentTypes: any; - /** Account Id */ accountId: string; - /** Show payment details */ showPaymentDetails = false; - /** Minimum Date allowed. */ minDate = new Date(2000, 0, 1); - /** Maximum Date allowed. */ maxDate = new Date(); - /** Deposits Recurring Deposit Account form. */ depositRecurringDepositForm: UntypedFormGroup; action: string; @@ -74,14 +80,32 @@ export class DepositRecurringDepositsAccountComponent implements OnInit { transactionCommand: TransactionCommand; transactionType: TransactionTypeFlags = { deposit: false, withdrawal: false }; - /** - * Retrieves action details transactions template data from `resolve` - * @param {FormBuilder} formBuilder Form Builder - * @param {ActivatedRoute} route Activated Route - * @param {Router} router Router - * @param {Dates} dateUtils Date Utils - * @param {SettingsService} settingsService Settings Service - */ + readonly notes: Denomination[] = [ + { value: 500, kind: 'note', label: '₹500' }, + { value: 200, kind: 'note', label: '₹200' }, + { value: 100, kind: 'note', label: '₹100' }, + { value: 50, kind: 'note', label: '₹50' }, + { value: 20, kind: 'note', label: '₹20' }, + { value: 10, kind: 'note', label: '₹10' } + ]; + + readonly coins: Denomination[] = [ + { value: 20, kind: 'coin', label: '₹20' }, + { value: 10, kind: 'coin', label: '₹10' }, + { value: 5, kind: 'coin', label: '₹5' }, + { value: 2, kind: 'coin', label: '₹2' }, + { value: 1, kind: 'coin', label: 'Re 1' } + ]; + + denominationCounts: Record = {}; + + accountNo: string | null = null; + clientName: string | null = null; + clientId: number | null = null; + kycStatus: 'Verified' | 'Partial' | 'Pending' = 'Pending'; + availableBalance = 0; + identityVerified = false; + constructor() { this.route.data.subscribe((data: { recurringDepositsAccountActionData: any }) => { this.transactionAmount = data.recurringDepositsAccountActionData.amount; @@ -102,18 +126,43 @@ export class DepositRecurringDepositsAccountComponent implements OnInit { } else { throw new Error(`Invalid transaction action: ${this.actionName}`); } - this.accountId = this.route.parent.snapshot.params['recurringDepositAccountId']; } ngOnInit() { this.maxDate = this.settingsService.businessDate; this.createdepositRecurringDepositForm(); + this.loadContextStrip(); + } + + private loadContextStrip(): void { + if (!this.accountId) return; + this.recurringDepositsService.getRecurringDepositsAccountData(this.accountId).subscribe({ + next: (account: any) => { + this.accountNo = account?.accountNo ?? null; + this.clientName = account?.clientName ?? account?.groupName ?? null; + this.clientId = account?.clientId ?? null; + if (!this.currency && account?.currency) this.currency = account.currency; + this.availableBalance = Number(account?.summary?.accountBalance ?? account?.summary?.availableBalance ?? 0); + if (account?.clientId) { + this.fetchClientKyc(String(account.clientId)); + } + }, + error: () => {} + }); + } + + private fetchClientKyc(clientId: string): void { + this.clientsService.getClientKyc(clientId).subscribe({ + next: (kyc: any) => { + const pan = !!kyc?.panVerified; + const aadhaar = !!kyc?.aadhaarVerified; + this.kycStatus = pan && aadhaar ? 'Verified' : pan || aadhaar ? 'Partial' : 'Pending'; + }, + error: () => {} + }); } - /** - * Creates the deposits form. - */ createdepositRecurringDepositForm() { this.depositRecurringDepositForm = this.formBuilder.group({ transactionDate: [ @@ -132,9 +181,71 @@ export class DepositRecurringDepositsAccountComponent implements OnInit { }); } - /** - * Add payment detail fields to the UI. - */ + // ── Denomination helpers ──────────────────────────────────────────── + denomKey(d: Denomination): string { + return `${d.kind}:${d.value}`; + } + + countFor(d: Denomination): number { + return this.denominationCounts[this.denomKey(d)] || 0; + } + + subtotalFor(d: Denomination): number { + return this.countFor(d) * d.value; + } + + onDenomChange(d: Denomination, raw: any): void { + const n = Math.max(0, Math.floor(Number(raw) || 0)); + this.denominationCounts[this.denomKey(d)] = n; + } + + bumpDenom(d: Denomination, delta: number): void { + const next = Math.max(0, this.countFor(d) + delta); + this.denominationCounts[this.denomKey(d)] = next; + } + + get notesTotal(): number { + return this.notes.reduce((s, d) => s + this.subtotalFor(d), 0); + } + + get coinsTotal(): number { + return this.coins.reduce((s, d) => s + this.subtotalFor(d), 0); + } + + get denominationTotal(): number { + return this.notesTotal + this.coinsTotal; + } + + get totalPieces(): number { + return Object.values(this.denominationCounts).reduce((s, n) => s + (n || 0), 0); + } + + get denominationMatchesForm(): boolean { + const formAmt = Number(this.depositRecurringDepositForm?.controls?.['transactionAmount']?.value || 0); + return this.denominationTotal > 0 && formAmt === this.denominationTotal; + } + + applyDenominationTotal(): void { + this.depositRecurringDepositForm.controls['transactionAmount'].setValue(this.denominationTotal); + this.depositRecurringDepositForm.controls['transactionAmount'].markAsDirty(); + } + + clearDenominations(): void { + this.denominationCounts = {}; + } + + openIdentityVerify(): void { + if (!this.clientId) return; + this.identityVerified = true; + this.dialog.open(IdentityVerifyDialogComponent, { + width: '500px', + maxWidth: '92vw', + panelClass: 'idv-panel', + autoFocus: false, + data: { clientId: this.clientId, clientName: this.clientName, accountNo: this.accountNo } + }); + } + addPaymentDetails() { this.showPaymentDetails = !this.showPaymentDetails; if (this.showPaymentDetails) { @@ -152,32 +263,51 @@ export class DepositRecurringDepositsAccountComponent implements OnInit { } } - /** - * Toggles the display of payment details - */ - toggleDisplay() { - this.showPaymentDetails = !this.showPaymentDetails; + submit() { + const formAmount = Number(this.depositRecurringDepositForm.value.transactionAmount) || 0; + const paymentTypeId = this.depositRecurringDepositForm.value.paymentTypeId; + const paymentTypeLabel = this.paymentTypes?.find((p: any) => p.id === paymentTypeId)?.name ?? null; + + const ref = this.dialog.open(TransactionConfirmDialogComponent, { + width: '480px', + maxWidth: '92vw', + panelClass: 'tcd-panel', + autoFocus: false, + data: { + kind: this.transactionType.deposit ? 'deposit' : 'withdrawal', + clientName: this.clientName, + accountNo: this.accountNo, + currentBalance: this.availableBalance, + transactionAmount: formAmount, + paymentTypeLabel + } + }); + ref.afterClosed().subscribe((result: any) => { + if (result?.confirm) { + this.postTransaction(); + } + }); } - /** - * Submits the deposits recurring deposit form - */ - submit() { - const depositRecurringDepositFormData = this.depositRecurringDepositForm.value; + private postTransaction(): void { + const formData = this.depositRecurringDepositForm.value; const dateFormat = this.settingsService.dateFormat; const locale = this.settingsService.language.code; - const prevTransactionDate = this.depositRecurringDepositForm.value.transactionDate; - if (depositRecurringDepositFormData.transactionDate instanceof Date) { - depositRecurringDepositFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat); + const prevDate = this.depositRecurringDepositForm.value.transactionDate; + if (formData.transactionDate instanceof Date) { + formData.transactionDate = this.dateUtils.formatDate(prevDate, dateFormat); } - const data = { - ...depositRecurringDepositFormData, - dateFormat, - locale - }; - + const data = { ...formData, dateFormat, locale }; delete data['note']; data['transactionAmount'] = data['transactionAmount'] * 1; + if (this.denominationTotal > 0) { + data['denominationBreakdown'] = [ + ...this.notes, + ...this.coins + ] + .map((d) => ({ denomination: d.value, kind: d.kind, count: this.countFor(d), subtotal: this.subtotalFor(d) })) + .filter((row) => row.count > 0); + } this.recurringDepositsService .executeRecurringDepositsAccountCommand(this.accountId, this.action, data) .subscribe(() => {