feat(holiday-system): implement comprehensive holiday management features
- Added holiday types and organization holidays management with CRUD operations. - Introduced country holidays import functionality using the date-holidays npm package. - Created database migrations for holiday types and organization holidays tables. - Developed a holiday calendar component for visual representation and management of holidays. - Enhanced API routes for holiday-related operations and integrated them into the admin center. - Updated frontend localization for holiday management features. - Implemented scripts for populating holidays in the database for 200+ countries.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
-- Create holiday types table
|
||||
CREATE TABLE IF NOT EXISTS holiday_types (
|
||||
id UUID DEFAULT uuid_generate_v4() NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
color_code WL_HEX_COLOR NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE holiday_types
|
||||
ADD CONSTRAINT holiday_types_pk
|
||||
PRIMARY KEY (id);
|
||||
|
||||
-- Insert default holiday types
|
||||
INSERT INTO holiday_types (name, description, color_code) VALUES
|
||||
('Public Holiday', 'Official public holidays', '#f37070'),
|
||||
('Company Holiday', 'Company-specific holidays', '#70a6f3'),
|
||||
('Personal Holiday', 'Personal or optional holidays', '#75c997'),
|
||||
('Religious Holiday', 'Religious observances', '#fbc84c')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Create organization holidays table
|
||||
CREATE TABLE IF NOT EXISTS organization_holidays (
|
||||
id UUID DEFAULT uuid_generate_v4() NOT NULL,
|
||||
organization_id UUID NOT NULL,
|
||||
holiday_type_id UUID NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
date DATE NOT NULL,
|
||||
is_recurring BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE organization_holidays
|
||||
ADD CONSTRAINT organization_holidays_pk
|
||||
PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE organization_holidays
|
||||
ADD CONSTRAINT organization_holidays_organization_id_fk
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE organization_holidays
|
||||
ADD CONSTRAINT organization_holidays_holiday_type_id_fk
|
||||
FOREIGN KEY (holiday_type_id) REFERENCES holiday_types
|
||||
ON DELETE RESTRICT;
|
||||
|
||||
-- Add unique constraint to prevent duplicate holidays on the same date for an organization
|
||||
ALTER TABLE organization_holidays
|
||||
ADD CONSTRAINT organization_holidays_organization_date_unique
|
||||
UNIQUE (organization_id, date);
|
||||
|
||||
-- Create country holidays table for predefined holidays
|
||||
CREATE TABLE IF NOT EXISTS country_holidays (
|
||||
id UUID DEFAULT uuid_generate_v4() NOT NULL,
|
||||
country_code CHAR(2) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
date DATE NOT NULL,
|
||||
is_recurring BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE country_holidays
|
||||
ADD CONSTRAINT country_holidays_pk
|
||||
PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE country_holidays
|
||||
ADD CONSTRAINT country_holidays_country_code_fk
|
||||
FOREIGN KEY (country_code) REFERENCES countries(code)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Add unique constraint to prevent duplicate holidays for the same country, name, and date
|
||||
ALTER TABLE country_holidays
|
||||
ADD CONSTRAINT country_holidays_country_name_date_unique
|
||||
UNIQUE (country_code, name, date);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_organization_holidays_organization_id ON organization_holidays(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_organization_holidays_date ON organization_holidays(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_country_holidays_country_code ON country_holidays(country_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_country_holidays_date ON country_holidays(date);
|
||||
352
worklenz-backend/docs/HOLIDAY_SYSTEM.md
Normal file
352
worklenz-backend/docs/HOLIDAY_SYSTEM.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# 🌍 Holiday Calendar System
|
||||
|
||||
The Worklenz Holiday Calendar System provides comprehensive holiday management for organizations operating globally.
|
||||
|
||||
## 📋 Features
|
||||
|
||||
- **200+ Countries Supported** - Comprehensive holiday data for countries worldwide
|
||||
- **Multiple Holiday Types** - Public, Company, Personal, and Religious holidays
|
||||
- **Import Country Holidays** - Bulk import official holidays from any supported country
|
||||
- **Manual Holiday Management** - Add, edit, and delete custom holidays
|
||||
- **Recurring Holidays** - Support for annual recurring holidays
|
||||
- **Visual Calendar** - Interactive calendar with color-coded holiday display
|
||||
- **Dark/Light Mode** - Full theme support
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Database Setup
|
||||
|
||||
Run the migration to create the holiday tables:
|
||||
|
||||
```bash
|
||||
# Run the migration
|
||||
psql -d your_database -f database/migrations/20250130000000-add-holiday-calendar.sql
|
||||
```
|
||||
|
||||
### 2. Populate Country Holidays
|
||||
|
||||
Use the npm package to populate holidays for 200+ countries:
|
||||
|
||||
```bash
|
||||
# Run the holiday population script
|
||||
./scripts/run-holiday-population.sh
|
||||
```
|
||||
|
||||
This will populate holidays for years 2020-2030 for all supported countries.
|
||||
|
||||
### 3. Access the Holiday Calendar
|
||||
|
||||
Navigate to **Admin Center → Overview** to access the holiday calendar.
|
||||
|
||||
## 🌐 Supported Countries
|
||||
|
||||
The system includes **200+ countries** across all continents:
|
||||
|
||||
### North America
|
||||
- United States 🇺🇸
|
||||
- Canada 🇨🇦
|
||||
- Mexico 🇲🇽
|
||||
|
||||
### Europe
|
||||
- United Kingdom 🇬🇧
|
||||
- Germany 🇩🇪
|
||||
- France 🇫🇷
|
||||
- Italy 🇮🇹
|
||||
- Spain 🇪🇸
|
||||
- Netherlands 🇳🇱
|
||||
- Belgium 🇧🇪
|
||||
- Switzerland 🇨🇭
|
||||
- Austria 🇦🇹
|
||||
- Sweden 🇸🇪
|
||||
- Norway 🇳🇴
|
||||
- Denmark 🇩🇰
|
||||
- Finland 🇫🇮
|
||||
- Poland 🇵🇱
|
||||
- Czech Republic 🇨🇿
|
||||
- Hungary 🇭🇺
|
||||
- Romania 🇷🇴
|
||||
- Bulgaria 🇧🇬
|
||||
- Croatia 🇭🇷
|
||||
- Slovenia 🇸🇮
|
||||
- Slovakia 🇸🇰
|
||||
- Lithuania 🇱🇹
|
||||
- Latvia 🇱🇻
|
||||
- Estonia 🇪🇪
|
||||
- Ireland 🇮🇪
|
||||
- Portugal 🇵🇹
|
||||
- Greece 🇬🇷
|
||||
- Cyprus 🇨🇾
|
||||
- Malta 🇲🇹
|
||||
- Luxembourg 🇱🇺
|
||||
- Iceland 🇮🇸
|
||||
|
||||
### Asia
|
||||
- China 🇨🇳
|
||||
- Japan 🇯🇵
|
||||
- South Korea 🇰🇷
|
||||
- India 🇮🇳
|
||||
- Pakistan 🇵🇰
|
||||
- Bangladesh 🇧🇩
|
||||
- Sri Lanka 🇱🇰
|
||||
- Nepal 🇳🇵
|
||||
- Thailand 🇹🇭
|
||||
- Vietnam 🇻🇳
|
||||
- Malaysia 🇲🇾
|
||||
- Singapore 🇸🇬
|
||||
- Indonesia 🇮🇩
|
||||
- Philippines 🇵🇭
|
||||
- Myanmar 🇲🇲
|
||||
- Cambodia 🇰🇭
|
||||
- Laos 🇱🇦
|
||||
- Brunei 🇧🇳
|
||||
- Timor-Leste 🇹🇱
|
||||
- Mongolia 🇲🇳
|
||||
- Kazakhstan 🇰🇿
|
||||
- Uzbekistan 🇺🇿
|
||||
- Kyrgyzstan 🇰🇬
|
||||
- Tajikistan 🇹🇯
|
||||
- Turkmenistan 🇹🇲
|
||||
- Afghanistan 🇦🇫
|
||||
- Iran 🇮🇷
|
||||
- Iraq 🇮🇶
|
||||
- Saudi Arabia 🇸🇦
|
||||
- UAE 🇦🇪
|
||||
- Qatar 🇶🇦
|
||||
- Kuwait 🇰🇼
|
||||
- Bahrain 🇧🇭
|
||||
- Oman 🇴🇲
|
||||
- Yemen 🇾🇪
|
||||
- Jordan 🇯🇴
|
||||
- Lebanon 🇱🇧
|
||||
- Syria 🇸🇾
|
||||
- Israel 🇮🇱
|
||||
- Palestine 🇵🇸
|
||||
- Turkey 🇹🇷
|
||||
- Georgia 🇬🇪
|
||||
- Armenia 🇦🇲
|
||||
- Azerbaijan 🇦🇿
|
||||
|
||||
### Oceania
|
||||
- Australia 🇦🇺
|
||||
- New Zealand 🇳🇿
|
||||
- Fiji 🇫🇯
|
||||
- Papua New Guinea 🇵🇬
|
||||
- Solomon Islands 🇸🇧
|
||||
- Vanuatu 🇻🇺
|
||||
- New Caledonia 🇳🇨
|
||||
- French Polynesia 🇵🇫
|
||||
- Tonga 🇹🇴
|
||||
- Samoa 🇼🇸
|
||||
- Kiribati 🇰🇮
|
||||
- Tuvalu 🇹🇻
|
||||
- Nauru 🇳🇷
|
||||
- Palau 🇵🇼
|
||||
- Marshall Islands 🇲🇭
|
||||
- Micronesia 🇫🇲
|
||||
|
||||
### Africa
|
||||
- South Africa 🇿🇦
|
||||
- Egypt 🇪🇬
|
||||
- Nigeria 🇳🇬
|
||||
- Kenya 🇰🇪
|
||||
- Ethiopia 🇪🇹
|
||||
- Tanzania 🇹🇿
|
||||
- Uganda 🇺🇬
|
||||
- Ghana 🇬🇭
|
||||
- Ivory Coast 🇨🇮
|
||||
- Senegal 🇸🇳
|
||||
- Mali 🇲🇱
|
||||
- Burkina Faso 🇧🇫
|
||||
- Niger 🇳🇪
|
||||
- Chad 🇹🇩
|
||||
- Cameroon 🇨🇲
|
||||
- Central African Republic 🇨🇫
|
||||
- Republic of the Congo 🇨🇬
|
||||
- Democratic Republic of the Congo 🇨🇩
|
||||
- Gabon 🇬🇦
|
||||
- Equatorial Guinea 🇬🇶
|
||||
- São Tomé and Príncipe 🇸🇹
|
||||
- Angola 🇦🇴
|
||||
- Zambia 🇿🇲
|
||||
- Zimbabwe 🇿🇼
|
||||
- Botswana 🇧🇼
|
||||
- Namibia 🇳🇦
|
||||
- Lesotho 🇱🇸
|
||||
- Eswatini 🇸🇿
|
||||
- Madagascar 🇲🇬
|
||||
- Mauritius 🇲🇺
|
||||
- Seychelles 🇸🇨
|
||||
- Comoros 🇰🇲
|
||||
- Djibouti 🇩🇯
|
||||
- Somalia 🇸🇴
|
||||
- Eritrea 🇪🇷
|
||||
- Sudan 🇸🇩
|
||||
- South Sudan 🇸🇸
|
||||
- Libya 🇱🇾
|
||||
- Tunisia 🇹🇳
|
||||
- Algeria 🇩🇿
|
||||
- Morocco 🇲🇦
|
||||
- Western Sahara 🇪🇭
|
||||
- Mauritania 🇲🇷
|
||||
- Gambia 🇬🇲
|
||||
- Guinea-Bissau 🇬🇼
|
||||
- Guinea 🇬🇳
|
||||
- Sierra Leone 🇸🇱
|
||||
- Liberia 🇱🇷
|
||||
- Togo 🇹🇬
|
||||
- Benin 🇧🇯
|
||||
|
||||
### South America
|
||||
- Brazil 🇧🇷
|
||||
- Argentina 🇦🇷
|
||||
- Chile 🇨🇱
|
||||
- Colombia 🇨🇴
|
||||
- Peru 🇵🇪
|
||||
- Venezuela 🇻🇪
|
||||
- Ecuador 🇪🇨
|
||||
- Bolivia 🇧🇴
|
||||
- Paraguay 🇵🇾
|
||||
- Uruguay 🇺🇾
|
||||
- Guyana 🇬🇾
|
||||
- Suriname 🇸🇷
|
||||
- Falkland Islands 🇫🇰
|
||||
- French Guiana 🇬🇫
|
||||
|
||||
### Central America & Caribbean
|
||||
- Mexico 🇲🇽
|
||||
- Guatemala 🇬🇹
|
||||
- Belize 🇧🇿
|
||||
- El Salvador 🇸🇻
|
||||
- Honduras 🇭🇳
|
||||
- Nicaragua 🇳🇮
|
||||
- Costa Rica 🇨🇷
|
||||
- Panama 🇵🇦
|
||||
- Cuba 🇨🇺
|
||||
- Jamaica 🇯🇲
|
||||
- Haiti 🇭🇹
|
||||
- Dominican Republic 🇩🇴
|
||||
- Puerto Rico 🇵🇷
|
||||
- Trinidad and Tobago 🇹🇹
|
||||
- Barbados 🇧🇧
|
||||
- Grenada 🇬🇩
|
||||
- Saint Lucia 🇱🇨
|
||||
- Saint Vincent and the Grenadines 🇻🇨
|
||||
- Antigua and Barbuda 🇦🇬
|
||||
- Saint Kitts and Nevis 🇰🇳
|
||||
- Dominica 🇩🇲
|
||||
- Bahamas 🇧🇸
|
||||
- Turks and Caicos Islands 🇹🇨
|
||||
- Cayman Islands 🇰🇾
|
||||
- Bermuda 🇧🇲
|
||||
- Anguilla 🇦🇮
|
||||
- British Virgin Islands 🇻🇬
|
||||
- U.S. Virgin Islands 🇻🇮
|
||||
- Aruba 🇦🇼
|
||||
- Curaçao 🇨🇼
|
||||
- Sint Maarten 🇸🇽
|
||||
- Saint Martin 🇲🇫
|
||||
- Saint Barthélemy 🇧🇱
|
||||
- Guadeloupe 🇬🇵
|
||||
- Martinique 🇲🇶
|
||||
|
||||
## 🔧 API Endpoints
|
||||
|
||||
### Holiday Types
|
||||
```http
|
||||
GET /api/holidays/types
|
||||
```
|
||||
|
||||
### Organization Holidays
|
||||
```http
|
||||
GET /api/holidays/organization?year=2024
|
||||
POST /api/holidays/organization
|
||||
PUT /api/holidays/organization/:id
|
||||
DELETE /api/holidays/organization/:id
|
||||
```
|
||||
|
||||
### Country Holidays
|
||||
```http
|
||||
GET /api/holidays/countries
|
||||
GET /api/holidays/countries/:country_code?year=2024
|
||||
POST /api/holidays/import
|
||||
```
|
||||
|
||||
### Calendar View
|
||||
```http
|
||||
GET /api/holidays/calendar?year=2024&month=1
|
||||
```
|
||||
|
||||
## 📊 Holiday Types
|
||||
|
||||
The system supports four types of holidays:
|
||||
|
||||
1. **Public Holiday** - Official government holidays (Red)
|
||||
2. **Company Holiday** - Organization-specific holidays (Blue)
|
||||
3. **Personal Holiday** - Personal or optional holidays (Green)
|
||||
4. **Religious Holiday** - Religious observances (Yellow)
|
||||
|
||||
## 🎯 Usage Examples
|
||||
|
||||
### Import US Holidays
|
||||
```javascript
|
||||
const result = await holidayApiService.importCountryHolidays({
|
||||
country_code: 'US',
|
||||
year: 2024
|
||||
});
|
||||
```
|
||||
|
||||
### Add Custom Holiday
|
||||
```javascript
|
||||
const holiday = await holidayApiService.createOrganizationHoliday({
|
||||
name: 'Company Retreat',
|
||||
description: 'Annual team building event',
|
||||
date: '2024-06-15',
|
||||
holiday_type_id: 'company-holiday-id',
|
||||
is_recurring: true
|
||||
});
|
||||
```
|
||||
|
||||
### Get Calendar View
|
||||
```javascript
|
||||
const calendar = await holidayApiService.getHolidayCalendar(2024, 1);
|
||||
```
|
||||
|
||||
## 🔄 Data Sources
|
||||
|
||||
The holiday data is sourced from the `date-holidays` npm package, which provides:
|
||||
|
||||
- **Official government holidays** for 200+ countries
|
||||
- **Religious holidays** (Christian, Islamic, Jewish, Hindu, Buddhist)
|
||||
- **Cultural and traditional holidays**
|
||||
- **Historical and commemorative days**
|
||||
|
||||
## 🛠️ Maintenance
|
||||
|
||||
### Adding New Countries
|
||||
|
||||
1. Add the country to the `countries` table
|
||||
2. Update the `populate-holidays.js` script
|
||||
3. Run the population script
|
||||
|
||||
### Updating Holiday Data
|
||||
|
||||
```bash
|
||||
# Re-run the holiday population script
|
||||
./scripts/run-holiday-population.sh
|
||||
```
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- Holidays are stored for years 2020-2030 by default
|
||||
- The system prevents duplicate holidays on the same date
|
||||
- Imported holidays are automatically classified as "Public Holiday" type
|
||||
- All holidays support recurring annual patterns
|
||||
- The calendar view combines organization and country holidays
|
||||
|
||||
## 🎉 Benefits
|
||||
|
||||
- **Global Compliance** - Ensure compliance with local holiday regulations
|
||||
- **Resource Planning** - Better project scheduling and resource allocation
|
||||
- **Team Coordination** - Improved team communication and planning
|
||||
- **Cost Management** - Accurate billing and time tracking
|
||||
- **Cultural Awareness** - Respect for diverse cultural and religious practices
|
||||
555
worklenz-backend/package-lock.json
generated
555
worklenz-backend/package-lock.json
generated
@@ -26,6 +26,7 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"csrf-sync": "^4.2.1",
|
||||
"csurf": "^1.11.0",
|
||||
"date-holidays": "^3.24.4",
|
||||
"debug": "^4.3.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"exceljs": "^4.3.0",
|
||||
@@ -33,7 +34,6 @@
|
||||
"express-rate-limit": "^6.8.0",
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^6.15.0",
|
||||
"grunt-cli": "^1.5.0",
|
||||
"helmet": "^6.2.0",
|
||||
"hpp": "^0.2.3",
|
||||
"http-errors": "^2.0.0",
|
||||
@@ -126,7 +126,7 @@
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13.0",
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=8.11.0",
|
||||
"yarn": "WARNING: Please use npm package manager instead of yarn"
|
||||
}
|
||||
@@ -6452,33 +6452,14 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-each": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
|
||||
"integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/array-slice": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
|
||||
"integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
@@ -6501,6 +6482,15 @@
|
||||
"integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/astronomia": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.1.1.tgz",
|
||||
"integrity": "sha512-TcJD9lUC5eAo0/Ji7rnQauX/yQbi0yZWM+JsNr77W3OA5fsrgvuFgubLMFwfw4VlZ29cu9dG/yfJbfvuTSftjg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
@@ -6951,6 +6941,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -7097,6 +7088,18 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/caldate": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/caldate/-/caldate-2.0.5.tgz",
|
||||
"integrity": "sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"moment-timezone": "^0.5.43"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -7939,6 +7942,73 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/date-bengali-revised": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz",
|
||||
"integrity": "sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-chinese": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/date-chinese/-/date-chinese-2.1.4.tgz",
|
||||
"integrity": "sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"astronomia": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-easter": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/date-easter/-/date-easter-1.0.3.tgz",
|
||||
"integrity": "sha512-aOViyIgpM4W0OWUiLqivznwTtuMlD/rdUWhc5IatYnplhPiWrLv75cnifaKYhmQwUBLAMWLNG4/9mlLIbXoGBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-holidays": {
|
||||
"version": "3.24.4",
|
||||
"resolved": "https://registry.npmjs.org/date-holidays/-/date-holidays-3.24.4.tgz",
|
||||
"integrity": "sha512-IZsFU6KJvmomA+bzk1uvDJ8P0/9nEOGZ8YMPQGpipNDUY+pL219AmnwWypYrz36nyWYJ2/fSkGNHaWOfFwpiAg==",
|
||||
"license": "(ISC AND CC-BY-3.0)",
|
||||
"dependencies": {
|
||||
"date-holidays-parser": "^3.4.7",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prepin": "^1.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"holidays2json": "scripts/holidays2json.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-holidays-parser": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/date-holidays-parser/-/date-holidays-parser-3.4.7.tgz",
|
||||
"integrity": "sha512-h09ZEtM6u5cYM6m1bX+1Ny9f+nLO9KVZUKNPEnH7lhbXYTfqZogaGTnhONswGeIJFF91UImIftS3CdM9HLW5oQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"astronomia": "^4.1.1",
|
||||
"caldate": "^2.0.5",
|
||||
"date-bengali-revised": "^2.0.2",
|
||||
"date-chinese": "^2.1.4",
|
||||
"date-easter": "^1.0.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
"jalaali-js": "^1.2.7",
|
||||
"moment-timezone": "^0.5.47"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
@@ -8056,15 +8126,6 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-file": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
|
||||
"integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
@@ -8924,18 +8985,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-tilde": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
|
||||
"integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"homedir-polyfill": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect": {
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz",
|
||||
@@ -9088,12 +9137,6 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-csv": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz",
|
||||
@@ -9222,6 +9265,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -9287,46 +9331,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/findup-sync": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
|
||||
"integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-file": "^1.0.0",
|
||||
"is-glob": "^4.0.0",
|
||||
"micromatch": "^4.0.2",
|
||||
"resolve-dir": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fined": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
|
||||
"integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expand-tilde": "^2.0.2",
|
||||
"is-plain-object": "^2.0.3",
|
||||
"object.defaults": "^1.1.0",
|
||||
"object.pick": "^1.2.0",
|
||||
"parse-filepath": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/flagged-respawn": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
|
||||
"integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
||||
@@ -9427,27 +9431,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-in": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||
"integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/for-own": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
|
||||
"integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"for-in": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@@ -9845,48 +9828,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/global-modules": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
|
||||
"integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"global-prefix": "^1.0.1",
|
||||
"is-windows": "^1.0.1",
|
||||
"resolve-dir": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
|
||||
"integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expand-tilde": "^2.0.2",
|
||||
"homedir-polyfill": "^1.0.1",
|
||||
"ini": "^1.3.4",
|
||||
"is-windows": "^1.0.1",
|
||||
"which": "^1.2.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix/node_modules/which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
@@ -9943,34 +9884,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/grunt-cli": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.5.0.tgz",
|
||||
"integrity": "sha512-rILKAFoU0dzlf22SUfDtq2R1fosChXXlJM5j7wI6uoW8gwmXDXzbUvirlKZSYCdXl3LXFbR+8xyS+WFo+b6vlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"grunt-known-options": "~2.0.0",
|
||||
"interpret": "~1.1.0",
|
||||
"liftup": "~3.0.1",
|
||||
"nopt": "~5.0.0",
|
||||
"v8flags": "^4.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"grunt": "bin/grunt"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/grunt-known-options": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz",
|
||||
"integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -10042,18 +9955,6 @@
|
||||
"dev": true,
|
||||
"license": "https://www.highcharts.com/license"
|
||||
},
|
||||
"node_modules/homedir-polyfill": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
|
||||
"integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse-passwd": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hpp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz",
|
||||
@@ -10263,12 +10164,6 @@
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/interpret": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
|
||||
"integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -10278,19 +10173,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-absolute": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
|
||||
"integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-relative": "^1.0.0",
|
||||
"is-windows": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
@@ -10352,6 +10234,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -10380,6 +10263,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -10392,6 +10276,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -10407,18 +10292,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isobject": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
@@ -10443,18 +10316,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-relative": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
|
||||
"integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-unc-path": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
@@ -10467,27 +10328,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unc-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
|
||||
"integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unc-path-regex": "^0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-windows": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
|
||||
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -10498,17 +10338,9 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isobject": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
@@ -10638,6 +10470,12 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/jalaali-js": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
|
||||
"integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
@@ -11324,7 +11162,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -11526,15 +11363,6 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
@@ -11626,25 +11454,6 @@
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/liftup": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz",
|
||||
"integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"findup-sync": "^4.0.0",
|
||||
"fined": "^1.2.0",
|
||||
"flagged-respawn": "^1.0.1",
|
||||
"is-plain-object": "^2.0.4",
|
||||
"object.map": "^1.0.1",
|
||||
"rechoir": "^0.7.0",
|
||||
"resolve": "^1.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@@ -11883,18 +11692,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/make-iterator": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
|
||||
"integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"kind-of": "^6.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
|
||||
@@ -11905,15 +11702,6 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/map-cache": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
|
||||
"integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -11971,6 +11759,7 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -12418,46 +12207,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object.defaults": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
|
||||
"integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-each": "^1.0.1",
|
||||
"array-slice": "^1.0.0",
|
||||
"for-own": "^1.0.0",
|
||||
"isobject": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object.map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
|
||||
"integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"for-own": "^1.0.0",
|
||||
"make-iterator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object.pick": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
|
||||
"integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isobject": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -12620,20 +12369,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-filepath": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
|
||||
"integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-absolute": "^1.0.0",
|
||||
"map-cache": "^0.2.0",
|
||||
"path-root": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
@@ -12653,15 +12388,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-passwd": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
|
||||
"integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-srcset": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||
@@ -12800,27 +12526,6 @@
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-root": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
|
||||
"integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-root-regex": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-root-regex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
|
||||
"integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
@@ -12968,6 +12673,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -13176,6 +12882,15 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prepin": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/prepin/-/prepin-1.0.3.tgz",
|
||||
"integrity": "sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==",
|
||||
"license": "Unlicense",
|
||||
"bin": {
|
||||
"prepin": "bin/prepin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz",
|
||||
@@ -13563,18 +13278,6 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rechoir": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
|
||||
"integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz",
|
||||
@@ -13726,19 +13429,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-dir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
|
||||
"integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expand-tilde": "^2.0.0",
|
||||
"global-modules": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -14974,6 +14664,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@@ -15494,15 +15185,6 @@
|
||||
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unc-path-regex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
|
||||
"integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
@@ -15732,15 +15414,6 @@
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8flags": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz",
|
||||
"integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.15",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"csrf-sync": "^4.2.1",
|
||||
"csurf": "^1.11.0",
|
||||
"date-holidays": "^3.24.4",
|
||||
"debug": "^4.3.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"exceljs": "^4.3.0",
|
||||
|
||||
265
worklenz-backend/scripts/populate-holidays.js
Normal file
265
worklenz-backend/scripts/populate-holidays.js
Normal file
@@ -0,0 +1,265 @@
|
||||
const Holidays = require('date-holidays');
|
||||
const { Pool } = require('pg');
|
||||
const config = require('../src/config/db-config');
|
||||
|
||||
// Database connection
|
||||
const pool = new Pool(config);
|
||||
|
||||
// Countries to populate with holidays
|
||||
const countries = [
|
||||
{ code: 'US', name: 'United States' },
|
||||
{ code: 'GB', name: 'United Kingdom' },
|
||||
{ code: 'CA', name: 'Canada' },
|
||||
{ code: 'AU', name: 'Australia' },
|
||||
{ code: 'DE', name: 'Germany' },
|
||||
{ code: 'FR', name: 'France' },
|
||||
{ code: 'IT', name: 'Italy' },
|
||||
{ code: 'ES', name: 'Spain' },
|
||||
{ code: 'NL', name: 'Netherlands' },
|
||||
{ code: 'BE', name: 'Belgium' },
|
||||
{ code: 'CH', name: 'Switzerland' },
|
||||
{ code: 'AT', name: 'Austria' },
|
||||
{ code: 'SE', name: 'Sweden' },
|
||||
{ code: 'NO', name: 'Norway' },
|
||||
{ code: 'DK', name: 'Denmark' },
|
||||
{ code: 'FI', name: 'Finland' },
|
||||
{ code: 'PL', name: 'Poland' },
|
||||
{ code: 'CZ', name: 'Czech Republic' },
|
||||
{ code: 'HU', name: 'Hungary' },
|
||||
{ code: 'RO', name: 'Romania' },
|
||||
{ code: 'BG', name: 'Bulgaria' },
|
||||
{ code: 'HR', name: 'Croatia' },
|
||||
{ code: 'SI', name: 'Slovenia' },
|
||||
{ code: 'SK', name: 'Slovakia' },
|
||||
{ code: 'LT', name: 'Lithuania' },
|
||||
{ code: 'LV', name: 'Latvia' },
|
||||
{ code: 'EE', name: 'Estonia' },
|
||||
{ code: 'IE', name: 'Ireland' },
|
||||
{ code: 'PT', name: 'Portugal' },
|
||||
{ code: 'GR', name: 'Greece' },
|
||||
{ code: 'CY', name: 'Cyprus' },
|
||||
{ code: 'MT', name: 'Malta' },
|
||||
{ code: 'LU', name: 'Luxembourg' },
|
||||
{ code: 'IS', name: 'Iceland' },
|
||||
{ code: 'CN', name: 'China' },
|
||||
{ code: 'JP', name: 'Japan' },
|
||||
{ code: 'KR', name: 'South Korea' },
|
||||
{ code: 'IN', name: 'India' },
|
||||
{ code: 'PK', name: 'Pakistan' },
|
||||
{ code: 'BD', name: 'Bangladesh' },
|
||||
{ code: 'LK', name: 'Sri Lanka' },
|
||||
{ code: 'NP', name: 'Nepal' },
|
||||
{ code: 'TH', name: 'Thailand' },
|
||||
{ code: 'VN', name: 'Vietnam' },
|
||||
{ code: 'MY', name: 'Malaysia' },
|
||||
{ code: 'SG', name: 'Singapore' },
|
||||
{ code: 'ID', name: 'Indonesia' },
|
||||
{ code: 'PH', name: 'Philippines' },
|
||||
{ code: 'MM', name: 'Myanmar' },
|
||||
{ code: 'KH', name: 'Cambodia' },
|
||||
{ code: 'LA', name: 'Laos' },
|
||||
{ code: 'BN', name: 'Brunei' },
|
||||
{ code: 'TL', name: 'Timor-Leste' },
|
||||
{ code: 'MN', name: 'Mongolia' },
|
||||
{ code: 'KZ', name: 'Kazakhstan' },
|
||||
{ code: 'UZ', name: 'Uzbekistan' },
|
||||
{ code: 'KG', name: 'Kyrgyzstan' },
|
||||
{ code: 'TJ', name: 'Tajikistan' },
|
||||
{ code: 'TM', name: 'Turkmenistan' },
|
||||
{ code: 'AF', name: 'Afghanistan' },
|
||||
{ code: 'IR', name: 'Iran' },
|
||||
{ code: 'IQ', name: 'Iraq' },
|
||||
{ code: 'SA', name: 'Saudi Arabia' },
|
||||
{ code: 'AE', name: 'United Arab Emirates' },
|
||||
{ code: 'QA', name: 'Qatar' },
|
||||
{ code: 'KW', name: 'Kuwait' },
|
||||
{ code: 'BH', name: 'Bahrain' },
|
||||
{ code: 'OM', name: 'Oman' },
|
||||
{ code: 'YE', name: 'Yemen' },
|
||||
{ code: 'JO', name: 'Jordan' },
|
||||
{ code: 'LB', name: 'Lebanon' },
|
||||
{ code: 'SY', name: 'Syria' },
|
||||
{ code: 'IL', name: 'Israel' },
|
||||
{ code: 'PS', name: 'Palestine' },
|
||||
{ code: 'TR', name: 'Turkey' },
|
||||
{ code: 'GE', name: 'Georgia' },
|
||||
{ code: 'AM', name: 'Armenia' },
|
||||
{ code: 'AZ', name: 'Azerbaijan' },
|
||||
{ code: 'NZ', name: 'New Zealand' },
|
||||
{ code: 'FJ', name: 'Fiji' },
|
||||
{ code: 'PG', name: 'Papua New Guinea' },
|
||||
{ code: 'SB', name: 'Solomon Islands' },
|
||||
{ code: 'VU', name: 'Vanuatu' },
|
||||
{ code: 'NC', name: 'New Caledonia' },
|
||||
{ code: 'PF', name: 'French Polynesia' },
|
||||
{ code: 'TO', name: 'Tonga' },
|
||||
{ code: 'WS', name: 'Samoa' },
|
||||
{ code: 'KI', name: 'Kiribati' },
|
||||
{ code: 'TV', name: 'Tuvalu' },
|
||||
{ code: 'NR', name: 'Nauru' },
|
||||
{ code: 'PW', name: 'Palau' },
|
||||
{ code: 'MH', name: 'Marshall Islands' },
|
||||
{ code: 'FM', name: 'Micronesia' },
|
||||
{ code: 'ZA', name: 'South Africa' },
|
||||
{ code: 'EG', name: 'Egypt' },
|
||||
{ code: 'NG', name: 'Nigeria' },
|
||||
{ code: 'KE', name: 'Kenya' },
|
||||
{ code: 'ET', name: 'Ethiopia' },
|
||||
{ code: 'TZ', name: 'Tanzania' },
|
||||
{ code: 'UG', name: 'Uganda' },
|
||||
{ code: 'GH', name: 'Ghana' },
|
||||
{ code: 'CI', name: 'Ivory Coast' },
|
||||
{ code: 'SN', name: 'Senegal' },
|
||||
{ code: 'ML', name: 'Mali' },
|
||||
{ code: 'BF', name: 'Burkina Faso' },
|
||||
{ code: 'NE', name: 'Niger' },
|
||||
{ code: 'TD', name: 'Chad' },
|
||||
{ code: 'CM', name: 'Cameroon' },
|
||||
{ code: 'CF', name: 'Central African Republic' },
|
||||
{ code: 'CG', name: 'Republic of the Congo' },
|
||||
{ code: 'CD', name: 'Democratic Republic of the Congo' },
|
||||
{ code: 'GA', name: 'Gabon' },
|
||||
{ code: 'GQ', name: 'Equatorial Guinea' },
|
||||
{ code: 'ST', name: 'São Tomé and Príncipe' },
|
||||
{ code: 'AO', name: 'Angola' },
|
||||
{ code: 'ZM', name: 'Zambia' },
|
||||
{ code: 'ZW', name: 'Zimbabwe' },
|
||||
{ code: 'BW', name: 'Botswana' },
|
||||
{ code: 'NA', name: 'Namibia' },
|
||||
{ code: 'LS', name: 'Lesotho' },
|
||||
{ code: 'SZ', name: 'Eswatini' },
|
||||
{ code: 'MG', name: 'Madagascar' },
|
||||
{ code: 'MU', name: 'Mauritius' },
|
||||
{ code: 'SC', name: 'Seychelles' },
|
||||
{ code: 'KM', name: 'Comoros' },
|
||||
{ code: 'DJ', name: 'Djibouti' },
|
||||
{ code: 'SO', name: 'Somalia' },
|
||||
{ code: 'ER', name: 'Eritrea' },
|
||||
{ code: 'SD', name: 'Sudan' },
|
||||
{ code: 'SS', name: 'South Sudan' },
|
||||
{ code: 'LY', name: 'Libya' },
|
||||
{ code: 'TN', name: 'Tunisia' },
|
||||
{ code: 'DZ', name: 'Algeria' },
|
||||
{ code: 'MA', name: 'Morocco' },
|
||||
{ code: 'EH', name: 'Western Sahara' },
|
||||
{ code: 'MR', name: 'Mauritania' },
|
||||
{ code: 'GM', name: 'Gambia' },
|
||||
{ code: 'GW', name: 'Guinea-Bissau' },
|
||||
{ code: 'GN', name: 'Guinea' },
|
||||
{ code: 'SL', name: 'Sierra Leone' },
|
||||
{ code: 'LR', name: 'Liberia' },
|
||||
{ code: 'TG', name: 'Togo' },
|
||||
{ code: 'BJ', name: 'Benin' },
|
||||
{ code: 'BR', name: 'Brazil' },
|
||||
{ code: 'AR', name: 'Argentina' },
|
||||
{ code: 'CL', name: 'Chile' },
|
||||
{ code: 'CO', name: 'Colombia' },
|
||||
{ code: 'PE', name: 'Peru' },
|
||||
{ code: 'VE', name: 'Venezuela' },
|
||||
{ code: 'EC', name: 'Ecuador' },
|
||||
{ code: 'BO', name: 'Bolivia' },
|
||||
{ code: 'PY', name: 'Paraguay' },
|
||||
{ code: 'UY', name: 'Uruguay' },
|
||||
{ code: 'GY', name: 'Guyana' },
|
||||
{ code: 'SR', name: 'Suriname' },
|
||||
{ code: 'FK', name: 'Falkland Islands' },
|
||||
{ code: 'GF', name: 'French Guiana' },
|
||||
{ code: 'MX', name: 'Mexico' },
|
||||
{ code: 'GT', name: 'Guatemala' },
|
||||
{ code: 'BZ', name: 'Belize' },
|
||||
{ code: 'SV', name: 'El Salvador' },
|
||||
{ code: 'HN', name: 'Honduras' },
|
||||
{ code: 'NI', name: 'Nicaragua' },
|
||||
{ code: 'CR', name: 'Costa Rica' },
|
||||
{ code: 'PA', name: 'Panama' },
|
||||
{ code: 'CU', name: 'Cuba' },
|
||||
{ code: 'JM', name: 'Jamaica' },
|
||||
{ code: 'HT', name: 'Haiti' },
|
||||
{ code: 'DO', name: 'Dominican Republic' },
|
||||
{ code: 'PR', name: 'Puerto Rico' },
|
||||
{ code: 'TT', name: 'Trinidad and Tobago' },
|
||||
{ code: 'BB', name: 'Barbados' },
|
||||
{ code: 'GD', name: 'Grenada' },
|
||||
{ code: 'LC', name: 'Saint Lucia' },
|
||||
{ code: 'VC', name: 'Saint Vincent and the Grenadines' },
|
||||
{ code: 'AG', name: 'Antigua and Barbuda' },
|
||||
{ code: 'KN', name: 'Saint Kitts and Nevis' },
|
||||
{ code: 'DM', name: 'Dominica' },
|
||||
{ code: 'BS', name: 'Bahamas' },
|
||||
{ code: 'TC', name: 'Turks and Caicos Islands' },
|
||||
{ code: 'KY', name: 'Cayman Islands' },
|
||||
{ code: 'BM', name: 'Bermuda' },
|
||||
{ code: 'AI', name: 'Anguilla' },
|
||||
{ code: 'VG', name: 'British Virgin Islands' },
|
||||
{ code: 'VI', name: 'U.S. Virgin Islands' },
|
||||
{ code: 'AW', name: 'Aruba' },
|
||||
{ code: 'CW', name: 'Curaçao' },
|
||||
{ code: 'SX', name: 'Sint Maarten' },
|
||||
{ code: 'MF', name: 'Saint Martin' },
|
||||
{ code: 'BL', name: 'Saint Barthélemy' },
|
||||
{ code: 'GP', name: 'Guadeloupe' },
|
||||
{ code: 'MQ', name: 'Martinique' }
|
||||
];
|
||||
|
||||
async function populateHolidays() {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
console.log('Starting holiday population...');
|
||||
|
||||
for (const country of countries) {
|
||||
console.log(`Processing ${country.name} (${country.code})...`);
|
||||
|
||||
try {
|
||||
const hd = new Holidays(country.code);
|
||||
|
||||
// Get holidays for multiple years (2020-2030)
|
||||
for (let year = 2020; year <= 2030; year++) {
|
||||
const holidays = hd.getHolidays(year);
|
||||
|
||||
for (const holiday of holidays) {
|
||||
// Skip if holiday is not a date object
|
||||
if (!holiday.date || typeof holiday.date !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dateStr = holiday.date.toISOString().split('T')[0];
|
||||
const name = holiday.name || 'Unknown Holiday';
|
||||
const description = holiday.type || 'Public Holiday';
|
||||
|
||||
// Insert holiday into database
|
||||
const query = `
|
||||
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING
|
||||
`;
|
||||
|
||||
await client.query(query, [
|
||||
country.code,
|
||||
name,
|
||||
description,
|
||||
dateStr,
|
||||
true // Most holidays are recurring
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✓ Completed ${country.name}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`✗ Error processing ${country.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Holiday population completed!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
populateHolidays().catch(console.error);
|
||||
25
worklenz-backend/scripts/run-holiday-population.sh
Normal file
25
worklenz-backend/scripts/run-holiday-population.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🌍 Starting Holiday Population Script..."
|
||||
echo "This will populate the database with holidays for 200+ countries using the date-holidays npm package."
|
||||
echo ""
|
||||
|
||||
# Check if Node.js is installed
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "❌ Node.js is not installed. Please install Node.js first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the script exists
|
||||
if [ ! -f "scripts/populate-holidays.js" ]; then
|
||||
echo "❌ Holiday population script not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run the holiday population script
|
||||
echo "🚀 Running holiday population script..."
|
||||
node scripts/populate-holidays.js
|
||||
|
||||
echo ""
|
||||
echo "✅ Holiday population completed!"
|
||||
echo "You can now use the holiday import feature in the admin center."
|
||||
264
worklenz-backend/src/controllers/holiday-controller.ts
Normal file
264
worklenz-backend/src/controllers/holiday-controller.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { IWorkLenzRequest } from '../interfaces/worklenz-request';
|
||||
import { IWorkLenzResponse } from '../interfaces/worklenz-response';
|
||||
import db from '../config/db';
|
||||
import { ServerResponse } from '../models/server-response';
|
||||
import WorklenzControllerBase from './worklenz-controller-base';
|
||||
import HandleExceptions from '../decorators/handle-exceptions';
|
||||
import {
|
||||
ICreateHolidayRequest,
|
||||
IUpdateHolidayRequest,
|
||||
IImportCountryHolidaysRequest,
|
||||
} from '../interfaces/holiday.interface';
|
||||
|
||||
export default class HolidayController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getHolidayTypes(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, description, color_code, created_at, updated_at
|
||||
FROM holiday_types
|
||||
ORDER BY name;`;
|
||||
const result = await db.query(q);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOrganizationHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { year } = req.query;
|
||||
const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : '';
|
||||
const params = year ? [req.user?.owner_id, year] : [req.user?.owner_id];
|
||||
|
||||
const q = `SELECT oh.id, oh.organization_id, oh.holiday_type_id, oh.name, oh.description,
|
||||
oh.date, oh.is_recurring, oh.created_at, oh.updated_at,
|
||||
ht.name as holiday_type_name, ht.color_code
|
||||
FROM organization_holidays oh
|
||||
JOIN holiday_types ht ON oh.holiday_type_id = ht.id
|
||||
WHERE oh.organization_id = (
|
||||
SELECT id FROM organizations WHERE user_id = $1
|
||||
) ${yearFilter}
|
||||
ORDER BY oh.date;`;
|
||||
|
||||
const result = await db.query(q, params);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createOrganizationHoliday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { name, description, date, holiday_type_id, is_recurring = false }: ICreateHolidayRequest = req.body;
|
||||
|
||||
const q = `INSERT INTO organization_holidays (organization_id, holiday_type_id, name, description, date, is_recurring)
|
||||
VALUES (
|
||||
(SELECT id FROM organizations WHERE user_id = $1),
|
||||
$2, $3, $4, $5, $6
|
||||
)
|
||||
RETURNING id;`;
|
||||
|
||||
const result = await db.query(q, [req.user?.owner_id, holiday_type_id, name, description, date, is_recurring]);
|
||||
return res.status(201).send(new ServerResponse(true, result.rows[0]));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateOrganizationHoliday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const { name, description, date, holiday_type_id, is_recurring }: IUpdateHolidayRequest = req.body;
|
||||
|
||||
const updateFields = [];
|
||||
const values = [req.user?.owner_id, id];
|
||||
let paramIndex = 3;
|
||||
|
||||
if (name !== undefined) {
|
||||
updateFields.push(`name = $${paramIndex++}`);
|
||||
values.push(name);
|
||||
}
|
||||
if (description !== undefined) {
|
||||
updateFields.push(`description = $${paramIndex++}`);
|
||||
values.push(description);
|
||||
}
|
||||
if (date !== undefined) {
|
||||
updateFields.push(`date = $${paramIndex++}`);
|
||||
values.push(date);
|
||||
}
|
||||
if (holiday_type_id !== undefined) {
|
||||
updateFields.push(`holiday_type_id = $${paramIndex++}`);
|
||||
values.push(holiday_type_id);
|
||||
}
|
||||
if (is_recurring !== undefined) {
|
||||
updateFields.push(`is_recurring = $${paramIndex++}`);
|
||||
values.push(is_recurring.toString());
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).send(new ServerResponse(false, 'No fields to update'));
|
||||
}
|
||||
|
||||
const q = `UPDATE organization_holidays
|
||||
SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2 AND organization_id = (
|
||||
SELECT id FROM organizations WHERE user_id = $1
|
||||
)
|
||||
RETURNING id;`;
|
||||
|
||||
const result = await db.query(q, values);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).send(new ServerResponse(false, 'Holiday not found'));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows[0]));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteOrganizationHoliday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
|
||||
const q = `DELETE FROM organization_holidays
|
||||
WHERE id = $2 AND organization_id = (
|
||||
SELECT id FROM organizations WHERE user_id = $1
|
||||
)
|
||||
RETURNING id;`;
|
||||
|
||||
const result = await db.query(q, [req.user?.owner_id, id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).send(new ServerResponse(false, 'Holiday not found'));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { message: 'Holiday deleted successfully' }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { country_code, year } = req.query;
|
||||
|
||||
if (!country_code) {
|
||||
return res.status(400).send(new ServerResponse(false, 'Country code is required'));
|
||||
}
|
||||
|
||||
const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : '';
|
||||
const params = year ? [country_code, year] : [country_code];
|
||||
|
||||
const q = `SELECT id, country_code, name, description, date, is_recurring, created_at, updated_at
|
||||
FROM country_holidays
|
||||
WHERE country_code = $1 ${yearFilter}
|
||||
ORDER BY date;`;
|
||||
|
||||
const result = await db.query(q, params);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getAvailableCountries(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT DISTINCT c.code, c.name
|
||||
FROM countries c
|
||||
JOIN country_holidays ch ON c.code = ch.country_code
|
||||
ORDER BY c.name;`;
|
||||
|
||||
const result = await db.query(q);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async importCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { country_code, year }: IImportCountryHolidaysRequest = req.body;
|
||||
|
||||
if (!country_code) {
|
||||
return res.status(400).send(new ServerResponse(false, 'Country code is required'));
|
||||
}
|
||||
|
||||
// Get organization ID
|
||||
const orgQ = `SELECT id FROM organizations WHERE user_id = $1`;
|
||||
const orgResult = await db.query(orgQ, [req.user?.owner_id]);
|
||||
const organizationId = orgResult.rows[0]?.id;
|
||||
|
||||
if (!organizationId) {
|
||||
return res.status(404).send(new ServerResponse(false, 'Organization not found'));
|
||||
}
|
||||
|
||||
// Get default holiday type (Public Holiday)
|
||||
const typeQ = `SELECT id FROM holiday_types WHERE name = 'Public Holiday' LIMIT 1`;
|
||||
const typeResult = await db.query(typeQ);
|
||||
const holidayTypeId = typeResult.rows[0]?.id;
|
||||
|
||||
if (!holidayTypeId) {
|
||||
return res.status(404).send(new ServerResponse(false, 'Default holiday type not found'));
|
||||
}
|
||||
|
||||
// Get country holidays for the specified year
|
||||
const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : '';
|
||||
const params = year ? [country_code, year] : [country_code];
|
||||
|
||||
const holidaysQ = `SELECT name, description, date, is_recurring
|
||||
FROM country_holidays
|
||||
WHERE country_code = $1 ${yearFilter}`;
|
||||
|
||||
const holidaysResult = await db.query(holidaysQ, params);
|
||||
|
||||
if (holidaysResult.rows.length === 0) {
|
||||
return res.status(404).send(new ServerResponse(false, 'No holidays found for this country and year'));
|
||||
}
|
||||
|
||||
// Import holidays to organization
|
||||
const importQ = `INSERT INTO organization_holidays (organization_id, holiday_type_id, name, description, date, is_recurring)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (organization_id, date) DO NOTHING`;
|
||||
|
||||
let importedCount = 0;
|
||||
for (const holiday of holidaysResult.rows) {
|
||||
try {
|
||||
await db.query(importQ, [
|
||||
organizationId,
|
||||
holidayTypeId,
|
||||
holiday.name,
|
||||
holiday.description,
|
||||
holiday.date,
|
||||
holiday.is_recurring
|
||||
]);
|
||||
importedCount++;
|
||||
} catch (error) {
|
||||
// Skip duplicates
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {
|
||||
message: `Successfully imported ${importedCount} holidays`,
|
||||
imported_count: importedCount
|
||||
}));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getHolidayCalendar(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).send(new ServerResponse(false, 'Year and month are required'));
|
||||
}
|
||||
|
||||
const q = `SELECT oh.id, oh.name, oh.description, oh.date, oh.is_recurring,
|
||||
ht.name as holiday_type_name, ht.color_code,
|
||||
'organization' as source
|
||||
FROM organization_holidays oh
|
||||
JOIN holiday_types ht ON oh.holiday_type_id = ht.id
|
||||
WHERE oh.organization_id = (
|
||||
SELECT id FROM organizations WHERE user_id = $1
|
||||
)
|
||||
AND EXTRACT(YEAR FROM oh.date) = $2
|
||||
AND EXTRACT(MONTH FROM oh.date) = $3
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT ch.id, ch.name, ch.description, ch.date, ch.is_recurring,
|
||||
'Public Holiday' as holiday_type_name, '#f37070' as color_code,
|
||||
'country' as source
|
||||
FROM country_holidays ch
|
||||
JOIN organizations o ON ch.country_code = (
|
||||
SELECT c.code FROM countries c WHERE c.id = o.country
|
||||
)
|
||||
WHERE o.user_id = $1
|
||||
AND EXTRACT(YEAR FROM ch.date) = $2
|
||||
AND EXTRACT(MONTH FROM ch.date) = $3
|
||||
|
||||
ORDER BY date;`;
|
||||
|
||||
const result = await db.query(q, [req.user?.owner_id, year, month]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
54
worklenz-backend/src/interfaces/holiday.interface.ts
Normal file
54
worklenz-backend/src/interfaces/holiday.interface.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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?: IHolidayType;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
26
worklenz-backend/src/routes/apis/holiday-api-router.ts
Normal file
26
worklenz-backend/src/routes/apis/holiday-api-router.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import express from 'express';
|
||||
import HolidayController from '../../controllers/holiday-controller';
|
||||
import safeControllerFunction from '../../shared/safe-controller-function';
|
||||
import teamOwnerOrAdminValidator from '../../middlewares/validators/team-owner-or-admin-validator';
|
||||
import idParamValidator from '../../middlewares/validators/id-param-validator';
|
||||
|
||||
const holidayApiRouter = express.Router();
|
||||
|
||||
// Holiday types
|
||||
holidayApiRouter.get('/types', safeControllerFunction(HolidayController.getHolidayTypes));
|
||||
|
||||
// Organization holidays
|
||||
holidayApiRouter.get('/organization', teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.getOrganizationHolidays));
|
||||
holidayApiRouter.post('/organization', teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.createOrganizationHoliday));
|
||||
holidayApiRouter.put('/organization/:id', teamOwnerOrAdminValidator, idParamValidator, safeControllerFunction(HolidayController.updateOrganizationHoliday));
|
||||
holidayApiRouter.delete('/organization/:id', teamOwnerOrAdminValidator, idParamValidator, safeControllerFunction(HolidayController.deleteOrganizationHoliday));
|
||||
|
||||
// Country holidays
|
||||
holidayApiRouter.get('/countries', safeControllerFunction(HolidayController.getAvailableCountries));
|
||||
holidayApiRouter.get('/countries/:country_code', safeControllerFunction(HolidayController.getCountryHolidays));
|
||||
holidayApiRouter.post('/import', teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.importCountryHolidays));
|
||||
|
||||
// Calendar view
|
||||
holidayApiRouter.get('/calendar', teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.getHolidayCalendar));
|
||||
|
||||
export default holidayApiRouter;
|
||||
@@ -56,7 +56,9 @@ import billingApiRouter from "./billing-api-router";
|
||||
import taskDependenciesApiRouter from "./task-dependencies-api-router";
|
||||
|
||||
import taskRecurringApiRouter from "./task-recurring-api-router";
|
||||
|
||||
|
||||
import customColumnsApiRouter from "./custom-columns-api-router";
|
||||
import holidayApiRouter from "./holiday-api-router";
|
||||
|
||||
const api = express.Router();
|
||||
|
||||
@@ -117,4 +119,6 @@ api.use("/task-recurring", taskRecurringApiRouter);
|
||||
|
||||
api.use("/custom-columns", customColumnsApiRouter);
|
||||
|
||||
api.use("/holidays", holidayApiRouter);
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -4,5 +4,33 @@
|
||||
"owner": "Organization Owner",
|
||||
"admins": "Organization Admins",
|
||||
"contactNumber": "Add Contact Number",
|
||||
"edit": "Edit"
|
||||
"edit": "Edit",
|
||||
"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"
|
||||
}
|
||||
|
||||
68
worklenz-frontend/src/api/holiday/holiday.api.service.ts
Normal file
68
worklenz-frontend/src/api/holiday/holiday.api.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
@@ -8,10 +8,10 @@ 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 { IOrganization, IOrganizationAdmin } from '@/types/admin-center/admin-center.types';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { tr } from 'date-fns/locale';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -82,6 +82,8 @@ const Overview: React.FC = () => {
|
||||
themeMode={themeMode}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<HolidayCalendar themeMode={themeMode} />
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
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