diff --git a/src/app/cashier/cashier-routing.module.ts b/src/app/cashier/cashier-routing.module.ts index caec5844d..5dfdcc3a8 100644 --- a/src/app/cashier/cashier-routing.module.ts +++ b/src/app/cashier/cashier-routing.module.ts @@ -10,6 +10,7 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { Route } from '../core/route/route.service'; +import { CashierOnlyGuard } from '../core/authentication/cashier-only.guard'; import { CashExchangeEntryComponent } from './cash-exchange-entry/cash-exchange-entry.component'; import { CurrencyExchangeDetailComponent } from './currency-exchange-detail/currency-exchange-detail.component'; import { CashTransferComponent } from './cash-transfer/cash-transfer.component'; @@ -24,6 +25,46 @@ import { ShareCollectionComponent } from './share-collection/share-collection.co const routes: Routes = [ Route.withShell([ + { + path: 'clerk', + data: { title: 'Clerk', breadcrumb: 'Clerk' }, + children: [ + { + path: 'dashboard', + loadComponent: () => + import('../clerk/clerk-dashboard/clerk-dashboard.component').then((m) => m.ClerkDashboardComponent), + data: { title: 'Clerk Dashboard', breadcrumb: 'Clerk Dashboard' } + }, + { + path: 'account-facilities', + loadComponent: () => + import('../clerk/account-facilities/account-facilities.component').then( + (m) => m.AccountFacilitiesComponent + ), + data: { title: 'Account Facilities', breadcrumb: 'Account Facilities' } + }, + { + path: 'photo-signature', + loadComponent: () => + import('../clerk/photo-signature/photo-signature.component').then((m) => m.PhotoSignatureComponent), + data: { title: 'Photo & Signature', breadcrumb: 'Photo & Signature' } + }, + { + path: 'standing-instructions', + loadComponent: () => + import('../clerk/standing-instructions/standing-instructions.component').then( + (m) => m.StandingInstructionsComponent + ), + data: { title: 'Standing Instructions', breadcrumb: 'Standing Instructions' } + }, + { + path: 'today-activity', + loadComponent: () => + import('../clerk/today-activity/today-activity.component').then((m) => m.TodayActivityComponent), + data: { title: "Today's Activity", breadcrumb: "Today's Activity" } + } + ] + }, { path: 'cashier', data: { title: 'Cashier', breadcrumb: 'Cashier' }, @@ -85,6 +126,7 @@ const routes: Routes = [ }, { path: 'transaction-entry', + canActivate: [CashierOnlyGuard], loadComponent: () => import('./transaction-entry/transaction-entry.component').then((m) => m.TransactionEntryComponent), data: { diff --git a/src/app/cashier/clerk-ui-state.service.ts b/src/app/cashier/clerk-ui-state.service.ts new file mode 100644 index 000000000..04ef1fdda --- /dev/null +++ b/src/app/cashier/clerk-ui-state.service.ts @@ -0,0 +1,63 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +export interface ClerkSavedAccount { + accountDetails: any; + clientDetails: any; + kycDetails: any; + nominees: any[]; + jointHolders: any[]; + clientAddresses: any[]; + nonPersonDetails: any; + clientPhotoUrl: string | null; + signatureUrl: string | null; + accountNotes: any[]; + standingInstructions: any[]; + todayTransactions: any[]; + linkedAccounts: any; + sameDayNeftRtgs: number; + sameDayChequeClearing: number; + selectedRow: any; +} + +export interface ClerkUiState { + accountLoaded: boolean; + facilities: { label: string; active: boolean }[]; + savedRow: { acNo: string; acTyp: string } | null; + savedAccount: ClerkSavedAccount | null; +} + +@Injectable({ providedIn: 'root' }) +export class ClerkUiStateService { + private readonly _state = new BehaviorSubject({ + accountLoaded: false, + facilities: [], + savedRow: null, + savedAccount: null + }); + readonly state$ = this._state.asObservable(); + + get snapshot(): ClerkUiState { + return this._state.value; + } + + setFacilities(facilities: { label: string; active: boolean }[], acNo: string, acTyp: string): void { + this._state.next({ ...this._state.value, accountLoaded: true, facilities, savedRow: { acNo, acTyp } }); + } + + saveFullState(account: ClerkSavedAccount): void { + this._state.next({ ...this._state.value, savedAccount: account }); + } + + clear(): void { + this._state.next({ accountLoaded: false, facilities: [], savedRow: null, savedAccount: null }); + } +} diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.html b/src/app/cashier/transaction-entry/transaction-entry.component.html index 3341aff51..8cb3d09b3 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.html +++ b/src/app/cashier/transaction-entry/transaction-entry.component.html @@ -252,7 +252,32 @@ @if (loadingAccount) { -
Loading account details…
+
+
+ + + + + + + + + + +
+
+ Loading Account + Fetching account details, please wait… +
} @if (accountError && !loadingAccount) {
{{ accountError }}
@@ -261,209 +286,122 @@ @if (accountDetails && !loadingAccount) {
- -
- -
-
- - - - - Personal Information -
-
-
- Branch{{ officeName }} -
-
- A/c No.{{ accountDetails.accountNo }} -
-
- Name{{ clientName }} -
-
- S/D/W/H/O{{ guardianName }} -
-
- Address{{ clientAddress }} -
-
- Email{{ clientEmail }} -
-
-
- Last Txn{{ lastTransactionDate }} -
-
- Type{{ accountDetails.depositType?.value ?? selectedRow?.acTyp ?? '—' }} -
-
-
-
- Opening Dt{{ openingDate }} -
-
- Status{{ accountDetails.status?.value ?? '—' }} -
-
-
- CIF No.{{ cifNumber }} -
- @if (isNonIndividual) { -
- Constitution - {{ - nonPersonDetails?.constitution?.name ?? nonPersonDetails?.constitutionType?.name ?? '—' - }} -
-
- Incorp. No. - {{ - nonPersonDetails?.incorpNo ?? nonPersonDetails?.incorporationNo ?? '—' - }} -
-
- Main Business - {{ - nonPersonDetails?.mainBusinessLine?.name ?? nonPersonDetails?.mainBusinessLineType?.name ?? '—' - }} -
- } -
-
- - -
-
- - - - - - - Joint A/c Holders - @if (accountDetails?.modeOfOperation) { - {{ modeOfOperationLabel }} - } -
-
- @if (jointHolders.length === 0) { - No joint holders - } - @for (jh of jointHolders; track jh.id ?? $index; let i = $index) { -
- {{ jh.displayName }} - - {{ jh.role ?? (jh.isPrimary || jh.primary ? 'Primary' : 'Co-Holder') }} - - @if (jh.sharePercentage) { - {{ jh.sharePercentage }}% - } - @if (jh.signingAuthority) { - - {{ jh.signingAuthority }} - - } - - - - -
- } - - - @if (withdrawalRule) { -
- {{ withdrawalRule.icon }} - {{ withdrawalRule.msg }} -
- } -
- - - @if (!isNonIndividual) { -
+ + @if (isClerk) { + +
+ +
+
- + + - Nominees + Personal Information
- @if (nominees.length === 0) { - No nominees - } - @for (n of nominees; track n.id ?? $index) { -
- {{ n.nomineeDisplayName ?? '—' }} - {{ n.relationshipName ?? '—' }} - @if (n.percentageShare) { - {{ n.percentageShare }}% - } - @if (n.nomineeIsMinor) { - Minor - } +
+ Branch{{ officeName }} +
+
+ A/c No.{{ accountDetails.accountNo }} +
+
+ Name{{ clientName }} +
+
+ S/D/W/H/O{{ guardianName }} +
+
+ Address{{ clientAddress }} +
+
+ Email{{ clientEmail }} +
+
+
+ Last Txn{{ lastTransactionDate }} +
+
+ Type{{ accountDetails.depositType?.value ?? selectedRow?.acTyp ?? '—' }} +
+
+
+
+ Opening Dt{{ openingDate }} +
+
+ Status{{ accountDetails.status?.value ?? '—' }} +
+
+
+ CIF No.{{ cifNumber }} +
+ @if (isNonIndividual) { +
+ Constitution + {{ + nonPersonDetails?.constitution?.name ?? nonPersonDetails?.constitutionType?.name ?? '—' + }} +
+
+ Incorp. No. + {{ + nonPersonDetails?.incorpNo ?? nonPersonDetails?.incorporationNo ?? '—' + }} +
+
+ Main Business + {{ + nonPersonDetails?.mainBusinessLine?.name ?? nonPersonDetails?.mainBusinessLineType?.name ?? '—' + }}
}
- } +
- @if (isNonIndividual) { -
+ +
+
- - + + + + - Non-Individual Account Details + Joint A/c Holders @if (accountDetails?.modeOfOperation) { {{ modeOfOperationLabel }} }
- @if (!accountDetails?.partners?.length) { - No partners / members linked + @if (jointHolders.length === 0) { + No joint holders } - @for (p of accountDetails?.partners ?? []; track p.id; let i = $index) { + @for (jh of jointHolders; track jh.id ?? $index; let i = $index) {
- {{ p.clientName ?? '—' }} - {{ p.clientCif ?? '' }} - @if (p.sharePercentage) { - {{ p.sharePercentage }}% + {{ jh.displayName }} + + {{ jh.role ?? (jh.isPrimary || jh.primary ? 'Primary' : 'Co-Holder') }} + + @if (jh.sharePercentage) { + {{ jh.sharePercentage }}% } - @if (p.isMandatory) { - Mandatory - } @else { - Optional + @if (jh.signingAuthority) { + + {{ jh.signingAuthority }} + } @@ -471,7 +409,9 @@
} - @if (withdrawalRule && isNonIndividual) { + + + @if (withdrawalRule) {
}
- } -
- -
-
- - - - - - - Other Information -
-
-
- Phone No.{{ clientPhone }} -
-
- Client ID{{ accountDetails.clientId }} -
-
- PAN{{ panNumber }} -
-
- KYC Status - {{ - kycStatus - }} -
-
- CKYC No.{{ ckycNumber }} -
-
- Aadhaar{{ aadhaarNumber }} -
-
- Product - {{ - accountDetails.savingsProductName || - accountDetails.depositProductName || - accountDetails.productName || - '—' - }} -
+ + @if (!isNonIndividual) { +
+ + + + Nominees +
+
+ @if (nominees.length === 0) { + No nominees + } + @for (n of nominees; track n.id ?? $index) { +
+ {{ n.nomineeDisplayName ?? '—' }} + {{ n.relationshipName ?? '—' }} + @if (n.percentageShare) { + {{ n.percentageShare }}% + } + @if (n.nomineeIsMinor) { + Minor + } +
+ } +
+ } + + @if (isNonIndividual) { +
+ + + + + Non-Individual Account Details + @if (accountDetails?.modeOfOperation) { + {{ modeOfOperationLabel }} + } +
+
+ @if (!accountDetails?.partners?.length) { + No partners / members linked + } + @for (p of accountDetails?.partners ?? []; track p.id; let i = $index) { +
+ {{ p.clientName ?? '—' }} + {{ p.clientCif ?? '' }} + @if (p.sharePercentage) { + {{ p.sharePercentage }}% + } + @if (p.isMandatory) { + Mandatory + } @else { + Optional + } + + + + +
+ } + @if (withdrawalRule && isNonIndividual) { +
+ {{ withdrawalRule.icon }} + {{ withdrawalRule.msg }} +
+ } +
+ }
-
- - - - - Account Information -
-
-
- 1.Current Balance - {{ currencySymbol }}{{ currentBalance | number: '1.2-2' }} -
-
- 2.Today's Clg. Amount - {{ currencySymbol }}{{ todayClgAmount | number: '1.2-2' }} -
-
- 3.Clear Balance (2-3) - {{ currencySymbol }}{{ clearBalance | number: '1.2-2' }} -
-
- 4.Freeze Amount - {{ currencySymbol }}{{ freezeAmount | number: '1.2-2' }} -
-
- 5.Withdrawable Amt. - {{ currencySymbol }}{{ withdrawableAmount | number: '1.2-2' }} -
-
-
-
- - - @if (accountAlerts.length > 0) { -
- @for (alert of accountAlerts; track $index) { -
+ +
+
- @if (alert.type === 'danger') { - - - - } - @if (alert.type === 'warning') { - - - - } - @if (alert.type === 'info') { - - - - } + + + + - {{ alert.message }} + Other Information +
+
+
+ Phone No.{{ clientPhone }} +
+
+ Client ID{{ accountDetails.clientId }} +
+
+ PAN{{ panNumber }} +
+
+ KYC Status + {{ + kycStatus + }} +
+
+ CKYC No.{{ ckycNumber }} +
+
+ Aadhaar{{ aadhaarNumber }} +
+
+ Product + {{ + accountDetails.savingsProductName || + accountDetails.depositProductName || + accountDetails.productName || + '—' + }} +
+
+ +
+ + + + + Account Information +
+
+
+ 1.Current Balance + {{ currencySymbol }}{{ currentBalance | number: '1.2-2' }} +
+
+ 2.Today's Clg. Amount + {{ currencySymbol }}{{ todayClgAmount | number: '1.2-2' }} +
+
+ 3.Clear Balance (2-3) + {{ currencySymbol }}{{ clearBalance | number: '1.2-2' }} +
+
+ 4.Freeze Amount + {{ currencySymbol }}{{ freezeAmount | number: '1.2-2' }} +
+
+ 5.Withdrawable Amt. + {{ currencySymbol }}{{ withdrawableAmount | number: '1.2-2' }} +
+
+
+
+ + + @if (accountAlerts.length > 0) { +
+ @for (alert of accountAlerts; track $index) { +
+ + @if (alert.type === 'danger') { + + + + } + @if (alert.type === 'warning') { + + + + } + @if (alert.type === 'info') { + + + + } + + {{ alert.message }} +
+ } +
+ } + + +
+
+ Net Balance + + {{ currencySymbol }}{{ clearBalance | number: '1.2-2' }} CR + +
+
+ Opening + {{ currencySymbol }}{{ currentBalance | number: '1.2-2' }} +
+
+ Cash Hold + 0.00 +
+
+ Transfer Hold + 0.00 +
+
+ Clearing Hold + 0.00 +
+
+ After Trans. + {{ currencySymbol }}{{ clearBalance | number: '1.2-2' }} +
+
+ Rate + {{ accountDetails.nominalAnnualInterestRate ?? 0 | number: '1.2-2' }}% +
+
+ Currency + {{ accountDetails.currency?.code ?? 'INR' }} +
+
+ } @else { + +
+ +
+ @if (clientPhotoUrl) { + Photo + } @else { +
+ + + + +
+ } +
+ {{ clientDetails?.displayName ?? accountDetails?.clientName ?? '—' }} + {{ accountDetails?.accountNo }} +
+
+
+ +
+ {{ + accountDetails?.subStatus?.value === 'Dormant' ? 'Dormant' : (accountDetails?.status?.value ?? '—') + }} + A/c + {{ + accountDetails?.depositType?.value || + (selectedRow?.acTyp === 'CA' + ? 'Current' + : selectedRow?.acTyp === 'SD' + ? 'Savings' + : selectedRow?.acTyp === 'FD' + ? 'Fixed Dep.' + : selectedRow?.acTyp === 'RD' + ? 'Recurring' + : '—') + }} + Branch {{ officeName ?? '—' }} + Product + {{ + accountDetails?.savingsProductName || + accountDetails?.depositProductName || + accountDetails?.productName || + '—' + }} + Rate {{ accountDetails?.nominalAnnualInterestRate ?? 0 | number: '1.2-2' }}% + CCY {{ accountDetails?.currency?.code ?? 'INR' }} +
+
+ +
+
+ Net Bal + {{ currencySymbol }}{{ clearBalance | number: '1.2-2' }} CR +
+
+ Opening + {{ currencySymbol }}{{ currentBalance | number: '1.2-2' }} +
+
+ Cash Hold + 0.00 +
+
+ Tr. Hold + 0.00 +
+
+ Clr. Hold + 0.00 +
+
+ After Trans. + {{ currencySymbol }}{{ clearBalance | number: '1.2-2' }} +
+
+ + @if (accountAlerts.length > 0) { +
+
+ @for (alert of accountAlerts; track alert.message) { + + + + + + + {{ alert.message }} + + }
}
} - -
- -
-
- - - - - - Client Photo -
-
- @if (clientPhotoUrl) { - Client Photo - } @else { -
- - - - - No Photo -
- } -
-
- - -
-
- - - - - Signature -
-
- @if (signatureUrl) { - Signature - } @else { -
- - - - No Signature -
- } -
-
- - -
-
- - - - - Account Facilities -
-
-
- @for (f of facilities; track f.label) { - - {{ f.label }} - - } -
- - @if (accountDetails?.withHoldTax) { -
- - - - - - - TDS: {{ accountDetails.taxGroupData?.name ?? 'Applicable' }} -
- } - - @if (accountDetails?.subStatus?.value && accountDetails.subStatus.value !== 'None') { -
- - - - - - {{ accountDetails.subStatus.value }} -
- } -
-
- - -
-
- - - - - - - Today's Activity - @if (todayTransactions.length > 0) { - {{ todayTransactions.length }} txn - } -
- -
-
- - - + + @if (!isClerk) { +
+ +
+
+ + + -
- Today's NEFT/RTGS - - ₹{{ sameDayNeftRtgs | number: '1.2-2' }} - -
+ Opening Position — {{ today }}
-
- - - +
+ @if (!openingLoaded) { + Loading… + } + @if (openingLoaded && openingDenoms.length === 0) { + No opening position found + } + @if (openingLoaded && openingDenoms.length > 0) { + + + + + + + + + + @for (d of sortedOpeningDenoms; track d.denominationId) { + @if (d.count > 0) { + + + + + + } + } + + + + + + + + +
DenominationOpening CountAmount
+ ₹{{ d.denominationValue }} + {{ d.count }}₹{{ d.count * d.denominationValue | number: '1.2-2' }}
Total + ₹{{ openingTotal | number: '1.2-2' }} +
+ } +
+
+ + +
+
+ + + -
- Cheque In Clearing - - ₹{{ sameDayChequeClearing | number: '1.2-2' }} - -
-
-
-
- @if (todayTransactions.length === 0) { - No transactions today - } -
- @for (t of todayTransactions; track t.id) { -
- - {{ t.transactionType?.value ?? '—' }} - - - {{ t.transactionType?.deposit ? '+' : '−' }}₹{{ t.amount | number: '1.2-2' }} - -
+ Denomination Entry + @if (denominationTotal > 0) { + ₹{{ denominationTotal | number: '1.2-2' }} + } + @if (isCashPayment && denominationTotal <= 0) { + ⚠ Required for Cash }
-
-
-
- - -
- -
-
- - - - - - - Linked Accounts -
-
- @if (!linkedAccounts) { - Loading… - } - @if (linkedAccounts) { - @if ((linkedAccounts.savingsAccounts?.length ?? 0) > 0) { -
Savings
- @for (a of linkedAccounts.savingsAccounts; track a.id) { -
- {{ a.accountNo }} - {{ a.productName }} - {{ - a.status?.value - }} -
- } - } - @if ((linkedAccounts.fixedDepositAccounts?.length ?? 0) > 0) { -
Fixed Deposits
- @for (a of linkedAccounts.fixedDepositAccounts; track a.id) { -
- {{ a.accountNo }} - {{ a.productName }} - {{ a.status?.value }} -
- } - } - @if ((linkedAccounts.recurringDepositAccounts?.length ?? 0) > 0) { -
Recurring Deposits
- @for (a of linkedAccounts.recurringDepositAccounts; track a.id) { -
- {{ a.accountNo }} - {{ a.productName }} - {{ a.status?.value }} -
- } - } - @if ((linkedAccounts.loanAccounts?.length ?? 0) > 0) { -
Loans
- @for (a of linkedAccounts.loanAccounts; track a.id) { -
- {{ a.accountNo }} - {{ a.productName }} - {{ a.status?.value }} -
- } - } - @if ( - (linkedAccounts.savingsAccounts?.length ?? 0) === 0 && - (linkedAccounts.fixedDepositAccounts?.length ?? 0) === 0 && - (linkedAccounts.recurringDepositAccounts?.length ?? 0) === 0 && - (linkedAccounts.loanAccounts?.length ?? 0) === 0 - ) { - No linked accounts - } - } -
-
- - -
-
- - - - - - - Standing Instructions - @if (standingInstructions.length > 0) { - {{ standingInstructions.length }} - } -
-
- @if (standingInstructions.length === 0) { - No standing instructions - } - @for (si of standingInstructions; track si.id) { -
-
{{ si.name }}
-
- ₹{{ si.amount | number: '1.2-2' }} - {{ - si.status?.value - }} -
-
- } -
-
-
- - -
-
- Net Balance - - {{ currencySymbol }}{{ clearBalance | number: '1.2-2' }} CR - -
-
- A/c Type - {{ - accountDetails.depositType?.value || - (selectedRow?.acTyp === 'CA' - ? 'Current Account' - : selectedRow?.acTyp === 'SD' - ? 'Savings Deposit' - : selectedRow?.acTyp === 'FD' - ? 'Fixed Deposit' - : selectedRow?.acTyp === 'RD' - ? 'Recurring Deposit' - : selectedRow?.acTyp) || - '—' - }} -
-
- Interest Rate - {{ accountDetails.nominalAnnualInterestRate ?? 0 | number: '1.2-2' }}% -
-
- Currency - {{ accountDetails.currency?.code ?? 'INR' }} -
-
- - -
-
- - - - - Basic Information -
-
-
- Opening Balance{{ currencySymbol }}{{ currentBalance | number: '1.2-2' }} -
-
- Cash Hold0.00 -
-
- Transfer Hold0.00 -
-
- Clearing Hold0.00 -
-
- Bal. After This Trans. - {{ currencySymbol }}{{ clearBalance | number: '1.2-2' }} -
-
-
- - -
- -
-
- - - - - Opening Position — {{ today }} -
-
- @if (!openingLoaded) { - Loading… - } - @if (openingLoaded && openingDenoms.length === 0) { - No opening position found - } - @if (openingLoaded && openingDenoms.length > 0) { +
- + - @for (d of sortedOpeningDenoms; track d.denominationId) { - @if (d.count > 0) { - - - - - - } + @for (d of allDenominations; track d.id) { + + + + + } @@ -990,183 +898,126 @@
DenominationOpening CountCount Amount
- ₹{{ d.denominationValue }} - {{ d.count }}₹{{ d.count * d.denominationValue | number: '1.2-2' }}
+ + {{ d.label }} + + +
+ + + +
+
+ {{ subtotalFor(d) > 0 ? (subtotalFor(d) | number: '1.2-2') : '—' }} +
Total - ₹{{ openingTotal | number: '1.2-2' }} + ₹{{ denominationTotal | number: '1.2-2' }}
- } -
-
- -
-
- - - - - Denomination Entry - @if (denominationTotal > 0) { - ₹{{ denominationTotal | number: '1.2-2' }} - } - @if (isCashPayment && denominationTotal <= 0) { - ⚠ Required for Cash - } -
-
- - - - - - - - - - @for (d of allDenominations; track d.id) { - - - - - - } - - - - - - - - -
DenominationCountAmount
- - {{ d.label }} - - -
- - - -
-
- {{ subtotalFor(d) > 0 ? (subtotalFor(d) | number: '1.2-2') : '—' }} -
Total - ₹{{ denominationTotal | number: '1.2-2' }} -
- - @if (denominationTotal > 0) { -
- - -
- } -
-
- - -
-
- - - - Closing Position (Preview) - @if (selectedRow?.dbCr === 'Dr' && hasWithdrawalDenomOverdraft) { - ⚠ Stock Overdraft - } -
-
- @if (selectedRow?.dbCr === 'Dr' && hasWithdrawalDenomOverdraft) { -
-
- - - - - - Insufficient Denomination Stock — Cannot Proceed + @if (denominationTotal > 0) { +
+ +
- + } + + + + +
+
+ + + + Closing Position (Preview) + @if (selectedRow?.dbCr === 'Dr' && hasWithdrawalDenomOverdraft) { + ⚠ Stock Overdraft + } +
+
+ @if (selectedRow?.dbCr === 'Dr' && hasWithdrawalDenomOverdraft) { +
+
+ + + + + + Insufficient Denomination Stock — Cannot Proceed +
+
+ + + + + + + + + + @for (o of denominationOverdrafts; track o.label) { + + + + + + + } + +
DenominationRequestedAvailableShort
{{ o.label }}{{ o.requested }}{{ o.available }} + −{{ o.shortfall }} +
+
Adjust denomination or request vault replenishment.
+
+ } + @if (closingDenoms.length === 0) { + Enter denomination to see closing position + } + @if (closingDenoms.length > 0) { + - - - + + + - @for (o of denominationOverdrafts; track o.label) { - - - - - + + + + } -
DenominationRequestedAvailableShortOpening{{ selectedRow?.dbCr === 'Cr' ? '+Deposit' : '-Withdrawal' }}Closing
{{ o.label }}{{ o.requested }}{{ o.available }} - −{{ o.shortfall }} + @for (d of sortedClosingDenoms; track d.denominationValue + d.kind) { +
+ ₹{{ d.denominationValue }} + {{ d.openCount }} + {{ selectedRow?.dbCr === 'Cr' ? '+' : '-' }}{{ d.entered }} + + {{ d.closingCount }}
-
Adjust denomination or request vault replenishment.
-
- } - @if (closingDenoms.length === 0) { - Enter denomination to see closing position - } - @if (closingDenoms.length > 0) { - - - - - - - - - - - @for (d of sortedClosingDenoms; track d.denominationValue + d.kind) { - - - + + + + - - } - - - - - - - - - -
DenominationOpening{{ selectedRow?.dbCr === 'Cr' ? '+Deposit' : '-Withdrawal' }}Closing
- ₹{{ d.denominationValue }} - {{ d.openCount }}
Total₹{{ openingTotal | number: '1.2-2' }} - {{ selectedRow?.dbCr === 'Cr' ? '+' : '-' }}{{ d.entered }} + {{ selectedRow?.dbCr === 'Cr' ? '+' : '-' }}₹{{ denominationTotal | number: '1.2-2' }} - {{ d.closingCount }} + + ₹{{ closingTotal | number: '1.2-2' }}
Total₹{{ openingTotal | number: '1.2-2' }} - {{ selectedRow?.dbCr === 'Cr' ? '+' : '-' }}₹{{ denominationTotal | number: '1.2-2' }} - - ₹{{ closingTotal | number: '1.2-2' }} -
- } + + + } +
-
+ }
diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.scss b/src/app/cashier/transaction-entry/transaction-entry.component.scss index 9aa39c7d5..90b526356 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.scss +++ b/src/app/cashier/transaction-entry/transaction-entry.component.scss @@ -8,10 +8,9 @@ :host { display: block; - min-height: 100vh; background: #f0f2f5; font-family: Inter, Roboto, sans-serif; - font-size: 13px; + font-size: 12px; } // ── Root wrapper ────────────────────────────────────────────────────────────── @@ -19,8 +18,8 @@ .te-root { display: flex; flex-direction: column; - gap: 10px; - padding: 10px 14px; + gap: 6px; + padding: 6px 10px; } // ── Day-status blocking banners ─────────────────────────────────────────────── @@ -60,6 +59,223 @@ } } +// ── Cashier compact account info strip ─────────────────────────────────────── + +.te-acs { + display: flex; + align-items: center; + background: #fff; + border: 1px solid #dce4ef; + border-radius: 8px; + overflow: hidden; + min-height: 44px; +} + +.te-acs-id { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + flex-shrink: 0; +} + +.te-acs-photo { + width: 30px; + height: 30px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; + border: 2px solid #dce4ef; + + &--ph { + background: #f0f4f9; + display: flex; + align-items: center; + justify-content: center; + } + + &--clickable { + cursor: zoom-in; + transition: + border-color 0.15s, + box-shadow 0.15s; + + &:hover { + border-color: #2563eb; + box-shadow: 0 0 0 3px rgb(37 99 235 / 18%); + } + } +} + +.te-acs-nameblock { + display: flex; + flex-direction: column; + gap: 1px; +} + +.te-acs-name { + font-size: 12px; + font-weight: 700; + color: #0d1b2e; + white-space: nowrap; +} + +.te-acs-acno { + font-size: 10px; + font-family: 'Roboto Mono', monospace; + color: #5a7089; + letter-spacing: 0.4px; +} + +.te-acs-sep { + width: 1px; + align-self: stretch; + background: #dce4ef; + flex-shrink: 0; +} + +.te-acs-meta { + display: flex; + align-items: center; + gap: 6px; + padding: 0 10px; + flex-shrink: 0; +} + +.te-acs-tag { + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 7px; + border-radius: 10px; + font-size: 10px; + font-weight: 700; + background: #f0fdf4; + color: #16a34a; + border: 1px solid #bbf7d0; + + &--blocked { + background: #fef2f2; + color: #dc2626; + border-color: #fecaca; + } + + &--dormant { + background: #fffbeb; + color: #d97706; + border-color: #fde68a; + } +} + +.te-acs-kv { + font-size: 11px; + color: #4a607a; + white-space: nowrap; + + b { + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.4px; + color: #8aa0b8; + margin-right: 2px; + } +} + +.te-acs-bals { + display: flex; + align-items: stretch; + flex: 1; +} + +.te-acs-b { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 5px 10px; + border-right: 1px solid #f0f4f9; + gap: 1px; + + &:last-child { + border-right: none; + } + + &--main { + background: #f8fbff; + } + + &--after { + background: #f0fdf6; + } +} + +.te-acs-b-l { + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #8aa0b8; + white-space: nowrap; +} + +.te-acs-b-v { + font-size: 11.5px; + font-weight: 700; + color: #1a2a3e; + font-variant-numeric: tabular-nums; + white-space: nowrap; + + &--cr { + color: #059669; + } +} + +.te-acs-alerts { + display: flex; + align-items: center; + gap: 4px; + padding: 0 10px; + flex-shrink: 0; +} + +.te-acs-alert { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 10px; + font-weight: 600; + border-radius: 4px; + padding: 3px 7px; + + &--danger { + background: #fef2f2; + color: #991b1b; + + svg { + stroke: #dc2626; + } + } + + &--warning { + background: #fffbeb; + color: #92400e; + + svg { + stroke: #d97706; + } + } + + &--info { + background: #eff6ff; + color: #1e40af; + + svg { + stroke: #3b82f6; + } + } +} + // ── CBS Blue Transaction Entry ──────────────────────────────────────────────── $ws-ink: #0d1b2e; // nav bar dark navy @@ -80,11 +296,11 @@ $ws-fld-bd: #c8d8eb; .ws-wrap { background: $ws-canvas; - border-radius: 12px; + border-radius: 10px; overflow: hidden; box-shadow: - 0 1px 3px rgb(13 27 46 / 8%), - 0 8px 24px -10px rgb(13 27 46 / 18%); + 0 1px 3px rgb(13 27 46 / 6%), + 0 4px 14px -8px rgb(13 27 46 / 14%); border: 1px solid $ws-border; font-family: 'Hanken Grotesk', Inter, sans-serif; } @@ -94,27 +310,27 @@ $ws-fld-bd: #c8d8eb; display: flex; align-items: center; justify-content: space-between; - padding: 0 18px; - height: 48px; + padding: 0 12px; + height: 32px; background: linear-gradient(90deg, #0d1b2e 0%, #162542 100%); border-bottom: 2px solid $ws-accent; &__left { display: flex; align-items: center; - gap: 10px; + gap: 8px; } &__right { display: flex; align-items: center; - gap: 14px; + gap: 10px; } &__icon { - width: 28px; - height: 28px; - border-radius: 7px; + width: 22px; + height: 22px; + border-radius: 6px; background: $ws-accent; display: flex; align-items: center; @@ -122,12 +338,14 @@ $ws-fld-bd: #c8d8eb; svg { stroke: #fff; + width: 12px; + height: 12px; } } &__title { font-family: Newsreader, Georgia, serif; - font-size: 16px; + font-size: 14px; font-weight: 500; color: #e8f0fb; letter-spacing: 0.01em; @@ -136,8 +354,8 @@ $ws-fld-bd: #c8d8eb; &__effdt { display: flex; align-items: center; - gap: 7px; - font-size: 11.5px; + gap: 6px; + font-size: 11px; color: #7a95b0; svg { @@ -146,14 +364,14 @@ $ws-fld-bd: #c8d8eb; } &__date { - height: 28px; - padding: 0 10px; + height: 24px; + padding: 0 8px; border: 1px solid #2a4060; - border-radius: 6px; + border-radius: 5px; background: #0a1628; color: #e8f0fb; font-family: 'Hanken Grotesk', monospace; - font-size: 12px; + font-size: 11px; font-weight: 600; outline: none; @@ -166,9 +384,9 @@ $ws-fld-bd: #c8d8eb; .ws-btn { display: inline-flex; align-items: center; - gap: 5px; - padding: 7px 15px; - border-radius: 8px; + gap: 4px; + padding: 5px 12px; + border-radius: 7px; border: none; font-family: 'Hanken Grotesk', sans-serif; font-size: 12px; @@ -243,7 +461,7 @@ $ws-fld-bd: #c8d8eb; background: $ws-accent; color: #fff; font-weight: 700; - padding: 8px 20px; + padding: 6px 16px; box-shadow: 0 2px 8px rgb(232 119 34 / 30%); &:hover:not(:disabled) { @@ -259,20 +477,20 @@ $ws-fld-bd: #c8d8eb; // ── Row cards ──────────────────────────────────────────────────────────────── .ws-rows { - padding: 12px 14px; + padding: 8px 10px; display: flex; flex-direction: column; - gap: 10px; + gap: 6px; } .ws-card { display: flex; align-items: flex-start; - gap: 10px; + gap: 6px; background: $ws-card; border: 1px solid $ws-border; - border-radius: 12px; - padding: 12px 14px 12px 10px; + border-radius: 10px; + padding: 6px 8px; cursor: pointer; transition: border-color 0.15s, @@ -293,17 +511,17 @@ $ws-fld-bd: #c8d8eb; &__badge { flex-shrink: 0; - width: 26px; - height: 26px; + width: 22px; + height: 22px; border-radius: 50%; background: $ws-ink; color: #c8daf0; - font-size: 11px; + font-size: 10px; font-weight: 700; display: flex; align-items: center; justify-content: center; - margin-top: 22px; + margin-top: 16px; } &__body { @@ -354,7 +572,7 @@ $ws-fld-bd: #c8d8eb; gap: 0; &--mt { - margin-top: 10px; + margin-top: 6px; } } @@ -372,7 +590,7 @@ $ws-fld-bd: #c8d8eb; &__fields { display: flex; align-items: flex-end; - gap: 8px; + gap: 6px; flex-wrap: nowrap; height: 100%; align-content: flex-end; @@ -390,7 +608,7 @@ $ws-fld-bd: #c8d8eb; .ws-vdivider { width: 1px; background: $ws-divider; - margin: 18px 10px 0; + margin: 12px 8px 0; flex-shrink: 0; } @@ -471,14 +689,14 @@ $ws-fld-bd: #c8d8eb; } &__ctl { - height: 34px; - padding: 0 9px; + height: 26px; + padding: 0 7px; border: 1px solid $ws-fld-bd; - border-radius: 8px; + border-radius: 6px; background: $ws-fld-bg; color: $ws-text; font-family: 'Hanken Grotesk', sans-serif; - font-size: 13px; + font-size: 11.5px; outline: none; width: 100%; transition: @@ -523,9 +741,9 @@ select.ws-f__ctl { display: flex; align-items: center; border: 1px solid $ws-fld-bd; - border-radius: 8px; + border-radius: 7px; background: $ws-fld-bg; - height: 34px; + height: 30px; transition: border-color 0.15s, box-shadow 0.15s; @@ -553,8 +771,8 @@ select.ws-f__ctl { } .ws-amt-prefix { - padding: 0 6px 0 9px; - font-size: 13px; + padding: 0 5px 0 8px; + font-size: 12px; color: $ws-muted; font-weight: 600; flex-shrink: 0; @@ -566,10 +784,10 @@ select.ws-f__ctl { border: none; background: transparent; font-family: 'Hanken Grotesk', monospace; - font-size: 13px; + font-size: 12px; font-weight: 600; text-align: right; - padding-right: 9px; + padding-right: 8px; color: $ws-text; outline: none; font-variant-numeric: tabular-nums; @@ -578,8 +796,8 @@ select.ws-f__ctl { // ── Segmented DR/CR toggle ─────────────────────────────────────────────────── .ws-seg { display: flex; - height: 34px; - border-radius: 8px; + height: 30px; + border-radius: 7px; overflow: hidden; border: 1px solid $ws-fld-bd; @@ -624,7 +842,7 @@ select.ws-f__ctl { display: flex; align-items: center; justify-content: space-between; - padding: 10px 18px; + padding: 7px 14px; background: linear-gradient(180deg, #f4f7fc, #edf2f9); border-top: 1px solid $ws-divider; @@ -903,24 +1121,50 @@ select.te-cell { // ── Status messages ────────────────────────────────────────────────────────── .te-loading { + position: fixed; + inset: 0; display: flex; + flex-direction: column; align-items: center; - gap: 10px; - padding: 14px 16px; - background: #fff; - border: 1px solid #e2e8f0; - border-radius: 10px; - color: #64748b; - font-size: 13px; + justify-content: center; + gap: 14px; + background: rgb(241 245 249 / 88%); + backdrop-filter: blur(6px); + z-index: 999; - &::before { - content: ''; - width: 16px; - height: 16px; - border: 2px solid #e2e8f0; - border-top-color: #3b82f6; + &__spinner { + position: relative; + width: 56px; + height: 56px; + + svg { + animation: te-spin 1s linear infinite; + } + } + + &__arc { + transform-origin: 28px 28px; + } + + &__pulse { + position: absolute; + inset: 6px; border-radius: 50%; - animation: te-spin 0.7s linear infinite; + background: radial-gradient(circle, rgb(37 99 235 / 12%), transparent 70%); + animation: te-pulse 1.4s ease-in-out infinite; + } + + &__title { + font-size: 15px; + font-weight: 700; + color: #1e293b; + letter-spacing: -0.01em; + } + + &__sub { + font-size: 12.5px; + color: #94a3b8; + margin-top: -6px; } } @@ -930,6 +1174,19 @@ select.te-cell { } } +@keyframes te-pulse { + 0%, + 100% { + opacity: 0.4; + transform: scale(0.9); + } + + 50% { + opacity: 1; + transform: scale(1.1); + } +} + .te-error { padding: 10px 16px; background: #fef2f2; @@ -945,13 +1202,13 @@ select.te-cell { .te-detail { display: flex; flex-direction: column; - gap: 10px; + gap: 5px; } .te-detail-top { display: grid; grid-template-columns: 1.1fr 0.9fr 1.1fr; - gap: 10px; + gap: 7px; @media (width <= 1100px) { grid-template-columns: 1fr 1fr; @@ -974,11 +1231,11 @@ select.te-cell { &__header { display: flex; align-items: center; - gap: 8px; - padding: 9px 14px; + gap: 5px; + padding: 4px 8px; background: linear-gradient(135deg, #1e293b, #334155); color: #e2e8f0; - font-size: 11px; + font-size: 9.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; @@ -991,13 +1248,13 @@ select.te-cell { } &__body { - padding: 10px 14px; + padding: 4px 8px; &--center { display: flex; align-items: center; justify-content: center; - min-height: 60px; + min-height: 32px; } } } @@ -1007,11 +1264,11 @@ select.te-cell { .te-field { display: flex; align-items: flex-start; - gap: 6px; - margin-bottom: 5px; - font-size: 12px; + gap: 5px; + margin-bottom: 3px; + font-size: 11px; border-bottom: 1px dotted #f1f5f9; - padding-bottom: 4px; + padding-bottom: 3px; &:last-child { border-bottom: none; @@ -1021,7 +1278,7 @@ select.te-cell { .te-field-row { display: flex; - gap: 10px; + gap: 8px; .te-field { flex: 1; @@ -1029,9 +1286,9 @@ select.te-cell { } .te-label { - min-width: 88px; + min-width: 76px; color: #64748b; - font-size: 11px; + font-size: 10.5px; font-weight: 600; white-space: nowrap; padding-top: 1px; @@ -1039,14 +1296,14 @@ select.te-cell { .te-value { color: #1e293b; - font-size: 12px; + font-size: 11px; font-weight: 500; flex: 1; &-name { font-weight: 700; color: #0f172a; - font-size: 13px; + font-size: 12px; } &-wrap { @@ -1069,10 +1326,10 @@ select.te-cell { .te-joint-row { display: flex; align-items: center; - gap: 8px; - padding: 7px 8px; + gap: 6px; + padding: 5px 6px; border-bottom: 1px solid #f1f5f9; - font-size: 12px; + font-size: 11px; border-radius: 6px; margin-bottom: 2px; @@ -1225,9 +1482,9 @@ select.te-cell { display: flex; align-items: center; justify-content: space-between; - padding: 5px 0; + padding: 3px 0; border-bottom: 1px dotted #f1f5f9; - font-size: 12px; + font-size: 11px; &:last-child { border-bottom: none; @@ -1252,7 +1509,7 @@ select.te-cell { .te-bal-label { flex: 1; color: #475569; - font-size: 12px; + font-size: 11px; } .te-bal-value { @@ -1288,8 +1545,8 @@ select.te-cell { .te-bal-item { display: flex; align-items: center; - gap: 6px; - padding: 10px 18px; + gap: 5px; + padding: 7px 14px; border-right: 1px solid #e2e8f0; &:last-child { @@ -1312,7 +1569,7 @@ select.te-cell { &__val { font-weight: 800; - font-size: 13px; + font-size: 12px; color: #1e293b; font-variant-numeric: tabular-nums; letter-spacing: 0.01em; @@ -1322,7 +1579,7 @@ select.te-cell { } &--big { - font-size: 16px; + font-size: 14px; } } } @@ -2246,7 +2503,7 @@ select.te-cell { .te-denom-wrap { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 10px; + gap: 6px; @media (width <= 1100px) { grid-template-columns: 1fr 1fr; @@ -2260,16 +2517,16 @@ select.te-cell { .te-denom-table { width: 100%; border-collapse: collapse; - font-size: 12px; + font-size: 11px; th { background: #f1f5f9; color: #64748b; - font-size: 10px; + font-size: 9.5px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; - padding: 5px 10px; + padding: 3px 8px; border-bottom: 1px solid #e2e8f0; text-align: left; @@ -2279,10 +2536,10 @@ select.te-cell { } td { - padding: 5px 10px; + padding: 2px 8px; border-bottom: 1px solid #f8fafc; color: #1e293b; - font-size: 12px; + font-size: 11px; &.num { text-align: right; @@ -2301,8 +2558,8 @@ select.te-cell { tfoot td { background: #f1f5f9; border-top: 2px solid #e2e8f0; - font-size: 12px; - padding: 6px 10px; + font-size: 11px; + padding: 4px 8px; } } @@ -2349,11 +2606,11 @@ select.te-cell { display: inline-flex; align-items: center; justify-content: center; - min-width: 48px; - height: 24px; - padding: 0 8px; - border-radius: 6px; - font-size: 12px; + min-width: 36px; + height: 20px; + padding: 0 6px; + border-radius: 5px; + font-size: 11px; font-weight: 700; background: linear-gradient(135deg, #ecfeff, #cffafe); color: #155e75; @@ -2361,10 +2618,11 @@ select.te-cell { &--coin { border-radius: 50%; - width: 32px; - height: 32px; - min-width: 32px; + width: 24px; + height: 24px; + min-width: 24px; padding: 0; + font-size: 10px; background: linear-gradient(135deg, #fef9c3, #fde68a); color: #854d0e; border-color: #fcd34d; @@ -2374,16 +2632,16 @@ select.te-cell { .te-denom-stepper { display: inline-flex; align-items: center; - gap: 6px; + gap: 4px; margin-left: auto; button { - width: 28px; - height: 28px; + width: 22px; + height: 22px; border-radius: 50%; border: 1.5px solid #e2e8f0; background: #f8fafc; - font-size: 16px; + font-size: 14px; font-weight: 600; color: #475569; cursor: pointer; @@ -2408,12 +2666,12 @@ select.te-cell { } input { - width: 64px; - height: 32px; + width: 50px; + height: 24px; border: 1.5px solid #e2e8f0; - border-radius: 8px; + border-radius: 6px; text-align: center; - font-size: 14px; + font-size: 11px; font-weight: 700; color: #1e293b; background: #fff; @@ -2441,9 +2699,9 @@ select.te-cell { .te-denom-actions { display: flex; - gap: 8px; - margin-top: 10px; - padding-top: 10px; + gap: 6px; + margin-top: 6px; + padding-top: 6px; border-top: 1px solid #e2e8f0; } @@ -2470,7 +2728,7 @@ select.te-cell { align-items: flex-start; border-right: 1px solid #f1f5f9; border-bottom: none; - padding: 10px 16px; + padding: 6px 12px; &:last-child { border-right: none; @@ -2478,18 +2736,18 @@ select.te-cell { &--total { border-top: none; - padding-top: 10px; + padding-top: 6px; margin-top: 0; } .te-bal-label { - font-size: 11px; + font-size: 10px; color: #94a3b8; - margin-bottom: 4px; + margin-bottom: 2px; } .te-bal-value { - font-size: 15px; + font-size: 13px; min-width: unset; } } diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.ts b/src/app/cashier/transaction-entry/transaction-entry.component.ts index 7b40e5c80..ca8cdba70 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.ts +++ b/src/app/cashier/transaction-entry/transaction-entry.component.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, inject } from '@angular/core'; +import { Component, OnInit, OnDestroy, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; @@ -14,6 +14,8 @@ import { catchError } from 'rxjs/operators'; import { forkJoin, of } from 'rxjs'; import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { CashierService } from '../cashier.service'; +import { AuthenticationService } from '../../core/authentication/authentication.service'; +import { ClerkUiStateService, ClerkSavedAccount } from '../clerk-ui-state.service'; interface TxnRow { typ: string; @@ -621,10 +623,12 @@ export class ImagePreviewDialogComponent { MatDialogModule ] }) -export class TransactionEntryComponent implements OnInit { +export class TransactionEntryComponent implements OnInit, OnDestroy { private http = inject(HttpClient); private cashierService = inject(CashierService); + private authService = inject(AuthenticationService); private dialog = inject(MatDialog); + private clerkUiState = inject(ClerkUiStateService); today = ''; rows: TxnRow[] = []; @@ -748,6 +752,56 @@ export class TransactionEntryComponent implements OnInit { .getPaymentTypes() .pipe(catchError(() => of([]))) .subscribe((types: any[]) => (this.paymentTypes = types ?? [])); + + if (this.isClerk) { + const saved = this.clerkUiState.snapshot.savedAccount; + if (saved) { + this.accountDetails = saved.accountDetails; + this.clientDetails = saved.clientDetails; + this.kycDetails = saved.kycDetails; + this.nominees = saved.nominees; + this.jointHolders = saved.jointHolders; + this.clientAddresses = saved.clientAddresses; + this.nonPersonDetails = saved.nonPersonDetails; + this.clientPhotoUrl = saved.clientPhotoUrl; + this.signatureUrl = saved.signatureUrl; + this.accountNotes = saved.accountNotes; + this.standingInstructions = saved.standingInstructions; + this.todayTransactions = saved.todayTransactions; + this.linkedAccounts = saved.linkedAccounts; + this.sameDayNeftRtgs = saved.sameDayNeftRtgs; + this.sameDayChequeClearing = saved.sameDayChequeClearing; + this.selectedRow = saved.selectedRow; + if (saved.accountDetails) { + const acNo = saved.accountDetails.accountNo ?? ''; + const acTyp = this.clerkUiState.snapshot.savedRow?.acTyp ?? ''; + this.rows[0] = { ...this.rows[0], acNo, acTyp }; + } + } + } + } + + ngOnDestroy(): void { + if (this.isClerk && this.accountDetails) { + this.clerkUiState.saveFullState({ + accountDetails: this.accountDetails, + clientDetails: this.clientDetails, + kycDetails: this.kycDetails, + nominees: this.nominees, + jointHolders: this.jointHolders, + clientAddresses: this.clientAddresses, + nonPersonDetails: this.nonPersonDetails, + clientPhotoUrl: this.clientPhotoUrl, + signatureUrl: this.signatureUrl, + accountNotes: this.accountNotes, + standingInstructions: this.standingInstructions, + todayTransactions: this.todayTransactions, + linkedAccounts: this.linkedAccounts, + sameDayNeftRtgs: this.sameDayNeftRtgs, + sameDayChequeClearing: this.sameDayChequeClearing, + selectedRow: this.selectedRow + }); + } } private loadDenominations(): void { @@ -1010,7 +1064,7 @@ export class TransactionEntryComponent implements OnInit { // returns all accounts as an array (e.g. recurringdepositaccounts) const match = allItems.find((x: any) => x.accountNo === acNo) ?? allItems[0]; if (match?.id) { - this.fetchAllDetails(match.id, acNo, cfg, token); + this.fetchAllDetails(match.id, acNo, cfg, token, acTyp); return; } // Fallback: global search @@ -1021,7 +1075,7 @@ export class TransactionEntryComponent implements OnInit { if (token !== this.searchToken) return; const list: any[] = Array.isArray(found) ? found : []; if (list.length > 0 && list[0].entityId) { - this.fetchAllDetails(list[0].entityId, acNo, cfg, token); + this.fetchAllDetails(list[0].entityId, acNo, cfg, token, acTyp); } else { this.loadingAccount = false; this.accountError = 'Account not found for: ' + acNo; @@ -1107,7 +1161,8 @@ export class TransactionEntryComponent implements OnInit { txnBase: string; txnSearchSupported: boolean; }, - token: number + token: number, + acTyp: string = '' ): void { this.http .get(`${cfg.detail}/${accountId}?associations=all`) @@ -1127,6 +1182,7 @@ export class TransactionEntryComponent implements OnInit { } this.accountDetails = acc; this.jointHolders = acc.jointHolders ?? []; + if (this.isClerk) this.clerkUiState.setFacilities(this.facilities, acNo, acTyp); // Compute same-day compliance indicators from included transactions this.computeSameDayNeftRtgs(acc.transactions); this.computeSameDayChequeClearing(acc.transactions); @@ -1263,11 +1319,6 @@ export class TransactionEntryComponent implements OnInit { if (acc.withHoldTax) alerts.push({ type: 'info', message: `TDS Applicable — Tax Group: ${acc.taxGroupData?.name ?? ''}` }); - // Notes - (this.accountNotes ?? []).forEach((n: any) => { - if (n.note) alerts.push({ type: 'info', message: n.note }); - }); - // Sanctions (from KYC) — DB stores 'YES'/'NO' strings, not booleans const isYes = (v: any) => v === true || v === 'YES' || v === 'yes' || v === 'Y'; if (isYes(this.kycDetails?.bannedEntityMatch)) @@ -1423,6 +1474,10 @@ export class TransactionEntryComponent implements OnInit { if (this.selectedRow && !this.rows.includes(this.selectedRow)) this.selectedRow = null; } + get isClerk(): boolean { + return this.authService.isClerkUser(); + } + get isNonIndividual(): boolean { // legalForm.id=1 → Individual, legalForm.id=2 → Non-Individual (Entity) const lf = this.clientDetails?.legalForm?.id ?? this.clientDetails?.legalFormId; @@ -1996,6 +2051,7 @@ export class TransactionEntryComponent implements OnInit { } onCancel(): void { + this.clerkUiState.clear(); this.accountDetails = null; this.clientDetails = null; this.kycDetails = null; diff --git a/src/app/clerk/account-facilities/account-facilities.component.scss b/src/app/clerk/account-facilities/account-facilities.component.scss new file mode 100644 index 000000000..020d40e68 --- /dev/null +++ b/src/app/clerk/account-facilities/account-facilities.component.scss @@ -0,0 +1,369 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +:host { + display: block; +} + +.af-page { + min-height: calc(100vh - 68px); + background: #f0f4f8; + padding: 24px 28px; + display: flex; + flex-direction: column; + gap: 20px; +} + +// ── Hero ────────────────────────────────────────────────────────────────────── + +.af-hero { + display: flex; + align-items: center; + gap: 18px; + background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 40%, #2563eb 100%); + border-radius: 16px; + padding: 22px 28px; + color: #fff; + box-shadow: 0 8px 32px rgb(37 99 235 / 35%); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: -40px; + right: -40px; + width: 200px; + height: 200px; + background: rgb(255 255 255 / 5%); + border-radius: 50%; + } + + &__icon-wrap { + width: 56px; + height: 56px; + background: rgb(255 255 255 / 15%); + border: 1px solid rgb(255 255 255 / 25%); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + backdrop-filter: blur(8px); + } + + &__text { + flex: 1; + } + + &__title { + font-size: 22px; + font-weight: 800; + letter-spacing: -0.01em; + margin: 0 0 5px; + } + + &__stats { + display: flex; + gap: 14px; + align-items: center; + } + + &__stat { + display: flex; + align-items: center; + gap: 5px; + font-size: 12.5px; + color: rgb(255 255 255 / 75%); + + strong { + color: #fff; + font-weight: 700; + } + + &--green strong { + color: #86efac; + } + + &--red strong { + color: #fca5a5; + } + } + + &__sep { + width: 1px; + height: 14px; + background: rgb(255 255 255 / 25%); + } +} + +.af-back-btn { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 10px 20px; + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 25%); + border-radius: 10px; + color: #fff; + font-size: 12.5px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + backdrop-filter: blur(8px); + + &:hover { + background: rgb(255 255 255 / 22%); + transform: translateX(-2px); + } +} + +// ── Empty state ──────────────────────────────────────────────────────────────── + +.af-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + padding: 60px 20px; + + &__icon { + width: 88px; + height: 88px; + background: #e2e8f0; + border: 1px solid #cbd5e1; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + } + + &__title { + font-size: 18px; + font-weight: 700; + color: #1e293b; + margin: 6px 0 0; + } + + &__sub { + font-size: 13.5px; + color: #64748b; + text-align: center; + margin: 0; + } + + &__btn { + margin-top: 10px; + padding: 10px 24px; + background: #2563eb; + border: none; + border-radius: 10px; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + + &:hover { + background: #1d4ed8; + } + } +} + +// ── Progress card ────────────────────────────────────────────────────────────── + +.af-progress-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 14px; + padding: 18px 22px; + box-shadow: 0 1px 4px rgb(0 0 0 / 6%); + + &__top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + } + + &__label { + font-size: 11.5px; + font-weight: 600; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.08em; + } + + &__fraction { + font-size: 13px; + font-weight: 700; + color: #16a34a; + } +} + +.af-progress-bar { + height: 10px; + background: #e2e8f0; + border-radius: 999px; + overflow: hidden; + + &__fill { + height: 100%; + background: linear-gradient(90deg, #16a34a, #22c55e); + border-radius: 999px; + transition: width 0.5s ease; + box-shadow: 0 0 8px rgb(34 197 94 / 35%); + } +} + +// ── Facilities card ──────────────────────────────────────────────────────────── + +.af-fac-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 14px; + overflow: hidden; + box-shadow: 0 1px 4px rgb(0 0 0 / 6%); + + &__header { + display: flex; + align-items: center; + gap: 9px; + padding: 12px 22px; + background: #1e293b; + color: #94a3b8; + font-size: 11.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + } +} + +.af-pills { + display: flex; + flex-wrap: wrap; + gap: 10px; + padding: 20px 22px; +} + +.af-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 15px; + border-radius: 999px; + font-size: 13px; + font-weight: 600; + border: 1.5px solid; + transition: + transform 0.15s, + box-shadow 0.15s; + cursor: default; + + &__dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; + } + + &--on { + background: #f0fdf4; + border-color: #22c55e; + color: #15803d; + + .af-pill__dot { + background: #22c55e; + box-shadow: 0 0 5px rgb(34 197 94 / 50%); + } + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgb(34 197 94 / 20%); + } + } + + &--off { + background: #f8fafc; + border-color: #e2e8f0; + color: #94a3b8; + + .af-pill__dot { + background: #cbd5e1; + } + } +} + +// ── Stats row ───────────────────────────────────────────────────────────────── + +.af-stats-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} + +.af-stat-card { + border-radius: 14px; + padding: 20px 24px; + display: flex; + align-items: center; + gap: 16px; + border: 1px solid; + box-shadow: 0 1px 4px rgb(0 0 0 / 6%); + + &__icon { + width: 44px; + height: 44px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 20px; + } + + &__num { + font-size: 30px; + font-weight: 800; + line-height: 1; + display: block; + } + + &__label { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + display: block; + margin-top: 3px; + opacity: 0.75; + } + + &--green { + background: linear-gradient(135deg, #f0fdf4, #dcfce7); + border-color: #86efac; + color: #15803d; + + .af-stat-card__icon { + background: #bbf7d0; + } + } + + &--red { + background: linear-gradient(135deg, #f8fafc, #f1f5f9); + border-color: #cbd5e1; + color: #475569; + + .af-stat-card__icon { + background: #e2e8f0; + } + } +} diff --git a/src/app/clerk/account-facilities/account-facilities.component.ts b/src/app/clerk/account-facilities/account-facilities.component.ts new file mode 100644 index 000000000..820f32b99 --- /dev/null +++ b/src/app/clerk/account-facilities/account-facilities.component.ts @@ -0,0 +1,141 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service'; + +@Component({ + selector: 'mifosx-account-facilities', + standalone: true, + imports: [CommonModule], + styleUrl: './account-facilities.component.scss', + template: ` +
+ +
+
+ + + + +
+
+

Account Facilities

+ @if (accountLoaded) { +
+ + {{ activeCount }} Active + + + + {{ facilities.length - activeCount }} Inactive + + + + {{ facilities.length }} Total + +
+ } +
+ +
+ + @if (!accountLoaded) { + +
+
+ + + + +
+

No Account Loaded

+

Search an account on the Clerk Dashboard first.

+ +
+ } @else { + +
+
+ Facility Coverage + {{ activeCount }} / {{ facilities.length }} +
+
+
+
+
+ + +
+
+ + + + + All Facilities +
+
+ @for (f of facilities; track f.label) { + + + {{ f.label }} + + } +
+
+ + +
+
+
✓
+
+ {{ activeCount }} + Active +
+
+
+
✗
+
+ {{ facilities.length - activeCount }} + Inactive +
+
+
+ } +
+ ` +}) +export class AccountFacilitiesComponent implements OnInit { + private clerkUiState = inject(ClerkUiStateService); + private router = inject(Router); + + accountLoaded = false; + facilities: { label: string; active: boolean }[] = []; + + get activeCount(): number { + return this.facilities.filter((f) => f.active).length; + } + + ngOnInit(): void { + this.clerkUiState.state$.subscribe((s) => { + this.accountLoaded = s.accountLoaded; + this.facilities = s.facilities; + }); + } + + goBack(): void { + this.router.navigate(['/clerk/dashboard']); + } +} diff --git a/src/app/clerk/clerk-dashboard/clerk-dashboard.component.ts b/src/app/clerk/clerk-dashboard/clerk-dashboard.component.ts new file mode 100644 index 000000000..bbaf7ff79 --- /dev/null +++ b/src/app/clerk/clerk-dashboard/clerk-dashboard.component.ts @@ -0,0 +1,18 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Component } from '@angular/core'; +import { TransactionEntryComponent } from '../../cashier/transaction-entry/transaction-entry.component'; + +@Component({ + selector: 'mifosx-clerk-dashboard', + standalone: true, + imports: [TransactionEntryComponent], + template: `` +}) +export class ClerkDashboardComponent {} diff --git a/src/app/clerk/photo-signature/photo-signature.component.scss b/src/app/clerk/photo-signature/photo-signature.component.scss new file mode 100644 index 000000000..83ab85195 --- /dev/null +++ b/src/app/clerk/photo-signature/photo-signature.component.scss @@ -0,0 +1,341 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +:host { + display: block; +} + +.ps-page { + min-height: calc(100vh - 68px); + background: #f0f4f8; + padding: 24px 28px; + display: flex; + flex-direction: column; + gap: 20px; +} + +// ── Hero ────────────────────────────────────────────────────────────────────── + +.ps-hero { + display: flex; + align-items: center; + gap: 18px; + background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 40%, #2563eb 100%); + border-radius: 16px; + padding: 22px 28px; + color: #fff; + box-shadow: 0 8px 32px rgb(37 99 235 / 35%); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: -40px; + right: -40px; + width: 200px; + height: 200px; + background: rgb(255 255 255 / 5%); + border-radius: 50%; + pointer-events: none; + } + + &__icon-wrap { + width: 56px; + height: 56px; + background: rgb(255 255 255 / 15%); + border: 1px solid rgb(255 255 255 / 25%); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + backdrop-filter: blur(8px); + } + + &__text { + flex: 1; + } + + &__title { + font-size: 22px; + font-weight: 800; + letter-spacing: -0.01em; + margin: 0 0 6px; + } + + &__meta { + display: flex; + align-items: center; + gap: 10px; + font-size: 12.5px; + color: rgb(255 255 255 / 75%); + } + + &__ac { + font-weight: 700; + color: #bfdbfe; + font-family: monospace; + font-size: 13px; + } + + &__sep { + width: 1px; + height: 13px; + background: rgb(255 255 255 / 30%); + } + + &__name { + color: rgb(255 255 255 / 85%); + } +} + +.ps-back-btn { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 10px 20px; + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 25%); + border-radius: 10px; + color: #fff; + font-size: 12.5px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + backdrop-filter: blur(8px); + + &:hover { + background: rgb(255 255 255 / 22%); + transform: translateX(-2px); + } +} + +// ── Empty state ──────────────────────────────────────────────────────────────── + +.ps-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + padding: 60px 20px; + + &__icon { + width: 88px; + height: 88px; + background: #e2e8f0; + border: 1px solid #cbd5e1; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + } + + &__title { + font-size: 18px; + font-weight: 700; + color: #1e293b; + margin: 6px 0 0; + } + + &__sub { + font-size: 13.5px; + color: #64748b; + text-align: center; + margin: 0; + } + + &__btn { + margin-top: 10px; + padding: 10px 24px; + background: #2563eb; + border: none; + border-radius: 10px; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + + &:hover { + background: #1d4ed8; + } + } +} + +// ── Cards row ───────────────────────────────────────────────────────────────── + +.ps-cards-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + + @media (width <= 680px) { + grid-template-columns: 1fr; + } +} + +.ps-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 2px 8px rgb(0 0 0 / 7%); + + &__header { + display: flex; + align-items: center; + gap: 9px; + padding: 12px 22px; + background: #1e293b; + color: #94a3b8; + font-size: 11.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + } + + &__body { + padding: 28px; + display: flex; + align-items: center; + justify-content: center; + min-height: 280px; + + &--sig { + background: repeating-linear-gradient(-45deg, #f8fafc, #f8fafc 4px, #fff 4px, #fff 12px); + } + } +} + +// ── Image display ────────────────────────────────────────────────────────────── + +.ps-img-wrap { + position: relative; + cursor: zoom-in; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 20px rgb(0 0 0 / 12%); + max-width: 240px; + max-height: 240px; + + &--sig { + max-width: 320px; + max-height: 160px; + background: #fff; + padding: 12px; + border: 1.5px solid #e2e8f0; + } + + &:hover .ps-img__overlay { + opacity: 1; + } +} + +.ps-img { + display: block; + width: 240px; + height: 240px; + object-fit: cover; + + &--sig { + width: 100%; + height: 140px; + object-fit: contain; + } +} + +.ps-img__overlay { + position: absolute; + inset: 0; + background: rgb(15 23 42 / 55%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + color: #fff; + font-size: 12px; + font-weight: 600; + opacity: 0; + transition: opacity 0.2s; + backdrop-filter: blur(2px); +} + +.ps-no-img { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + color: #94a3b8; + font-size: 13px; + font-weight: 500; +} + +// ── Lightbox ────────────────────────────────────────────────────────────────── + +.ps-lightbox { + position: fixed; + inset: 0; + background: rgb(2 6 23 / 82%); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(6px); + + &__inner { + background: #fff; + border-radius: 16px; + overflow: hidden; + max-width: 90vw; + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: 0 24px 80px rgb(0 0 0 / 50%); + } + + &__bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 18px; + background: #1e293b; + color: #94a3b8; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + } + + &__close { + background: none; + border: none; + color: #94a3b8; + font-size: 16px; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + line-height: 1; + transition: color 0.15s; + + &:hover { + color: #fff; + } + } + + &__img { + display: block; + max-width: 90vw; + max-height: calc(90vh - 48px); + object-fit: contain; + } +} diff --git a/src/app/clerk/photo-signature/photo-signature.component.ts b/src/app/clerk/photo-signature/photo-signature.component.ts new file mode 100644 index 000000000..afdba490c --- /dev/null +++ b/src/app/clerk/photo-signature/photo-signature.component.ts @@ -0,0 +1,187 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service'; + +@Component({ + selector: 'mifosx-photo-signature', + standalone: true, + imports: [CommonModule], + styleUrl: './photo-signature.component.scss', + template: ` +
+ +
+
+ + + + +
+
+

Photo & Signature

+ @if (accountLoaded) { +
+ {{ accountNo }} + @if (clientName) { + + {{ clientName }} + } +
+ } +
+ +
+ + @if (!accountLoaded) { + +
+
+ + + + +
+

No Account Loaded

+

Search an account on the Clerk Dashboard first.

+ +
+ } @else { +
+ +
+
+ + + + + + Client Photo +
+
+ @if (photoUrl) { +
+ Client Photo +
+ + + + + + + Click to enlarge +
+
+ } @else { +
+ + + + + No photo on file +
+ } +
+
+ + +
+
+ + + + Signature +
+
+ @if (signatureUrl) { +
+ Signature +
+ + + + + + + Click to enlarge +
+
+ } @else { +
+ + + + No signature on file +
+ } +
+
+
+ } +
+ + + @if (previewSrc) { +
+
+
+ {{ previewLabel }} + +
+ +
+
+ } + ` +}) +export class PhotoSignatureComponent implements OnInit { + private clerkUiState = inject(ClerkUiStateService); + private router = inject(Router); + + accountLoaded = false; + photoUrl: string | null = null; + signatureUrl: string | null = null; + accountNo = ''; + clientName = ''; + + previewSrc: string | null = null; + previewLabel = ''; + + ngOnInit(): void { + const s = this.clerkUiState.snapshot; + this.accountLoaded = s.accountLoaded; + if (s.savedAccount) { + this.photoUrl = s.savedAccount.clientPhotoUrl; + this.signatureUrl = s.savedAccount.signatureUrl; + this.accountNo = s.savedAccount.accountDetails?.accountNo ?? s.savedRow?.acNo ?? ''; + const cd = s.savedAccount.clientDetails; + this.clientName = cd ? `${cd.firstname ?? ''} ${cd.lastname ?? ''}`.trim() : ''; + } + } + + preview(src: string, label: string): void { + this.previewSrc = src; + this.previewLabel = label; + } + + closePreview(): void { + this.previewSrc = null; + } + + goBack(): void { + this.router.navigate(['/clerk/dashboard']); + } +} diff --git a/src/app/clerk/standing-instructions/standing-instructions.component.scss b/src/app/clerk/standing-instructions/standing-instructions.component.scss new file mode 100644 index 000000000..c0d144e70 --- /dev/null +++ b/src/app/clerk/standing-instructions/standing-instructions.component.scss @@ -0,0 +1,373 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +:host { + display: block; +} + +.si-page { + min-height: calc(100vh - 68px); + background: #f0f4f8; + padding: 24px 28px; + display: flex; + flex-direction: column; + gap: 20px; +} + +// ── Hero ────────────────────────────────────────────────────────────────────── + +.si-hero { + display: flex; + align-items: center; + gap: 18px; + background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 40%, #2563eb 100%); + border-radius: 16px; + padding: 22px 28px; + color: #fff; + box-shadow: 0 8px 32px rgb(37 99 235 / 35%); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: -40px; + right: -40px; + width: 200px; + height: 200px; + background: rgb(255 255 255 / 5%); + border-radius: 50%; + pointer-events: none; + } + + &__icon-wrap { + width: 56px; + height: 56px; + background: rgb(255 255 255 / 15%); + border: 1px solid rgb(255 255 255 / 25%); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + backdrop-filter: blur(8px); + } + + &__text { + flex: 1; + } + + &__title { + font-size: 22px; + font-weight: 800; + letter-spacing: -0.01em; + margin: 0 0 6px; + } + + &__meta { + display: flex; + align-items: center; + gap: 10px; + font-size: 12.5px; + color: rgb(255 255 255 / 75%); + flex-wrap: wrap; + } + + &__ac { + font-weight: 700; + color: #bfdbfe; + font-family: monospace; + font-size: 13px; + } + + &__sep { + width: 1px; + height: 13px; + background: rgb(255 255 255 / 30%); + } + + &__name { + color: rgb(255 255 255 / 85%); + } + + &__count { + color: rgb(255 255 255 / 75%); + + strong { + color: #fff; + } + } +} + +.si-back-btn { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 10px 20px; + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 25%); + border-radius: 10px; + color: #fff; + font-size: 12.5px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + backdrop-filter: blur(8px); + + &:hover { + background: rgb(255 255 255 / 22%); + transform: translateX(-2px); + } +} + +// ── Empty state ──────────────────────────────────────────────────────────────── + +.si-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + padding: 60px 20px; + + &__icon { + width: 88px; + height: 88px; + background: #e2e8f0; + border: 1px solid #cbd5e1; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + } + + &__title { + font-size: 18px; + font-weight: 700; + color: #1e293b; + margin: 6px 0 0; + } + + &__sub { + font-size: 13.5px; + color: #64748b; + text-align: center; + margin: 0; + } + + &__btn { + margin-top: 10px; + padding: 10px 24px; + background: #2563eb; + border: none; + border-radius: 10px; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + + &:hover { + background: #1d4ed8; + } + } +} + +// ── Stats row ───────────────────────────────────────────────────────────────── + +.si-stats-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 14px; +} + +.si-stat { + border-radius: 14px; + padding: 18px 22px; + display: flex; + align-items: center; + gap: 14px; + border: 1px solid; + box-shadow: 0 1px 4px rgb(0 0 0 / 6%); + + &__icon { + width: 42px; + height: 42px; + border-radius: 11px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 18px; + font-weight: 700; + } + + &__num { + font-size: 28px; + font-weight: 800; + line-height: 1; + display: block; + } + + &__label { + font-size: 11.5px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.07em; + display: block; + margin-top: 3px; + opacity: 0.7; + } + + &--blue { + background: linear-gradient(135deg, #eff6ff, #dbeafe); + border-color: #93c5fd; + color: #1d4ed8; + + .si-stat__icon { + background: #bfdbfe; + } + } + + &--green { + background: linear-gradient(135deg, #f0fdf4, #dcfce7); + border-color: #86efac; + color: #15803d; + + .si-stat__icon { + background: #bbf7d0; + } + } + + &--grey { + background: linear-gradient(135deg, #f8fafc, #f1f5f9); + border-color: #cbd5e1; + color: #475569; + + .si-stat__icon { + background: #e2e8f0; + } + } +} + +// ── Table card ───────────────────────────────────────────────────────────────── + +.si-table-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 2px 8px rgb(0 0 0 / 7%); + + &__header { + display: flex; + align-items: center; + gap: 9px; + padding: 12px 22px; + background: #1e293b; + color: #94a3b8; + font-size: 11.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + } +} + +.si-table-wrap { + overflow-x: auto; +} + +.si-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + thead tr { + background: #f8fafc; + border-bottom: 2px solid #e2e8f0; + } + + th { + padding: 10px 16px; + text-align: left; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: #64748b; + white-space: nowrap; + } + + td { + padding: 12px 16px; + border-bottom: 1px solid #f1f5f9; + color: #334155; + vertical-align: middle; + } + + tbody tr:last-child td { + border-bottom: none; + } + + tbody tr:hover td { + background: #f8fafc; + } +} + +.si-row { + &--active td { + background: #fafffe; + } +} + +.si-cell--num { + color: #94a3b8; + font-size: 12px; + width: 40px; +} + +.si-cell--name { + font-weight: 600; + color: #1e293b; +} + +.si-cell--amt { + font-weight: 700; + color: #0f172a; + font-family: monospace; +} + +.si-cell--ac { + font-family: monospace; + font-size: 12px; + color: #475569; +} + +.si-badge { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 999px; + font-size: 11.5px; + font-weight: 700; + border: 1px solid; + + &--active { + background: #f0fdf4; + border-color: #22c55e; + color: #15803d; + } + + &--inactive { + background: #f8fafc; + border-color: #e2e8f0; + color: #94a3b8; + } +} diff --git a/src/app/clerk/standing-instructions/standing-instructions.component.ts b/src/app/clerk/standing-instructions/standing-instructions.component.ts new file mode 100644 index 000000000..a346c7a18 --- /dev/null +++ b/src/app/clerk/standing-instructions/standing-instructions.component.ts @@ -0,0 +1,196 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service'; + +@Component({ + selector: 'mifosx-standing-instructions', + standalone: true, + imports: [CommonModule], + styleUrl: './standing-instructions.component.scss', + template: ` +
+ +
+
+ + + + + + +
+
+

Standing Instructions

+ @if (accountLoaded) { +
+ {{ accountNo }} + @if (clientName) { + + {{ clientName }} + } + + + {{ instructions.length }} instruction{{ instructions.length !== 1 ? 's' : '' }} + +
+ } +
+ +
+ + @if (!accountLoaded) { +
+
+ + + + + + +
+

No Account Loaded

+

Search an account on the Clerk Dashboard first.

+ +
+ } @else if (instructions.length === 0) { +
+
+ + + + + + +
+

No Standing Instructions

+

This account has no standing instructions set up.

+
+ } @else { + +
+
+
+ + + + + + +
+
+ {{ instructions.length }} + Total +
+
+
+
✓
+
+ {{ activeCount }} + Active +
+
+
+
—
+
+ {{ instructions.length - activeCount }} + Inactive +
+
+
+ + +
+
+ + + + + + + All Standing Instructions +
+
+ + + + + + + + + + + + + + + @for (si of instructions; track si.id) { + + + + + + + + + + + } + +
#NameAmountFrom AccountTo AccountFrequencyValid TillStatus
{{ $index + 1 }}{{ si.name ?? '—' }}₹{{ si.amount ?? 0 | number: '1.2-2' }}{{ si.fromAccountId ?? '—' }}{{ si.toAccountId ?? '—' }}{{ si.recurrenceType?.value ?? si.instructionType?.value ?? '—' }}{{ si.validTill ?? si.endDate ?? '—' }} + + {{ si.status?.value ?? '—' }} + +
+
+
+ } +
+ ` +}) +export class StandingInstructionsComponent implements OnInit { + private clerkUiState = inject(ClerkUiStateService); + private router = inject(Router); + + accountLoaded = false; + instructions: any[] = []; + accountNo = ''; + clientName = ''; + + get activeCount(): number { + return this.instructions.filter((si) => si.status?.value === 'Active').length; + } + + ngOnInit(): void { + const s = this.clerkUiState.snapshot; + this.accountLoaded = s.accountLoaded; + if (s.savedAccount) { + this.instructions = s.savedAccount.standingInstructions ?? []; + this.accountNo = s.savedAccount.accountDetails?.accountNo ?? s.savedRow?.acNo ?? ''; + const cd = s.savedAccount.clientDetails; + this.clientName = cd ? `${cd.firstname ?? ''} ${cd.lastname ?? ''}`.trim() : ''; + } + } + + goBack(): void { + this.router.navigate(['/clerk/dashboard']); + } +} diff --git a/src/app/clerk/today-activity/today-activity.component.scss b/src/app/clerk/today-activity/today-activity.component.scss new file mode 100644 index 000000000..40ce6cddc --- /dev/null +++ b/src/app/clerk/today-activity/today-activity.component.scss @@ -0,0 +1,395 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +:host { + display: block; +} + +.ta-page { + min-height: calc(100vh - 68px); + background: #f0f4f8; + padding: 24px 28px; + display: flex; + flex-direction: column; + gap: 20px; +} + +// ── Hero ────────────────────────────────────────────────────────────────────── + +.ta-hero { + display: flex; + align-items: center; + gap: 18px; + background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 40%, #2563eb 100%); + border-radius: 16px; + padding: 22px 28px; + color: #fff; + box-shadow: 0 8px 32px rgb(37 99 235 / 35%); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: -40px; + right: -40px; + width: 200px; + height: 200px; + background: rgb(255 255 255 / 5%); + border-radius: 50%; + pointer-events: none; + } + + &__icon-wrap { + width: 56px; + height: 56px; + background: rgb(255 255 255 / 15%); + border: 1px solid rgb(255 255 255 / 25%); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + backdrop-filter: blur(8px); + } + + &__text { + flex: 1; + } + + &__title { + font-size: 22px; + font-weight: 800; + letter-spacing: -0.01em; + margin: 0 0 6px; + } + + &__meta { + display: flex; + align-items: center; + gap: 10px; + font-size: 12.5px; + color: rgb(255 255 255 / 75%); + flex-wrap: wrap; + } + + &__ac { + font-weight: 700; + color: #bfdbfe; + font-family: monospace; + font-size: 13px; + } + + &__sep { + width: 1px; + height: 13px; + background: rgb(255 255 255 / 30%); + } + + &__name { + color: rgb(255 255 255 / 85%); + } + + &__count { + strong { + color: #fff; + } + } +} + +.ta-back-btn { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 10px 20px; + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 25%); + border-radius: 10px; + color: #fff; + font-size: 12.5px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + backdrop-filter: blur(8px); + + &:hover { + background: rgb(255 255 255 / 22%); + transform: translateX(-2px); + } +} + +// ── Empty ────────────────────────────────────────────────────────────────────── + +.ta-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + padding: 60px 20px; + + &__icon { + width: 88px; + height: 88px; + background: #e2e8f0; + border: 1px solid #cbd5e1; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + } + + &__title { + font-size: 18px; + font-weight: 700; + color: #1e293b; + margin: 6px 0 0; + } + + &__sub { + font-size: 13.5px; + color: #64748b; + text-align: center; + margin: 0; + } + + &__btn { + margin-top: 10px; + padding: 10px 24px; + background: #2563eb; + border: none; + border-radius: 10px; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + + &:hover { + background: #1d4ed8; + } + } +} + +// ── AML Compliance row ──────────────────────────────────────────────────────── + +.ta-aml-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} + +.ta-aml-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 14px; + padding: 18px 22px; + display: flex; + align-items: flex-start; + gap: 14px; + box-shadow: 0 1px 4px rgb(0 0 0 / 5%); + transition: border-color 0.2s; + + &--alert { + border-color: #fca5a5; + background: #fff5f5; + } + + &__icon { + width: 40px; + height: 40px; + background: #f1f5f9; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: #475569; + } + + &__body { + display: flex; + flex-direction: column; + gap: 3px; + } + + &__label { + font-size: 11.5px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.07em; + color: #64748b; + } + + &__val { + font-size: 22px; + font-weight: 800; + color: #0f172a; + font-family: monospace; + + &--alert { + color: #dc2626; + } + } + + &__warn { + font-size: 11px; + color: #dc2626; + font-weight: 600; + } +} + +// ── Table card ───────────────────────────────────────────────────────────────── + +.ta-table-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 2px 8px rgb(0 0 0 / 7%); + + &__header { + display: flex; + align-items: center; + gap: 9px; + padding: 12px 22px; + background: #1e293b; + color: #94a3b8; + font-size: 11.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + } +} + +.ta-badge { + margin-left: 6px; + background: #2563eb; + color: #fff; + font-size: 10.5px; + font-weight: 700; + padding: 2px 8px; + border-radius: 999px; +} + +.ta-no-txn { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 40px 20px; + color: #94a3b8; + font-size: 13px; +} + +.ta-table-wrap { + overflow-x: auto; +} + +.ta-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + thead tr { + background: #f8fafc; + border-bottom: 2px solid #e2e8f0; + } + + th { + padding: 10px 16px; + text-align: left; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: #64748b; + white-space: nowrap; + } + + td { + padding: 12px 16px; + border-bottom: 1px solid #f1f5f9; + color: #334155; + vertical-align: middle; + } + + tbody tr:last-child td { + border-bottom: none; + } + + tbody tr:hover td { + background: #f8fafc; + } +} + +.ta-row--cr { + td { + background: rgb(240 253 244 / 50%); + } +} + +.ta-row--dr { + td { + background: rgb(255 245 245 / 50%); + } +} + +.ta-cell--num { + color: #94a3b8; + font-size: 12px; + width: 40px; +} + +.ta-cell--date { + color: #64748b; + font-size: 12px; +} + +.ta-cell--ref { + font-family: monospace; + font-size: 12px; + color: #94a3b8; +} + +.ta-cell--amt { + font-weight: 700; + font-family: monospace; +} + +.ta-cell--cr { + color: #16a34a; +} + +.ta-cell--dr { + color: #dc2626; +} + +.ta-type-badge { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 999px; + font-size: 11.5px; + font-weight: 700; + border: 1px solid; + + &--cr { + background: #f0fdf4; + border-color: #22c55e; + color: #15803d; + } + + &--dr { + background: #fff5f5; + border-color: #fca5a5; + color: #dc2626; + } +} diff --git a/src/app/clerk/today-activity/today-activity.component.ts b/src/app/clerk/today-activity/today-activity.component.ts new file mode 100644 index 000000000..7fdde388e --- /dev/null +++ b/src/app/clerk/today-activity/today-activity.component.ts @@ -0,0 +1,206 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service'; + +@Component({ + selector: 'mifosx-today-activity', + standalone: true, + imports: [CommonModule], + styleUrl: './today-activity.component.scss', + template: ` +
+ +
+
+ + + + + + +
+
+

Today's Activity

+ @if (accountLoaded) { +
+ {{ accountNo }} + @if (clientName) { + + {{ clientName }} + } + @if (transactions.length > 0) { + + {{ transactions.length }} txn today + } +
+ } +
+ +
+ + @if (!accountLoaded) { +
+
+ + + + + + +
+

No Account Loaded

+

Search an account on the Clerk Dashboard first.

+ +
+ } @else { + +
+
+
+ + + + +
+
+ Today's NEFT / RTGS + + ₹{{ neftRtgs | number: '1.2-2' }} + + @if (neftRtgs > 200000) { + Exceeds ₹2,00,000 limit + } +
+
+
+
+ + + + +
+
+ Cheque In Clearing + + ₹{{ chequeClearing | number: '1.2-2' }} + +
+
+
+ + +
+
+ + + + + + + Transactions Today + @if (transactions.length > 0) { + {{ transactions.length }} + } +
+ @if (transactions.length === 0) { +
+ + + + + + + No transactions today +
+ } @else { +
+ + + + + + + + + + + + @for (t of transactions; track t.id) { + + + + + + + + } + +
#TypeAmountDateReference
{{ $index + 1 }} + + {{ t.transactionType?.value ?? '—' }} + + + {{ t.transactionType?.deposit ? '+' : '−' }}₹{{ t.amount ?? 0 | number: '1.2-2' }} + {{ t.date ?? '—' }}{{ t.id ?? '—' }}
+
+ } +
+ } +
+ ` +}) +export class TodayActivityComponent implements OnInit { + private clerkUiState = inject(ClerkUiStateService); + private router = inject(Router); + + accountLoaded = false; + transactions: any[] = []; + neftRtgs = 0; + chequeClearing = 0; + accountNo = ''; + clientName = ''; + + ngOnInit(): void { + const s = this.clerkUiState.snapshot; + this.accountLoaded = s.accountLoaded; + if (s.savedAccount) { + this.transactions = s.savedAccount.todayTransactions ?? []; + this.neftRtgs = s.savedAccount.sameDayNeftRtgs ?? 0; + this.chequeClearing = s.savedAccount.sameDayChequeClearing ?? 0; + this.accountNo = s.savedAccount.accountDetails?.accountNo ?? s.savedRow?.acNo ?? ''; + const cd = s.savedAccount.clientDetails; + this.clientName = cd ? `${cd.firstname ?? ''} ${cd.lastname ?? ''}`.trim() : ''; + } + } + + goBack(): void { + this.router.navigate(['/clerk/dashboard']); + } +} diff --git a/src/app/core/authentication/authentication.service.ts b/src/app/core/authentication/authentication.service.ts index 73c4891d8..57e5f30fc 100644 --- a/src/app/core/authentication/authentication.service.ts +++ b/src/app/core/authentication/authentication.service.ts @@ -388,6 +388,14 @@ export class AuthenticationService { return JSON.parse(this.storage.getItem(this.credentialsStorageKey)); } + /** Returns true when the logged-in user has the "Clerk" role. */ + isClerkUser(): boolean { + const creds = this.getCredentials(); + if (!creds || !creds.roles) return false; + const roleNames: string[] = (creds.roles as any[]).map((r) => (r.name || r.role || '').toLowerCase().trim()); + return roleNames.some((n) => n === 'clerk'); + } + /** * Returns true ONLY for regular cashier role (not Head Cashier). * Regular cashier → full-screen transaction entry, no nav. diff --git a/src/app/core/authentication/cashier-only.guard.ts b/src/app/core/authentication/cashier-only.guard.ts new file mode 100644 index 000000000..ceec18cd0 --- /dev/null +++ b/src/app/core/authentication/cashier-only.guard.ts @@ -0,0 +1,29 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Injectable, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthenticationService } from './authentication.service'; + +/** + * Restricts /cashier/transaction-entry to users with the Cashier role only. + * Clerk and other roles are redirected to /home. + */ +@Injectable({ providedIn: 'root' }) +export class CashierOnlyGuard { + private router = inject(Router); + private authService = inject(AuthenticationService); + + canActivate(): boolean { + if (this.authService.isCashierUser()) { + return true; + } + this.router.navigate(['/home'], { replaceUrl: true }); + return false; + } +} diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index a9d6ed133..c2305d279 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -57,73 +57,138 @@ - + @if (hideShellLayout) { - + + @if (currentUrl.includes('/clerk/')) { + + } @else { + + + } } @@ -300,6 +365,15 @@ Cashier + + Clerk + Clearing diff --git a/src/app/core/shell/shell.component.scss b/src/app/core/shell/shell.component.scss index 3da7b166b..11a7f1dd4 100644 --- a/src/app/core/shell/shell.component.scss +++ b/src/app/core/shell/shell.component.scss @@ -135,6 +135,49 @@ $color: #e2e4ec; background: #1a6ac8; } } + + &--sec { + font-size: 11.5px; + padding: 4px 10px; + border: 1px solid rgb(255 255 255 / 12%); + cursor: pointer; + } + } + + &__esc-hint { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 11px; + color: #475569; + cursor: pointer; + padding: 4px 10px; + border-radius: 4px; + border: 1px solid #2a3a5e; + transition: + color 0.15s, + border-color 0.15s; + + kbd { + background: #0f172a; + border: 1px solid #334155; + border-radius: 4px; + padding: 1px 5px; + font-size: 10px; + font-weight: 700; + color: #94a3b8; + font-family: monospace; + } + + &:hover { + color: #e2e8f0; + border-color: #0ea5e9; + } + } + + &--clerk { + border-bottom-color: #e87722; } } diff --git a/src/app/core/shell/shell.component.ts b/src/app/core/shell/shell.component.ts index fbe69a324..366e63484 100644 --- a/src/app/core/shell/shell.component.ts +++ b/src/app/core/shell/shell.component.ts @@ -8,10 +8,11 @@ /** Angular Imports */ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; -import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { ChangeDetectorRef, Component, HostListener, OnDestroy, OnInit, inject } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router, NavigationEnd } from '@angular/router'; import { AuthenticationService } from '../authentication/authentication.service'; +import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service'; /** rxjs Imports */ import { Observable, Subscription } from 'rxjs'; @@ -64,6 +65,7 @@ export class ShellComponent implements OnInit, OnDestroy { private dialog = inject(MatDialog); private router = inject(Router); private authService = inject(AuthenticationService); + private clerkUiStateService = inject(ClerkUiStateService); /** Subscription to breakpoint observer for handset. */ isHandset$: Observable = this.breakpointObserver @@ -79,6 +81,11 @@ export class ShellComponent implements OnInit, OnDestroy { hideShellLayout = false; private routeSub: Subscription; + /** Account Facilities — populated from ClerkUiStateService when account loads */ + clerkAccountLoaded = false; + clerkFacilities: { label: string; active: boolean }[] = []; + private clerkStateSub: Subscription; + /** Sub Ledger mega menu state */ subLedgerOpen = false; subLedgerSearch = ''; @@ -118,13 +125,27 @@ export class ShellComponent implements OnInit, OnDestroy { this.cdr.detectChanges(); }); - // Cashier users: always hide shell layout (role-based, not URL-based) + // Cashier-only users: hide shell. Clerk/dashboard: also fullscreen by default. const checkLayout = () => { - this.hideShellLayout = this.authService.isCashierUser(); + const url = this.router.url; + const isCashierOnly = this.authService.isCashierUser() && !this.authService.isClerkUser(); + const isClerkRoute = + url.includes('/clerk/dashboard') || + url.includes('/clerk/account-facilities') || + url.includes('/clerk/photo-signature') || + url.includes('/clerk/standing-instructions') || + url.includes('/clerk/today-activity'); + this.hideShellLayout = isCashierOnly || isClerkRoute; this.cdr.detectChanges(); }; this.routeSub = this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe(() => checkLayout()); setTimeout(() => checkLayout(), 0); + + this.clerkStateSub = this.clerkUiStateService.state$.subscribe((s) => { + this.clerkAccountLoaded = s.accountLoaded; + this.clerkFacilities = s.facilities; + this.cdr.detectChanges(); + }); } /** @@ -141,6 +162,20 @@ export class ShellComponent implements OnInit, OnDestroy { this.cdr.detectChanges(); } + /** Expose current URL to template for route-based conditionals. */ + get currentUrl(): string { + return this.router.url; + } + + /** ESC key — exit fullscreen at Clerk Dashboard, restoring full shell header. */ + @HostListener('document:keydown.escape') + onEscapeKey(): void { + if (this.hideShellLayout && this.router.url.includes('/clerk/')) { + this.hideShellLayout = false; + this.cdr.detectChanges(); + } + } + openConfigWizard(): void { // Placeholder — config wizard is handled by toolbar component } @@ -275,5 +310,8 @@ export class ShellComponent implements OnInit, OnDestroy { if (this.routeSub) { this.routeSub.unsubscribe(); } + if (this.clerkStateSub) { + this.clerkStateSub.unsubscribe(); + } } } diff --git a/src/environments/.env.ts b/src/environments/.env.ts index c88202170..7a7e60132 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260608', - 'hash': 'f6404708' + 'version': '260609', + 'hash': 'f7d9259e' }, 'allow_switching_backend_instance': true };