diff --git a/worklenz-backend/database/migrations/20250130000000-add-holiday-calendar.sql b/worklenz-backend/database/migrations/20250130000000-add-holiday-calendar.sql new file mode 100644 index 00000000..24b76fe0 --- /dev/null +++ b/worklenz-backend/database/migrations/20250130000000-add-holiday-calendar.sql @@ -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); \ No newline at end of file diff --git a/worklenz-backend/docs/HOLIDAY_SYSTEM.md b/worklenz-backend/docs/HOLIDAY_SYSTEM.md new file mode 100644 index 00000000..6a4adcc1 --- /dev/null +++ b/worklenz-backend/docs/HOLIDAY_SYSTEM.md @@ -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 \ No newline at end of file diff --git a/worklenz-backend/package-lock.json b/worklenz-backend/package-lock.json index 1a0f78d3..9c7a1756 100644 --- a/worklenz-backend/package-lock.json +++ b/worklenz-backend/package-lock.json @@ -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", diff --git a/worklenz-backend/package.json b/worklenz-backend/package.json index d4e07de2..02b9f0b0 100644 --- a/worklenz-backend/package.json +++ b/worklenz-backend/package.json @@ -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", diff --git a/worklenz-backend/scripts/populate-holidays.js b/worklenz-backend/scripts/populate-holidays.js new file mode 100644 index 00000000..35b53c22 --- /dev/null +++ b/worklenz-backend/scripts/populate-holidays.js @@ -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); \ No newline at end of file diff --git a/worklenz-backend/scripts/run-holiday-population.sh b/worklenz-backend/scripts/run-holiday-population.sh new file mode 100644 index 00000000..a4779eec --- /dev/null +++ b/worklenz-backend/scripts/run-holiday-population.sh @@ -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." \ No newline at end of file diff --git a/worklenz-backend/src/controllers/holiday-controller.ts b/worklenz-backend/src/controllers/holiday-controller.ts new file mode 100644 index 00000000..b045e153 --- /dev/null +++ b/worklenz-backend/src/controllers/holiday-controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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)); + } +} \ No newline at end of file diff --git a/worklenz-backend/src/interfaces/holiday.interface.ts b/worklenz-backend/src/interfaces/holiday.interface.ts new file mode 100644 index 00000000..df300ae9 --- /dev/null +++ b/worklenz-backend/src/interfaces/holiday.interface.ts @@ -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; +} \ No newline at end of file diff --git a/worklenz-backend/src/routes/apis/holiday-api-router.ts b/worklenz-backend/src/routes/apis/holiday-api-router.ts new file mode 100644 index 00000000..e24f036c --- /dev/null +++ b/worklenz-backend/src/routes/apis/holiday-api-router.ts @@ -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; \ No newline at end of file diff --git a/worklenz-backend/src/routes/apis/index.ts b/worklenz-backend/src/routes/apis/index.ts index 5a2019c8..dc05fe92 100644 --- a/worklenz-backend/src/routes/apis/index.ts +++ b/worklenz-backend/src/routes/apis/index.ts @@ -1,120 +1,124 @@ -import express from "express"; - -import AccessControlsController from "../../controllers/access-controls-controller"; -import AuthController from "../../controllers/auth-controller"; -import LogsController from "../../controllers/logs-controller"; -import OverviewController from "../../controllers/overview-controller"; -import TaskPrioritiesController from "../../controllers/task-priorities-controller"; - -import attachmentsApiRouter from "./attachments-api-router"; -import clientsApiRouter from "./clients-api-router"; -import jobTitlesApiRouter from "./job-titles-api-router"; -import notificationsApiRouter from "./notifications-api-router"; -import personalOverviewApiRouter from "./personal-overview-api-router"; -import projectMembersApiRouter from "./project-members-api-router"; -import projectsApiRouter from "./projects-api-router"; -import settingsApiRouter from "./settings-api-router"; -import statusesApiRouter from "./statuses-api-router"; -import subTasksApiRouter from "./sub-tasks-api-router"; -import taskCommentsApiRouter from "./task-comments-api-router"; -import taskWorkLogApiRouter from "./task-work-log-api-router"; -import tasksApiRouter from "./tasks-api-router"; -import teamMembersApiRouter from "./team-members-api-router"; -import teamsApiRouter from "./teams-api-router"; -import timezonesApiRouter from "./timezones-api-router"; -import todoListApiRouter from "./todo-list-api-router"; -import projectStatusesApiRouter from "./project-statuses-api-router"; -import labelsApiRouter from "./labels-api-router"; -import sharedProjectsApiRouter from "./shared-projects-api-router"; -import resourceAllocationApiRouter from "./resource-allocation-api-router"; -import taskTemplatesApiRouter from "./task-templates-api-router"; -import projectInsightsApiRouter from "./project-insights-api-router"; -import passwordValidator from "../../middlewares/validators/password-validator"; -import adminCenterApiRouter from "./admin-center-api-router"; -import reportingApiRouter from "./reporting-api-router"; -import activityLogsApiRouter from "./activity-logs-api-router"; -import safeControllerFunction from "../../shared/safe-controller-function"; -import projectFoldersApiRouter from "./project-folders-api-router"; -import taskPhasesApiRouter from "./task-phases-api-router"; -import projectCategoriesApiRouter from "./project-categories-api-router"; -import homePageApiRouter from "./home-page-api-router"; -import ganttApiRouter from "./gantt-api-router"; -import projectCommentsApiRouter from "./project-comments-api-router"; -import reportingExportApiRouter from "./reporting-export-api-router"; -import projectHealthsApiRouter from "./project-healths-api-router"; -import ptTasksApiRouter from "./pt-tasks-api-router"; -import projectTemplatesApiRouter from "./project-templates-api"; -import ptTaskPhasesApiRouter from "./pt_task-phases-api-router"; -import ptStatusesApiRouter from "./pt-statuses-api-router"; -import workloadApiRouter from "./gannt-apis/workload-api-router"; -import roadmapApiRouter from "./gannt-apis/roadmap-api-router"; -import scheduleApiRouter from "./gannt-apis/schedule-api-router"; -import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router"; -import projectManagerApiRouter from "./project-managers-api-router"; - -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 express from "express"; + +import AccessControlsController from "../../controllers/access-controls-controller"; +import AuthController from "../../controllers/auth-controller"; +import LogsController from "../../controllers/logs-controller"; +import OverviewController from "../../controllers/overview-controller"; +import TaskPrioritiesController from "../../controllers/task-priorities-controller"; + +import attachmentsApiRouter from "./attachments-api-router"; +import clientsApiRouter from "./clients-api-router"; +import jobTitlesApiRouter from "./job-titles-api-router"; +import notificationsApiRouter from "./notifications-api-router"; +import personalOverviewApiRouter from "./personal-overview-api-router"; +import projectMembersApiRouter from "./project-members-api-router"; +import projectsApiRouter from "./projects-api-router"; +import settingsApiRouter from "./settings-api-router"; +import statusesApiRouter from "./statuses-api-router"; +import subTasksApiRouter from "./sub-tasks-api-router"; +import taskCommentsApiRouter from "./task-comments-api-router"; +import taskWorkLogApiRouter from "./task-work-log-api-router"; +import tasksApiRouter from "./tasks-api-router"; +import teamMembersApiRouter from "./team-members-api-router"; +import teamsApiRouter from "./teams-api-router"; +import timezonesApiRouter from "./timezones-api-router"; +import todoListApiRouter from "./todo-list-api-router"; +import projectStatusesApiRouter from "./project-statuses-api-router"; +import labelsApiRouter from "./labels-api-router"; +import sharedProjectsApiRouter from "./shared-projects-api-router"; +import resourceAllocationApiRouter from "./resource-allocation-api-router"; +import taskTemplatesApiRouter from "./task-templates-api-router"; +import projectInsightsApiRouter from "./project-insights-api-router"; +import passwordValidator from "../../middlewares/validators/password-validator"; +import adminCenterApiRouter from "./admin-center-api-router"; +import reportingApiRouter from "./reporting-api-router"; +import activityLogsApiRouter from "./activity-logs-api-router"; +import safeControllerFunction from "../../shared/safe-controller-function"; +import projectFoldersApiRouter from "./project-folders-api-router"; +import taskPhasesApiRouter from "./task-phases-api-router"; +import projectCategoriesApiRouter from "./project-categories-api-router"; +import homePageApiRouter from "./home-page-api-router"; +import ganttApiRouter from "./gantt-api-router"; +import projectCommentsApiRouter from "./project-comments-api-router"; +import reportingExportApiRouter from "./reporting-export-api-router"; +import projectHealthsApiRouter from "./project-healths-api-router"; +import ptTasksApiRouter from "./pt-tasks-api-router"; +import projectTemplatesApiRouter from "./project-templates-api"; +import ptTaskPhasesApiRouter from "./pt_task-phases-api-router"; +import ptStatusesApiRouter from "./pt-statuses-api-router"; +import workloadApiRouter from "./gannt-apis/workload-api-router"; +import roadmapApiRouter from "./gannt-apis/roadmap-api-router"; +import scheduleApiRouter from "./gannt-apis/schedule-api-router"; +import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router"; +import projectManagerApiRouter from "./project-managers-api-router"; + +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(); + +api.use("/projects", projectsApiRouter); +api.use("/team-members", teamMembersApiRouter); +api.use("/job-titles", jobTitlesApiRouter); +api.use("/clients", clientsApiRouter); +api.use("/teams", teamsApiRouter); +api.use("/tasks", tasksApiRouter); +api.use("/settings", settingsApiRouter); +api.use("/personal-overview", personalOverviewApiRouter); +api.use("/statuses", statusesApiRouter); +api.use("/todo-list", todoListApiRouter); +api.use("/notifications", notificationsApiRouter); +api.use("/attachments", attachmentsApiRouter); +api.use("/sub-tasks", subTasksApiRouter); +api.use("/project-members", projectMembersApiRouter); +api.use("/task-time-log", taskWorkLogApiRouter); +api.use("/task-comments", taskCommentsApiRouter); +api.use("/timezones", timezonesApiRouter); +api.use("/project-statuses", projectStatusesApiRouter); +api.use("/labels", labelsApiRouter); +api.use("/resource-allocation", resourceAllocationApiRouter); +api.use("/shared/projects", sharedProjectsApiRouter); +api.use("/task-templates", taskTemplatesApiRouter); +api.use("/project-insights", projectInsightsApiRouter); +api.use("/admin-center", adminCenterApiRouter); +api.use("/reporting", reportingApiRouter); +api.use("/activity-logs", activityLogsApiRouter); +api.use("/projects-folders", projectFoldersApiRouter); +api.use("/task-phases", taskPhasesApiRouter); +api.use("/project-categories", projectCategoriesApiRouter); +api.use("/home", homePageApiRouter); +api.use("/gantt", ganttApiRouter); +api.use("/project-comments", projectCommentsApiRouter); +api.use("/reporting-export", reportingExportApiRouter); +api.use("/project-healths", projectHealthsApiRouter); +api.use("/project-templates", projectTemplatesApiRouter); +api.use("/pt-tasks", ptTasksApiRouter); +api.use("/pt-task-phases", ptTaskPhasesApiRouter); +api.use("/pt-statuses", ptStatusesApiRouter); +api.use("/workload-gannt", workloadApiRouter); +api.use("/roadmap-gannt", roadmapApiRouter); +api.use("/schedule-gannt", scheduleApiRouter); +api.use("/schedule-gannt-v2", scheduleApiV2Router); +api.use("/project-managers", projectManagerApiRouter); + +api.get("/overview/:id", safeControllerFunction(OverviewController.getById)); +api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get)); +api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword)); +api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles)); +api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog)); + +api.use("/billing", billingApiRouter); +api.use("/task-dependencies", taskDependenciesApiRouter); + +api.use("/task-recurring", taskRecurringApiRouter); -const api = express.Router(); - -api.use("/projects", projectsApiRouter); -api.use("/team-members", teamMembersApiRouter); -api.use("/job-titles", jobTitlesApiRouter); -api.use("/clients", clientsApiRouter); -api.use("/teams", teamsApiRouter); -api.use("/tasks", tasksApiRouter); -api.use("/settings", settingsApiRouter); -api.use("/personal-overview", personalOverviewApiRouter); -api.use("/statuses", statusesApiRouter); -api.use("/todo-list", todoListApiRouter); -api.use("/notifications", notificationsApiRouter); -api.use("/attachments", attachmentsApiRouter); -api.use("/sub-tasks", subTasksApiRouter); -api.use("/project-members", projectMembersApiRouter); -api.use("/task-time-log", taskWorkLogApiRouter); -api.use("/task-comments", taskCommentsApiRouter); -api.use("/timezones", timezonesApiRouter); -api.use("/project-statuses", projectStatusesApiRouter); -api.use("/labels", labelsApiRouter); -api.use("/resource-allocation", resourceAllocationApiRouter); -api.use("/shared/projects", sharedProjectsApiRouter); -api.use("/task-templates", taskTemplatesApiRouter); -api.use("/project-insights", projectInsightsApiRouter); -api.use("/admin-center", adminCenterApiRouter); -api.use("/reporting", reportingApiRouter); -api.use("/activity-logs", activityLogsApiRouter); -api.use("/projects-folders", projectFoldersApiRouter); -api.use("/task-phases", taskPhasesApiRouter); -api.use("/project-categories", projectCategoriesApiRouter); -api.use("/home", homePageApiRouter); -api.use("/gantt", ganttApiRouter); -api.use("/project-comments", projectCommentsApiRouter); -api.use("/reporting-export", reportingExportApiRouter); -api.use("/project-healths", projectHealthsApiRouter); -api.use("/project-templates", projectTemplatesApiRouter); -api.use("/pt-tasks", ptTasksApiRouter); -api.use("/pt-task-phases", ptTaskPhasesApiRouter); -api.use("/pt-statuses", ptStatusesApiRouter); -api.use("/workload-gannt", workloadApiRouter); -api.use("/roadmap-gannt", roadmapApiRouter); -api.use("/schedule-gannt", scheduleApiRouter); -api.use("/schedule-gannt-v2", scheduleApiV2Router); -api.use("/project-managers", projectManagerApiRouter); - -api.get("/overview/:id", safeControllerFunction(OverviewController.getById)); -api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get)); -api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword)); -api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles)); -api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog)); - -api.use("/billing", billingApiRouter); -api.use("/task-dependencies", taskDependenciesApiRouter); - -api.use("/task-recurring", taskRecurringApiRouter); - api.use("/custom-columns", customColumnsApiRouter); -export default api; +api.use("/holidays", holidayApiRouter); + +export default api; diff --git a/worklenz-frontend/public/locales/en/admin-center/overview.json b/worklenz-frontend/public/locales/en/admin-center/overview.json index efc42855..57a35ad2 100644 --- a/worklenz-frontend/public/locales/en/admin-center/overview.json +++ b/worklenz-frontend/public/locales/en/admin-center/overview.json @@ -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" } diff --git a/worklenz-frontend/src/api/holiday/holiday.api.service.ts b/worklenz-frontend/src/api/holiday/holiday.api.service.ts new file mode 100644 index 00000000..5a7d4444 --- /dev/null +++ b/worklenz-frontend/src/api/holiday/holiday.api.service.ts @@ -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> => { + const response = await apiClient.get>(`${rootUrl}/types`); + return response.data; + }, + + // Organization holidays + getOrganizationHolidays: async (year?: number): Promise> => { + const params = year ? `?year=${year}` : ''; + const response = await apiClient.get>(`${rootUrl}/organization${params}`); + return response.data; + }, + + createOrganizationHoliday: async (data: ICreateHolidayRequest): Promise> => { + const response = await apiClient.post>(`${rootUrl}/organization`, data); + return response.data; + }, + + updateOrganizationHoliday: async (id: string, data: IUpdateHolidayRequest): Promise> => { + const response = await apiClient.put>(`${rootUrl}/organization/${id}`, data); + return response.data; + }, + + deleteOrganizationHoliday: async (id: string): Promise> => { + const response = await apiClient.delete>(`${rootUrl}/organization/${id}`); + return response.data; + }, + + // Country holidays + getAvailableCountries: async (): Promise> => { + const response = await apiClient.get>(`${rootUrl}/countries`); + return response.data; + }, + + getCountryHolidays: async (countryCode: string, year?: number): Promise> => { + const params = year ? `?year=${year}` : ''; + const response = await apiClient.get>(`${rootUrl}/countries/${countryCode}${params}`); + return response.data; + }, + + importCountryHolidays: async (data: IImportCountryHolidaysRequest): Promise> => { + const response = await apiClient.post>(`${rootUrl}/import`, data); + return response.data; + }, + + // Calendar view + getHolidayCalendar: async (year: number, month: number): Promise> => { + const response = await apiClient.get>(`${rootUrl}/calendar?year=${year}&month=${month}`); + return response.data; + }, +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.css b/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.css new file mode 100644 index 00000000..c583b360 --- /dev/null +++ b/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.css @@ -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; + } +} \ No newline at end of file diff --git a/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.tsx b/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.tsx new file mode 100644 index 00000000..588516cb --- /dev/null +++ b/worklenz-frontend/src/components/admin-center/overview/holiday-calendar/holiday-calendar.tsx @@ -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 = ({ themeMode }) => { + const { t } = useTranslation('admin-center/overview'); + const [form] = Form.useForm(); + const [editForm] = Form.useForm(); + + const [holidayTypes, setHolidayTypes] = useState([]); + const [organizationHolidays, setOrganizationHolidays] = useState([]); + const [availableCountries, setAvailableCountries] = useState([]); + const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [importModalVisible, setImportModalVisible] = useState(false); + const [selectedHoliday, setSelectedHoliday] = useState(null); + const [currentDate, setCurrentDate] = useState(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 ( +
+ + {holiday.name} + +
+ ); + } + return null; + }; + + const onPanelChange = (value: Dayjs) => { + setCurrentDate(value); + }; + + return ( + +
+ + {t('holidayCalendar')} + + + + + +
+ + + + {/* Create Holiday Modal */} + { + setModalVisible(false); + form.resetFields(); + }} + footer={null} + destroyOnClose + > +
+ + + + + +