feat(holiday-system): add holiday population functionality and API integration

- Implemented a new API endpoint to populate country holidays in the database.
- Enhanced the HolidayController to handle holiday population logic for multiple countries.
- Updated the holiday API router to include the new populate endpoint.
- Added a service method in the frontend to trigger holiday population.
- Integrated a button in the admin center overview for easy access to populate holidays.
- Improved error handling and user feedback during the holiday population process.
This commit is contained in:
chamikaJ
2025-07-17 11:27:29 +05:30
parent 5214368354
commit f73c151da2
5 changed files with 380 additions and 240 deletions

View File

@@ -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();

View File

@@ -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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
const Holidays = require("date-holidays");
const countries = [
{ code: "US", name: "United States" },
{ code: "GB", name: "United Kingdom" },
{ code: "CA", name: "Canada" },
{ code: "AU", name: "Australia" },
{ code: "DE", name: "Germany" },
{ code: "FR", name: "France" },
{ code: "IT", name: "Italy" },
{ code: "ES", name: "Spain" },
{ code: "NL", name: "Netherlands" },
{ code: "BE", name: "Belgium" },
{ code: "CH", name: "Switzerland" },
{ code: "AT", name: "Austria" },
{ code: "SE", name: "Sweden" },
{ code: "NO", name: "Norway" },
{ code: "DK", name: "Denmark" },
{ code: "FI", name: "Finland" },
{ code: "PL", name: "Poland" },
{ code: "CZ", name: "Czech Republic" },
{ code: "HU", name: "Hungary" },
{ code: "RO", name: "Romania" },
{ code: "BG", name: "Bulgaria" },
{ code: "HR", name: "Croatia" },
{ code: "SI", name: "Slovenia" },
{ code: "SK", name: "Slovakia" },
{ code: "LT", name: "Lithuania" },
{ code: "LV", name: "Latvia" },
{ code: "EE", name: "Estonia" },
{ code: "IE", name: "Ireland" },
{ code: "PT", name: "Portugal" },
{ code: "GR", name: "Greece" },
{ code: "CY", name: "Cyprus" },
{ code: "MT", name: "Malta" },
{ code: "LU", name: "Luxembourg" },
{ code: "IS", name: "Iceland" },
{ code: "CN", name: "China" },
{ code: "JP", name: "Japan" },
{ code: "KR", name: "South Korea" },
{ code: "IN", name: "India" },
{ code: "BR", name: "Brazil" },
{ code: "AR", name: "Argentina" },
{ code: "MX", name: "Mexico" },
{ code: "ZA", name: "South Africa" },
{ code: "NZ", name: "New Zealand" }
];
let totalPopulated = 0;
const errors = [];
for (const country of countries) {
try {
const hd = new Holidays(country.code);
for (let year = 2020; year <= 2030; year++) {
const holidays = hd.getHolidays(year);
for (const holiday of holidays) {
if (!holiday.date || typeof holiday.date !== "object") {
continue;
}
const dateStr = holiday.date.toISOString().split("T")[0];
const name = holiday.name || "Unknown Holiday";
const description = holiday.type || "Public Holiday";
const query = `
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (country_code, name, date) DO NOTHING
`;
await db.query(query, [
country.code,
name,
description,
dateStr,
true
]);
totalPopulated++;
}
}
} catch (error) {
errors.push(`${country.name}: ${error.message}`);
}
}
const response = {
success: true,
message: `Successfully populated ${totalPopulated} holidays`,
total_populated: totalPopulated,
errors: errors.length > 0 ? errors : undefined
};
return res.status(200).send(new ServerResponse(true, response));
}
}

View File

@@ -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;

View File

@@ -65,4 +65,10 @@ export const holidayApiService = {
const response = await apiClient.get<IServerResponse<IHolidayCalendarEvent[]>>(`${rootUrl}/calendar?year=${year}&month=${month}`);
return response.data;
},
// Populate holidays
populateCountryHolidays: async (): Promise<IServerResponse<any>> => {
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/populate`);
return response.data;
},
};

View File

@@ -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<IOrganization | null>(null);
const [organizationAdmins, setOrganizationAdmins] = useState<IOrganizationAdmin[] | null>(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 (
<div style={{ width: '100%' }}>
<PageHeader title={<span>{t('overview')}</span>} style={{ padding: '16px 0' }} />
<PageHeader
title={<span>{t('overview')}</span>}
style={{ padding: '16px 0' }}
extra={[
<Button
key="populate-holidays"
icon={<DatabaseOutlined />}
onClick={handlePopulateHolidays}
loading={populatingHolidays}
>
Populate Holidays Database
</Button>
]}
/>
<Space direction="vertical" style={{ width: '100%' }} size={22}>
<OrganizationName