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:
@@ -4,6 +4,6 @@
|
||||
"teams": "Teams",
|
||||
"billing": "Billing",
|
||||
"projects": "Projects",
|
||||
"settings": "Settings",
|
||||
"settings": "Utilization Settings",
|
||||
"adminCenter": "Admin Center"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
ICombinedHolidaysRequest,
|
||||
IHolidayDateRange,
|
||||
} from '@/types/holiday/holiday.types';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { error } from 'console';
|
||||
|
||||
const rootUrl = `${API_BASE_URL}/holidays`;
|
||||
|
||||
@@ -764,9 +766,7 @@ export const holidayApiService = {
|
||||
}
|
||||
|
||||
// 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);
|
||||
console.log('🏢 Organization holidays response:', customRes);
|
||||
|
||||
if (customRes.done && customRes.body) {
|
||||
const customHolidays = customRes.body
|
||||
@@ -777,6 +777,7 @@ export const holidayApiService = {
|
||||
description: h.description,
|
||||
date: h.date,
|
||||
is_recurring: h.is_recurring,
|
||||
holiday_type_id: h.holiday_type_id,
|
||||
holiday_type_name: h.holiday_type_name || 'Custom',
|
||||
color_code: h.color_code || '#f37070',
|
||||
source: h.source || 'custom' as const,
|
||||
@@ -787,19 +788,14 @@ export const holidayApiService = {
|
||||
const existingDates = new Set(allHolidays.map(h => 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);
|
||||
} else {
|
||||
console.log('⚠️ No organization holidays returned from API');
|
||||
}
|
||||
|
||||
console.log(`🎉 Total holidays combined: ${allHolidays.length}`, allHolidays);
|
||||
return {
|
||||
done: true,
|
||||
body: allHolidays,
|
||||
} as IServerResponse<IHolidayCalendarEvent[]>;
|
||||
} catch (error) {
|
||||
console.error('Error fetching combined holidays:', error);
|
||||
logger.error('Error fetching combined holidays:', error);
|
||||
return {
|
||||
done: false,
|
||||
body: [],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
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 { useAuthService } from '@/hooks/useAuth';
|
||||
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||
|
||||
@@ -44,7 +44,7 @@ interface HolidayCalendarProps {
|
||||
const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDays = [] }) => {
|
||||
const { t } = useTranslation('admin-center/overview');
|
||||
const dispatch = useAppDispatch();
|
||||
const { holidays, loadingHolidays, holidaySettings } = useAppSelector(
|
||||
const { holidays, holidaySettings } = useAppSelector(
|
||||
(state: RootState) => state.adminCenterReducer
|
||||
);
|
||||
const [form] = Form.useForm();
|
||||
@@ -57,6 +57,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
|
||||
const [currentDate, setCurrentDate] = useState<Dayjs>(dayjs());
|
||||
const [isPopulatingHolidays, setIsPopulatingHolidays] = useState(false);
|
||||
const [hasAttemptedPopulation, setHasAttemptedPopulation] = useState(false);
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
|
||||
const fetchHolidayTypes = async () => {
|
||||
try {
|
||||
@@ -79,18 +80,16 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
|
||||
!isPopulatingHolidays
|
||||
) {
|
||||
try {
|
||||
console.log('🔄 No holidays found, attempting to populate official holidays...');
|
||||
setIsPopulatingHolidays(true);
|
||||
setHasAttemptedPopulation(true);
|
||||
|
||||
const populateRes = await holidayApiService.populateCountryHolidays();
|
||||
if (populateRes.done) {
|
||||
console.log('✅ Official holidays populated successfully');
|
||||
// Refresh holidays after population
|
||||
fetchHolidaysForDateRange(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Could not populate official holidays:', error);
|
||||
logger.error('populateHolidaysIfNeeded', error);
|
||||
} finally {
|
||||
setIsPopulatingHolidays(false);
|
||||
}
|
||||
@@ -217,7 +216,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
|
||||
name: holiday.name,
|
||||
description: holiday.description,
|
||||
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,
|
||||
});
|
||||
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 dayName = date.format('dddd');
|
||||
// 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 isCurrentMonth = date.isSame(currentDate, 'month');
|
||||
|
||||
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 && (
|
||||
<div className="holiday-cell">
|
||||
{dateHolidays.map((holiday, index) => {
|
||||
@@ -250,16 +252,22 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
|
||||
borderRadius: '4px',
|
||||
display: 'block',
|
||||
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',
|
||||
}}
|
||||
title={`${holiday.name}${isOfficial ? ' (Official Holiday)' : ' (Custom Holiday)'}`}
|
||||
>
|
||||
{isCustom && (
|
||||
<span className="custom-holiday-icon" style={{ marginRight: '2px' }}>⭐</span>
|
||||
<span className="custom-holiday-icon" style={{ marginRight: '2px' }}>
|
||||
⭐
|
||||
</span>
|
||||
)}
|
||||
{isOfficial && (
|
||||
<span className="official-holiday-icon" style={{ marginRight: '2px' }}>🏛️</span>
|
||||
<span className="official-holiday-icon" style={{ marginRight: '2px' }}>
|
||||
🏛️
|
||||
</span>
|
||||
)}
|
||||
{holiday.name}
|
||||
</Tag>
|
||||
@@ -277,10 +285,23 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
|
||||
};
|
||||
|
||||
const onPanelChange = (value: Dayjs) => {
|
||||
setIsNavigating(true);
|
||||
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) => {
|
||||
// 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
|
||||
const existingCustomHoliday = holidays.find(
|
||||
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)',
|
||||
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.boxShadow = '0 4px 12px rgba(24, 144, 255, 0.3)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="calendar-container">
|
||||
<div className={`calendar-container holiday-calendar ${themeMode}`}>
|
||||
<Calendar
|
||||
value={currentDate}
|
||||
onPanelChange={onPanelChange}
|
||||
@@ -474,7 +495,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
|
||||
setSelectedHoliday(null);
|
||||
}}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={editForm} layout="vertical" onFinish={handleUpdateHoliday}>
|
||||
<Form.Item
|
||||
@@ -540,18 +561,22 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDay
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
{selectedHoliday && selectedHoliday.source === 'custom' && selectedHoliday.is_editable && (
|
||||
<Popconfirm
|
||||
title={t('deleteHolidayConfirm') || 'Are you sure you want to delete this holiday?'}
|
||||
onConfirm={() => handleDeleteHoliday(selectedHoliday.id)}
|
||||
okText={t('yes') || 'Yes'}
|
||||
cancelText={t('no') || 'No'}
|
||||
>
|
||||
<Button type="primary" danger icon={<DeleteOutlined />}>
|
||||
{t('delete') || 'Delete'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{selectedHoliday &&
|
||||
selectedHoliday.source === 'custom' &&
|
||||
selectedHoliday.is_editable && (
|
||||
<Popconfirm
|
||||
title={
|
||||
t('deleteHolidayConfirm') || 'Are you sure you want to delete this holiday?'
|
||||
}
|
||||
onConfirm={() => handleDeleteHoliday(selectedHoliday.id)}
|
||||
okText={t('yes') || 'Yes'}
|
||||
cancelText={t('no') || 'No'}
|
||||
>
|
||||
<Button type="primary" danger icon={<DeleteOutlined />}>
|
||||
{t('delete') || 'Delete'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -493,11 +493,13 @@
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d9d9d9;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .calendar-legend {
|
||||
background: #1f1f1f;
|
||||
border-color: #434343;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
@@ -505,11 +507,33 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
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 {
|
||||
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 {
|
||||
@@ -531,6 +555,7 @@
|
||||
.legend-dot.today-dot {
|
||||
background: #1890ff;
|
||||
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 {
|
||||
@@ -546,6 +571,7 @@
|
||||
.holiday-calendar.dark .legend-dot.today-dot {
|
||||
background: #1890ff;
|
||||
box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.3);
|
||||
animation: todayPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Legend badge and tag styles */
|
||||
@@ -559,19 +585,22 @@
|
||||
display: flex;
|
||||
align-items: 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 {
|
||||
background: #52c41a;
|
||||
border: 1px solid #d9f7be;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(82, 196, 26, 0.2);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .legend-badge.working-day-badge {
|
||||
background: #52c41a;
|
||||
border: 1px solid #237804;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
|
||||
.legend-tag {
|
||||
@@ -583,35 +612,41 @@
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
border: 1px solid;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.legend-tag-text {
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-holiday-legend {
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
color: #52c41a;
|
||||
box-shadow: 0 1px 2px rgba(82, 196, 26, 0.1);
|
||||
}
|
||||
|
||||
.official-holiday-legend {
|
||||
background: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
color: #1890ff;
|
||||
box-shadow: 0 1px 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .custom-holiday-legend {
|
||||
background: #162312;
|
||||
border: 1px solid #274916;
|
||||
color: #95de64;
|
||||
box-shadow: 0 1px 2px rgba(149, 222, 100, 0.2);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .official-holiday-legend {
|
||||
background: #111b26;
|
||||
border: 1px solid #13334c;
|
||||
color: #69c0ff;
|
||||
box-shadow: 0 1px 2px rgba(105, 192, 255, 0.2);
|
||||
}
|
||||
|
||||
.legend-tag .custom-holiday-icon,
|
||||
@@ -646,6 +681,16 @@
|
||||
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 {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
||||
@@ -7,18 +7,14 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
|
||||
const AdminCenterLayout: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isTablet = useMediaQuery({ query: '(min-width:768px)' });
|
||||
const isMarginAvailable = useMediaQuery({ query: '(min-width: 1000px)' });
|
||||
const { t } = useTranslation('admin-center/sidebar');
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBlock: 96,
|
||||
marginBlock: 24,
|
||||
minHeight: '90vh',
|
||||
marginLeft: `${isMarginAvailable ? '5%' : ''}`,
|
||||
marginRight: `${isMarginAvailable ? '5%' : ''}`,
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4}>{t('adminCenter')}</Typography.Title>
|
||||
|
||||
@@ -12,7 +12,7 @@ const SettingsLayout = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div style={{ marginBlock: 96, minHeight: '90vh' }}>
|
||||
<div style={{ marginBlock: 24, minHeight: '90vh' }}>
|
||||
<Typography.Title level={4}>Settings</Typography.Title>
|
||||
|
||||
{isTablet ? (
|
||||
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
SettingOutlined,
|
||||
} from '@/shared/antd-imports';
|
||||
import React, { ReactNode, lazy } from 'react';
|
||||
const Overview = lazy(() => import('./overview/overview'));
|
||||
const Users = lazy(() => import('./users/users'));
|
||||
const Teams = lazy(() => import('./teams/teams'));
|
||||
const Billing = lazy(() => import('./billing/billing'));
|
||||
const Projects = lazy(() => import('./projects/projects'));
|
||||
const Settings = lazy(() => import('./settings/settings'));
|
||||
const Overview = lazy(() => import('../pages/admin-center/overview/overview'));
|
||||
const Users = lazy(() => import('../pages/admin-center/users/users'));
|
||||
const Teams = lazy(() => import('../pages/admin-center/teams/teams'));
|
||||
const Billing = lazy(() => import('../pages/admin-center/billing/billing'));
|
||||
const Projects = lazy(() => import('../pages/admin-center/projects/projects'));
|
||||
const Settings = lazy(() => import('../pages/admin-center/settings/Settings'));
|
||||
|
||||
// type of a menu item in admin center sidebar
|
||||
type AdminCenterMenuItems = {
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './settings';
|
||||
@@ -20,7 +20,7 @@ import { scheduleAPIService } from '@/api/schedule/schedule.api.service';
|
||||
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
||||
import { Settings } from '@/types/schedule/schedule-v2.types';
|
||||
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 { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { RootState } from '@/app/store';
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { colors } from '../../../styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { adminCenterItems } from '../admin-center-constants';
|
||||
import { adminCenterItems } from '../../../lib/admin-center-constants';
|
||||
import './sidebar.css';
|
||||
|
||||
const AdminCenterSidebar: React.FC = () => {
|
||||
|
||||
@@ -91,6 +91,7 @@ export interface IHolidayCalendarEvent {
|
||||
description?: string;
|
||||
date: string;
|
||||
is_recurring: boolean;
|
||||
holiday_type_id?: string;
|
||||
holiday_type_name: string;
|
||||
color_code: string;
|
||||
source: 'official' | 'custom';
|
||||
|
||||
Reference in New Issue
Block a user