Merge branch 'feature/add-calender-and-holidays' of https://github.com/Worklenz/worklenz into feature/team-utilization

This commit is contained in:
chamiakJ
2025-07-26 12:31:35 +05:30
17 changed files with 2209 additions and 444 deletions

View File

@@ -0,0 +1,270 @@
.holiday-calendar {
width: 100%;
}
.holiday-calendar .ant-picker-calendar {
background: transparent;
}
.holiday-calendar .ant-picker-calendar-header {
padding: 12px 0;
}
.holiday-calendar .ant-picker-calendar-date {
position: relative;
height: 80px;
padding: 4px 8px;
border: 1px solid #f0f0f0;
border-radius: 6px;
transition: all 0.3s;
}
.holiday-calendar.dark .ant-picker-calendar-date {
border-color: #303030;
background: #1f1f1f;
}
.holiday-calendar .ant-picker-calendar-date:hover {
background: #f5f5f5;
}
.holiday-calendar.dark .ant-picker-calendar-date:hover {
background: #2a2a2a;
}
.holiday-calendar .ant-picker-calendar-date-value {
font-size: 12px;
font-weight: 500;
color: #262626;
}
.holiday-calendar.dark .ant-picker-calendar-date-value {
color: #ffffff;
}
.holiday-calendar .ant-picker-calendar-date-content {
height: 100%;
overflow: hidden;
}
.holiday-cell {
position: absolute;
bottom: 2px;
left: 2px;
right: 2px;
z-index: 1;
}
.holiday-cell .ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border: none;
font-weight: 500;
}
.holiday-calendar .ant-picker-calendar-date-today {
border-color: #1890ff;
background: #e6f7ff;
}
.holiday-calendar.dark .ant-picker-calendar-date-today {
border-color: #177ddc;
background: #111b26;
}
.holiday-calendar .ant-picker-calendar-date-selected {
border-color: #1890ff;
background: #e6f7ff;
}
.holiday-calendar.dark .ant-picker-calendar-date-selected {
border-color: #177ddc;
background: #111b26;
}
.holiday-calendar .ant-picker-calendar-date-other {
color: #bfbfbf;
}
.holiday-calendar.dark .ant-picker-calendar-date-other {
color: #595959;
}
.holiday-calendar .ant-picker-calendar-date-other .ant-picker-calendar-date-value {
color: #bfbfbf;
}
.holiday-calendar.dark .ant-picker-calendar-date-other .ant-picker-calendar-date-value {
color: #595959;
}
.holiday-calendar .ant-picker-calendar-mini {
border: 1px solid #f0f0f0;
border-radius: 6px;
}
.holiday-calendar.dark .ant-picker-calendar-mini {
border-color: #303030;
background: #1f1f1f;
}
.holiday-calendar .ant-picker-calendar-mini .ant-picker-calendar-date {
border: none;
border-radius: 0;
}
.holiday-calendar .ant-picker-calendar-mini .ant-picker-calendar-date:hover {
background: #f5f5f5;
}
.holiday-calendar.dark .ant-picker-calendar-mini .ant-picker-calendar-date:hover {
background: #2a2a2a;
}
/* Modal styles */
.holiday-calendar .ant-modal-content {
border-radius: 8px;
}
.holiday-calendar.dark .ant-modal-content {
background: #1f1f1f;
border: 1px solid #303030;
}
.holiday-calendar.dark .ant-modal-header {
background: #1f1f1f;
border-bottom: 1px solid #303030;
}
.holiday-calendar.dark .ant-modal-title {
color: #ffffff;
}
.holiday-calendar.dark .ant-modal-close {
color: #ffffff;
}
.holiday-calendar.dark .ant-form-item-label > label {
color: #ffffff;
}
.holiday-calendar.dark .ant-input,
.holiday-calendar.dark .ant-input-textarea {
background: #2a2a2a;
border-color: #434343;
color: #ffffff;
}
.holiday-calendar.dark .ant-input:focus,
.holiday-calendar.dark .ant-input-textarea:focus {
border-color: #177ddc;
box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.2);
}
.holiday-calendar.dark .ant-select-selector {
background: #2a2a2a !important;
border-color: #434343 !important;
color: #ffffff !important;
}
.holiday-calendar.dark .ant-select-focused .ant-select-selector {
border-color: #177ddc !important;
box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.2) !important;
}
.holiday-calendar.dark .ant-picker {
background: #2a2a2a;
border-color: #434343;
}
.holiday-calendar.dark .ant-picker-input > input {
color: #ffffff;
}
.holiday-calendar.dark .ant-picker-focused {
border-color: #177ddc;
box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.2);
}
/* Button styles */
.holiday-calendar .ant-btn {
border-radius: 6px;
font-weight: 500;
}
.holiday-calendar.dark .ant-btn {
border-color: #434343;
}
.holiday-calendar.dark .ant-btn-primary {
background: #177ddc;
border-color: #177ddc;
}
.holiday-calendar.dark .ant-btn-primary:hover {
background: #1890ff;
border-color: #1890ff;
}
/* Card styles */
.holiday-calendar .ant-card {
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
}
.holiday-calendar.dark .ant-card {
background: #1f1f1f;
border-color: #303030;
}
.holiday-calendar.dark .ant-card-head {
border-bottom-color: #303030;
}
.holiday-calendar.dark .ant-card-head-title {
color: #ffffff;
}
/* Typography styles */
.holiday-calendar.dark .ant-typography {
color: #ffffff;
}
.holiday-calendar.dark .ant-typography h5 {
color: #ffffff;
}
/* Space styles */
.holiday-calendar .ant-space {
gap: 8px !important;
}
/* Tag styles */
.holiday-calendar .ant-tag {
border-radius: 4px;
font-size: 10px;
line-height: 1.2;
padding: 1px 4px;
margin: 0;
border: none;
font-weight: 500;
}
/* Responsive design */
@media (max-width: 768px) {
.holiday-calendar .ant-picker-calendar-date {
height: 60px;
padding: 2px 4px;
}
.holiday-calendar .ant-picker-calendar-date-value {
font-size: 11px;
}
.holiday-cell .ant-tag {
font-size: 9px;
padding: 0 2px;
}
}

View File

@@ -0,0 +1,433 @@
import React, { useEffect, useState } from 'react';
import { Calendar, Card, Typography, Button, Modal, Form, Input, Select, DatePicker, Switch, Space, Tag, Popconfirm, message } from 'antd';
import { PlusOutlined, DeleteOutlined, EditOutlined, GlobalOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import dayjs, { Dayjs } from 'dayjs';
import { holidayApiService } from '@/api/holiday/holiday.api.service';
import {
IHolidayType,
IOrganizationHoliday,
IAvailableCountry,
ICreateHolidayRequest,
IUpdateHolidayRequest,
} from '@/types/holiday/holiday.types';
import logger from '@/utils/errorLogger';
import './holiday-calendar.css';
const { Title, Text } = Typography;
const { Option } = Select;
const { TextArea } = Input;
interface HolidayCalendarProps {
themeMode: string;
}
const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
const { t } = useTranslation('admin-center/overview');
const [form] = Form.useForm();
const [editForm] = Form.useForm();
const [holidayTypes, setHolidayTypes] = useState<IHolidayType[]>([]);
const [organizationHolidays, setOrganizationHolidays] = useState<IOrganizationHoliday[]>([]);
const [availableCountries, setAvailableCountries] = useState<IAvailableCountry[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [importModalVisible, setImportModalVisible] = useState(false);
const [selectedHoliday, setSelectedHoliday] = useState<IOrganizationHoliday | null>(null);
const [currentDate, setCurrentDate] = useState<Dayjs>(dayjs());
const fetchHolidayTypes = async () => {
try {
const res = await holidayApiService.getHolidayTypes();
if (res.done) {
setHolidayTypes(res.body);
}
} catch (error) {
logger.error('Error fetching holiday types', error);
}
};
const fetchOrganizationHolidays = async () => {
setLoading(true);
try {
const res = await holidayApiService.getOrganizationHolidays(currentDate.year());
if (res.done) {
setOrganizationHolidays(res.body);
}
} catch (error) {
logger.error('Error fetching organization holidays', error);
} finally {
setLoading(false);
}
};
const fetchAvailableCountries = async () => {
try {
const res = await holidayApiService.getAvailableCountries();
if (res.done) {
setAvailableCountries(res.body);
}
} catch (error) {
logger.error('Error fetching available countries', error);
}
};
useEffect(() => {
fetchHolidayTypes();
fetchOrganizationHolidays();
fetchAvailableCountries();
}, [currentDate.year()]);
const handleCreateHoliday = async (values: any) => {
try {
const holidayData: ICreateHolidayRequest = {
name: values.name,
description: values.description,
date: values.date.format('YYYY-MM-DD'),
holiday_type_id: values.holiday_type_id,
is_recurring: values.is_recurring || false,
};
const res = await holidayApiService.createOrganizationHoliday(holidayData);
if (res.done) {
message.success(t('holidayCreated'));
setModalVisible(false);
form.resetFields();
fetchOrganizationHolidays();
}
} catch (error) {
logger.error('Error creating holiday', error);
message.error(t('errorCreatingHoliday'));
}
};
const handleUpdateHoliday = async (values: any) => {
if (!selectedHoliday) return;
try {
const holidayData: IUpdateHolidayRequest = {
id: selectedHoliday.id,
name: values.name,
description: values.description,
date: values.date?.format('YYYY-MM-DD'),
holiday_type_id: values.holiday_type_id,
is_recurring: values.is_recurring,
};
const res = await holidayApiService.updateOrganizationHoliday(selectedHoliday.id, holidayData);
if (res.done) {
message.success(t('holidayUpdated'));
setEditModalVisible(false);
editForm.resetFields();
setSelectedHoliday(null);
fetchOrganizationHolidays();
}
} catch (error) {
logger.error('Error updating holiday', error);
message.error(t('errorUpdatingHoliday'));
}
};
const handleDeleteHoliday = async (holidayId: string) => {
try {
const res = await holidayApiService.deleteOrganizationHoliday(holidayId);
if (res.done) {
message.success(t('holidayDeleted'));
fetchOrganizationHolidays();
}
} catch (error) {
logger.error('Error deleting holiday', error);
message.error(t('errorDeletingHoliday'));
}
};
const handleImportCountryHolidays = async (values: any) => {
try {
const res = await holidayApiService.importCountryHolidays({
country_code: values.country_code,
year: values.year || currentDate.year(),
});
if (res.done) {
message.success(t('holidaysImported', { count: res.body.imported_count }));
setImportModalVisible(false);
fetchOrganizationHolidays();
}
} catch (error) {
logger.error('Error importing country holidays', error);
message.error(t('errorImportingHolidays'));
}
};
const handleEditHoliday = (holiday: IOrganizationHoliday) => {
setSelectedHoliday(holiday);
editForm.setFieldsValue({
name: holiday.name,
description: holiday.description,
date: dayjs(holiday.date),
holiday_type_id: holiday.holiday_type_id,
is_recurring: holiday.is_recurring,
});
setEditModalVisible(true);
};
const getHolidayDateCellRender = (date: Dayjs) => {
const holiday = organizationHolidays.find(h => dayjs(h.date).isSame(date, 'day'));
if (holiday) {
const holidayType = holidayTypes.find(ht => ht.id === holiday.holiday_type_id);
return (
<div className="holiday-cell">
<Tag
color={holidayType?.color_code || '#f37070'}
style={{
fontSize: '10px',
padding: '1px 4px',
margin: 0,
borderRadius: '2px'
}}
>
{holiday.name}
</Tag>
</div>
);
}
return null;
};
const onPanelChange = (value: Dayjs) => {
setCurrentDate(value);
};
return (
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<Title level={5} style={{ margin: 0 }}>
{t('holidayCalendar')}
</Title>
<Space>
<Button
icon={<GlobalOutlined />}
onClick={() => setImportModalVisible(true)}
size="small"
>
{t('importCountryHolidays')}
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setModalVisible(true)}
size="small"
>
{t('addHoliday')}
</Button>
</Space>
</div>
<Calendar
value={currentDate}
onPanelChange={onPanelChange}
dateCellRender={getHolidayDateCellRender}
className={`holiday-calendar ${themeMode}`}
/>
{/* Create Holiday Modal */}
<Modal
title={t('addHoliday')}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
form.resetFields();
}}
footer={null}
destroyOnClose
>
<Form form={form} layout="vertical" onFinish={handleCreateHoliday}>
<Form.Item
name="name"
label={t('holidayName')}
rules={[{ required: true, message: t('holidayNameRequired') }]}
>
<Input />
</Form.Item>
<Form.Item name="description" label={t('description')}>
<TextArea rows={3} />
</Form.Item>
<Form.Item
name="date"
label={t('date')}
rules={[{ required: true, message: t('dateRequired') }]}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="holiday_type_id"
label={t('holidayType')}
rules={[{ required: true, message: t('holidayTypeRequired') }]}
>
<Select>
{holidayTypes.map(type => (
<Option key={type.id} value={type.id}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div
style={{
width: 12,
height: 12,
borderRadius: '50%',
backgroundColor: type.color_code,
marginRight: 8
}}
/>
{type.name}
</div>
</Option>
))}
</Select>
</Form.Item>
<Form.Item name="is_recurring" label={t('recurring')} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
{t('save')}
</Button>
<Button onClick={() => {
setModalVisible(false);
form.resetFields();
}}>
{t('cancel')}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* Edit Holiday Modal */}
<Modal
title={t('editHoliday')}
open={editModalVisible}
onCancel={() => {
setEditModalVisible(false);
editForm.resetFields();
setSelectedHoliday(null);
}}
footer={null}
destroyOnClose
>
<Form form={editForm} layout="vertical" onFinish={handleUpdateHoliday}>
<Form.Item
name="name"
label={t('holidayName')}
rules={[{ required: true, message: t('holidayNameRequired') }]}
>
<Input />
</Form.Item>
<Form.Item name="description" label={t('description')}>
<TextArea rows={3} />
</Form.Item>
<Form.Item
name="date"
label={t('date')}
rules={[{ required: true, message: t('dateRequired') }]}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="holiday_type_id"
label={t('holidayType')}
rules={[{ required: true, message: t('holidayTypeRequired') }]}
>
<Select>
{holidayTypes.map(type => (
<Option key={type.id} value={type.id}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div
style={{
width: 12,
height: 12,
borderRadius: '50%',
backgroundColor: type.color_code,
marginRight: 8
}}
/>
{type.name}
</div>
</Option>
))}
</Select>
</Form.Item>
<Form.Item name="is_recurring" label={t('recurring')} valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
{t('update')}
</Button>
<Button onClick={() => {
setEditModalVisible(false);
editForm.resetFields();
setSelectedHoliday(null);
}}>
{t('cancel')}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* Import Country Holidays Modal */}
<Modal
title={t('importCountryHolidays')}
open={importModalVisible}
onCancel={() => setImportModalVisible(false)}
footer={null}
destroyOnClose
>
<Form layout="vertical" onFinish={handleImportCountryHolidays}>
<Form.Item
name="country_code"
label={t('country')}
rules={[{ required: true, message: t('countryRequired') }]}
>
<Select placeholder={t('selectCountry')}>
{availableCountries.map(country => (
<Option key={country.code} value={country.code}>
{country.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item name="year" label={t('year')}>
<DatePicker picker="year" style={{ width: '100%' }} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
{t('import')}
</Button>
<Button onClick={() => setImportModalVisible(false)}>
{t('cancel')}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</Card>
);
};
export default HolidayCalendar;