Merge branch 'feature/add-calender-and-holidays' of https://github.com/Worklenz/worklenz into feature/team-utilization
This commit is contained in:
@@ -28,5 +28,33 @@
|
||||
"manDaysCalculationDescription": "All project costs will be calculated using estimated man days × daily rates",
|
||||
"calculationMethodTooltip": "This setting applies to all projects in your organization",
|
||||
"calculationMethodUpdated": "Organization calculation method updated successfully",
|
||||
"calculationMethodUpdateError": "Failed to update calculation method"
|
||||
"calculationMethodUpdateError": "Failed to update calculation method",
|
||||
"holidayCalendar": "Holiday Calendar",
|
||||
"addHoliday": "Add Holiday",
|
||||
"editHoliday": "Edit Holiday",
|
||||
"holidayName": "Holiday Name",
|
||||
"holidayNameRequired": "Please enter holiday name",
|
||||
"description": "Description",
|
||||
"date": "Date",
|
||||
"dateRequired": "Please select a date",
|
||||
"holidayType": "Holiday Type",
|
||||
"holidayTypeRequired": "Please select a holiday type",
|
||||
"recurring": "Recurring",
|
||||
"save": "Save",
|
||||
"update": "Update",
|
||||
"cancel": "Cancel",
|
||||
"holidayCreated": "Holiday created successfully",
|
||||
"holidayUpdated": "Holiday updated successfully",
|
||||
"holidayDeleted": "Holiday deleted successfully",
|
||||
"errorCreatingHoliday": "Error creating holiday",
|
||||
"errorUpdatingHoliday": "Error updating holiday",
|
||||
"errorDeletingHoliday": "Error deleting holiday",
|
||||
"importCountryHolidays": "Import Country Holidays",
|
||||
"country": "Country",
|
||||
"countryRequired": "Please select a country",
|
||||
"selectCountry": "Select a country",
|
||||
"year": "Year",
|
||||
"import": "Import",
|
||||
"holidaysImported": "Successfully imported {{count}} holidays",
|
||||
"errorImportingHolidays": "Error importing holidays"
|
||||
}
|
||||
|
||||
74
worklenz-frontend/src/api/holiday/holiday.api.service.ts
Normal file
74
worklenz-frontend/src/api/holiday/holiday.api.service.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { API_BASE_URL } from '@/shared/constants';
|
||||
import apiClient from '../api-client';
|
||||
import { IServerResponse } from '@/types/common.types';
|
||||
import {
|
||||
IHolidayType,
|
||||
IOrganizationHoliday,
|
||||
ICountryHoliday,
|
||||
IAvailableCountry,
|
||||
ICreateHolidayRequest,
|
||||
IUpdateHolidayRequest,
|
||||
IImportCountryHolidaysRequest,
|
||||
IHolidayCalendarEvent,
|
||||
} from '@/types/holiday/holiday.types';
|
||||
|
||||
const rootUrl = `${API_BASE_URL}/holidays`;
|
||||
|
||||
export const holidayApiService = {
|
||||
// Holiday types
|
||||
getHolidayTypes: async (): Promise<IServerResponse<IHolidayType[]>> => {
|
||||
const response = await apiClient.get<IServerResponse<IHolidayType[]>>(`${rootUrl}/types`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Organization holidays
|
||||
getOrganizationHolidays: async (year?: number): Promise<IServerResponse<IOrganizationHoliday[]>> => {
|
||||
const params = year ? `?year=${year}` : '';
|
||||
const response = await apiClient.get<IServerResponse<IOrganizationHoliday[]>>(`${rootUrl}/organization${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createOrganizationHoliday: async (data: ICreateHolidayRequest): Promise<IServerResponse<any>> => {
|
||||
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/organization`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateOrganizationHoliday: async (id: string, data: IUpdateHolidayRequest): Promise<IServerResponse<any>> => {
|
||||
const response = await apiClient.put<IServerResponse<any>>(`${rootUrl}/organization/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteOrganizationHoliday: async (id: string): Promise<IServerResponse<any>> => {
|
||||
const response = await apiClient.delete<IServerResponse<any>>(`${rootUrl}/organization/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Country holidays
|
||||
getAvailableCountries: async (): Promise<IServerResponse<IAvailableCountry[]>> => {
|
||||
const response = await apiClient.get<IServerResponse<IAvailableCountry[]>>(`${rootUrl}/countries`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getCountryHolidays: async (countryCode: string, year?: number): Promise<IServerResponse<ICountryHoliday[]>> => {
|
||||
const params = year ? `?year=${year}` : '';
|
||||
const response = await apiClient.get<IServerResponse<ICountryHoliday[]>>(`${rootUrl}/countries/${countryCode}${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
importCountryHolidays: async (data: IImportCountryHolidaysRequest): Promise<IServerResponse<any>> => {
|
||||
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/import`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Calendar view
|
||||
getHolidayCalendar: async (year: number, month: number): Promise<IServerResponse<IHolidayCalendarEvent[]>> => {
|
||||
const response = await apiClient.get<IServerResponse<IHolidayCalendarEvent[]>>(`${rootUrl}/calendar?year=${year}&month=${month}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Populate holidays
|
||||
populateCountryHolidays: async (): Promise<IServerResponse<any>> => {
|
||||
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/populate`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,6 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DatabaseOutlined,
|
||||
message,
|
||||
Space,
|
||||
Typography,
|
||||
} from '@/shared/antd-imports';
|
||||
@@ -11,7 +14,9 @@ import { RootState } from '@/app/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OrganizationName from '@/components/admin-center/overview/organization-name/organization-name';
|
||||
import OrganizationOwner from '@/components/admin-center/overview/organization-owner/organization-owner';
|
||||
import HolidayCalendar from '@/components/admin-center/overview/holiday-calendar/holiday-calendar';
|
||||
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
||||
import { holidayApiService } from '@/api/holiday/holiday.api.service';
|
||||
import { IOrganization, IOrganizationAdmin } from '@/types/admin-center/admin-center.types';
|
||||
import logger from '@/utils/errorLogger';
|
||||
|
||||
@@ -19,6 +24,7 @@ const Overview: React.FC = () => {
|
||||
const [organization, setOrganization] = useState<IOrganization | null>(null);
|
||||
const [organizationAdmins, setOrganizationAdmins] = useState<IOrganizationAdmin[] | null>(null);
|
||||
const [loadingAdmins, setLoadingAdmins] = useState(false);
|
||||
const [populatingHolidays, setPopulatingHolidays] = useState(false);
|
||||
|
||||
const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode);
|
||||
const { t } = useTranslation('admin-center/overview');
|
||||
@@ -48,6 +54,21 @@ const Overview: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePopulateHolidays = async () => {
|
||||
setPopulatingHolidays(true);
|
||||
try {
|
||||
const res = await holidayApiService.populateCountryHolidays();
|
||||
if (res.done) {
|
||||
message.success(`Successfully populated ${res.body.total_populated} holidays`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error populating holidays', error);
|
||||
message.error('Failed to populate holidays');
|
||||
} finally {
|
||||
setPopulatingHolidays(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getOrganizationDetails();
|
||||
getOrganizationAdmins();
|
||||
@@ -55,7 +76,20 @@ const Overview: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<PageHeader title={<span>{t('overview')}</span>} style={{ padding: '16px 0' }} />
|
||||
<PageHeader
|
||||
title={<span>{t('overview')}</span>}
|
||||
style={{ padding: '16px 0' }}
|
||||
extra={[
|
||||
<Button
|
||||
key="populate-holidays"
|
||||
icon={<DatabaseOutlined />}
|
||||
onClick={handlePopulateHolidays}
|
||||
loading={populatingHolidays}
|
||||
>
|
||||
Populate Holidays Database
|
||||
</Button>
|
||||
]}
|
||||
/>
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={22}>
|
||||
<OrganizationName
|
||||
@@ -82,6 +116,8 @@ const Overview: React.FC = () => {
|
||||
themeMode={themeMode}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<HolidayCalendar themeMode={themeMode} />
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -186,7 +186,8 @@ export {
|
||||
ArrowDownOutlined,
|
||||
CalculatorOutlined,
|
||||
DollarOutlined,
|
||||
DollarCircleOutlined
|
||||
DollarCircleOutlined,
|
||||
DatabaseOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
// Re-export all components with React
|
||||
|
||||
71
worklenz-frontend/src/types/holiday/holiday.types.ts
Normal file
71
worklenz-frontend/src/types/holiday/holiday.types.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export interface IHolidayType {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
color_code: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface IOrganizationHoliday {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
holiday_type_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
is_recurring: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
holiday_type_name?: string;
|
||||
color_code?: string;
|
||||
}
|
||||
|
||||
export interface ICountryHoliday {
|
||||
id: string;
|
||||
country_code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
is_recurring: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface IAvailableCountry {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ICreateHolidayRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
holiday_type_id: string;
|
||||
is_recurring?: boolean;
|
||||
}
|
||||
|
||||
export interface IUpdateHolidayRequest {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
date?: string;
|
||||
holiday_type_id?: string;
|
||||
is_recurring?: boolean;
|
||||
}
|
||||
|
||||
export interface IImportCountryHolidaysRequest {
|
||||
country_code: string;
|
||||
year?: number;
|
||||
}
|
||||
|
||||
export interface IHolidayCalendarEvent {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
is_recurring: boolean;
|
||||
holiday_type_name: string;
|
||||
color_code: string;
|
||||
source: 'organization' | 'country';
|
||||
}
|
||||
Reference in New Issue
Block a user