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..cc8a79c9 --- /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("../build/config/db-config").default; + +// Database connection +const pool = new Pool(config); + +// Countries to populate with holidays +const countries = [ + { code: "US", name: "United States" }, + { code: "GB", name: "United Kingdom" }, + { code: "CA", name: "Canada" }, + { code: "AU", name: "Australia" }, + { code: "DE", name: "Germany" }, + { code: "FR", name: "France" }, + { code: "IT", name: "Italy" }, + { code: "ES", name: "Spain" }, + { code: "NL", name: "Netherlands" }, + { code: "BE", name: "Belgium" }, + { code: "CH", name: "Switzerland" }, + { code: "AT", name: "Austria" }, + { code: "SE", name: "Sweden" }, + { code: "NO", name: "Norway" }, + { code: "DK", name: "Denmark" }, + { code: "FI", name: "Finland" }, + { code: "PL", name: "Poland" }, + { code: "CZ", name: "Czech Republic" }, + { code: "HU", name: "Hungary" }, + { code: "RO", name: "Romania" }, + { code: "BG", name: "Bulgaria" }, + { code: "HR", name: "Croatia" }, + { code: "SI", name: "Slovenia" }, + { code: "SK", name: "Slovakia" }, + { code: "LT", name: "Lithuania" }, + { code: "LV", name: "Latvia" }, + { code: "EE", name: "Estonia" }, + { code: "IE", name: "Ireland" }, + { code: "PT", name: "Portugal" }, + { code: "GR", name: "Greece" }, + { code: "CY", name: "Cyprus" }, + { code: "MT", name: "Malta" }, + { code: "LU", name: "Luxembourg" }, + { code: "IS", name: "Iceland" }, + { code: "CN", name: "China" }, + { code: "JP", name: "Japan" }, + { code: "KR", name: "South Korea" }, + { code: "IN", name: "India" }, + { code: "PK", name: "Pakistan" }, + { code: "BD", name: "Bangladesh" }, + { code: "LK", name: "Sri Lanka" }, + { code: "NP", name: "Nepal" }, + { code: "TH", name: "Thailand" }, + { code: "VN", name: "Vietnam" }, + { code: "MY", name: "Malaysia" }, + { code: "SG", name: "Singapore" }, + { code: "ID", name: "Indonesia" }, + { code: "PH", name: "Philippines" }, + { code: "MM", name: "Myanmar" }, + { code: "KH", name: "Cambodia" }, + { code: "LA", name: "Laos" }, + { code: "BN", name: "Brunei" }, + { code: "TL", name: "Timor-Leste" }, + { code: "MN", name: "Mongolia" }, + { code: "KZ", name: "Kazakhstan" }, + { code: "UZ", name: "Uzbekistan" }, + { code: "KG", name: "Kyrgyzstan" }, + { code: "TJ", name: "Tajikistan" }, + { code: "TM", name: "Turkmenistan" }, + { code: "AF", name: "Afghanistan" }, + { code: "IR", name: "Iran" }, + { code: "IQ", name: "Iraq" }, + { code: "SA", name: "Saudi Arabia" }, + { code: "AE", name: "United Arab Emirates" }, + { code: "QA", name: "Qatar" }, + { code: "KW", name: "Kuwait" }, + { code: "BH", name: "Bahrain" }, + { code: "OM", name: "Oman" }, + { code: "YE", name: "Yemen" }, + { code: "JO", name: "Jordan" }, + { code: "LB", name: "Lebanon" }, + { code: "SY", name: "Syria" }, + { code: "IL", name: "Israel" }, + { code: "PS", name: "Palestine" }, + { code: "TR", name: "Turkey" }, + { code: "GE", name: "Georgia" }, + { code: "AM", name: "Armenia" }, + { code: "AZ", name: "Azerbaijan" }, + { code: "NZ", name: "New Zealand" }, + { code: "FJ", name: "Fiji" }, + { code: "PG", name: "Papua New Guinea" }, + { code: "SB", name: "Solomon Islands" }, + { code: "VU", name: "Vanuatu" }, + { code: "NC", name: "New Caledonia" }, + { code: "PF", name: "French Polynesia" }, + { code: "TO", name: "Tonga" }, + { code: "WS", name: "Samoa" }, + { code: "KI", name: "Kiribati" }, + { code: "TV", name: "Tuvalu" }, + { code: "NR", name: "Nauru" }, + { code: "PW", name: "Palau" }, + { code: "MH", name: "Marshall Islands" }, + { code: "FM", name: "Micronesia" }, + { code: "ZA", name: "South Africa" }, + { code: "EG", name: "Egypt" }, + { code: "NG", name: "Nigeria" }, + { code: "KE", name: "Kenya" }, + { code: "ET", name: "Ethiopia" }, + { code: "TZ", name: "Tanzania" }, + { code: "UG", name: "Uganda" }, + { code: "GH", name: "Ghana" }, + { code: "CI", name: "Ivory Coast" }, + { code: "SN", name: "Senegal" }, + { code: "ML", name: "Mali" }, + { code: "BF", name: "Burkina Faso" }, + { code: "NE", name: "Niger" }, + { code: "TD", name: "Chad" }, + { code: "CM", name: "Cameroon" }, + { code: "CF", name: "Central African Republic" }, + { code: "CG", name: "Republic of the Congo" }, + { code: "CD", name: "Democratic Republic of the Congo" }, + { code: "GA", name: "Gabon" }, + { code: "GQ", name: "Equatorial Guinea" }, + { code: "ST", name: "Sรฃo Tomรฉ and Prรญncipe" }, + { code: "AO", name: "Angola" }, + { code: "ZM", name: "Zambia" }, + { code: "ZW", name: "Zimbabwe" }, + { code: "BW", name: "Botswana" }, + { code: "NA", name: "Namibia" }, + { code: "LS", name: "Lesotho" }, + { code: "SZ", name: "Eswatini" }, + { code: "MG", name: "Madagascar" }, + { code: "MU", name: "Mauritius" }, + { code: "SC", name: "Seychelles" }, + { code: "KM", name: "Comoros" }, + { code: "DJ", name: "Djibouti" }, + { code: "SO", name: "Somalia" }, + { code: "ER", name: "Eritrea" }, + { code: "SD", name: "Sudan" }, + { code: "SS", name: "South Sudan" }, + { code: "LY", name: "Libya" }, + { code: "TN", name: "Tunisia" }, + { code: "DZ", name: "Algeria" }, + { code: "MA", name: "Morocco" }, + { code: "EH", name: "Western Sahara" }, + { code: "MR", name: "Mauritania" }, + { code: "GM", name: "Gambia" }, + { code: "GW", name: "Guinea-Bissau" }, + { code: "GN", name: "Guinea" }, + { code: "SL", name: "Sierra Leone" }, + { code: "LR", name: "Liberia" }, + { code: "TG", name: "Togo" }, + { code: "BJ", name: "Benin" }, + { code: "BR", name: "Brazil" }, + { code: "AR", name: "Argentina" }, + { code: "CL", name: "Chile" }, + { code: "CO", name: "Colombia" }, + { code: "PE", name: "Peru" }, + { code: "VE", name: "Venezuela" }, + { code: "EC", name: "Ecuador" }, + { code: "BO", name: "Bolivia" }, + { code: "PY", name: "Paraguay" }, + { code: "UY", name: "Uruguay" }, + { code: "GY", name: "Guyana" }, + { code: "SR", name: "Suriname" }, + { code: "FK", name: "Falkland Islands" }, + { code: "GF", name: "French Guiana" }, + { code: "MX", name: "Mexico" }, + { code: "GT", name: "Guatemala" }, + { code: "BZ", name: "Belize" }, + { code: "SV", name: "El Salvador" }, + { code: "HN", name: "Honduras" }, + { code: "NI", name: "Nicaragua" }, + { code: "CR", name: "Costa Rica" }, + { code: "PA", name: "Panama" }, + { code: "CU", name: "Cuba" }, + { code: "JM", name: "Jamaica" }, + { code: "HT", name: "Haiti" }, + { code: "DO", name: "Dominican Republic" }, + { code: "PR", name: "Puerto Rico" }, + { code: "TT", name: "Trinidad and Tobago" }, + { code: "BB", name: "Barbados" }, + { code: "GD", name: "Grenada" }, + { code: "LC", name: "Saint Lucia" }, + { code: "VC", name: "Saint Vincent and the Grenadines" }, + { code: "AG", name: "Antigua and Barbuda" }, + { code: "KN", name: "Saint Kitts and Nevis" }, + { code: "DM", name: "Dominica" }, + { code: "BS", name: "Bahamas" }, + { code: "TC", name: "Turks and Caicos Islands" }, + { code: "KY", name: "Cayman Islands" }, + { code: "BM", name: "Bermuda" }, + { code: "AI", name: "Anguilla" }, + { code: "VG", name: "British Virgin Islands" }, + { code: "VI", name: "U.S. Virgin Islands" }, + { code: "AW", name: "Aruba" }, + { code: "CW", name: "Curaรงao" }, + { code: "SX", name: "Sint Maarten" }, + { code: "MF", name: "Saint Martin" }, + { code: "BL", name: "Saint Barthรฉlemy" }, + { code: "GP", name: "Guadeloupe" }, + { code: "MQ", name: "Martinique" } +]; + +async function populateHolidays() { + const client = await pool.connect(); + + try { + console.log("Starting holiday population..."); + + for (const country of countries) { + console.log(`Processing ${country.name} (${country.code})...`); + + try { + const hd = new Holidays(country.code); + + // Get holidays for multiple years (2020-2030) + for (let year = 2020; year <= 2030; year++) { + const holidays = hd.getHolidays(year); + + for (const holiday of holidays) { + // Skip if holiday is not a date object + if (!holiday.date || typeof holiday.date !== "object") { + continue; + } + + const dateStr = holiday.date.toISOString().split("T")[0]; + const name = holiday.name || "Unknown Holiday"; + const description = holiday.type || "Public Holiday"; + + // Insert holiday into database + const query = ` + INSERT INTO country_holidays (country_code, name, description, date, is_recurring) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (country_code, name, date) DO NOTHING + `; + + await client.query(query, [ + country.code, + name, + description, + dateStr, + true // Most holidays are recurring + ]); + } + } + + console.log(`โœ“ Completed ${country.name}`); + + } catch (error) { + console.log(`โœ— Error processing ${country.name}: ${error.message}`); + } + } + + console.log("Holiday population completed!"); + + } catch (error) { + console.error("Database error:", error); + } finally { + client.release(); + await pool.end(); + } +} + +// Run the script +populateHolidays().catch(console.error); \ 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..11e00e15 --- /dev/null +++ b/worklenz-backend/src/controllers/holiday-controller.ts @@ -0,0 +1,365 @@ +import { IWorkLenzRequest } from "../interfaces/worklenz-request"; +import { IWorkLenzResponse } from "../interfaces/worklenz-response"; +import db from "../config/db"; +import { ServerResponse } from "../models/server-response"; +import WorklenzControllerBase from "./worklenz-controller-base"; +import HandleExceptions from "../decorators/handle-exceptions"; +import { + ICreateHolidayRequest, + IUpdateHolidayRequest, + IImportCountryHolidaysRequest, +} from "../interfaces/holiday.interface"; + +export default class HolidayController extends WorklenzControllerBase { + @HandleExceptions() + public static async getHolidayTypes(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + 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)); + } + + @HandleExceptions() + public static async populateCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const Holidays = require("date-holidays"); + + const countries = [ + { code: "US", name: "United States" }, + { code: "GB", name: "United Kingdom" }, + { code: "CA", name: "Canada" }, + { code: "AU", name: "Australia" }, + { code: "DE", name: "Germany" }, + { code: "FR", name: "France" }, + { code: "IT", name: "Italy" }, + { code: "ES", name: "Spain" }, + { code: "NL", name: "Netherlands" }, + { code: "BE", name: "Belgium" }, + { code: "CH", name: "Switzerland" }, + { code: "AT", name: "Austria" }, + { code: "SE", name: "Sweden" }, + { code: "NO", name: "Norway" }, + { code: "DK", name: "Denmark" }, + { code: "FI", name: "Finland" }, + { code: "PL", name: "Poland" }, + { code: "CZ", name: "Czech Republic" }, + { code: "HU", name: "Hungary" }, + { code: "RO", name: "Romania" }, + { code: "BG", name: "Bulgaria" }, + { code: "HR", name: "Croatia" }, + { code: "SI", name: "Slovenia" }, + { code: "SK", name: "Slovakia" }, + { code: "LT", name: "Lithuania" }, + { code: "LV", name: "Latvia" }, + { code: "EE", name: "Estonia" }, + { code: "IE", name: "Ireland" }, + { code: "PT", name: "Portugal" }, + { code: "GR", name: "Greece" }, + { code: "CY", name: "Cyprus" }, + { code: "MT", name: "Malta" }, + { code: "LU", name: "Luxembourg" }, + { code: "IS", name: "Iceland" }, + { code: "CN", name: "China" }, + { code: "JP", name: "Japan" }, + { code: "KR", name: "South Korea" }, + { code: "IN", name: "India" }, + { code: "BR", name: "Brazil" }, + { code: "AR", name: "Argentina" }, + { code: "MX", name: "Mexico" }, + { code: "ZA", name: "South Africa" }, + { code: "NZ", name: "New Zealand" } + ]; + + let totalPopulated = 0; + const errors = []; + + for (const country of countries) { + try { + const hd = new Holidays(country.code); + + for (let year = 2020; year <= 2030; year++) { + const holidays = hd.getHolidays(year); + + for (const holiday of holidays) { + if (!holiday.date || typeof holiday.date !== "object") { + continue; + } + + const dateStr = holiday.date.toISOString().split("T")[0]; + const name = holiday.name || "Unknown Holiday"; + const description = holiday.type || "Public Holiday"; + + const query = ` + INSERT INTO country_holidays (country_code, name, description, date, is_recurring) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (country_code, name, date) DO NOTHING + `; + + await db.query(query, [ + country.code, + name, + description, + dateStr, + true + ]); + + totalPopulated++; + } + } + } catch (error) { + errors.push(`${country.name}: ${error.message}`); + } + } + + const response = { + success: true, + message: `Successfully populated ${totalPopulated} holidays`, + total_populated: totalPopulated, + errors: errors.length > 0 ? errors : undefined + }; + + return res.status(200).send(new ServerResponse(true, response)); + } +} \ 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..1bede223 --- /dev/null +++ b/worklenz-backend/src/routes/apis/holiday-api-router.ts @@ -0,0 +1,29 @@ +import express from "express"; +import HolidayController from "../../controllers/holiday-controller"; +import safeControllerFunction from "../../shared/safe-controller-function"; +import teamOwnerOrAdminValidator from "../../middlewares/validators/team-owner-or-admin-validator"; +import idParamValidator from "../../middlewares/validators/id-param-validator"; + +const holidayApiRouter = express.Router(); + +// Holiday types +holidayApiRouter.get("/types", safeControllerFunction(HolidayController.getHolidayTypes)); + +// Organization holidays +holidayApiRouter.get("/organization", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.getOrganizationHolidays)); +holidayApiRouter.post("/organization", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.createOrganizationHoliday)); +holidayApiRouter.put("/organization/:id", teamOwnerOrAdminValidator, idParamValidator, safeControllerFunction(HolidayController.updateOrganizationHoliday)); +holidayApiRouter.delete("/organization/:id", teamOwnerOrAdminValidator, idParamValidator, safeControllerFunction(HolidayController.deleteOrganizationHoliday)); + +// Country holidays +holidayApiRouter.get("/countries", safeControllerFunction(HolidayController.getAvailableCountries)); +holidayApiRouter.get("/countries/:country_code", safeControllerFunction(HolidayController.getCountryHolidays)); +holidayApiRouter.post("/import", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.importCountryHolidays)); + +// Calendar view +holidayApiRouter.get("/calendar", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.getHolidayCalendar)); + +// Populate holidays +holidayApiRouter.post("/populate", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.populateCountryHolidays)); + +export default holidayApiRouter; \ 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 f5bcc5e1..65b66cb8 100644 --- a/worklenz-backend/src/routes/apis/index.ts +++ b/worklenz-backend/src/routes/apis/index.ts @@ -61,6 +61,7 @@ import customColumnsApiRouter from "./custom-columns-api-router"; import projectFinanceApiRouter from "./project-finance-api-router"; import projectRatecardApiRouter from "./project-ratecard-api-router"; import ratecardApiRouter from "./ratecard-api-router"; +import holidayApiRouter from "./holiday-api-router"; const api = express.Router(); @@ -127,4 +128,6 @@ api.use("/project-ratecard", projectRatecardApiRouter); api.use("/ratecard", ratecardApiRouter); +api.use("/holidays", holidayApiRouter); + export default api; diff --git a/worklenz-frontend/public/locales/en/admin-center/overview.json b/worklenz-frontend/public/locales/en/admin-center/overview.json index 24ee6a36..594205ed 100644 --- a/worklenz-frontend/public/locales/en/admin-center/overview.json +++ b/worklenz-frontend/public/locales/en/admin-center/overview.json @@ -28,5 +28,33 @@ "manDaysCalculationDescription": "All project costs will be calculated using estimated man days ร— daily rates", "calculationMethodTooltip": "This setting applies to all projects in your organization", "calculationMethodUpdated": "Organization calculation method updated successfully", - "calculationMethodUpdateError": "Failed to update calculation method" + "calculationMethodUpdateError": "Failed to update calculation method", + "holidayCalendar": "Holiday Calendar", + "addHoliday": "Add Holiday", + "editHoliday": "Edit Holiday", + "holidayName": "Holiday Name", + "holidayNameRequired": "Please enter holiday name", + "description": "Description", + "date": "Date", + "dateRequired": "Please select a date", + "holidayType": "Holiday Type", + "holidayTypeRequired": "Please select a holiday type", + "recurring": "Recurring", + "save": "Save", + "update": "Update", + "cancel": "Cancel", + "holidayCreated": "Holiday created successfully", + "holidayUpdated": "Holiday updated successfully", + "holidayDeleted": "Holiday deleted successfully", + "errorCreatingHoliday": "Error creating holiday", + "errorUpdatingHoliday": "Error updating holiday", + "errorDeletingHoliday": "Error deleting holiday", + "importCountryHolidays": "Import Country Holidays", + "country": "Country", + "countryRequired": "Please select a country", + "selectCountry": "Select a country", + "year": "Year", + "import": "Import", + "holidaysImported": "Successfully imported {{count}} holidays", + "errorImportingHolidays": "Error importing holidays" } 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..ceac5f65 --- /dev/null +++ b/worklenz-frontend/src/api/holiday/holiday.api.service.ts @@ -0,0 +1,74 @@ +import { API_BASE_URL } from '@/shared/constants'; +import apiClient from '../api-client'; +import { IServerResponse } from '@/types/common.types'; +import { + IHolidayType, + IOrganizationHoliday, + ICountryHoliday, + IAvailableCountry, + ICreateHolidayRequest, + IUpdateHolidayRequest, + IImportCountryHolidaysRequest, + IHolidayCalendarEvent, +} from '@/types/holiday/holiday.types'; + +const rootUrl = `${API_BASE_URL}/holidays`; + +export const holidayApiService = { + // Holiday types + getHolidayTypes: async (): Promise> => { + 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; + }, + + // Populate holidays + populateCountryHolidays: async (): Promise> => { + const response = await apiClient.post>(`${rootUrl}/populate`); + 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 + > +
+ + + + + +