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:
Aditya
2026-04-04 15:10:51 +05:30
co-authored by Claude Opus 4.6
parent ab01111424
commit b890d24bb7
522 changed files with 51991 additions and 21885 deletions
@@ -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 &amp; 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 &amp; 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' })} &bull; CBS Nexora &bull; 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();
}
});
}
}