The Bank Calendar never read the Working Days setting. It hard-coded
Mon-Sat as working days and always marked only Sundays and the 2nd and
4th Saturday as off, so unticking Saturday (or any weekday) in
Admin > Working Days changed nothing on the calendar.
- Load the setting from /workingdays and parse its recurrence
(BYDAY=MO,TU,...). Until it answers, or if it cannot be read, the old
Mon-Sat default stays in place.
- A weekday left out of Working Days is off every week. When Saturday
is left out, every Saturday is off; when it is a working day, the RBI
rule still keeps the 2nd and 4th Saturday off. Sunday shows as a
working day if Working Days ticks it.
- Month and year counts, the Saturday stat card label ("2nd & 4th Sat"
or "Saturdays"), the legend, tooltips and the month summary tags
("N Sat Off", "N Weekly Off") follow the same rule.
Verified with prettier, eslint, tsc and ng build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
371 lines
14 KiB
HTML
371 lines
14 KiB
HTML
<!--
|
|
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/.
|
|
-->
|
|
|
|
<div class="cal-container">
|
|
<!-- Header -->
|
|
<div class="cal-header">
|
|
<div class="cal-header__left">
|
|
<fa-icon icon="calendar-alt" class="cal-header__icon"></fa-icon>
|
|
<span class="cal-header__title">Bank Calendar</span>
|
|
</div>
|
|
<div class="cal-header__nav">
|
|
<button class="cal-nav-btn" (click)="previousYear()" matTooltip="Previous Year">
|
|
<fa-icon icon="chevron-left"></fa-icon>
|
|
</button>
|
|
<span class="cal-header__year">{{ currentYear }}</span>
|
|
<button class="cal-nav-btn" (click)="nextYear()" matTooltip="Next Year">
|
|
<fa-icon icon="chevron-right"></fa-icon>
|
|
</button>
|
|
<button class="cal-today-btn" (click)="goToCurrentYear()">Today</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overall Stats -->
|
|
<div class="cal-stats">
|
|
<div class="cal-stat working">
|
|
<fa-icon icon="briefcase" class="cal-stat__icon"></fa-icon>
|
|
<div class="cal-stat__info">
|
|
<div class="cal-stat__value">{{ overallStats.working }}</div>
|
|
<div class="cal-stat__label">Working Days</div>
|
|
</div>
|
|
</div>
|
|
<div class="cal-stat sundays">
|
|
<fa-icon icon="ban" class="cal-stat__icon"></fa-icon>
|
|
<div class="cal-stat__info">
|
|
<div class="cal-stat__value">{{ overallStats.sundays }}</div>
|
|
<div class="cal-stat__label">Sundays</div>
|
|
</div>
|
|
</div>
|
|
<div class="cal-stat sat2nd4th">
|
|
<fa-icon icon="calendar-times" class="cal-stat__icon"></fa-icon>
|
|
<div class="cal-stat__info">
|
|
<div class="cal-stat__value">{{ overallStats.sat2nd4th + overallStats.weeklyOff }}</div>
|
|
<div class="cal-stat__label">
|
|
{{ saturdayIsWorkingDay ? '2nd & 4th Sat' : 'Saturdays'
|
|
}}{{ overallStats.weeklyOff > 0 ? ' / Weekly Off' : '' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="cal-stat holidays">
|
|
<fa-icon icon="star" class="cal-stat__icon"></fa-icon>
|
|
<div class="cal-stat__info">
|
|
<div class="cal-stat__value">{{ overallStats.holidays }}</div>
|
|
<div class="cal-stat__label">Holidays</div>
|
|
</div>
|
|
</div>
|
|
<div class="cal-stat total">
|
|
<fa-icon icon="calendar" class="cal-stat__icon"></fa-icon>
|
|
<div class="cal-stat__info">
|
|
<div class="cal-stat__value">{{ overallStats.totalDays }}</div>
|
|
<div class="cal-stat__label">Total Days</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Legend -->
|
|
<div class="cal-legend">
|
|
<div class="cal-legend__items">
|
|
<div class="cal-legend__item"><span class="cal-legend__dot working"></span> Working Day</div>
|
|
<div class="cal-legend__item"><span class="cal-legend__dot sunday"></span> Sunday</div>
|
|
<div class="cal-legend__item">
|
|
<span class="cal-legend__dot sat2nd4th"></span>
|
|
{{ saturdayIsWorkingDay ? '2nd / 4th Saturday' : 'Saturday'
|
|
}}{{ overallStats.weeklyOff > 0 ? ' / Weekly Off' : '' }}
|
|
</div>
|
|
<div class="cal-legend__item"><span class="cal-legend__dot holiday"></span> Holiday</div>
|
|
<div class="cal-legend__item"><span class="cal-legend__dot today"></span> Today</div>
|
|
</div>
|
|
<div class="cal-legend__actions">
|
|
<button
|
|
class="cal-legend__tmpl-btn"
|
|
(click)="downloadImportTemplate()"
|
|
matTooltip="Download a pre-formatted .xlsx with sample rows + instructions"
|
|
>
|
|
<fa-icon icon="file-excel"></fa-icon> Download Template
|
|
</button>
|
|
<button
|
|
class="cal-legend__import-btn"
|
|
(click)="triggerImport()"
|
|
[disabled]="importing"
|
|
matTooltip="Bulk-create holidays from a filled-in template"
|
|
>
|
|
<fa-icon [icon]="importing ? 'spinner' : 'upload'" [class.fa-spin]="importing"></fa-icon>
|
|
{{ importing ? 'Importing…' : 'Import Holidays' }}
|
|
</button>
|
|
<input #importInput type="file" accept=".xlsx" (change)="onImportFileChosen($event)" hidden />
|
|
<button class="cal-legend__print-btn" (click)="printHolidayList()">
|
|
<fa-icon icon="print"></fa-icon> Print Holiday List
|
|
</button>
|
|
<button class="cal-legend__add-btn" (click)="openAddHolidayDialog()">
|
|
<fa-icon icon="plus"></fa-icon> Add Holiday
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import result banner -->
|
|
@if (importResult) {
|
|
<div
|
|
class="cal-import-result"
|
|
[class.cal-import-result--ok]="importResult.imported > 0 && importResult.skipped.length === 0"
|
|
[class.cal-import-result--mixed]="importResult.imported > 0 && importResult.skipped.length > 0"
|
|
[class.cal-import-result--fail]="importResult.imported === 0"
|
|
>
|
|
<div class="cal-import-result__head">
|
|
<fa-icon
|
|
[icon]="
|
|
importResult.imported > 0 && importResult.skipped.length === 0
|
|
? 'check-circle'
|
|
: importResult.imported === 0
|
|
? 'times-circle'
|
|
: 'exclamation-triangle'
|
|
"
|
|
class="cal-import-result__ic"
|
|
></fa-icon>
|
|
<div>
|
|
<div class="cal-import-result__title">
|
|
Import — {{ importResult.imported }} of {{ importResult.total }} added
|
|
</div>
|
|
<div class="cal-import-result__sub">
|
|
@if (importResult.skipped.length === 0) {
|
|
All rows were imported successfully.
|
|
} @else {
|
|
{{ importResult.skipped.length }} row{{ importResult.skipped.length === 1 ? '' : 's' }} skipped — see
|
|
below.
|
|
}
|
|
</div>
|
|
</div>
|
|
<button class="cal-import-result__close" (click)="dismissImportResult()" matTooltip="Dismiss">
|
|
<fa-icon icon="times"></fa-icon>
|
|
</button>
|
|
</div>
|
|
@if (importResult.skipped.length > 0) {
|
|
<ul class="cal-import-result__list">
|
|
@for (s of importResult.skipped; track s.row) {
|
|
<li>
|
|
<strong>Row {{ s.row }}:</strong> {{ s.reason }}
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- Holiday List Panel (office-wise) -->
|
|
<div class="cal-list">
|
|
<div class="cal-list__header">
|
|
<div class="cal-list__title">
|
|
<fa-icon icon="list-alt" class="cal-list__title-ic"></fa-icon>
|
|
<span>Holiday List {{ currentYear }}</span>
|
|
<span class="cal-list__count">{{ filteredHolidayRows.length }} of {{ holidayRows.length }}</span>
|
|
</div>
|
|
<div class="cal-list__filters">
|
|
<div class="cal-list__search">
|
|
<fa-icon icon="search" class="cal-list__search-ic"></fa-icon>
|
|
<input type="text" placeholder="Search by name, office, description…" [(ngModel)]="filterSearch" />
|
|
</div>
|
|
<select [(ngModel)]="filterOfficeId" class="cal-list__select">
|
|
<option [ngValue]="''">All Offices</option>
|
|
@for (o of offices; track o.id) {
|
|
<option [ngValue]="o.id">{{ o.name }}</option>
|
|
}
|
|
</select>
|
|
<select [(ngModel)]="filterType" class="cal-list__select">
|
|
<option value="">All Types</option>
|
|
<option value="GAZETTED">Gazetted (RBI)</option>
|
|
<option value="CUSTOM">Custom</option>
|
|
</select>
|
|
@if (filterSearch || filterOfficeId !== '' || filterType) {
|
|
<button class="cal-list__clear" (click)="clearListFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cal-list__table-wrap">
|
|
<table class="cal-list__table">
|
|
<thead>
|
|
<tr>
|
|
<th class="num">#</th>
|
|
<th>Date</th>
|
|
<th>Day</th>
|
|
<th>Holiday / Occasion</th>
|
|
<th>Office</th>
|
|
<th>Type</th>
|
|
<th>Status</th>
|
|
<th class="actions">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (row of filteredHolidayRows; track trackHolidayRow($index, row); let i = $index) {
|
|
<tr>
|
|
<td class="num">{{ i + 1 }}</td>
|
|
<td class="mono">{{ formatRowDate(row.fromDate) }}</td>
|
|
<td>{{ rowDayName(row.fromDate) }}</td>
|
|
<td>
|
|
<div class="cal-list__name">{{ row.name }}</div>
|
|
@if (row.description && row.description !== row.name) {
|
|
<div class="cal-list__desc">{{ row.description }}</div>
|
|
}
|
|
</td>
|
|
<td>
|
|
@for (n of row.officeNames; track n) {
|
|
<span class="cal-list__office-chip">{{ n }}</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
@if (row.isGazetted) {
|
|
<span class="cal-list__pill gazetted">Gazetted</span>
|
|
} @else {
|
|
<span class="cal-list__pill custom">Custom</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<span
|
|
class="cal-list__pill status"
|
|
[class.active]="row.status === 'Active' || row.status === 'holidayStatusType.active'"
|
|
>
|
|
{{ row.status === 'holidayStatusType.active' ? 'Active' : row.status }}
|
|
</span>
|
|
</td>
|
|
<td class="actions">
|
|
<button
|
|
class="cal-list__act edit"
|
|
(click)="editHoliday(row)"
|
|
[disabled]="row.isGazetted"
|
|
[matTooltip]="row.isGazetted ? 'Gazetted holiday — read-only' : 'Edit holiday'"
|
|
>
|
|
<fa-icon icon="pencil-alt"></fa-icon>
|
|
</button>
|
|
<button
|
|
class="cal-list__act remove"
|
|
(click)="removeHoliday(row)"
|
|
[disabled]="row.isGazetted"
|
|
[matTooltip]="row.isGazetted ? 'Gazetted holiday — cannot remove' : 'Remove holiday'"
|
|
>
|
|
<fa-icon icon="trash"></fa-icon>
|
|
</button>
|
|
<button
|
|
class="cal-list__act more"
|
|
[matMenuTriggerFor]="rowMenu"
|
|
matTooltip="More actions"
|
|
(click)="$event.stopPropagation()"
|
|
>
|
|
<fa-icon icon="ellipsis-v"></fa-icon>
|
|
</button>
|
|
<mat-menu #rowMenu="matMenu" xPosition="before" class="cal-list__menu">
|
|
<button
|
|
mat-menu-item
|
|
(click)="activateHoliday(row)"
|
|
[disabled]="row.isGazetted || isHolidayActive(row)"
|
|
>
|
|
<fa-icon icon="check-circle" class="cal-list__menu-ic ok"></fa-icon>
|
|
@if (isHolidayActive(row)) {
|
|
Already Active
|
|
} @else {
|
|
Activate
|
|
}
|
|
</button>
|
|
<button mat-menu-item (click)="editHoliday(row)" [disabled]="row.isGazetted">
|
|
<fa-icon icon="pencil-alt" class="cal-list__menu-ic"></fa-icon>
|
|
Edit Holiday
|
|
</button>
|
|
<button mat-menu-item (click)="removeHoliday(row)" [disabled]="row.isGazetted">
|
|
<fa-icon icon="trash" class="cal-list__menu-ic warn"></fa-icon>
|
|
Remove Holiday
|
|
</button>
|
|
<button mat-menu-item (click)="copyHolidayId(row)" [disabled]="row.id == null">
|
|
<fa-icon icon="copy" class="cal-list__menu-ic"></fa-icon>
|
|
Copy Holiday ID
|
|
</button>
|
|
</mat-menu>
|
|
</td>
|
|
</tr>
|
|
}
|
|
@if (filteredHolidayRows.length === 0) {
|
|
<tr>
|
|
<td colspan="8" class="cal-list__empty">
|
|
@if (holidayListLoading) {
|
|
<fa-icon icon="spinner" class="fa-spin"></fa-icon> Loading holidays…
|
|
} @else {
|
|
No holidays match the current filters.
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Monthly Grids -->
|
|
<div class="cal-months">
|
|
@for (month of months; track month.month) {
|
|
<div class="cal-month-card">
|
|
<div class="cal-month-card__header">
|
|
<span class="cal-month-card__name">{{ month.name }}</span>
|
|
<span class="cal-month-card__counts">
|
|
<span class="count-working">{{ month.workingDays }}W</span>
|
|
<span class="count-off">{{ month.nonWorkingDays }}Off</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="cal-grid">
|
|
<!-- Week day headers -->
|
|
@for (wd of weekDays; track wd) {
|
|
<div class="cal-grid__wday" [class.cal-grid__wday--sun]="wd === 'Sun'">{{ wd }}</div>
|
|
}
|
|
|
|
<!-- Day cells -->
|
|
@for (day of month.days; track $index) {
|
|
<div
|
|
class="cal-grid__day"
|
|
[class.cal-grid__day--other]="!day.isCurrentMonth"
|
|
[class.cal-grid__day--today]="day.isToday"
|
|
[class.cal-grid__day--sunday]="day.isSunday && day.isCurrentMonth"
|
|
[class.cal-grid__day--sat2nd4th]="
|
|
(day.is2ndSaturday || day.is4thSaturday || day.isWeeklyOff) && day.isCurrentMonth
|
|
"
|
|
[class.cal-grid__day--holiday]="day.isHoliday && day.isCurrentMonth"
|
|
[class.cal-grid__day--working]="day.isWorking && day.isCurrentMonth"
|
|
[matTooltip]="
|
|
day.holidayName ||
|
|
(day.is2ndSaturday
|
|
? '2nd Saturday Off'
|
|
: day.is4thSaturday
|
|
? '4th Saturday Off'
|
|
: day.isWeeklyOff && day.isCurrentMonth
|
|
? weekDays[day.dayOfWeek] + ' Off (Working Days)'
|
|
: day.isSunday && day.isCurrentMonth
|
|
? 'Sunday'
|
|
: '')
|
|
"
|
|
>
|
|
{{ day.isCurrentMonth ? day.date : '' }}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Month summary bar -->
|
|
<div class="cal-month-card__summary">
|
|
@if (month.holidays > 0) {
|
|
<span class="summary-tag holiday">{{ month.holidays }} Holiday{{ month.holidays > 1 ? 's' : '' }}</span>
|
|
}
|
|
@if (month.sundays > 0) {
|
|
<span class="summary-tag sun">{{ month.sundays }} Sun</span>
|
|
}
|
|
@if (month.secondFourthSat > 0) {
|
|
<span class="summary-tag sat">{{ month.secondFourthSat }} Sat Off</span>
|
|
}
|
|
@if (month.weeklyOff > 0) {
|
|
<span class="summary-tag sat">{{ month.weeklyOff }} Weekly Off</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|