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

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

View File

@@ -0,0 +1,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);

View 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

View File

@@ -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",

View File

@@ -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",

View File

@@ -0,0 +1,265 @@
const Holidays = require("date-holidays");
const { Pool } = require("pg");
const config = require("../build/config/db-config").default;
// 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);

View 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."

View File

@@ -0,0 +1,365 @@
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));
}
@HandleExceptions()
public static async populateCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const Holidays = require("date-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: "BR", name: "Brazil" },
{ code: "AR", name: "Argentina" },
{ code: "MX", name: "Mexico" },
{ code: "ZA", name: "South Africa" },
{ code: "NZ", name: "New Zealand" }
];
let totalPopulated = 0;
const errors = [];
for (const country of countries) {
try {
const hd = new Holidays(country.code);
for (let year = 2020; year <= 2030; year++) {
const holidays = hd.getHolidays(year);
for (const holiday of holidays) {
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";
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 db.query(query, [
country.code,
name,
description,
dateStr,
true
]);
totalPopulated++;
}
}
} catch (error) {
errors.push(`${country.name}: ${error.message}`);
}
}
const response = {
success: true,
message: `Successfully populated ${totalPopulated} holidays`,
total_populated: totalPopulated,
errors: errors.length > 0 ? errors : undefined
};
return res.status(200).send(new ServerResponse(true, response));
}
}

View 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;
}

View File

@@ -0,0 +1,29 @@
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));
// Populate holidays
holidayApiRouter.post("/populate", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.populateCountryHolidays));
export default holidayApiRouter;

View File

@@ -61,6 +61,7 @@ import customColumnsApiRouter from "./custom-columns-api-router";
import projectFinanceApiRouter from "./project-finance-api-router";
import projectRatecardApiRouter from "./project-ratecard-api-router";
import ratecardApiRouter from "./ratecard-api-router";
import holidayApiRouter from "./holiday-api-router";
const api = express.Router();
@@ -127,4 +128,6 @@ api.use("/project-ratecard", projectRatecardApiRouter);
api.use("/ratecard", ratecardApiRouter);
api.use("/holidays", holidayApiRouter);
export default api;