feat(holiday-calendar): implement holiday calendar component with CRUD functionality

- Added a new `HolidayCalendar` component for managing custom and official holidays.
- Integrated holiday type fetching and population logic to streamline holiday management.
- Enhanced UI with modals for creating, editing, and deleting holidays, including validation and user feedback.
- Updated styles for improved visual presentation and user interaction.
- Refactored sidebar and routes to accommodate new component structure and localization updates.
This commit is contained in:
Chamika J
2025-08-01 16:54:58 +05:30
parent 1a64115063
commit 8f407b45a9
12 changed files with 121 additions and 59 deletions

View File

@@ -4,6 +4,6 @@
"teams": "Teams", "teams": "Teams",
"billing": "Billing", "billing": "Billing",
"projects": "Projects", "projects": "Projects",
"settings": "Settings", "settings": "Utilization Settings",
"adminCenter": "Admin Center" "adminCenter": "Admin Center"
} }

View File

@@ -15,6 +15,8 @@ import {
ICombinedHolidaysRequest, ICombinedHolidaysRequest,
IHolidayDateRange, IHolidayDateRange,
} from '@/types/holiday/holiday.types'; } from '@/types/holiday/holiday.types';
import logger from '@/utils/errorLogger';
import { error } from 'console';
const rootUrl = `${API_BASE_URL}/holidays`; const rootUrl = `${API_BASE_URL}/holidays`;
@@ -764,9 +766,7 @@ export const holidayApiService = {
} }
// Get organization holidays from database (includes both custom and country-specific) // Get organization holidays from database (includes both custom and country-specific)
console.log(`🏢 Fetching organization holidays for year: ${year}`);
const customRes = await holidayApiService.getOrganizationHolidays(year); const customRes = await holidayApiService.getOrganizationHolidays(year);
console.log('🏢 Organization holidays response:', customRes);
if (customRes.done && customRes.body) { if (customRes.done && customRes.body) {
const customHolidays = customRes.body const customHolidays = customRes.body
@@ -777,6 +777,7 @@ export const holidayApiService = {
description: h.description, description: h.description,
date: h.date, date: h.date,
is_recurring: h.is_recurring, is_recurring: h.is_recurring,
holiday_type_id: h.holiday_type_id,
holiday_type_name: h.holiday_type_name || 'Custom', holiday_type_name: h.holiday_type_name || 'Custom',
color_code: h.color_code || '#f37070', color_code: h.color_code || '#f37070',
source: h.source || 'custom' as const, source: h.source || 'custom' as const,
@@ -787,19 +788,14 @@ export const holidayApiService = {
const existingDates = new Set(allHolidays.map(h => h.date)); const existingDates = new Set(allHolidays.map(h => h.date));
const uniqueCustomHolidays = customHolidays.filter((h: any) => !existingDates.has(h.date)); const uniqueCustomHolidays = customHolidays.filter((h: any) => !existingDates.has(h.date));
console.log(`✅ Found ${customHolidays.length} organization holidays (${uniqueCustomHolidays.length} unique)`);
allHolidays.push(...uniqueCustomHolidays); allHolidays.push(...uniqueCustomHolidays);
} else {
console.log('⚠️ No organization holidays returned from API');
} }
console.log(`🎉 Total holidays combined: ${allHolidays.length}`, allHolidays);
return { return {
done: true, done: true,
body: allHolidays, body: allHolidays,
} as IServerResponse<IHolidayCalendarEvent[]>; } as IServerResponse<IHolidayCalendarEvent[]>;
} catch (error) { } catch (error) {
console.error('Error fetching combined holidays:', error); logger.error('Error fetching combined holidays:', error);
return { return {
done: false, done: false,
body: [], body: [],

View File

@@ -1,6 +1,6 @@
import { RouteObject } from 'react-router-dom'; import { RouteObject } from 'react-router-dom';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { adminCenterItems } from '@/pages/admin-center/admin-center-constants'; import { adminCenterItems } from '@/lib/admin-center-constants';
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { useAuthService } from '@/hooks/useAuth'; import { useAuthService } from '@/hooks/useAuth';
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback'; import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';

View File

@@ -44,7 +44,7 @@ interface HolidayCalendarProps {
const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDays = [] }) => { const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDays = [] }) => {
const { t } = useTranslation('admin-center/overview'); const { t } = useTranslation('admin-center/overview');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { holidays, loadingHolidays, holidaySettings } = useAppSelector( const { holidays, holidaySettings } = useAppSelector(
(state: RootState) => state.adminCenterReducer (state: RootState) => state.adminCenterReducer
); );
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -57,6 +57,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
const [currentDate, setCurrentDate] = useState<Dayjs>(dayjs()); const [currentDate, setCurrentDate] = useState<Dayjs>(dayjs());
const [isPopulatingHolidays, setIsPopulatingHolidays] = useState(false); const [isPopulatingHolidays, setIsPopulatingHolidays] = useState(false);
const [hasAttemptedPopulation, setHasAttemptedPopulation] = useState(false); const [hasAttemptedPopulation, setHasAttemptedPopulation] = useState(false);
const [isNavigating, setIsNavigating] = useState(false);
const fetchHolidayTypes = async () => { const fetchHolidayTypes = async () => {
try { try {
@@ -79,18 +80,16 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
!isPopulatingHolidays !isPopulatingHolidays
) { ) {
try { try {
console.log('🔄 No holidays found, attempting to populate official holidays...');
setIsPopulatingHolidays(true); setIsPopulatingHolidays(true);
setHasAttemptedPopulation(true); setHasAttemptedPopulation(true);
const populateRes = await holidayApiService.populateCountryHolidays(); const populateRes = await holidayApiService.populateCountryHolidays();
if (populateRes.done) { if (populateRes.done) {
console.log('✅ Official holidays populated successfully');
// Refresh holidays after population // Refresh holidays after population
fetchHolidaysForDateRange(true); fetchHolidaysForDateRange(true);
} }
} catch (error) { } catch (error) {
console.warn('⚠️ Could not populate official holidays:', error); logger.error('populateHolidaysIfNeeded', error);
} finally { } finally {
setIsPopulatingHolidays(false); setIsPopulatingHolidays(false);
} }
@@ -217,7 +216,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
name: holiday.name, name: holiday.name,
description: holiday.description, description: holiday.description,
date: dayjs(holiday.date), date: dayjs(holiday.date),
holiday_type_id: holiday.holiday_type_name, // This might need adjustment based on backend holiday_type_id: holiday.holiday_type_id,
is_recurring: holiday.is_recurring, is_recurring: holiday.is_recurring,
}); });
setEditModalVisible(true); setEditModalVisible(true);
@@ -227,12 +226,15 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
const dateHolidays = holidays.filter(h => dayjs(h.date).isSame(date, 'day')); const dateHolidays = holidays.filter(h => dayjs(h.date).isSame(date, 'day'));
const dayName = date.format('dddd'); const dayName = date.format('dddd');
// Check if this day is in the working days array from API response // Check if this day is in the working days array from API response
const isWorkingDay = workingDays && workingDays.length > 0 ? workingDays.includes(dayName) : false; const isWorkingDay =
workingDays && workingDays.length > 0 ? workingDays.includes(dayName) : false;
const isToday = date.isSame(dayjs(), 'day'); const isToday = date.isSame(dayjs(), 'day');
const isCurrentMonth = date.isSame(currentDate, 'month'); const isCurrentMonth = date.isSame(currentDate, 'month');
return ( return (
<div className={`calendar-cell ${isWorkingDay ? 'working-day' : 'non-working-day'} ${isToday ? 'today' : ''} ${!isCurrentMonth ? 'other-month' : ''}`}> <div
className={`calendar-cell ${isWorkingDay ? 'working-day' : 'non-working-day'} ${isToday ? 'today' : ''} ${!isCurrentMonth ? 'other-month' : ''}`}
>
{dateHolidays.length > 0 && ( {dateHolidays.length > 0 && (
<div className="holiday-cell"> <div className="holiday-cell">
{dateHolidays.map((holiday, index) => { {dateHolidays.map((holiday, index) => {
@@ -250,16 +252,22 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
borderRadius: '4px', borderRadius: '4px',
display: 'block', display: 'block',
fontWeight: isCustom ? 600 : 500, fontWeight: isCustom ? 600 : 500,
border: isCustom ? '1px solid rgba(82, 196, 26, 0.6)' : '1px solid rgba(24, 144, 255, 0.4)', border: isCustom
? '1px solid rgba(82, 196, 26, 0.6)'
: '1px solid rgba(24, 144, 255, 0.4)',
position: 'relative', position: 'relative',
}} }}
title={`${holiday.name}${isOfficial ? ' (Official Holiday)' : ' (Custom Holiday)'}`} title={`${holiday.name}${isOfficial ? ' (Official Holiday)' : ' (Custom Holiday)'}`}
> >
{isCustom && ( {isCustom && (
<span className="custom-holiday-icon" style={{ marginRight: '2px' }}></span> <span className="custom-holiday-icon" style={{ marginRight: '2px' }}>
</span>
)} )}
{isOfficial && ( {isOfficial && (
<span className="official-holiday-icon" style={{ marginRight: '2px' }}>🏛</span> <span className="official-holiday-icon" style={{ marginRight: '2px' }}>
🏛
</span>
)} )}
{holiday.name} {holiday.name}
</Tag> </Tag>
@@ -277,10 +285,23 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
}; };
const onPanelChange = (value: Dayjs) => { const onPanelChange = (value: Dayjs) => {
setIsNavigating(true);
setCurrentDate(value); setCurrentDate(value);
// Reset navigation flag after a short delay to allow the onSelect event to check it
setTimeout(() => setIsNavigating(false), 100);
}; };
const onDateSelect = (date: Dayjs) => { const onDateSelect = (date: Dayjs) => {
// Prevent modal from opening during navigation (month/year changes)
if (isNavigating) {
return;
}
// Prevent modal from opening if the date is from a different month (navigation click)
if (!date.isSame(currentDate, 'month')) {
return;
}
// Check if there's already a custom holiday on this date // Check if there's already a custom holiday on this date
const existingCustomHoliday = holidays.find( const existingCustomHoliday = holidays.find(
h => dayjs(h.date).isSame(date, 'day') && h.source === 'custom' && h.is_editable h => dayjs(h.date).isSame(date, 'day') && h.source === 'custom' && h.is_editable
@@ -336,11 +357,11 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
boxShadow: '0 2px 8px rgba(24, 144, 255, 0.2)', boxShadow: '0 2px 8px rgba(24, 144, 255, 0.2)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
}} }}
onMouseEnter={(e) => { onMouseEnter={e => {
e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(24, 144, 255, 0.3)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(24, 144, 255, 0.3)';
}} }}
onMouseLeave={(e) => { onMouseLeave={e => {
e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(24, 144, 255, 0.2)'; e.currentTarget.style.boxShadow = '0 2px 8px rgba(24, 144, 255, 0.2)';
}} }}
@@ -349,7 +370,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
</Button> </Button>
</div> </div>
<div className="calendar-container"> <div className={`calendar-container holiday-calendar ${themeMode}`}>
<Calendar <Calendar
value={currentDate} value={currentDate}
onPanelChange={onPanelChange} onPanelChange={onPanelChange}
@@ -474,7 +495,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
setSelectedHoliday(null); setSelectedHoliday(null);
}} }}
footer={null} footer={null}
destroyOnClose destroyOnHidden
> >
<Form form={editForm} layout="vertical" onFinish={handleUpdateHoliday}> <Form form={editForm} layout="vertical" onFinish={handleUpdateHoliday}>
<Form.Item <Form.Item
@@ -540,18 +561,22 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
> >
{t('cancel')} {t('cancel')}
</Button> </Button>
{selectedHoliday && selectedHoliday.source === 'custom' && selectedHoliday.is_editable && ( {selectedHoliday &&
<Popconfirm selectedHoliday.source === 'custom' &&
title={t('deleteHolidayConfirm') || 'Are you sure you want to delete this holiday?'} selectedHoliday.is_editable && (
onConfirm={() => handleDeleteHoliday(selectedHoliday.id)} <Popconfirm
okText={t('yes') || 'Yes'} title={
cancelText={t('no') || 'No'} t('deleteHolidayConfirm') || 'Are you sure you want to delete this holiday?'
> }
<Button type="primary" danger icon={<DeleteOutlined />}> onConfirm={() => handleDeleteHoliday(selectedHoliday.id)}
{t('delete') || 'Delete'} okText={t('yes') || 'Yes'}
</Button> cancelText={t('no') || 'No'}
</Popconfirm> >
)} <Button type="primary" danger icon={<DeleteOutlined />}>
{t('delete') || 'Delete'}
</Button>
</Popconfirm>
)}
</Space> </Space>
</Form.Item> </Form.Item>
</Form> </Form>

View File

@@ -493,11 +493,13 @@
background: #fafafa; background: #fafafa;
border-radius: 6px; border-radius: 6px;
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
} }
.holiday-calendar.dark .calendar-legend { .holiday-calendar.dark .calendar-legend {
background: #1f1f1f; background: #1f1f1f;
border-color: #434343; border-color: #434343;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
} }
.legend-item { .legend-item {
@@ -505,11 +507,33 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 12px; font-size: 12px;
color: rgba(0, 0, 0, 0.65); color: rgba(0, 0, 0, 0.85);
font-weight: 500;
} }
.holiday-calendar.dark .legend-item { .holiday-calendar.dark .legend-item {
color: rgba(255, 255, 255, 0.65); color: rgba(255, 255, 255, 0.85);
}
/* Legend item hover effects */
.legend-item:hover {
transform: translateY(-1px);
transition: transform 0.2s ease;
}
.legend-item:hover .legend-badge {
transform: scale(1.1);
transition: transform 0.2s ease;
}
.legend-item:hover .legend-tag {
transform: scale(1.05);
transition: transform 0.2s ease;
}
.legend-item:hover .legend-dot {
transform: scale(1.2);
transition: transform 0.2s ease;
} }
.legend-dot { .legend-dot {
@@ -531,6 +555,7 @@
.legend-dot.today-dot { .legend-dot.today-dot {
background: #1890ff; background: #1890ff;
box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.2); box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.2);
animation: todayPulse 2s ease-in-out infinite;
} }
.holiday-calendar.dark .legend-dot.working-day-dot { .holiday-calendar.dark .legend-dot.working-day-dot {
@@ -546,6 +571,7 @@
.holiday-calendar.dark .legend-dot.today-dot { .holiday-calendar.dark .legend-dot.today-dot {
background: #1890ff; background: #1890ff;
box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.3); box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.3);
animation: todayPulse 2s ease-in-out infinite;
} }
/* Legend badge and tag styles */ /* Legend badge and tag styles */
@@ -559,19 +585,22 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
transition: all 0.2s ease;
} }
.legend-badge.working-day-badge { .legend-badge.working-day-badge {
background: #52c41a; background: #52c41a;
border: 1px solid #d9f7be; border: 1px solid #d9f7be;
color: #ffffff; color: #ffffff;
box-shadow: 0 1px 3px rgba(82, 196, 26, 0.2);
} }
.holiday-calendar.dark .legend-badge.working-day-badge { .holiday-calendar.dark .legend-badge.working-day-badge {
background: #52c41a; background: #52c41a;
border: 1px solid #237804; border: 1px solid #237804;
color: #ffffff; color: #ffffff;
box-shadow: 0 1px 3px rgba(82, 196, 26, 0.3);
} }
.legend-tag { .legend-tag {
@@ -583,35 +612,41 @@
font-size: 9px; font-size: 9px;
font-weight: 500; font-weight: 500;
border: 1px solid; border: 1px solid;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
} }
.legend-tag-text { .legend-tag-text {
font-size: 8px; font-size: 8px;
line-height: 1; line-height: 1;
font-weight: 600;
} }
.custom-holiday-legend { .custom-holiday-legend {
background: #f6ffed; background: #f6ffed;
border: 1px solid #b7eb8f; border: 1px solid #b7eb8f;
color: #52c41a; color: #52c41a;
box-shadow: 0 1px 2px rgba(82, 196, 26, 0.1);
} }
.official-holiday-legend { .official-holiday-legend {
background: #e6f7ff; background: #e6f7ff;
border: 1px solid #91d5ff; border: 1px solid #91d5ff;
color: #1890ff; color: #1890ff;
box-shadow: 0 1px 2px rgba(24, 144, 255, 0.1);
} }
.holiday-calendar.dark .custom-holiday-legend { .holiday-calendar.dark .custom-holiday-legend {
background: #162312; background: #162312;
border: 1px solid #274916; border: 1px solid #274916;
color: #95de64; color: #95de64;
box-shadow: 0 1px 2px rgba(149, 222, 100, 0.2);
} }
.holiday-calendar.dark .official-holiday-legend { .holiday-calendar.dark .official-holiday-legend {
background: #111b26; background: #111b26;
border: 1px solid #13334c; border: 1px solid #13334c;
color: #69c0ff; color: #69c0ff;
box-shadow: 0 1px 2px rgba(105, 192, 255, 0.2);
} }
.legend-tag .custom-holiday-icon, .legend-tag .custom-holiday-icon,
@@ -646,6 +681,16 @@
padding: 10px 12px; padding: 10px 12px;
} }
.legend-item:hover {
transform: none;
}
.legend-item:hover .legend-badge,
.legend-item:hover .legend-tag,
.legend-item:hover .legend-dot {
transform: none;
}
.working-day-badge { .working-day-badge {
width: 14px; width: 14px;
height: 14px; height: 14px;

View File

@@ -7,18 +7,14 @@ import { useTranslation } from 'react-i18next';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
const AdminCenterLayout: React.FC = () => { const AdminCenterLayout: React.FC = () => {
const dispatch = useAppDispatch();
const isTablet = useMediaQuery({ query: '(min-width:768px)' }); const isTablet = useMediaQuery({ query: '(min-width:768px)' });
const isMarginAvailable = useMediaQuery({ query: '(min-width: 1000px)' });
const { t } = useTranslation('admin-center/sidebar'); const { t } = useTranslation('admin-center/sidebar');
return ( return (
<div <div
style={{ style={{
marginBlock: 96, marginBlock: 24,
minHeight: '90vh', minHeight: '90vh',
marginLeft: `${isMarginAvailable ? '5%' : ''}`,
marginRight: `${isMarginAvailable ? '5%' : ''}`,
}} }}
> >
<Typography.Title level={4}>{t('adminCenter')}</Typography.Title> <Typography.Title level={4}>{t('adminCenter')}</Typography.Title>

View File

@@ -12,7 +12,7 @@ const SettingsLayout = () => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div style={{ marginBlock: 96, minHeight: '90vh' }}> <div style={{ marginBlock: 24, minHeight: '90vh' }}>
<Typography.Title level={4}>Settings</Typography.Title> <Typography.Title level={4}>Settings</Typography.Title>
{isTablet ? ( {isTablet ? (

View File

@@ -7,12 +7,12 @@ import {
SettingOutlined, SettingOutlined,
} from '@/shared/antd-imports'; } from '@/shared/antd-imports';
import React, { ReactNode, lazy } from 'react'; import React, { ReactNode, lazy } from 'react';
const Overview = lazy(() => import('./overview/overview')); const Overview = lazy(() => import('../pages/admin-center/overview/overview'));
const Users = lazy(() => import('./users/users')); const Users = lazy(() => import('../pages/admin-center/users/users'));
const Teams = lazy(() => import('./teams/teams')); const Teams = lazy(() => import('../pages/admin-center/teams/teams'));
const Billing = lazy(() => import('./billing/billing')); const Billing = lazy(() => import('../pages/admin-center/billing/billing'));
const Projects = lazy(() => import('./projects/projects')); const Projects = lazy(() => import('../pages/admin-center/projects/projects'));
const Settings = lazy(() => import('./settings/settings')); const Settings = lazy(() => import('../pages/admin-center/settings/Settings'));
// type of a menu item in admin center sidebar // type of a menu item in admin center sidebar
type AdminCenterMenuItems = { type AdminCenterMenuItems = {

View File

@@ -1 +0,0 @@
export { default } from './settings';

View File

@@ -20,7 +20,7 @@ import { scheduleAPIService } from '@/api/schedule/schedule.api.service';
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service'; import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
import { Settings } from '@/types/schedule/schedule-v2.types'; import { Settings } from '@/types/schedule/schedule-v2.types';
import OrganizationCalculationMethod from '@/components/admin-center/overview/organization-calculation-method/organization-calculation-method'; import OrganizationCalculationMethod from '@/components/admin-center/overview/organization-calculation-method/organization-calculation-method';
import HolidayCalendar from '@/components/admin-center/overview/holiday-calendar/holiday-calendar'; import HolidayCalendar from '@/components/admin-center/overview/holiday-calendar/HolidayCalendar';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { RootState } from '@/app/store'; import { RootState } from '@/app/store';

View File

@@ -4,7 +4,7 @@ import React from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { colors } from '../../../styles/colors'; import { colors } from '../../../styles/colors';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { adminCenterItems } from '../admin-center-constants'; import { adminCenterItems } from '../../../lib/admin-center-constants';
import './sidebar.css'; import './sidebar.css';
const AdminCenterSidebar: React.FC = () => { const AdminCenterSidebar: React.FC = () => {

View File

@@ -91,6 +91,7 @@ export interface IHolidayCalendarEvent {
description?: string; description?: string;
date: string; date: string;
is_recurring: boolean; is_recurring: boolean;
holiday_type_id?: string;
holiday_type_name: string; holiday_type_name: string;
color_code: string; color_code: string;
source: 'official' | 'custom'; source: 'official' | 'custom';