A cash deposit posted to the wrong account is moved to the right one in four steps: unpass the voucher, a manager approves, the cashier enters the correct account, a manager approves again. Nothing changes on any account until the final approval. - Voucher Correction page with four-step strip and popups: today's vouchers to unpass (admin or clerk), the unpassed vouchers waiting for their correct account (cashier) with the wrong and correct account side by side and the balance check, and the caller's own requests. A cashier whose customer has used the money closes the request and deposits separately. - Reached from the cashier bar, the clerk bar and the Cashier menu; the manager reviews both steps under Approvals > Voucher Corrections, laid out like the other approval tabs. Routes for Cash Transfers approvals are added to the same tab bar. - A Correction column on My Vouchers, the day book and the voucher register (and in the voucher detail popup) says whether a voucher is unpassed, revoked in favour of another, a correction, or closed, with the reason. - The deposit taken off the wrong account no longer shows on its passbook or transaction list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
406 lines
17 KiB
TypeScript
406 lines
17 KiB
TypeScript
/**
|
|
* 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 { 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';
|
|
import { CashSettlementComponent } from './cash-settlement/cash-settlement.component';
|
|
import { InterCashierTransferComponent } from './inter-cashier-transfer/inter-cashier-transfer.component';
|
|
import { CashierPositionComponent } from './cashier-position/cashier-position.component';
|
|
import { ReconciliationComponent } from './reconciliation/reconciliation.component';
|
|
import { CashHistoryComponent } from './cash-history/cash-history.component';
|
|
import { DayBookComponent } from './day-book/day-book.component';
|
|
import { VoucherReportComponent } from './voucher-report/voucher-report.component';
|
|
import { BranchDayComponent } from './branch-day/branch-day.component';
|
|
import { VaultPositionComponent } from './vault-position/vault-position.component';
|
|
import { BranchCashPositionComponent } from './branch-cash-position/branch-cash-position.component';
|
|
import { AccountRegisterComponent } from './account-register/account-register.component';
|
|
import { ClosingCashReportComponent } from './closing-cash-report/closing-cash-report.component';
|
|
import { ExcessTransactionReportComponent } from './excess-transaction-report/excess-transaction-report.component';
|
|
|
|
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: 'withdrawal-request',
|
|
loadComponent: () =>
|
|
import('../clerk/withdrawal-request/withdrawal-request.component').then(
|
|
(m) => m.WithdrawalRequestComponent
|
|
),
|
|
data: { title: 'New Withdrawal Request', breadcrumb: 'New Withdrawal Request', mode: 'create' }
|
|
},
|
|
{
|
|
path: 'withdrawal-request/:id',
|
|
loadComponent: () =>
|
|
import('../clerk/withdrawal-edit/withdrawal-edit.component').then((m) => m.WithdrawalEditComponent),
|
|
data: { title: 'Edit Withdrawal Request', breadcrumb: 'Edit Withdrawal Request' }
|
|
},
|
|
{
|
|
path: 'withdrawal-requests',
|
|
loadComponent: () => import('../clerk/my-requests/my-requests.component').then((m) => m.MyRequestsComponent),
|
|
data: { title: 'My Withdrawal Requests', breadcrumb: 'My Withdrawal Requests' }
|
|
},
|
|
{
|
|
path: 'my-disbursement-requests',
|
|
loadComponent: () =>
|
|
import('../clerk/my-disbursement-requests/my-disbursement-requests.component').then(
|
|
(m) => m.MyDisbursementRequestsComponent
|
|
),
|
|
data: { title: 'Disbursements & Transfer', breadcrumb: 'Disbursements & Transfer' }
|
|
},
|
|
{
|
|
path: 'share-withdrawal-request',
|
|
loadComponent: () =>
|
|
import('../clerk/share-withdrawal-request/share-withdrawal-request.component').then(
|
|
(m) => m.ShareWithdrawalRequestComponent
|
|
),
|
|
data: { title: 'New Share Withdrawal Request', breadcrumb: 'New Share Withdrawal Request', mode: 'create' }
|
|
},
|
|
{
|
|
path: 'share-withdrawal-requests',
|
|
loadComponent: () =>
|
|
import('../clerk/my-share-withdrawal-requests/my-share-withdrawal-requests.component').then(
|
|
(m) => m.MyShareWithdrawalRequestsComponent
|
|
),
|
|
data: { title: 'My Share Requests', breadcrumb: 'My Share Requests' }
|
|
},
|
|
{
|
|
// The same page as the cashier's, reached from the clerk dashboard: a clerk unpasses vouchers here.
|
|
path: 'voucher-correction',
|
|
loadComponent: () =>
|
|
import('./voucher-correction/voucher-correction.component').then((m) => m.VoucherCorrectionComponent),
|
|
data: { title: 'Voucher Correction', breadcrumb: 'Voucher Correction' }
|
|
},
|
|
{
|
|
path: 'closure-requests',
|
|
loadComponent: () =>
|
|
import('../clerk/my-closure-requests/my-closure-requests.component').then(
|
|
(m) => m.MyClosureRequestsComponent
|
|
),
|
|
data: { title: 'My Closure Requests', breadcrumb: 'My Closure Requests' }
|
|
},
|
|
{
|
|
path: 'account-facilities',
|
|
loadComponent: () =>
|
|
import('../clerk/account-facilities/account-facilities.component').then(
|
|
(m) => m.AccountFacilitiesComponent
|
|
),
|
|
data: { title: 'Account Facilities', breadcrumb: 'Account Facilities' }
|
|
},
|
|
{
|
|
path: 'account-facilities/cheque-book',
|
|
loadComponent: () =>
|
|
import('../clerk/cheque-book/cheque-book.component').then((m) => m.ClerkChequeBookComponent),
|
|
data: { title: 'Cheque Book', breadcrumb: 'Cheque Book' }
|
|
},
|
|
{
|
|
path: 'account-facilities/cheque-book/:id',
|
|
loadComponent: () =>
|
|
import('../clerk/cheque-book/cheque-book-detail.component').then((m) => m.ClerkChequeBookDetailComponent),
|
|
data: { title: 'Cheque Book Detail', breadcrumb: 'Cheque Book Detail' }
|
|
},
|
|
{
|
|
path: 'photo-signature',
|
|
loadComponent: () =>
|
|
import('../clerk/photo-signature/photo-signature.component').then((m) => m.PhotoSignatureComponent),
|
|
data: { title: 'Photo & Signature', breadcrumb: 'Photo & Signature' }
|
|
},
|
|
// The same register the cashier screens use, without their cash-only filter: a clerk raises
|
|
// book entries — a closure settled to savings, a share redemption, an interest posting the
|
|
// system made on their approval — and cash-only would hide exactly those.
|
|
{
|
|
path: 'received-report',
|
|
component: VoucherReportComponent,
|
|
data: {
|
|
title: 'Received Vouchers',
|
|
breadcrumb: 'Received Vouchers',
|
|
side: 'CR',
|
|
cashOnly: false,
|
|
cashiersOnly: true
|
|
}
|
|
},
|
|
{
|
|
path: 'payment-report',
|
|
component: VoucherReportComponent,
|
|
data: {
|
|
title: 'Payment Vouchers',
|
|
breadcrumb: 'Payment Vouchers',
|
|
side: 'DR',
|
|
cashOnly: false,
|
|
cashiersOnly: true
|
|
}
|
|
},
|
|
// Both sides in one list, scoped to whoever is signed in — the "what did I do today"
|
|
// question the two registers above cannot answer, because an approval raises no voucher.
|
|
{
|
|
path: 'my-work',
|
|
loadComponent: () => import('../cashier/my-work/my-work.component').then((m) => m.MyWorkComponent),
|
|
data: { title: 'My Vouchers', breadcrumb: 'My Vouchers' }
|
|
},
|
|
{
|
|
path: 'end-of-day',
|
|
loadComponent: () =>
|
|
import('../cashier/end-of-day-report/end-of-day-report.component').then((m) => m.EndOfDayReportComponent),
|
|
data: { title: 'End of the Day Report', breadcrumb: 'End of the Day Report' }
|
|
},
|
|
// The system's own postings — interest, charges, escheats, journals. Both sides in one
|
|
// list: a night's run credits and debits, and splitting it across two screens would hide
|
|
// half of it from whichever one you opened.
|
|
{
|
|
path: 'system-vouchers',
|
|
component: VoucherReportComponent,
|
|
data: {
|
|
title: 'System Vouchers',
|
|
breadcrumb: 'System Vouchers',
|
|
side: 'ALL',
|
|
cashOnly: false,
|
|
cashiersOnly: false,
|
|
systemOnly: true
|
|
}
|
|
},
|
|
{
|
|
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: 'dividend-dashboard',
|
|
loadComponent: () =>
|
|
import('../clerk/dividend-dashboard/dividend-dashboard.component').then(
|
|
(m) => m.DividendDashboardComponent
|
|
),
|
|
data: { title: 'Dividend Dashboard', breadcrumb: 'Dividend Dashboard' }
|
|
}
|
|
]
|
|
},
|
|
{
|
|
// Branch-level operations, kept out of the cashier tree on purpose: opening and closing the
|
|
// day governs the whole branch, and is not something a cashier does.
|
|
path: 'branch-ops',
|
|
data: { title: 'Branch Operations', breadcrumb: 'Branch Operations' },
|
|
children: [
|
|
{
|
|
path: 'day-register',
|
|
component: BranchDayComponent,
|
|
data: { title: 'Day Open / Day Close', breadcrumb: 'Day Register' }
|
|
},
|
|
{
|
|
// Same screen, narrowed to the days that were never closed. A separate route rather than
|
|
// a second link to the register, so choosing it visibly does something.
|
|
path: 'missed-close',
|
|
component: BranchDayComponent,
|
|
data: { title: 'Missed Close Register', breadcrumb: 'Missed Close', missedOnly: true }
|
|
},
|
|
{
|
|
// Lives here alongside the day register because both answer "what did this branch look
|
|
// like on that date"; it is linked from the Reports menu, where people go to find it.
|
|
path: 'account-register',
|
|
component: AccountRegisterComponent,
|
|
data: { title: 'Account Register', breadcrumb: 'Account Register' }
|
|
},
|
|
{
|
|
// Same branch-level scope as the day register: this is the vault's own stock, not any
|
|
// one cashier's float, so it sits with the other branch-wide screens.
|
|
path: 'vault-position',
|
|
component: VaultPositionComponent,
|
|
data: { title: 'Vault Position', breadcrumb: 'Vault Position' }
|
|
},
|
|
{
|
|
// What the whole branch holds per denomination: the vault plus every cashier's drawer, so an
|
|
// exchange (which leaves the vault alone) is visible in the bank's total.
|
|
path: 'branch-position',
|
|
component: BranchCashPositionComponent,
|
|
data: { title: 'Branch Cash Position', breadcrumb: 'Branch Cash Position' }
|
|
},
|
|
{
|
|
// The same day book the clerk and cashier read, reached from where a principal officer
|
|
// signing the day off actually looks. It carries the Officer Verification Sheet — each
|
|
// person's own credit, debit and net — which is the branch-level reading of the day, not
|
|
// a counter one.
|
|
path: 'day-book',
|
|
component: DayBookComponent,
|
|
data: { title: 'Day Book & Verification', breadcrumb: 'Day Book' }
|
|
},
|
|
// The one sheet the day is signed off on: the transaction summary, the drawer counted
|
|
// against the books, and the notes that say why it cannot be closed yet.
|
|
{
|
|
path: 'end-of-day',
|
|
loadComponent: () =>
|
|
import('./end-of-day-report/end-of-day-report.component').then((m) => m.EndOfDayReportComponent),
|
|
data: { title: 'End of the Day Report', breadcrumb: 'End of the Day Report' }
|
|
}
|
|
]
|
|
},
|
|
{
|
|
path: 'cashier',
|
|
data: { title: 'Cashier', breadcrumb: 'Cashier' },
|
|
children: [
|
|
{
|
|
path: 'cash-exchange',
|
|
component: CashExchangeEntryComponent,
|
|
data: { title: 'Cash Exchange Entry', breadcrumb: 'Cash Exchange Entry' }
|
|
},
|
|
{
|
|
path: 'exchange-detail',
|
|
component: CurrencyExchangeDetailComponent,
|
|
data: { title: 'Currency Exchange Detail', breadcrumb: 'Currency Exchange Detail' }
|
|
},
|
|
{
|
|
path: 'cash-transfer',
|
|
component: CashTransferComponent,
|
|
data: { title: 'Cash Transfer', breadcrumb: 'Cash Transfer' }
|
|
},
|
|
{
|
|
path: 'cash-settlement',
|
|
component: CashSettlementComponent,
|
|
data: { title: 'Cash Settlement', breadcrumb: 'Cash Settlement' }
|
|
},
|
|
{
|
|
path: 'inter-cashier',
|
|
component: InterCashierTransferComponent,
|
|
data: { title: 'Inter-Cashier Transfer', breadcrumb: 'Inter-Cashier Transfer' }
|
|
},
|
|
{
|
|
path: 'cashier-position',
|
|
component: CashierPositionComponent,
|
|
data: { title: 'Cashier Position', breadcrumb: 'Cashier Position' }
|
|
},
|
|
{
|
|
path: 'reconciliation',
|
|
component: ReconciliationComponent,
|
|
data: { title: 'Reconciliation', breadcrumb: 'Reconciliation' }
|
|
},
|
|
{
|
|
path: 'voucher-correction',
|
|
loadComponent: () =>
|
|
import('./voucher-correction/voucher-correction.component').then((m) => m.VoucherCorrectionComponent),
|
|
data: { title: 'Voucher Correction', breadcrumb: 'Voucher Correction' }
|
|
},
|
|
{
|
|
path: 'cash-history',
|
|
component: CashHistoryComponent,
|
|
data: { title: 'Cash History', breadcrumb: 'Cash History' }
|
|
},
|
|
{
|
|
path: 'day-book',
|
|
component: DayBookComponent,
|
|
data: { title: 'Day Book', breadcrumb: 'Day Book' }
|
|
},
|
|
// The register is read one side at a time: Received is the CR side, Payment the DR side.
|
|
// Same component both times — the side it shows comes from `data.side`.
|
|
//
|
|
// `cashOnly` is what makes these the cashier's own registers rather than the whole voucher
|
|
// book: a cashier deals in notes and coins, so only vouchers that actually moved cash
|
|
// belong here, and the two reports then reconcile against the drawer and the closing
|
|
// report. A back-office screen wanting every voucher points at the same component without
|
|
// the flag.
|
|
{
|
|
path: 'received-report',
|
|
component: VoucherReportComponent,
|
|
data: {
|
|
title: 'Received Report',
|
|
breadcrumb: 'Received Report',
|
|
side: 'CR',
|
|
cashOnly: true,
|
|
cashiersOnly: true
|
|
}
|
|
},
|
|
{
|
|
path: 'payment-report',
|
|
component: VoucherReportComponent,
|
|
data: {
|
|
title: 'Payment Report',
|
|
breadcrumb: 'Payment Report',
|
|
side: 'DR',
|
|
cashOnly: true,
|
|
cashiersOnly: true
|
|
}
|
|
},
|
|
{
|
|
path: 'my-work',
|
|
loadComponent: () => import('./my-work/my-work.component').then((m) => m.MyWorkComponent),
|
|
data: { title: 'My Vouchers', breadcrumb: 'My Vouchers' }
|
|
},
|
|
{
|
|
path: 'closing-report',
|
|
component: ClosingCashReportComponent,
|
|
data: { title: 'Closing Cash Report', breadcrumb: 'Closing Cash Report' }
|
|
},
|
|
{
|
|
path: 'end-of-day',
|
|
loadComponent: () =>
|
|
import('./end-of-day-report/end-of-day-report.component').then((m) => m.EndOfDayReportComponent),
|
|
data: { title: 'End of the Day Report', breadcrumb: 'End of the Day Report' }
|
|
},
|
|
{
|
|
path: 'excess-report',
|
|
component: ExcessTransactionReportComponent,
|
|
data: { title: 'Excess Transaction Report', breadcrumb: 'Excess Transaction Report' }
|
|
},
|
|
{
|
|
path: 'transaction-entry',
|
|
canActivate: [CashierOnlyGuard],
|
|
loadComponent: () =>
|
|
import('./transaction-entry/transaction-entry.component').then((m) => m.TransactionEntryComponent),
|
|
data: {
|
|
title: 'Cashier Transaction Entry',
|
|
breadcrumb: 'Transaction Entry',
|
|
hideShellLayout: true
|
|
}
|
|
},
|
|
{
|
|
path: 'my-deposit-requests',
|
|
loadComponent: () =>
|
|
import('./my-deposit-requests/my-deposit-requests.component').then((m) => m.MyDepositRequestsComponent),
|
|
data: { title: 'My Deposit Requests', breadcrumb: 'My Deposit Requests' }
|
|
},
|
|
{
|
|
path: 'cash-clearing-transfer-summary',
|
|
loadComponent: () =>
|
|
import('./cash-clearing-transfer-summary/cash-clearing-transfer-summary.component').then(
|
|
(m) => m.CashClearingTransferSummaryComponent
|
|
),
|
|
data: {
|
|
title: 'Cash / Clearing / Transfer - Summary Report',
|
|
breadcrumb: 'Cash / Clearing / Transfer - Summary Report'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
])
|
|
];
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forChild(routes)],
|
|
exports: [RouterModule]
|
|
})
|
|
export class CashierRoutingModule {}
|