}
diff --git a/src/app/bank-calendar/bank-calendar.component.ts b/src/app/bank-calendar/bank-calendar.component.ts
index bfcacb185..9102f10d6 100644
--- a/src/app/bank-calendar/bank-calendar.component.ts
+++ b/src/app/bank-calendar/bank-calendar.component.ts
@@ -63,6 +63,8 @@ interface CalendarDay {
isSunday: boolean;
is2ndSaturday: boolean;
is4thSaturday: boolean;
+ /** Off every week because Admin > Working Days leaves this weekday out (Sunday aside). */
+ isWeeklyOff: boolean;
holidayName?: string;
dayOfWeek: number; // 0=Sun, 6=Sat
}
@@ -76,9 +78,23 @@ interface MonthData {
nonWorkingDays: number;
holidays: number;
sundays: number;
+ /** Saturdays off: the 2nd and 4th when Saturday is a working day, every one when it is not. */
secondFourthSat: number;
+ /** Other weekdays off every week because Working Days leaves them out. */
+ weeklyOff: number;
}
+/** Weekday codes in Date#getDay() order, as used by the /workingdays recurrence (BYDAY=MO,TU,...). */
+const DAY_CODES = [
+ 'SU',
+ 'MO',
+ 'TU',
+ 'WE',
+ 'TH',
+ 'FR',
+ 'SA'
+];
+
@Component({
selector: 'mifosx-bank-calendar',
templateUrl: './bank-calendar.component.html',
@@ -118,7 +134,7 @@ export class BankCalendarComponent implements OnInit {
];
workingDaysConfig: string[] = [];
holidays: any[] = [];
- overallStats = { working: 0, nonWorking: 0, holidays: 0, sundays: 0, sat2nd4th: 0, totalDays: 0 };
+ overallStats = { working: 0, nonWorking: 0, holidays: 0, sundays: 0, sat2nd4th: 0, weeklyOff: 0, totalDays: 0 };
// ───── Holiday list (office-wise) ─────
offices: OfficeLite[] = [];
@@ -202,7 +218,8 @@ export class BankCalendarComponent implements OnInit {
}
private loadWorkingDays() {
- // Default Indian banking working days: Mon-Sat (Sun off, 2nd & 4th Sat off)
+ // Default Indian banking working days: Mon-Sat (Sun off, 2nd & 4th Sat off), used until
+ // Admin > Working Days answers, or if it cannot be read.
this.workingDaysConfig = [
'MO',
'TU',
@@ -215,6 +232,19 @@ export class BankCalendarComponent implements OnInit {
this.buildCalendar();
this.rebuildHolidayRows();
+ // The calendar has to agree with Admin > Working Days: a weekday unticked there is off every
+ // week here too.
+ this.organizationService.getWorkingDays().subscribe({
+ next: (data: any) => {
+ const configured = this.parseWorkingDays(data?.recurrence);
+ if (configured.length > 0) {
+ this.workingDaysConfig = configured;
+ this.buildCalendar();
+ }
+ },
+ error: () => {}
+ });
+
// Pull offices then fan-out a per-office holiday fetch so each office's
// calendar entries are surfaced in the list panel (calendar grid still
// unions everything for the year-at-a-glance view).
@@ -273,6 +303,23 @@ export class BankCalendarComponent implements OnInit {
});
}
+ /** Weekday codes out of a recurrence such as "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR". */
+ private parseWorkingDays(recurrence: string | undefined): string[] {
+ const byDay = /BYDAY=([^;]*)/.exec(recurrence || '');
+ if (!byDay) {
+ return [];
+ }
+ return byDay[1]
+ .split(',')
+ .map((d) => d.trim().toUpperCase())
+ .filter((d) => DAY_CODES.includes(d));
+ }
+
+ /** True when Admin > Working Days counts Saturday as a working day (so only the 2nd and 4th are off). */
+ get saturdayIsWorkingDay(): boolean {
+ return this.workingDaysConfig.includes('SA');
+ }
+
/** Flatten the holidays array into a presentable list with office labels. */
private rebuildHolidayRows(): void {
this.holidayRows = this.holidays.map((h: any) => {
@@ -511,7 +558,15 @@ export class BankCalendarComponent implements OnInit {
buildCalendar() {
this.months = [];
- this.overallStats = { working: 0, nonWorking: 0, holidays: 0, sundays: 0, sat2nd4th: 0, totalDays: 0 };
+ this.overallStats = {
+ working: 0,
+ nonWorking: 0,
+ holidays: 0,
+ sundays: 0,
+ sat2nd4th: 0,
+ weeklyOff: 0,
+ totalDays: 0
+ };
for (let m = 0; m < 12; m++) {
const monthData = this.buildMonth(m, this.currentYear);
@@ -521,6 +576,7 @@ export class BankCalendarComponent implements OnInit {
this.overallStats.holidays += monthData.holidays;
this.overallStats.sundays += monthData.sundays;
this.overallStats.sat2nd4th += monthData.secondFourthSat;
+ this.overallStats.weeklyOff += monthData.weeklyOff;
}
this.overallStats.totalDays = this.overallStats.working + this.overallStats.nonWorking;
}
@@ -552,6 +608,7 @@ export class BankCalendarComponent implements OnInit {
let holidayCount = 0;
let sundayCount = 0;
let sat2nd4thCount = 0;
+ let weeklyOffCount = 0;
// Previous month padding
const prevMonthDays = new Date(year, month, 0).getDate();
@@ -567,6 +624,7 @@ export class BankCalendarComponent implements OnInit {
isSunday: false,
is2ndSaturday: false,
is4thSaturday: false,
+ isWeeklyOff: false,
dayOfWeek: (startDayOfWeek - i - 1 + 7) % 7
});
}
@@ -576,24 +634,29 @@ export class BankCalendarComponent implements OnInit {
for (let d = 1; d <= daysInMonth; d++) {
const date = new Date(year, month, d);
const dow = date.getDay();
- const isSunday = dow === 0;
const isSaturday = dow === 6;
+ const isWorkingWeekday = this.workingDaysConfig.includes(DAY_CODES[dow]);
+ const isSunday = dow === 0 && !isWorkingWeekday;
if (isSaturday) saturdayCount++;
- const is2ndSat = isSaturday && saturdayCount === 2;
- const is4thSat = isSaturday && saturdayCount === 4;
+ // RBI: when Saturday is a working day the 2nd and 4th are still off. When Working Days leaves
+ // Saturday out, every Saturday is a weekly off instead.
+ const is2ndSat = isSaturday && isWorkingWeekday && saturdayCount === 2;
+ const is4thSat = isSaturday && isWorkingWeekday && saturdayCount === 4;
+ const isWeeklyOff = dow !== 0 && !isWorkingWeekday;
const isToday = date.toDateString() === today.toDateString();
// Check if holiday
const holiday = this.getHolidayForDate(year, month, d);
const isHoliday = !!holiday;
- // Non-working: Sunday, 2nd Sat, 4th Sat, or holiday
- const isNonWorking = isSunday || is2ndSat || is4thSat || isHoliday;
+ // Non-working: Sunday, 2nd Sat, 4th Sat, a weekly off from Working Days, or holiday
+ const isNonWorking = isSunday || is2ndSat || is4thSat || isWeeklyOff || isHoliday;
if (isSunday) sundayCount++;
- if (is2ndSat || is4thSat) sat2nd4thCount++;
+ if (is2ndSat || is4thSat || (isSaturday && isWeeklyOff)) sat2nd4thCount++;
+ if (isWeeklyOff && !isSaturday) weeklyOffCount++;
if (isHoliday) holidayCount++;
if (isNonWorking) {
@@ -613,6 +676,7 @@ export class BankCalendarComponent implements OnInit {
isSunday,
is2ndSaturday: is2ndSat,
is4thSaturday: is4thSat,
+ isWeeklyOff,
holidayName: holiday?.name,
dayOfWeek: dow
});
@@ -632,6 +696,7 @@ export class BankCalendarComponent implements OnInit {
isSunday: false,
is2ndSaturday: false,
is4thSaturday: false,
+ isWeeklyOff: false,
dayOfWeek: days.length % 7
});
}
@@ -645,7 +710,8 @@ export class BankCalendarComponent implements OnInit {
nonWorkingDays: nonWorkingCount,
holidays: holidayCount,
sundays: sundayCount,
- secondFourthSat: sat2nd4thCount
+ secondFourthSat: sat2nd4thCount,
+ weeklyOff: weeklyOffCount
};
}
diff --git a/src/app/cashier/branch-cash-position/branch-cash-position.component.html b/src/app/cashier/branch-cash-position/branch-cash-position.component.html
new file mode 100644
index 000000000..d8f476a2e
--- /dev/null
+++ b/src/app/cashier/branch-cash-position/branch-cash-position.component.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
BRANCH OPERATIONS · BRANCH CASH POSITION
+
Branch Cash Position
+
+ The notes and coins the branch holds, denomination by denomination: the vault plus every cashier's drawer. Cash
+ moving between the vault and a cashier nets out; deposits, withdrawals and exchanges change it.
+
+ Bank total₹{{ p.totalAmount | number: '1.2-2' }}
+
+
+
+ @if (!p.vaultHasBreakdown) {
+
+ No vault denomination breakdown was recorded on or before {{ p.asOfDate }}, so the vault column reads 0. Open
+ the business day with a note/coin count to start tracking it.
+
+ Exchange today is the notes taken in less the notes given out by cash exchanges on
+ {{ p.asOfDate }}. It is already part of the Cashiers column; it is shown beside the total so a change in the mix
+ can be seen.
+
+ }
+
diff --git a/src/app/cashier/branch-cash-position/branch-cash-position.component.scss b/src/app/cashier/branch-cash-position/branch-cash-position.component.scss
new file mode 100644
index 000000000..d86edfbbc
--- /dev/null
+++ b/src/app/cashier/branch-cash-position/branch-cash-position.component.scss
@@ -0,0 +1,82 @@
+/**
+ * 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/.
+ */
+
+.bcp-error {
+ color: #dc2626;
+}
+
+.bcp-note {
+ margin-bottom: 12px;
+ text-align: left;
+}
+
+.bcp-totals {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-bottom: 14px;
+}
+
+.bcp-total {
+ flex: 1 1 180px;
+ padding: 12px 14px;
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
+
+ span {
+ display: block;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: #64748b;
+ }
+
+ strong {
+ font-size: 18px;
+ font-weight: 700;
+ color: #0f172a;
+ }
+
+ &--main {
+ background: #e6f4ee;
+ border-color: #1f7a4d;
+
+ strong {
+ color: #0f4f3c;
+ }
+ }
+}
+
+.bcp-card {
+ margin-bottom: 14px;
+}
+
+.bcp-strong {
+ font-weight: 700;
+}
+
+.bcp-move {
+ font-weight: 700;
+ color: #94a3b8;
+
+ &--in {
+ color: #16a34a;
+ }
+
+ &--out {
+ color: #dc2626;
+ }
+}
+
+.bcp-foot {
+ margin: 0;
+ font-size: 12px;
+ color: #64748b;
+}
diff --git a/src/app/cashier/branch-cash-position/branch-cash-position.component.ts b/src/app/cashier/branch-cash-position/branch-cash-position.component.ts
new file mode 100644
index 000000000..ef5e64330
--- /dev/null
+++ b/src/app/cashier/branch-cash-position/branch-cash-position.component.ts
@@ -0,0 +1,112 @@
+/**
+ * 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 { MatIconModule } from '@angular/material/icon';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { of } from 'rxjs';
+import { catchError } from 'rxjs/operators';
+import { CashierService } from '../cashier.service';
+import { businessToday } from 'app/core/utils/business-today';
+
+interface BranchDenomRow {
+ denominationId: number;
+ denominationValue: number;
+ kind: string;
+ vaultCount: number;
+ cashierCount: number;
+ totalCount: number;
+ totalAmount: number;
+ /** Notes taken in less notes given out by exchanges on the day. */
+ exchangeNet: number;
+}
+
+interface BranchPosition {
+ officeId: number;
+ officeName: string;
+ asOfDate: string;
+ vaultHasBreakdown: boolean;
+ vaultAmount: number;
+ cashiersAmount: number;
+ totalAmount: number;
+ denominations: BranchDenomRow[];
+}
+
+/**
+ * What the branch holds, denomination by denomination: the vault plus every cashier's drawer. An exchange
+ * (a customer hands in 10 x ₹500 and leaves with 50 x ₹100) leaves the vault alone and changes a drawer, so
+ * it only shows up in this total -- +10 on ₹500, -50 on ₹100.
+ */
+@Component({
+ selector: 'mifosx-branch-cash-position',
+ templateUrl: './branch-cash-position.component.html',
+ styleUrls: [
+ '../cashier-shared.scss',
+ '../cashier-position/cashier-position.component.scss',
+ './branch-cash-position.component.scss'
+ ],
+ standalone: true,
+ imports: [
+ CommonModule,
+ MatIconModule,
+ MatFormFieldModule,
+ MatInputModule
+ ]
+})
+export class BranchCashPositionComponent implements OnInit {
+ private service = inject(CashierService);
+
+ asOfDate = '';
+ loading = false;
+ errorMsg = '';
+ position: BranchPosition | null = null;
+
+ ngOnInit(): void {
+ const d = businessToday();
+ this.asOfDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
+ this.load();
+ }
+
+ load(): void {
+ this.loading = true;
+ this.errorMsg = '';
+ this.position = null;
+ this.service
+ .branchCashPosition(this.asOfDate)
+ .pipe(catchError(() => of(null)))
+ .subscribe((data: BranchPosition | null) => {
+ this.loading = false;
+ if (!data) {
+ this.errorMsg = 'Could not load the branch cash position.';
+ return;
+ }
+ this.position = data;
+ });
+ }
+
+ onDateChange(value: string): void {
+ this.asOfDate = value;
+ this.load();
+ }
+
+ notes(): BranchDenomRow[] {
+ return (this.position?.denominations || []).filter((d) => d.kind !== 'coin');
+ }
+
+ coins(): BranchDenomRow[] {
+ return (this.position?.denominations || []).filter((d) => d.kind === 'coin');
+ }
+
+ /** "+10", "-50", or a dash when the day's exchanges did not touch the denomination. */
+ movement(net: number): string {
+ if (!net) return '—';
+ return net > 0 ? `+${net}` : `${net}`;
+ }
+}
diff --git a/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.html b/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.html
index e08dc73b0..d46a2d0d6 100644
--- a/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.html
+++ b/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.html
@@ -22,50 +22,255 @@
+ person_off
+ Your login is not linked to a cashier. An inter-cashier transfer is made by the cashier handing the cash over, so
+ log in as that cashier.
+