feat: non-individual entity UI improvements — hide individual-only sections, fix legal form display, send extended fields to API
- Hide Family Members tab/section for non-individual entities in client view, create flow, and preview - Hide Financial & Classification section for non-individual entities in personal data and preview - Fix legal form column in clients list — fetch legalForm from client API for correct display - Add person/business icon before client name in list rows - Remove unsupportedNpd filter — send authorizedSignatory, additionalInfo, dateOfRegistration to API - Add registration number format hint (mat-hint) based on constitution type - Add spacing between verification row and constitution type dropdowns - Reduce table cell padding and prevent action buttons from being clipped - Add missing MPL-2.0 license headers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ab01111424
commit
b890d24bb7
@@ -0,0 +1,116 @@
|
||||
<!--
|
||||
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="hol-dialog">
|
||||
<!-- Header -->
|
||||
<div class="hol-header">
|
||||
<div class="hol-header__left">
|
||||
<div class="hol-header__icon"><fa-icon icon="calendar-alt"></fa-icon></div>
|
||||
<span class="hol-header__title">Add Holiday</span>
|
||||
</div>
|
||||
<button class="hol-header__close" (click)="dialogRef.close()">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<mat-dialog-content class="hol-body">
|
||||
<form [formGroup]="holidayForm">
|
||||
<div class="hol-row">
|
||||
<mat-form-field appearance="outline" class="hol-field">
|
||||
<mat-label>Holiday Name</mat-label>
|
||||
<input matInput required formControlName="name" placeholder="e.g. Republic Day" />
|
||||
@if (holidayForm.controls.name.hasError('required')) {
|
||||
<mat-error>Name is <strong>required</strong></mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="hol-row">
|
||||
<mat-form-field appearance="outline" class="hol-field">
|
||||
<mat-label>Description</mat-label>
|
||||
<input matInput formControlName="description" placeholder="Optional description" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="hol-row hol-row--two">
|
||||
<mat-form-field appearance="outline" class="hol-field" (click)="fromPicker.open()">
|
||||
<mat-label>From Date</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="fromPicker" required formControlName="fromDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
@if (holidayForm.controls.fromDate.hasError('required')) {
|
||||
<mat-error>From Date is <strong>required</strong></mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="hol-field" (click)="toPicker.open()">
|
||||
<mat-label>To Date</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="toPicker" required formControlName="toDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
@if (holidayForm.controls.toDate.hasError('required')) {
|
||||
<mat-error>To Date is <strong>required</strong></mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="hol-row hol-row--two">
|
||||
<mat-form-field appearance="outline" class="hol-field">
|
||||
<mat-label>Repayment Scheduling</mat-label>
|
||||
<mat-select required formControlName="reschedulingType">
|
||||
@for (type of repaymentSchedulingTypes; track type) {
|
||||
<mat-option [value]="type.id">{{ type.value }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (holidayForm.controls.reschedulingType.hasError('required')) {
|
||||
<mat-error>Required</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="hol-field">
|
||||
<mat-label>Office</mat-label>
|
||||
<mat-select required formControlName="officeId">
|
||||
@for (office of offices; track office) {
|
||||
<mat-option [value]="office.id">{{ office.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (holidayForm.controls.officeId.hasError('required')) {
|
||||
<mat-error>Required</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@if (holidayForm.contains('repaymentsRescheduledTo')) {
|
||||
<div class="hol-row">
|
||||
<mat-form-field appearance="outline" class="hol-field" (click)="reschPicker.open()">
|
||||
<mat-label>Repayments Rescheduled To</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="reschPicker" required formControlName="repaymentsRescheduledTo" />
|
||||
<mat-datepicker-toggle matSuffix [for]="reschPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #reschPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<!-- Actions -->
|
||||
<mat-dialog-actions class="hol-actions">
|
||||
<button mat-stroked-button class="hol-actions__cancel" (click)="dialogRef.close()">Cancel</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
class="hol-actions__submit"
|
||||
[disabled]="holidayForm.invalid || submitting"
|
||||
(click)="submit()"
|
||||
>
|
||||
<fa-icon icon="check" class="hol-actions__icon"></fa-icon>
|
||||
{{ submitting ? 'Saving...' : 'Add Holiday' }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
$blue: #1e88e5;
|
||||
$navy: #0d1b2a;
|
||||
|
||||
.hol-dialog {
|
||||
max-width: 560px;
|
||||
width: 560px;
|
||||
}
|
||||
|
||||
// Header
|
||||
.hol-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 24px 14px;
|
||||
border-bottom: 1px solid #e8eaed;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
background: #eff6ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $blue;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
}
|
||||
|
||||
&__close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f1f3f4;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #5f6368;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #e8eaed;
|
||||
color: $navy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Body
|
||||
.hol-body {
|
||||
padding: 18px 24px !important;
|
||||
overflow: visible !important;
|
||||
max-height: 65vh !important;
|
||||
}
|
||||
|
||||
.hol-row {
|
||||
margin-bottom: 4px;
|
||||
|
||||
&--two {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.hol-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Actions
|
||||
.hol-actions {
|
||||
padding: 12px 24px 18px !important;
|
||||
border-top: 1px solid #e8eaed;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
justify-content: flex-end !important;
|
||||
gap: 10px !important;
|
||||
|
||||
&__cancel {
|
||||
border-radius: 8px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 20px !important;
|
||||
height: 38px !important;
|
||||
}
|
||||
|
||||
&__submit {
|
||||
border-radius: 8px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0 20px !important;
|
||||
height: 38px !important;
|
||||
box-shadow: 0 1px 3px rgba($blue, 0.3) !important;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 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 { UntypedFormBuilder, UntypedFormGroup, Validators, UntypedFormControl } from '@angular/forms';
|
||||
import { MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog';
|
||||
import { OrganizationService } from '../../organization/organization.service';
|
||||
import { SettingsService } from '../../settings/settings.service';
|
||||
import { Dates } from '../../core/utils/dates';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-add-holiday-dialog',
|
||||
templateUrl: './add-holiday-dialog.component.html',
|
||||
styleUrls: ['./add-holiday-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent,
|
||||
MatDialogContent,
|
||||
MatDialogActions
|
||||
]
|
||||
})
|
||||
export class AddHolidayDialogComponent implements OnInit {
|
||||
private fb = inject(UntypedFormBuilder);
|
||||
private organizationService = inject(OrganizationService);
|
||||
private settings = inject(SettingsService);
|
||||
private dateUtils = inject(Dates);
|
||||
public dialogRef = inject(MatDialogRef<AddHolidayDialogComponent>);
|
||||
|
||||
holidayForm: UntypedFormGroup;
|
||||
offices: any[] = [];
|
||||
repaymentSchedulingTypes: any[] = [];
|
||||
submitting = false;
|
||||
minDate = new Date(2000, 0, 1);
|
||||
maxDate = new Date(2100, 0, 1);
|
||||
|
||||
ngOnInit() {
|
||||
this.holidayForm = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
description: [''],
|
||||
fromDate: ['', Validators.required],
|
||||
toDate: ['', Validators.required],
|
||||
reschedulingType: ['', Validators.required],
|
||||
officeId: ['', Validators.required]
|
||||
});
|
||||
|
||||
this.loadData();
|
||||
this.buildDependencies();
|
||||
}
|
||||
|
||||
private loadData() {
|
||||
this.organizationService.getOffices().subscribe((data: any[]) => {
|
||||
this.offices = data || [];
|
||||
if (this.offices.length > 0) {
|
||||
this.holidayForm.patchValue({ officeId: this.offices[0].id });
|
||||
}
|
||||
});
|
||||
|
||||
this.organizationService.getHolidayTemplate().subscribe((data: any) => {
|
||||
this.repaymentSchedulingTypes = data || [];
|
||||
});
|
||||
}
|
||||
|
||||
private buildDependencies() {
|
||||
this.holidayForm.get('reschedulingType').valueChanges.subscribe((option: any) => {
|
||||
if (option === 2) {
|
||||
this.holidayForm.addControl('repaymentsRescheduledTo', new UntypedFormControl('', Validators.required));
|
||||
} else {
|
||||
this.holidayForm.removeControl('repaymentsRescheduledTo');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.holidayForm.invalid || this.submitting) return;
|
||||
this.submitting = true;
|
||||
|
||||
const formData = { ...this.holidayForm.value };
|
||||
const dateFormat = this.settings.dateFormat;
|
||||
const locale = this.settings.language.code;
|
||||
const momentFormat = 'DD MMMM YYYY';
|
||||
|
||||
const coerceDate = (value: unknown): Date | null => {
|
||||
if (value instanceof Date) return value;
|
||||
if (value == null || value === '') return null;
|
||||
const d = new Date(value as any);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
};
|
||||
|
||||
const fromDate = coerceDate(formData.fromDate);
|
||||
const toDate = coerceDate(formData.toDate);
|
||||
if (!fromDate || !toDate) {
|
||||
this.submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
formData.fromDate = this.dateUtils.formatDateAsString(fromDate, momentFormat);
|
||||
formData.toDate = this.dateUtils.formatDateAsString(toDate, momentFormat);
|
||||
|
||||
if (this.holidayForm.contains('repaymentsRescheduledTo')) {
|
||||
const reschDate = coerceDate(formData.repaymentsRescheduledTo);
|
||||
if (!reschDate) {
|
||||
this.submitting = false;
|
||||
return;
|
||||
}
|
||||
formData.repaymentsRescheduledTo = this.dateUtils.formatDateAsString(reschDate, momentFormat);
|
||||
}
|
||||
|
||||
const officeId = formData.officeId;
|
||||
delete formData.officeId;
|
||||
|
||||
const payload = {
|
||||
...formData,
|
||||
dateFormat,
|
||||
locale,
|
||||
offices: [{ officeId }]
|
||||
};
|
||||
|
||||
this.organizationService.createHoliday(payload).subscribe({
|
||||
next: () => {
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.submitting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { BankCalendarComponent } from './bank-calendar.component';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
{
|
||||
path: 'bank-calendar',
|
||||
data: { title: 'Bank Calendar', breadcrumb: 'Bank Calendar' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: BankCalendarComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class BankCalendarRoutingModule {}
|
||||
@@ -0,0 +1,134 @@
|
||||
<!--
|
||||
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 }}</div>
|
||||
<div class="cal-stat__label">2nd & 4th Sat</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> 2nd / 4th Saturday</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__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>
|
||||
|
||||
<!-- 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.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.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>
|
||||
}
|
||||
<span class="summary-tag sun">{{ month.sundays }} Sun</span>
|
||||
@if (month.secondFourthSat > 0) {
|
||||
<span class="summary-tag sat">{{ month.secondFourthSat }} Sat Off</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,466 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$green: #10b981;
|
||||
$orange: #f59e0b;
|
||||
$red: #ef4444;
|
||||
$purple: #7c3aed;
|
||||
|
||||
// ─── Container ───
|
||||
.cal-container {
|
||||
padding: 20px 24px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// ─── Header ───
|
||||
.cal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 22px;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: $navy;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__year {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.cal-nav-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid #d0d5dd;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #475569;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: $blue;
|
||||
color: #fff;
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.cal-today-btn {
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid $blue;
|
||||
background: #eff6ff;
|
||||
color: $blue;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: $blue;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stats Row ───
|
||||
.cal-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.cal-stat {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 1px solid #e8ecf0;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
|
||||
&__icon {
|
||||
font-size: 18px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: $navy;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.working .cal-stat__icon {
|
||||
background: #ecfdf5;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
&.sundays .cal-stat__icon {
|
||||
background: #fef2f2;
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&.sat2nd4th .cal-stat__icon {
|
||||
background: #fff7ed;
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
&.holidays .cal-stat__icon {
|
||||
background: #f5f3ff;
|
||||
color: $purple;
|
||||
}
|
||||
|
||||
&.total .cal-stat__icon {
|
||||
background: #eff6ff;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Legend ───
|
||||
.cal-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e8ecf0;
|
||||
|
||||
&__items {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__print-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 18px;
|
||||
border: 1px solid #d0d5dd;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #94a3b8;
|
||||
color: $navy;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 18px;
|
||||
border: none;
|
||||
background: $blue;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: darken($blue, 8%);
|
||||
box-shadow: 0 2px 8px rgb(30 136 229 / 35%);
|
||||
}
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.working {
|
||||
background: #ecfdf5;
|
||||
border: 1px solid $green;
|
||||
}
|
||||
|
||||
&.sunday {
|
||||
background: #fef2f2;
|
||||
border: 1px solid $red;
|
||||
}
|
||||
|
||||
&.sat2nd4th {
|
||||
background: #fff7ed;
|
||||
border: 1px solid $orange;
|
||||
}
|
||||
|
||||
&.holiday {
|
||||
background: #f5f3ff;
|
||||
border: 1px solid $purple;
|
||||
}
|
||||
|
||||
&.today {
|
||||
background: $blue;
|
||||
border: 1px solid $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Monthly Grid ───
|
||||
.cal-months {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cal-month-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
border: 1px solid #e8ecf0;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #0d47a1;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0d47a1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&__counts {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
||||
.count-working {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: $green;
|
||||
background: #ecfdf5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.count-off {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: $red;
|
||||
background: #fef2f2;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__summary {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.holiday {
|
||||
background: #f5f3ff;
|
||||
color: $purple;
|
||||
}
|
||||
|
||||
&.sun {
|
||||
background: #fef2f2;
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&.sat {
|
||||
background: #fff7ed;
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Day Grid ───
|
||||
.cal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
|
||||
&__wday {
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
padding: 4px 0;
|
||||
text-transform: uppercase;
|
||||
|
||||
&--sun {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
&__day {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 5px 2px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
color: #334155;
|
||||
transition: all 0.1s;
|
||||
|
||||
&--other {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&--working {
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
&--sunday {
|
||||
background: #fef2f2;
|
||||
color: $red;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&--sat2nd4th {
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&--holiday {
|
||||
background: #f5f3ff;
|
||||
color: $purple;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&--today {
|
||||
background: $blue !important;
|
||||
color: #fff !important;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 2px 6px rgb(30 136 229 / 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Responsive ───
|
||||
@media screen and (max-width: 1200px) {
|
||||
.cal-months {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.cal-stats {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.cal-months {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cal-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.cal-legend {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cal-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
/**
|
||||
* 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 { MatDialog } from '@angular/material/dialog';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { OrganizationService } from '../organization/organization.service';
|
||||
|
||||
interface CalendarDay {
|
||||
date: number;
|
||||
month: number;
|
||||
year: number;
|
||||
isCurrentMonth: boolean;
|
||||
isToday: boolean;
|
||||
isWorking: boolean;
|
||||
isHoliday: boolean;
|
||||
isSunday: boolean;
|
||||
is2ndSaturday: boolean;
|
||||
is4thSaturday: boolean;
|
||||
holidayName?: string;
|
||||
dayOfWeek: number; // 0=Sun, 6=Sat
|
||||
}
|
||||
|
||||
interface MonthData {
|
||||
month: number;
|
||||
year: number;
|
||||
name: string;
|
||||
days: CalendarDay[];
|
||||
workingDays: number;
|
||||
nonWorkingDays: number;
|
||||
holidays: number;
|
||||
sundays: number;
|
||||
secondFourthSat: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-bank-calendar',
|
||||
templateUrl: './bank-calendar.component.html',
|
||||
styleUrls: ['./bank-calendar.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent,
|
||||
MatTooltip
|
||||
]
|
||||
})
|
||||
export class BankCalendarComponent implements OnInit {
|
||||
private organizationService = inject(OrganizationService);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
currentYear: number;
|
||||
months: MonthData[] = [];
|
||||
weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
workingDaysConfig: string[] = [];
|
||||
holidays: any[] = [];
|
||||
overallStats = { working: 0, nonWorking: 0, holidays: 0, sundays: 0, sat2nd4th: 0, totalDays: 0 };
|
||||
|
||||
/** Indian bank gazetted holidays by year */
|
||||
private readonly gazettedHolidays: Record<number, { month: number; day: number; name: string }[]> = {
|
||||
2025: [
|
||||
{ month: 1, day: 26, name: 'Republic Day' },
|
||||
{ month: 2, day: 26, name: 'Maha Shivaratri' },
|
||||
{ month: 3, day: 14, name: 'Holi' },
|
||||
{ month: 3, day: 31, name: 'Id-ul-Fitr (Ramzan Eid)' },
|
||||
{ month: 4, day: 6, name: 'Ram Navami' },
|
||||
{ month: 4, day: 10, name: 'Mahavir Jayanti' },
|
||||
{ month: 4, day: 14, name: 'Dr. Ambedkar Jayanti' },
|
||||
{ month: 4, day: 18, name: 'Good Friday' },
|
||||
{ month: 5, day: 1, name: 'May Day' },
|
||||
{ month: 5, day: 12, name: 'Buddha Purnima' },
|
||||
{ month: 6, day: 7, name: 'Id-ul-Zuha (Bakrid)' },
|
||||
{ month: 7, day: 6, name: 'Muharram' },
|
||||
{ month: 8, day: 15, name: 'Independence Day' },
|
||||
{ month: 8, day: 16, name: 'Janmashtami' },
|
||||
{ month: 9, day: 5, name: 'Milad-un-Nabi (Eid-e-Milad)' },
|
||||
{ month: 10, day: 2, name: 'Mahatma Gandhi Jayanti' },
|
||||
{ month: 10, day: 2, name: 'Dussehra (Vijaya Dashami)' },
|
||||
{ month: 10, day: 20, name: 'Diwali (Deepavali)' },
|
||||
{ month: 11, day: 5, name: 'Guru Nanak Jayanti' },
|
||||
{ month: 12, day: 25, name: 'Christmas Day' }
|
||||
],
|
||||
2026: [
|
||||
{ month: 1, day: 26, name: 'Republic Day' },
|
||||
{ month: 3, day: 3, name: 'Holi' },
|
||||
{ month: 3, day: 21, name: 'Id-ul-Fitr (Ramzan Eid)' },
|
||||
{ month: 3, day: 26, name: 'Ram Navami' },
|
||||
{ month: 3, day: 31, name: 'Mahavir Jayanti' },
|
||||
{ month: 4, day: 1, name: 'Annual Closing of Bank Accounts' },
|
||||
{ month: 4, day: 3, name: 'Good Friday' },
|
||||
{ month: 5, day: 1, name: 'Buddha Purnima / May Day' },
|
||||
{ month: 5, day: 27, name: 'Id-ul-Zuha (Bakrid)' },
|
||||
{ month: 6, day: 26, name: 'Muharram' },
|
||||
{ month: 8, day: 15, name: 'Independence Day' },
|
||||
{ month: 8, day: 26, name: 'Milad-un-Nabi (Eid-e-Milad)' },
|
||||
{ month: 9, day: 4, name: 'Janmashtami' },
|
||||
{ month: 10, day: 2, name: 'Mahatma Gandhi Jayanti' },
|
||||
{ month: 10, day: 20, name: 'Dussehra (Vijaya Dashami)' },
|
||||
{ month: 11, day: 8, name: 'Diwali (Deepavali)' },
|
||||
{ month: 11, day: 24, name: 'Guru Nanak Jayanti' },
|
||||
{ month: 12, day: 25, name: 'Christmas Day' }
|
||||
],
|
||||
2027: [
|
||||
{ month: 1, day: 26, name: 'Republic Day' },
|
||||
{ month: 2, day: 20, name: 'Holi' },
|
||||
{ month: 3, day: 11, name: 'Id-ul-Fitr (Ramzan Eid)' },
|
||||
{ month: 3, day: 15, name: 'Ram Navami' },
|
||||
{ month: 3, day: 21, name: 'Mahavir Jayanti' },
|
||||
{ month: 3, day: 26, name: 'Good Friday' },
|
||||
{ month: 4, day: 1, name: 'Annual Closing of Bank Accounts' },
|
||||
{ month: 5, day: 1, name: 'May Day' },
|
||||
{ month: 5, day: 2, name: 'Buddha Purnima' },
|
||||
{ month: 5, day: 17, name: 'Id-ul-Zuha (Bakrid)' },
|
||||
{ month: 6, day: 16, name: 'Muharram' },
|
||||
{ month: 8, day: 15, name: 'Independence Day' },
|
||||
{ month: 8, day: 16, name: 'Milad-un-Nabi (Eid-e-Milad)' },
|
||||
{ month: 8, day: 25, name: 'Janmashtami' },
|
||||
{ month: 10, day: 2, name: 'Mahatma Gandhi Jayanti' },
|
||||
{ month: 10, day: 9, name: 'Dussehra (Vijaya Dashami)' },
|
||||
{ month: 10, day: 28, name: 'Diwali (Deepavali)' },
|
||||
{ month: 11, day: 14, name: 'Guru Nanak Jayanti' },
|
||||
{ month: 12, day: 25, name: 'Christmas Day' }
|
||||
]
|
||||
};
|
||||
|
||||
ngOnInit() {
|
||||
this.currentYear = new Date().getFullYear();
|
||||
this.loadWorkingDays();
|
||||
}
|
||||
|
||||
private loadWorkingDays() {
|
||||
// Default Indian banking working days: Mon-Sat (Sun off, 2nd & 4th Sat off)
|
||||
this.workingDaysConfig = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
||||
this.mergeGazettedHolidays();
|
||||
this.buildCalendar();
|
||||
|
||||
// Try loading from API
|
||||
this.organizationService.getOffices().subscribe((offices: any[]) => {
|
||||
if (offices?.length > 0) {
|
||||
this.organizationService.getHolidays(offices[0].id.toString()).subscribe((holidayData: any[]) => {
|
||||
this.holidays = holidayData || [];
|
||||
this.mergeGazettedHolidays();
|
||||
this.buildCalendar();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Merge static gazetted holidays into the holidays array (API format) */
|
||||
private mergeGazettedHolidays() {
|
||||
const gazetted = this.gazettedHolidays[this.currentYear] || [];
|
||||
for (const gh of gazetted) {
|
||||
const exists = this.holidays.some((h: any) => {
|
||||
if (h.fromDate) {
|
||||
const [y, m, d] = h.fromDate;
|
||||
return y === this.currentYear && m === gh.month && d === gh.day;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!exists) {
|
||||
this.holidays.push({
|
||||
name: gh.name,
|
||||
fromDate: [this.currentYear, gh.month, gh.day],
|
||||
toDate: [this.currentYear, gh.month, gh.day],
|
||||
description: gh.name,
|
||||
status: { value: 'Active' }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildCalendar() {
|
||||
this.months = [];
|
||||
this.overallStats = { working: 0, nonWorking: 0, holidays: 0, sundays: 0, sat2nd4th: 0, totalDays: 0 };
|
||||
|
||||
for (let m = 0; m < 12; m++) {
|
||||
const monthData = this.buildMonth(m, this.currentYear);
|
||||
this.months.push(monthData);
|
||||
this.overallStats.working += monthData.workingDays;
|
||||
this.overallStats.nonWorking += monthData.nonWorkingDays;
|
||||
this.overallStats.holidays += monthData.holidays;
|
||||
this.overallStats.sundays += monthData.sundays;
|
||||
this.overallStats.sat2nd4th += monthData.secondFourthSat;
|
||||
}
|
||||
this.overallStats.totalDays = this.overallStats.working + this.overallStats.nonWorking;
|
||||
}
|
||||
|
||||
private buildMonth(month: number, year: number): MonthData {
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const startDayOfWeek = firstDay.getDay();
|
||||
const today = new Date();
|
||||
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
|
||||
const days: CalendarDay[] = [];
|
||||
let workingCount = 0;
|
||||
let nonWorkingCount = 0;
|
||||
let holidayCount = 0;
|
||||
let sundayCount = 0;
|
||||
let sat2nd4thCount = 0;
|
||||
|
||||
// Previous month padding
|
||||
const prevMonthDays = new Date(year, month, 0).getDate();
|
||||
for (let i = startDayOfWeek - 1; i >= 0; i--) {
|
||||
days.push({
|
||||
date: prevMonthDays - i,
|
||||
month: month - 1,
|
||||
year,
|
||||
isCurrentMonth: false,
|
||||
isToday: false,
|
||||
isWorking: false,
|
||||
isHoliday: false,
|
||||
isSunday: false,
|
||||
is2ndSaturday: false,
|
||||
is4thSaturday: false,
|
||||
dayOfWeek: (startDayOfWeek - i - 1 + 7) % 7
|
||||
});
|
||||
}
|
||||
|
||||
// Current month days
|
||||
let saturdayCount = 0;
|
||||
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;
|
||||
|
||||
if (isSaturday) saturdayCount++;
|
||||
|
||||
const is2ndSat = isSaturday && saturdayCount === 2;
|
||||
const is4thSat = isSaturday && saturdayCount === 4;
|
||||
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;
|
||||
|
||||
if (isSunday) sundayCount++;
|
||||
if (is2ndSat || is4thSat) sat2nd4thCount++;
|
||||
if (isHoliday) holidayCount++;
|
||||
|
||||
if (isNonWorking) {
|
||||
nonWorkingCount++;
|
||||
} else {
|
||||
workingCount++;
|
||||
}
|
||||
|
||||
days.push({
|
||||
date: d,
|
||||
month,
|
||||
year,
|
||||
isCurrentMonth: true,
|
||||
isToday,
|
||||
isWorking: !isNonWorking,
|
||||
isHoliday,
|
||||
isSunday,
|
||||
is2ndSaturday: is2ndSat,
|
||||
is4thSaturday: is4thSat,
|
||||
holidayName: holiday?.name,
|
||||
dayOfWeek: dow
|
||||
});
|
||||
}
|
||||
|
||||
// Next month padding
|
||||
const remaining = 42 - days.length;
|
||||
for (let i = 1; i <= remaining; i++) {
|
||||
days.push({
|
||||
date: i,
|
||||
month: month + 1,
|
||||
year,
|
||||
isCurrentMonth: false,
|
||||
isToday: false,
|
||||
isWorking: false,
|
||||
isHoliday: false,
|
||||
isSunday: false,
|
||||
is2ndSaturday: false,
|
||||
is4thSaturday: false,
|
||||
dayOfWeek: (days.length) % 7
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
month,
|
||||
year,
|
||||
name: monthNames[month],
|
||||
days,
|
||||
workingDays: workingCount,
|
||||
nonWorkingDays: nonWorkingCount,
|
||||
holidays: holidayCount,
|
||||
sundays: sundayCount,
|
||||
secondFourthSat: sat2nd4thCount
|
||||
};
|
||||
}
|
||||
|
||||
private getHolidayForDate(year: number, month: number, day: number): any {
|
||||
return this.holidays.find((h: any) => {
|
||||
if (h.fromDate) {
|
||||
const [y, m, d] = h.fromDate;
|
||||
return y === year && m === month + 1 && d === day;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
previousYear() {
|
||||
this.currentYear--;
|
||||
this.holidays = [];
|
||||
this.mergeGazettedHolidays();
|
||||
this.buildCalendar();
|
||||
}
|
||||
|
||||
nextYear() {
|
||||
this.currentYear++;
|
||||
this.holidays = [];
|
||||
this.mergeGazettedHolidays();
|
||||
this.buildCalendar();
|
||||
}
|
||||
|
||||
goToCurrentYear() {
|
||||
this.currentYear = new Date().getFullYear();
|
||||
this.holidays = [];
|
||||
this.mergeGazettedHolidays();
|
||||
this.buildCalendar();
|
||||
}
|
||||
|
||||
printHolidayList() {
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
// --- Section 1: Gazetted / API Holidays ---
|
||||
const gazettedList: { sno: number; date: string; day: string; occasion: string }[] = [];
|
||||
const sortedHolidays = [...this.holidays].sort((a: any, b: any) => {
|
||||
const da = a.fromDate[0] * 10000 + a.fromDate[1] * 100 + a.fromDate[2];
|
||||
const db = b.fromDate[0] * 10000 + b.fromDate[1] * 100 + b.fromDate[2];
|
||||
return da - db;
|
||||
});
|
||||
|
||||
sortedHolidays.forEach((h: any, i: number) => {
|
||||
if (h.fromDate) {
|
||||
const [y, m, d] = h.fromDate;
|
||||
const date = new Date(y, m - 1, d);
|
||||
gazettedList.push({
|
||||
sno: i + 1,
|
||||
date: `${monthNames[m - 1]} ${String(d).padStart(2, '0')}, ${y}`,
|
||||
day: dayNames[date.getDay()],
|
||||
occasion: h.name || 'Holiday'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- Section 2: 2nd & 4th Saturdays ---
|
||||
const satList: { sno: number; date: string; day: string; occasion: string }[] = [];
|
||||
let satSno = 0;
|
||||
for (let m = 0; m < 12; m++) {
|
||||
const daysInMonth = new Date(this.currentYear, m + 1, 0).getDate();
|
||||
let satCount = 0;
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const date = new Date(this.currentYear, m, d);
|
||||
if (date.getDay() === 6) {
|
||||
satCount++;
|
||||
if (satCount === 2 || satCount === 4) {
|
||||
satSno++;
|
||||
satList.push({
|
||||
sno: satSno,
|
||||
date: `${monthNames[m]} ${String(d).padStart(2, '0')}, ${this.currentYear}`,
|
||||
day: 'Saturday',
|
||||
occasion: satCount === 2 ? '2nd Saturday Off' : '4th Saturday Off'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Section 3: Sundays ---
|
||||
const sunList: { sno: number; date: string; day: string; occasion: string }[] = [];
|
||||
let sunSno = 0;
|
||||
for (let m = 0; m < 12; m++) {
|
||||
const daysInMonth = new Date(this.currentYear, m + 1, 0).getDate();
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const date = new Date(this.currentYear, m, d);
|
||||
if (date.getDay() === 0) {
|
||||
sunSno++;
|
||||
sunList.push({
|
||||
sno: sunSno,
|
||||
date: `${monthNames[m]} ${String(d).padStart(2, '0')}, ${this.currentYear}`,
|
||||
day: 'Sunday',
|
||||
occasion: 'Weekly Off'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buildRows = (list: typeof gazettedList) =>
|
||||
list.map(h => `<tr><td>${h.sno}</td><td>${h.date}</td><td>${h.day}</td><td>${h.occasion}</td></tr>`).join('');
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Holiday List ${this.currentYear} — CBS Nexora</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; padding: 30px 40px; color: #1a1a2e; }
|
||||
.header { text-align: center; margin-bottom: 24px; border-bottom: 3px solid #0d47a1; padding-bottom: 16px; }
|
||||
.header h1 { font-size: 20px; color: #0d47a1; margin-bottom: 4px; letter-spacing: 0.5px; }
|
||||
.header h2 { font-size: 15px; color: #475569; font-weight: 500; }
|
||||
.header .year { font-size: 26px; font-weight: 800; color: #0d1b2a; margin-top: 6px; }
|
||||
.stats { display: flex; justify-content: center; gap: 24px; margin-bottom: 24px; font-size: 13px; flex-wrap: wrap; }
|
||||
.stats span { padding: 5px 14px; border-radius: 6px; font-weight: 600; }
|
||||
.stats .gh { background: #f5f3ff; color: #7c3aed; }
|
||||
.stats .sat { background: #fff7ed; color: #c2410c; }
|
||||
.stats .sun { background: #fef2f2; color: #ef4444; }
|
||||
.stats .total { background: #eff6ff; color: #1e88e5; }
|
||||
.section-title { font-size: 14px; font-weight: 700; color: #0d47a1; margin: 22px 0 8px; padding: 6px 12px; background: #e3f2fd; border-radius: 6px; border-left: 4px solid #0d47a1; display: flex; align-items: center; gap: 8px; }
|
||||
.section-title .count { margin-left: auto; font-size: 12px; background: #0d47a1; color: #fff; padding: 2px 10px; border-radius: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin-bottom: 6px; }
|
||||
th { background: #0d47a1; color: #fff; padding: 9px 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
td { padding: 7px 12px; border-bottom: 1px solid #e2e8f0; }
|
||||
tr:nth-child(even) { background: #f8fafc; }
|
||||
td:first-child, th:first-child { text-align: center; width: 55px; }
|
||||
th:nth-child(2), td:nth-child(2) { width: 200px; }
|
||||
th:nth-child(3), td:nth-child(3) { width: 120px; }
|
||||
.footer { text-align: center; margin-top: 28px; font-size: 11px; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 12px; }
|
||||
@media print {
|
||||
body { padding: 15px 20px; }
|
||||
.section-title { break-inside: avoid; }
|
||||
table { page-break-inside: auto; }
|
||||
tr { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>CBS Nexora — Bank Holiday List</h1>
|
||||
<h2>Gazetted Holidays, 2nd/4th Saturdays & Sundays</h2>
|
||||
<div class="year">${this.currentYear}</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<span class="gh">Gazetted Holidays: ${gazettedList.length}</span>
|
||||
<span class="sat">2nd/4th Saturday Offs: ${satList.length}</span>
|
||||
<span class="sun">Sundays: ${sunList.length}</span>
|
||||
<span class="total">Total Non-Working Days: ${gazettedList.length + satList.length + sunList.length}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Gazetted / Bank Holidays <span class="count">${gazettedList.length}</span></div>
|
||||
<table>
|
||||
<thead><tr><th>Sr. No.</th><th>Date</th><th>Day</th><th>Holiday / Occasion</th></tr></thead>
|
||||
<tbody>${buildRows(gazettedList)}</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">2nd & 4th Saturday Offs <span class="count">${satList.length}</span></div>
|
||||
<table>
|
||||
<thead><tr><th>Sr. No.</th><th>Date</th><th>Day</th><th>Holiday / Occasion</th></tr></thead>
|
||||
<tbody>${buildRows(satList)}</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Sundays (Weekly Off) <span class="count">${sunList.length}</span></div>
|
||||
<table>
|
||||
<thead><tr><th>Sr. No.</th><th>Date</th><th>Day</th><th>Holiday / Occasion</th></tr></thead>
|
||||
<tbody>${buildRows(sunList)}</tbody>
|
||||
</table>
|
||||
|
||||
<div class="footer">Generated on ${new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'long', year: 'numeric' })} • CBS Nexora • Internal Use Only</div>
|
||||
<script>window.onload = function() { window.print(); }</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const printWindow = window.open('', '_blank');
|
||||
if (printWindow) {
|
||||
printWindow.document.write(html);
|
||||
printWindow.document.close();
|
||||
}
|
||||
}
|
||||
|
||||
async openAddHolidayDialog() {
|
||||
const { AddHolidayDialogComponent } = await import('./add-holiday-dialog/add-holiday-dialog.component');
|
||||
const dialogRef = this.dialog.open(AddHolidayDialogComponent, {
|
||||
width: '600px',
|
||||
maxWidth: '95vw',
|
||||
maxHeight: '90vh',
|
||||
autoFocus: false,
|
||||
panelClass: 'add-holiday-dialog-panel'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
||||
if (result) {
|
||||
// Reload holidays after successful creation
|
||||
this.loadWorkingDays();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { SharedModule } from '../shared/shared.module';
|
||||
import { BankCalendarRoutingModule } from './bank-calendar-routing.module';
|
||||
import { BankCalendarComponent } from './bank-calendar.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
BankCalendarRoutingModule,
|
||||
BankCalendarComponent
|
||||
]
|
||||
})
|
||||
export class BankCalendarModule {}
|
||||
Reference in New Issue
Block a user