diff --git a/worklenz-backend/scripts/populate-holidays.js b/worklenz-backend/scripts/populate-holidays.js index 35b53c22..cc8a79c9 100644 --- a/worklenz-backend/scripts/populate-holidays.js +++ b/worklenz-backend/scripts/populate-holidays.js @@ -1,211 +1,211 @@ -const Holidays = require('date-holidays'); -const { Pool } = require('pg'); -const config = require('../src/config/db-config'); +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' } + { 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...'); + console.log("Starting holiday population..."); for (const country of countries) { console.log(`Processing ${country.name} (${country.code})...`); @@ -219,13 +219,13 @@ async function populateHolidays() { for (const holiday of holidays) { // Skip if holiday is not a date object - if (!holiday.date || typeof holiday.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'; + 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 = ` @@ -251,10 +251,10 @@ async function populateHolidays() { } } - console.log('Holiday population completed!'); + console.log("Holiday population completed!"); } catch (error) { - console.error('Database error:', error); + console.error("Database error:", error); } finally { client.release(); await pool.end(); diff --git a/worklenz-backend/src/controllers/holiday-controller.ts b/worklenz-backend/src/controllers/holiday-controller.ts index b045e153..11e00e15 100644 --- a/worklenz-backend/src/controllers/holiday-controller.ts +++ b/worklenz-backend/src/controllers/holiday-controller.ts @@ -1,14 +1,14 @@ -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 { 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'; +} from "../interfaces/holiday.interface"; export default class HolidayController extends WorklenzControllerBase { @HandleExceptions() @@ -23,7 +23,7 @@ export default class HolidayController extends WorklenzControllerBase { @HandleExceptions() public static async getOrganizationHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { year } = req.query; - const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : ''; + 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, @@ -86,11 +86,11 @@ export default class HolidayController extends WorklenzControllerBase { } if (updateFields.length === 0) { - return res.status(400).send(new ServerResponse(false, 'No fields to update')); + return res.status(400).send(new ServerResponse(false, "No fields to update")); } const q = `UPDATE organization_holidays - SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP + SET ${updateFields.join(", ")}, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND organization_id = ( SELECT id FROM organizations WHERE user_id = $1 ) @@ -99,7 +99,7 @@ export default class HolidayController extends WorklenzControllerBase { 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(404).send(new ServerResponse(false, "Holiday not found")); } return res.status(200).send(new ServerResponse(true, result.rows[0])); @@ -118,10 +118,10 @@ export default class HolidayController extends WorklenzControllerBase { 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(404).send(new ServerResponse(false, "Holiday not found")); } - return res.status(200).send(new ServerResponse(true, { message: 'Holiday deleted successfully' })); + return res.status(200).send(new ServerResponse(true, { message: "Holiday deleted successfully" })); } @HandleExceptions() @@ -129,10 +129,10 @@ export default class HolidayController extends WorklenzControllerBase { const { country_code, year } = req.query; if (!country_code) { - return res.status(400).send(new ServerResponse(false, 'Country code is required')); + return res.status(400).send(new ServerResponse(false, "Country code is required")); } - const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : ''; + 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 @@ -160,7 +160,7 @@ export default class HolidayController extends WorklenzControllerBase { const { country_code, year }: IImportCountryHolidaysRequest = req.body; if (!country_code) { - return res.status(400).send(new ServerResponse(false, 'Country code is required')); + return res.status(400).send(new ServerResponse(false, "Country code is required")); } // Get organization ID @@ -169,7 +169,7 @@ export default class HolidayController extends WorklenzControllerBase { const organizationId = orgResult.rows[0]?.id; if (!organizationId) { - return res.status(404).send(new ServerResponse(false, 'Organization not found')); + return res.status(404).send(new ServerResponse(false, "Organization not found")); } // Get default holiday type (Public Holiday) @@ -178,11 +178,11 @@ export default class HolidayController extends WorklenzControllerBase { const holidayTypeId = typeResult.rows[0]?.id; if (!holidayTypeId) { - return res.status(404).send(new ServerResponse(false, 'Default holiday type not found')); + 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 yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : ""; const params = year ? [country_code, year] : [country_code]; const holidaysQ = `SELECT name, description, date, is_recurring @@ -192,7 +192,7 @@ export default class HolidayController extends WorklenzControllerBase { 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')); + return res.status(404).send(new ServerResponse(false, "No holidays found for this country and year")); } // Import holidays to organization @@ -229,7 +229,7 @@ export default class HolidayController extends WorklenzControllerBase { const { year, month } = req.query; if (!year || !month) { - return res.status(400).send(new ServerResponse(false, 'Year and month are required')); + 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, @@ -261,4 +261,105 @@ export default class HolidayController extends WorklenzControllerBase { 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/routes/apis/holiday-api-router.ts b/worklenz-backend/src/routes/apis/holiday-api-router.ts index e24f036c..1bede223 100644 --- a/worklenz-backend/src/routes/apis/holiday-api-router.ts +++ b/worklenz-backend/src/routes/apis/holiday-api-router.ts @@ -1,26 +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'; +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)); +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)); +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)); +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)); +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-frontend/src/api/holiday/holiday.api.service.ts b/worklenz-frontend/src/api/holiday/holiday.api.service.ts index 5a7d4444..ceac5f65 100644 --- a/worklenz-frontend/src/api/holiday/holiday.api.service.ts +++ b/worklenz-frontend/src/api/holiday/holiday.api.service.ts @@ -65,4 +65,10 @@ export const holidayApiService = { 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/pages/admin-center/overview/overview.tsx b/worklenz-frontend/src/pages/admin-center/overview/overview.tsx index 93a36280..e4cf6a57 100644 --- a/worklenz-frontend/src/pages/admin-center/overview/overview.tsx +++ b/worklenz-frontend/src/pages/admin-center/overview/overview.tsx @@ -1,6 +1,6 @@ -import { EditOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons'; +import { EditOutlined, MailOutlined, PhoneOutlined, DatabaseOutlined } from '@ant-design/icons'; import { PageHeader } from '@ant-design/pro-components'; -import { Button, Card, Input, Space, Tooltip, Typography } from 'antd'; +import { Button, Card, Input, Space, Tooltip, Typography, message } from 'antd'; import React, { useEffect, useState } from 'react'; import OrganizationAdminsTable from '@/components/admin-center/overview/organization-admins-table/organization-admins-table'; import { useAppSelector } from '@/hooks/useAppSelector'; @@ -10,6 +10,7 @@ import OrganizationName from '@/components/admin-center/overview/organization-na import OrganizationOwner from '@/components/admin-center/overview/organization-owner/organization-owner'; import HolidayCalendar from '@/components/admin-center/overview/holiday-calendar/holiday-calendar'; import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service'; +import { holidayApiService } from '@/api/holiday/holiday.api.service'; import { IOrganization, IOrganizationAdmin } from '@/types/admin-center/admin-center.types'; import logger from '@/utils/errorLogger'; @@ -19,6 +20,7 @@ const Overview: React.FC = () => { const [organization, setOrganization] = useState(null); const [organizationAdmins, setOrganizationAdmins] = useState(null); const [loadingAdmins, setLoadingAdmins] = useState(false); + const [populatingHolidays, setPopulatingHolidays] = useState(false); const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode); const { t } = useTranslation('admin-center/overview'); @@ -48,6 +50,21 @@ const Overview: React.FC = () => { } }; + const handlePopulateHolidays = async () => { + setPopulatingHolidays(true); + try { + const res = await holidayApiService.populateCountryHolidays(); + if (res.done) { + message.success(`Successfully populated ${res.body.total_populated} holidays`); + } + } catch (error) { + logger.error('Error populating holidays', error); + message.error('Failed to populate holidays'); + } finally { + setPopulatingHolidays(false); + } + }; + useEffect(() => { getOrganizationDetails(); getOrganizationAdmins(); @@ -55,7 +72,20 @@ const Overview: React.FC = () => { return (
- {t('overview')}} style={{ padding: '16px 0' }} /> + {t('overview')}} + style={{ padding: '16px 0' }} + extra={[ + + ]} + />