feat(holiday-settings): implement organization holiday settings management
- Added SQL migration for creating organization holiday settings and state holidays tables with necessary constraints and indexes. - Implemented API endpoints in AdminCenterController for retrieving and updating organization holiday settings. - Updated admin-center API router to include routes for holiday settings management. - Enhanced localization files to support new holiday settings UI elements in multiple languages. - Improved holiday calendar component to display working days and integrate holiday settings.
This commit is contained in:
@@ -4,20 +4,37 @@
|
||||
|
||||
.holiday-calendar .ant-picker-calendar {
|
||||
background: transparent;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .ant-picker-calendar {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-header {
|
||||
padding: 12px 0;
|
||||
padding: 20px 24px;
|
||||
background: linear-gradient(135deg, #fafbfc 0%, #f1f3f4 100%);
|
||||
border-bottom: 1px solid #e8eaed;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .ant-picker-calendar-header {
|
||||
background: linear-gradient(135deg, #1f1f1f 0%, #262626 100%);
|
||||
border-bottom-color: #303030;
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-date {
|
||||
position: relative;
|
||||
height: 80px;
|
||||
padding: 4px 8px;
|
||||
height: 85px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .ant-picker-calendar-date {
|
||||
@@ -25,18 +42,161 @@
|
||||
background: #1f1f1f;
|
||||
}
|
||||
|
||||
/* Calendar cell wrapper */
|
||||
.calendar-cell {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Working day styles */
|
||||
.calendar-cell.working-day {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d9d9d9;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.calendar-cell.working-day::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(82, 196, 26, 0.04);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .calendar-cell.working-day {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #434343;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .calendar-cell.working-day::before {
|
||||
background: rgba(82, 196, 26, 0.06);
|
||||
}
|
||||
|
||||
/* Non-working day styles */
|
||||
.calendar-cell.non-working-day {
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .calendar-cell.non-working-day {
|
||||
background: #141414;
|
||||
border: 1px solid #303030;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* Today styles */
|
||||
.calendar-cell.today {
|
||||
border: 2px solid #1890ff;
|
||||
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15), 0 4px 12px rgba(24, 144, 255, 0.1);
|
||||
animation: todayPulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .calendar-cell.today {
|
||||
border: 2px solid #177ddc;
|
||||
box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.3);
|
||||
}
|
||||
|
||||
/* Other month styles */
|
||||
.calendar-cell.other-month {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .calendar-cell.other-month {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-date:hover {
|
||||
background: #f5f5f5;
|
||||
transform: translateY(-1px) scale(1.02);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.holiday-calendar .ant-picker-calendar-date:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.holiday-cell .ant-tag:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .ant-picker-calendar-date:hover {
|
||||
background: #2a2a2a;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Working day indicator */
|
||||
.working-day-indicator {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.working-day-badge {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
background: #52c41a;
|
||||
color: #ffffff;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 1px 3px rgba(82, 196, 26, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #d9f7be;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .working-day-badge {
|
||||
background: #52c41a;
|
||||
box-shadow: 0 1px 3px rgba(82, 196, 26, 0.4);
|
||||
border-color: #237804;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.3);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes todayPulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15), 0 4px 12px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.25), 0 6px 16px rgba(24, 144, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-date-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f1f1f;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .ant-picker-calendar-date-value {
|
||||
@@ -44,16 +204,22 @@
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-date-content {
|
||||
height: 100%;
|
||||
height: calc(100% - 20px);
|
||||
overflow: hidden;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.holiday-cell {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
z-index: 1;
|
||||
max-height: 65%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.holiday-cell .ant-tag {
|
||||
@@ -61,14 +227,27 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.holiday-cell .ant-tag:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transform: scale(1.05) translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
z-index: 10;
|
||||
overflow: visible;
|
||||
white-space: normal;
|
||||
text-overflow: unset;
|
||||
max-width: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-date-today {
|
||||
@@ -253,20 +432,243 @@
|
||||
|
||||
/* Tag styles */
|
||||
.holiday-calendar .ant-tag {
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
padding: 1px 4px;
|
||||
line-height: 1.3;
|
||||
padding: 2px 6px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
font-weight: 500;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Holiday tag specific styles */
|
||||
.holiday-tag.official-holiday {
|
||||
background: #e6f7ff !important;
|
||||
border-color: #91d5ff !important;
|
||||
color: #1890ff !important;
|
||||
}
|
||||
|
||||
.holiday-tag.custom-holiday {
|
||||
background: #f6ffed !important;
|
||||
border-color: #b7eb8f !important;
|
||||
color: #52c41a !important;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .holiday-tag.official-holiday {
|
||||
background: #111b26 !important;
|
||||
border-color: #13334c !important;
|
||||
color: #69c0ff !important;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .holiday-tag.custom-holiday {
|
||||
background: #162312 !important;
|
||||
border-color: #274916 !important;
|
||||
color: #95de64 !important;
|
||||
}
|
||||
|
||||
/* Holiday tag icons */
|
||||
.custom-holiday-icon {
|
||||
font-size: 8px;
|
||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
.official-holiday-icon {
|
||||
font-size: 8px;
|
||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
/* Calendar container and legend */
|
||||
.calendar-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .calendar-legend {
|
||||
background: #1f1f1f;
|
||||
border-color: #434343;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .legend-item {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legend-dot.working-day-dot {
|
||||
background: #52c41a;
|
||||
box-shadow: 0 0 0 1px rgba(82, 196, 26, 0.2);
|
||||
}
|
||||
|
||||
.legend-dot.holiday-dot {
|
||||
background: #ff4d4f;
|
||||
box-shadow: 0 0 0 1px rgba(255, 77, 79, 0.2);
|
||||
}
|
||||
|
||||
.legend-dot.today-dot {
|
||||
background: #1890ff;
|
||||
box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .legend-dot.working-day-dot {
|
||||
background: #52c41a;
|
||||
box-shadow: 0 0 0 1px rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .legend-dot.holiday-dot {
|
||||
background: #ff4d4f;
|
||||
box-shadow: 0 0 0 1px rgba(255, 77, 79, 0.3);
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .legend-dot.today-dot {
|
||||
background: #1890ff;
|
||||
box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Legend badge and tag styles */
|
||||
.legend-badge {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.legend-badge.working-day-badge {
|
||||
background: #52c41a;
|
||||
border: 1px solid #d9f7be;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .legend-badge.working-day-badge {
|
||||
background: #52c41a;
|
||||
border: 1px solid #237804;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.legend-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.legend-tag-text {
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.custom-holiday-legend {
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.official-holiday-legend {
|
||||
background: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .custom-holiday-legend {
|
||||
background: #162312;
|
||||
border: 1px solid #274916;
|
||||
color: #95de64;
|
||||
}
|
||||
|
||||
.holiday-calendar.dark .official-holiday-legend {
|
||||
background: #111b26;
|
||||
border: 1px solid #13334c;
|
||||
color: #69c0ff;
|
||||
}
|
||||
|
||||
.legend-tag .custom-holiday-icon,
|
||||
.legend-tag .official-holiday-icon {
|
||||
font-size: 7px;
|
||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.holiday-calendar .ant-picker-calendar-date {
|
||||
height: 60px;
|
||||
padding: 2px 4px;
|
||||
height: 70px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-date-value {
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.holiday-cell .ant-tag {
|
||||
font-size: 9px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.working-day-badge {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-header {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-date:hover {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.holiday-calendar .ant-picker-calendar-date {
|
||||
height: 65px;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.holiday-calendar .ant-picker-calendar-date-value {
|
||||
@@ -274,7 +676,47 @@
|
||||
}
|
||||
|
||||
.holiday-cell .ant-tag {
|
||||
font-size: 9px;
|
||||
padding: 0 2px;
|
||||
font-size: 8px;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.working-day-badge {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
font-size: 7px;
|
||||
}
|
||||
|
||||
.custom-holiday-icon,
|
||||
.official-holiday-icon {
|
||||
font-size: 7px;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.legend-badge {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.legend-tag {
|
||||
padding: 1px 4px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.legend-tag-text {
|
||||
font-size: 7px;
|
||||
}
|
||||
|
||||
.legend-tag .custom-holiday-icon,
|
||||
.legend-tag .official-holiday-icon {
|
||||
font-size: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
Tag,
|
||||
Popconfirm,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
} from '@/shared/antd-imports';
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@/shared/antd-imports';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { holidayApiService } from '@/api/holiday/holiday.api.service';
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { RootState } from '@/app/store';
|
||||
import { fetchHolidays } from '@/features/admin-center/admin-center.slice';
|
||||
import { fetchHolidays, clearHolidaysCache } from '@/features/admin-center/admin-center.slice';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import './holiday-calendar.css';
|
||||
|
||||
@@ -38,9 +38,10 @@ const { TextArea } = Input;
|
||||
|
||||
interface HolidayCalendarProps {
|
||||
themeMode: string;
|
||||
workingDays?: string[];
|
||||
}
|
||||
|
||||
const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
|
||||
const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDays = [] }) => {
|
||||
const { t } = useTranslation('admin-center/overview');
|
||||
const dispatch = useAppDispatch();
|
||||
const { holidays, loadingHolidays, holidaySettings } = useAppSelector(
|
||||
@@ -66,10 +67,32 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchHolidaysForDateRange = () => {
|
||||
const populateHolidaysIfNeeded = async () => {
|
||||
// Check if we have holiday settings with a country code but no holidays
|
||||
if (holidaySettings?.country_code && holidays.length === 0) {
|
||||
try {
|
||||
console.log('🔄 No holidays found, attempting to populate official holidays...');
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchHolidaysForDateRange = (forceRefresh = false) => {
|
||||
const startOfYear = currentDate.startOf('year');
|
||||
const endOfYear = currentDate.endOf('year');
|
||||
|
||||
// If forceRefresh is true, clear the cached holidays first
|
||||
if (forceRefresh) {
|
||||
dispatch(clearHolidaysCache());
|
||||
}
|
||||
|
||||
dispatch(
|
||||
fetchHolidays({
|
||||
from_date: startOfYear.format('YYYY-MM-DD'),
|
||||
@@ -84,6 +107,11 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
|
||||
fetchHolidaysForDateRange();
|
||||
}, [currentDate.year()]);
|
||||
|
||||
// Check if we need to populate holidays when holiday settings are loaded
|
||||
useEffect(() => {
|
||||
populateHolidaysIfNeeded();
|
||||
}, [holidaySettings, holidays.length]);
|
||||
|
||||
const customHolidays = useMemo(() => {
|
||||
return holidays.filter(holiday => holiday.source === 'custom');
|
||||
}, [holidays]);
|
||||
@@ -103,7 +131,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
|
||||
message.success(t('holidayCreated'));
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
fetchHolidaysForDateRange();
|
||||
fetchHolidaysForDateRange(true);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error creating holiday', error);
|
||||
@@ -133,7 +161,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
|
||||
setEditModalVisible(false);
|
||||
editForm.resetFields();
|
||||
setSelectedHoliday(null);
|
||||
fetchHolidaysForDateRange();
|
||||
fetchHolidaysForDateRange(true);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error updating holiday', error);
|
||||
@@ -146,7 +174,12 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
|
||||
const res = await holidayApiService.deleteOrganizationHoliday(holidayId);
|
||||
if (res.done) {
|
||||
message.success(t('holidayDeleted'));
|
||||
fetchHolidaysForDateRange();
|
||||
// Close the edit modal and reset form
|
||||
setEditModalVisible(false);
|
||||
editForm.resetFields();
|
||||
setSelectedHoliday(null);
|
||||
// Refresh holidays data
|
||||
fetchHolidaysForDateRange(true);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error deleting holiday', error);
|
||||
@@ -174,31 +207,55 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
|
||||
|
||||
const getHolidayDateCellRender = (date: Dayjs) => {
|
||||
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 isToday = date.isSame(dayjs(), 'day');
|
||||
const isCurrentMonth = date.isSame(currentDate, 'month');
|
||||
|
||||
if (dateHolidays.length > 0) {
|
||||
return (
|
||||
<div className="holiday-cell">
|
||||
{dateHolidays.map((holiday, index) => (
|
||||
<Tag
|
||||
key={`${holiday.id}-${index}`}
|
||||
color={holiday.color_code || (holiday.source === 'official' ? '#1890ff' : '#f37070')}
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
padding: '1px 4px',
|
||||
margin: '1px 0',
|
||||
borderRadius: '2px',
|
||||
display: 'block',
|
||||
opacity: holiday.source === 'official' ? 0.8 : 1,
|
||||
}}
|
||||
title={`${holiday.name}${holiday.source === 'official' ? ' (Official)' : ' (Custom)'}`}
|
||||
>
|
||||
{holiday.name}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<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) => {
|
||||
const isOfficial = holiday.source === 'official';
|
||||
const isCustom = holiday.source === 'custom';
|
||||
return (
|
||||
<Tag
|
||||
key={`${holiday.id}-${index}`}
|
||||
color={holiday.color_code || (isOfficial ? '#1890ff' : '#52c41a')}
|
||||
className={`holiday-tag ${isOfficial ? 'official-holiday' : 'custom-holiday'}`}
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
padding: '2px 6px',
|
||||
margin: '1px 0',
|
||||
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)',
|
||||
position: 'relative',
|
||||
}}
|
||||
title={`${holiday.name}${isOfficial ? ' (Official Holiday)' : ' (Custom Holiday)'}`}
|
||||
>
|
||||
{isCustom && (
|
||||
<span className="custom-holiday-icon" style={{ marginRight: '2px' }}>⭐</span>
|
||||
)}
|
||||
{isOfficial && (
|
||||
<span className="official-holiday-icon" style={{ marginRight: '2px' }}>🏛️</span>
|
||||
)}
|
||||
{holiday.name}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{isWorkingDay && (
|
||||
<div className="working-day-indicator" title={`${dayName} - Working Day`}>
|
||||
<div className="working-day-badge">W</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const onPanelChange = (value: Dayjs) => {
|
||||
@@ -227,40 +284,83 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 18,
|
||||
padding: '4px 0',
|
||||
}}
|
||||
>
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
{t('holidayCalendar')}
|
||||
</Title>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setModalVisible(true)}
|
||||
size="small"
|
||||
>
|
||||
{t('addCustomHoliday') || 'Add Custom Holiday'}
|
||||
</Button>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: '0 0 4px 0', fontWeight: 600 }}>
|
||||
{t('holidayCalendar')}
|
||||
</Title>
|
||||
{holidaySettings?.country_code && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: '13px', fontWeight: 500 }}>
|
||||
{t('officialHolidaysFrom') || 'Official holidays from'}:{' '}
|
||||
{holidaySettings.country_code}
|
||||
{holidaySettings.state_code && ` (${holidaySettings.state_code})`}
|
||||
<span style={{ color: themeMode === 'dark' ? '#40a9ff' : '#1890ff' }}>
|
||||
{holidaySettings.country_code}
|
||||
{holidaySettings.state_code && ` (${holidaySettings.state_code})`}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setModalVisible(true)}
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
fontWeight: 500,
|
||||
boxShadow: '0 2px 8px rgba(24, 144, 255, 0.2)',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(24, 144, 255, 0.3)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(24, 144, 255, 0.2)';
|
||||
}}
|
||||
>
|
||||
{t('addCustomHoliday') || 'Add Custom Holiday'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Calendar
|
||||
value={currentDate}
|
||||
onPanelChange={onPanelChange}
|
||||
onSelect={onDateSelect}
|
||||
dateCellRender={getHolidayDateCellRender}
|
||||
className={`holiday-calendar ${themeMode}`}
|
||||
loading={loadingHolidays}
|
||||
/>
|
||||
<div className="calendar-container">
|
||||
<Calendar
|
||||
value={currentDate}
|
||||
onPanelChange={onPanelChange}
|
||||
onSelect={onDateSelect}
|
||||
dateCellRender={getHolidayDateCellRender}
|
||||
className={`holiday-calendar ${themeMode}`}
|
||||
/>
|
||||
|
||||
{/* Calendar Legend */}
|
||||
<div className="calendar-legend">
|
||||
<div className="legend-item">
|
||||
<div className="legend-badge working-day-badge">W</div>
|
||||
<span>{t('workingDay') || 'Working Day'}</span>
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<div className="legend-tag custom-holiday-legend">
|
||||
<span className="custom-holiday-icon">⭐</span>
|
||||
<span className="legend-tag-text">Custom</span>
|
||||
</div>
|
||||
<span>{t('customHoliday') || 'Custom Holiday'}</span>
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<div className="legend-tag official-holiday-legend">
|
||||
<span className="official-holiday-icon">🏛️</span>
|
||||
<span className="legend-tag-text">Official</span>
|
||||
</div>
|
||||
<span>{t('officialHoliday') || 'Official Holiday'}</span>
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<div className="legend-dot today-dot"></div>
|
||||
<span>{t('today') || 'Today'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create Holiday Modal */}
|
||||
<Modal
|
||||
@@ -417,6 +517,18 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
Reference in New Issue
Block a user