feat(holiday-settings): implement organization holiday settings management

- Added SQL migration for creating organization holiday settings and state holidays tables with necessary constraints and indexes.
- Implemented API endpoints in AdminCenterController for retrieving and updating organization holiday settings.
- Updated admin-center API router to include routes for holiday settings management.
- Enhanced localization files to support new holiday settings UI elements in multiple languages.
- Improved holiday calendar component to display working days and integrate holiday settings.
This commit is contained in:
chamikaJ
2025-07-28 13:07:15 +05:30
parent c18b289e4f
commit f81d0f9594
21 changed files with 1265 additions and 207 deletions

View File

@@ -0,0 +1,63 @@
-- Create organization holiday settings table
CREATE TABLE IF NOT EXISTS organization_holiday_settings (
id UUID DEFAULT uuid_generate_v4() NOT NULL,
organization_id UUID NOT NULL,
country_code CHAR(2),
state_code TEXT,
auto_sync_holidays 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 organization_holiday_settings
ADD CONSTRAINT organization_holiday_settings_pk
PRIMARY KEY (id);
ALTER TABLE organization_holiday_settings
ADD CONSTRAINT organization_holiday_settings_organization_id_fk
FOREIGN KEY (organization_id) REFERENCES organizations
ON DELETE CASCADE;
ALTER TABLE organization_holiday_settings
ADD CONSTRAINT organization_holiday_settings_country_code_fk
FOREIGN KEY (country_code) REFERENCES countries(code)
ON DELETE SET NULL;
-- Ensure one settings record per organization
ALTER TABLE organization_holiday_settings
ADD CONSTRAINT organization_holiday_settings_organization_unique
UNIQUE (organization_id);
-- Create index for better performance
CREATE INDEX IF NOT EXISTS idx_organization_holiday_settings_organization_id ON organization_holiday_settings(organization_id);
-- Add state holidays table for more granular holiday data
CREATE TABLE IF NOT EXISTS state_holidays (
id UUID DEFAULT uuid_generate_v4() NOT NULL,
country_code CHAR(2) NOT NULL,
state_code TEXT 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 state_holidays
ADD CONSTRAINT state_holidays_pk
PRIMARY KEY (id);
ALTER TABLE state_holidays
ADD CONSTRAINT state_holidays_country_code_fk
FOREIGN KEY (country_code) REFERENCES countries(code)
ON DELETE CASCADE;
-- Add unique constraint to prevent duplicate holidays for the same state, name, and date
ALTER TABLE state_holidays
ADD CONSTRAINT state_holidays_state_name_date_unique
UNIQUE (country_code, state_code, name, date);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_state_holidays_country_state ON state_holidays(country_code, state_code);
CREATE INDEX IF NOT EXISTS idx_state_holidays_date ON state_holidays(date);

View File

@@ -1119,4 +1119,115 @@ export default class AdminCenterController extends WorklenzControllerBase {
return res.status(200).send(new ServerResponse(true, response));
}
@HandleExceptions()
public static async getOrganizationHolidaySettings(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const q = `SELECT ohs.id, ohs.organization_id, ohs.country_code, ohs.state_code,
ohs.auto_sync_holidays, ohs.created_at, ohs.updated_at
FROM organization_holiday_settings ohs
JOIN organizations o ON ohs.organization_id = o.id
WHERE o.user_id = $1;`;
const result = await db.query(q, [req.user?.owner_id]);
// If no settings exist, return default settings
if (result.rows.length === 0) {
return res.status(200).send(new ServerResponse(true, {
country_code: null,
state_code: null,
auto_sync_holidays: true
}));
}
return res.status(200).send(new ServerResponse(true, result.rows[0]));
}
@HandleExceptions()
public static async updateOrganizationHolidaySettings(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const { country_code, state_code, auto_sync_holidays } = req.body;
// First, get the organization ID
const orgQ = `SELECT id FROM organizations WHERE user_id = $1;`;
const orgResult = await db.query(orgQ, [req.user?.owner_id]);
if (orgResult.rows.length === 0) {
return res.status(404).send(new ServerResponse(false, "Organization not found"));
}
const organizationId = orgResult.rows[0].id;
// Check if settings already exist
const checkQ = `SELECT id FROM organization_holiday_settings WHERE organization_id = $1;`;
const checkResult = await db.query(checkQ, [organizationId]);
let result;
if (checkResult.rows.length > 0) {
// Update existing settings
const updateQ = `UPDATE organization_holiday_settings
SET country_code = $2,
state_code = $3,
auto_sync_holidays = $4,
updated_at = CURRENT_TIMESTAMP
WHERE organization_id = $1
RETURNING *;`;
result = await db.query(updateQ, [organizationId, country_code, state_code, auto_sync_holidays]);
} else {
// Insert new settings
const insertQ = `INSERT INTO organization_holiday_settings
(organization_id, country_code, state_code, auto_sync_holidays)
VALUES ($1, $2, $3, $4)
RETURNING *;`;
result = await db.query(insertQ, [organizationId, country_code, state_code, auto_sync_holidays]);
}
return res.status(200).send(new ServerResponse(true, result.rows[0]));
}
@HandleExceptions()
public static async getCountriesWithStates(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
// Get all countries
const countriesQ = `SELECT code, name FROM countries ORDER BY name;`;
const countriesResult = await db.query(countriesQ);
// For now, we'll return a basic structure
// In a real implementation, you would have a states table
const countriesWithStates = countriesResult.rows.map(country => ({
code: country.code,
name: country.name,
states: [] as Array<{ code: string; name: string }> // Would be populated from a states table
}));
// Add some example states for US and Canada
const usIndex = countriesWithStates.findIndex(c => c.code === 'US');
if (usIndex !== -1) {
countriesWithStates[usIndex].states = [
{ code: 'CA', name: 'California' },
{ code: 'NY', name: 'New York' },
{ code: 'TX', name: 'Texas' },
{ code: 'FL', name: 'Florida' },
{ code: 'WA', name: 'Washington' }
];
}
const caIndex = countriesWithStates.findIndex(c => c.code === 'CA');
if (caIndex !== -1) {
countriesWithStates[caIndex].states = [
{ code: 'ON', name: 'Ontario' },
{ code: 'QC', name: 'Quebec' },
{ code: 'BC', name: 'British Columbia' },
{ code: 'AB', name: 'Alberta' }
];
}
return res.status(200).send(new ServerResponse(true, countriesWithStates));
}
}

View File

@@ -14,6 +14,11 @@ adminCenterApiRouter.put("/organization", teamOwnerOrAdminValidator, organizatio
adminCenterApiRouter.put("/organization/calculation-method", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOrganizationCalculationMethod));
adminCenterApiRouter.put("/organization/owner/contact-number", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOwnerContactNumber));
// holiday settings
adminCenterApiRouter.get("/organization/holiday-settings", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationHolidaySettings));
adminCenterApiRouter.put("/organization/holiday-settings", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOrganizationHolidaySettings));
adminCenterApiRouter.get("/countries-with-states", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getCountriesWithStates));
// users
adminCenterApiRouter.get("/organization/users", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationUsers));

View File

@@ -28,5 +28,45 @@
"manDaysCalculationDescription": "Të gjitha kostot e projektit do të llogariten duke përdorur ditët e vlerësuara të njeriut × normat ditore",
"calculationMethodTooltip": "Ky cilësim zbatohet për të gjitha projektet në organizatën tuaj",
"calculationMethodUpdated": "Metoda e llogaritjes së organizatës u përditësua me sukses",
"calculationMethodUpdateError": "Dështoi përditësimi i metodës së llogaritjes"
"calculationMethodUpdateError": "Dështoi përditësimi i metodës së llogaritjes",
"holidayCalendar": "Kalnedari i Festave",
"addHoliday": "Shto Festë",
"editHoliday": "Redakto Festë",
"holidayName": "Emri i Festës",
"holidayNameRequired": "Ju lutemi shkruani emrin e festës",
"description": "Përshkrim",
"date": "Data",
"dateRequired": "Ju lutemi zgjidhni një datë",
"holidayType": "Lloji i Festës",
"holidayTypeRequired": "Ju lutemi zgjidhni një lloj feste",
"recurring": "Përsëritëse",
"save": "Ruaj",
"update": "Përditëso",
"cancel": "Anulo",
"holidayCreated": "Festa u krijua me sukses",
"holidayUpdated": "Festa u përditësua me sukses",
"holidayDeleted": "Festa u fshi me sukses",
"errorCreatingHoliday": "Gabim gjatë krijimit të festës",
"errorUpdatingHoliday": "Gabim gjatë përditësimit të festës",
"errorDeletingHoliday": "Gabim gjatë fshirjes së festës",
"importCountryHolidays": "Importo Festat e Vendit",
"country": "Vendi",
"countryRequired": "Ju lutemi zgjidhni një vend",
"selectCountry": "Zgjidhni një vend",
"year": "Viti",
"import": "Importo",
"holidaysImported": "U importuan me sukses {{count}} festa",
"errorImportingHolidays": "Gabim gjatë importimit të festave",
"addCustomHoliday": "Shto Festë të Përshtatur",
"officialHolidaysFrom": "Festat zyrtare nga",
"workingDay": "Ditë Pune",
"holiday": "Festë",
"today": "Sot",
"cannotEditOfficialHoliday": "Nuk mund të redaktoni festat zyrtare",
"customHoliday": "Festë e Përshtatur",
"officialHoliday": "Festë Zyrtare",
"delete": "Fshi",
"deleteHolidayConfirm": "A jeni i sigurt që dëshironi të fshini këtë festë?",
"yes": "Po",
"no": "Jo"
}

View File

@@ -13,5 +13,21 @@
"sunday": "E Dielë",
"saveButton": "Ruaj",
"saved": "Cilësimet u ruajtën me sukses",
"errorSaving": "Gabim gjatë ruajtjes së cilësimeve"
"errorSaving": "Gabim gjatë ruajtjes së cilësimeve",
"holidaySettings": "Cilësimet e pushimeve",
"country": "Vendi",
"countryRequired": "Ju lutemi zgjidhni një vend",
"selectCountry": "Zgjidhni vendin",
"state": "Shteti/Provinca",
"selectState": "Zgjidhni shtetin/provincën (opsionale)",
"autoSyncHolidays": "Sinkronizo automatikisht pushimet zyrtare",
"saveHolidaySettings": "Ruaj cilësimet e pushimeve",
"holidaySettingsSaved": "Cilësimet e pushimeve u ruajtën me sukses",
"errorSavingHolidaySettings": "Gabim gjatë ruajtjes së cilësimeve të pushimeve",
"addCustomHoliday": "Shto Festë të Përshtatur",
"officialHolidaysFrom": "Festat zyrtare nga",
"workingDay": "Ditë Pune",
"holiday": "Festë",
"today": "Sot",
"cannotEditOfficialHoliday": "Nuk mund të redaktoni festat zyrtare"
}

View File

@@ -28,5 +28,45 @@
"manDaysCalculationDescription": "Alle Projektkosten werden anhand geschätzter Mann-Tage × Tagessätze berechnet",
"calculationMethodTooltip": "Diese Einstellung gilt für alle Projekte in Ihrer Organisation",
"calculationMethodUpdated": "Organisations-Berechnungsmethode erfolgreich aktualisiert",
"calculationMethodUpdateError": "Fehler beim Aktualisieren der Berechnungsmethode"
"calculationMethodUpdateError": "Fehler beim Aktualisieren der Berechnungsmethode",
"holidayCalendar": "Feiertagskalender",
"addHoliday": "Feiertag hinzufügen",
"editHoliday": "Feiertag bearbeiten",
"holidayName": "Feiertagsname",
"holidayNameRequired": "Bitte geben Sie den Feiertagsnamen ein",
"description": "Beschreibung",
"date": "Datum",
"dateRequired": "Bitte wählen Sie ein Datum aus",
"holidayType": "Feiertagstyp",
"holidayTypeRequired": "Bitte wählen Sie einen Feiertagstyp aus",
"recurring": "Wiederkehrend",
"save": "Speichern",
"update": "Aktualisieren",
"cancel": "Abbrechen",
"holidayCreated": "Feiertag erfolgreich erstellt",
"holidayUpdated": "Feiertag erfolgreich aktualisiert",
"holidayDeleted": "Feiertag erfolgreich gelöscht",
"errorCreatingHoliday": "Fehler beim Erstellen des Feiertags",
"errorUpdatingHoliday": "Fehler beim Aktualisieren des Feiertags",
"errorDeletingHoliday": "Fehler beim Löschen des Feiertags",
"importCountryHolidays": "Landesfeiertage importieren",
"country": "Land",
"countryRequired": "Bitte wählen Sie ein Land aus",
"selectCountry": "Ein Land auswählen",
"year": "Jahr",
"import": "Importieren",
"holidaysImported": "{{count}} Feiertage erfolgreich importiert",
"errorImportingHolidays": "Fehler beim Importieren der Feiertage",
"addCustomHoliday": "Benutzerdefinierten Feiertag hinzufügen",
"officialHolidaysFrom": "Offizielle Feiertage aus",
"workingDay": "Arbeitstag",
"holiday": "Feiertag",
"today": "Heute",
"cannotEditOfficialHoliday": "Offizielle Feiertage können nicht bearbeitet werden",
"customHoliday": "Benutzerdefinierter Feiertag",
"officialHoliday": "Offizieller Feiertag",
"delete": "Löschen",
"deleteHolidayConfirm": "Sind Sie sicher, dass Sie diesen Feiertag löschen möchten?",
"yes": "Ja",
"no": "Nein"
}

View File

@@ -13,5 +13,21 @@
"sunday": "Sonntag",
"saveButton": "Speichern",
"saved": "Einstellungen erfolgreich gespeichert",
"errorSaving": "Fehler beim Speichern der Einstellungen"
"errorSaving": "Fehler beim Speichern der Einstellungen",
"holidaySettings": "Feiertagseinstellungen",
"country": "Land",
"countryRequired": "Bitte wählen Sie ein Land aus",
"selectCountry": "Land auswählen",
"state": "Bundesland/Provinz",
"selectState": "Bundesland/Provinz auswählen (optional)",
"autoSyncHolidays": "Offizielle Feiertage automatisch synchronisieren",
"saveHolidaySettings": "Feiertagseinstellungen speichern",
"holidaySettingsSaved": "Feiertagseinstellungen erfolgreich gespeichert",
"errorSavingHolidaySettings": "Fehler beim Speichern der Feiertagseinstellungen",
"addCustomHoliday": "Benutzerdefinierten Feiertag hinzufügen",
"officialHolidaysFrom": "Offizielle Feiertage aus",
"workingDay": "Arbeitstag",
"holiday": "Feiertag",
"today": "Heute",
"cannotEditOfficialHoliday": "Offizielle Feiertage können nicht bearbeitet werden"
}

View File

@@ -56,5 +56,17 @@
"year": "Year",
"import": "Import",
"holidaysImported": "Successfully imported {{count}} holidays",
"errorImportingHolidays": "Error importing holidays"
"errorImportingHolidays": "Error importing holidays",
"addCustomHoliday": "Add Custom Holiday",
"officialHolidaysFrom": "Official holidays from",
"workingDay": "Working Day",
"holiday": "Holiday",
"today": "Today",
"cannotEditOfficialHoliday": "Cannot edit official holidays",
"customHoliday": "Custom Holiday",
"officialHoliday": "Official Holiday",
"delete": "Delete",
"deleteHolidayConfirm": "Are you sure you want to delete this holiday?",
"yes": "Yes",
"no": "No"
}

View File

@@ -13,5 +13,15 @@
"sunday": "Sunday",
"saveButton": "Save",
"saved": "Settings saved successfully",
"errorSaving": "Error saving settings"
"errorSaving": "Error saving settings",
"holidaySettings": "Holiday Settings",
"country": "Country",
"countryRequired": "Please select a country",
"selectCountry": "Select country",
"state": "State/Province",
"selectState": "Select state/province (optional)",
"autoSyncHolidays": "Automatically sync official holidays",
"saveHolidaySettings": "Save Holiday Settings",
"holidaySettingsSaved": "Holiday settings saved successfully",
"errorSavingHolidaySettings": "Error saving holiday settings"
}

View File

@@ -28,5 +28,45 @@
"manDaysCalculationDescription": "Todos los costos del proyecto se calcularán usando días hombre estimados × tarifas diarias",
"calculationMethodTooltip": "Esta configuración se aplica a todos los proyectos en su organización",
"calculationMethodUpdated": "Método de cálculo de la organización actualizado exitosamente",
"calculationMethodUpdateError": "Error al actualizar el método de cálculo"
"calculationMethodUpdateError": "Error al actualizar el método de cálculo",
"holidayCalendar": "Calendario de Días Festivos",
"addHoliday": "Agregar Día Festivo",
"editHoliday": "Editar Día Festivo",
"holidayName": "Nombre del Día Festivo",
"holidayNameRequired": "Por favor ingrese el nombre del día festivo",
"description": "Descripción",
"date": "Fecha",
"dateRequired": "Por favor seleccione una fecha",
"holidayType": "Tipo de Día Festivo",
"holidayTypeRequired": "Por favor seleccione un tipo de día festivo",
"recurring": "Recurrente",
"save": "Guardar",
"update": "Actualizar",
"cancel": "Cancelar",
"holidayCreated": "Día festivo creado exitosamente",
"holidayUpdated": "Día festivo actualizado exitosamente",
"holidayDeleted": "Día festivo eliminado exitosamente",
"errorCreatingHoliday": "Error al crear el día festivo",
"errorUpdatingHoliday": "Error al actualizar el día festivo",
"errorDeletingHoliday": "Error al eliminar el día festivo",
"importCountryHolidays": "Importar Días Festivos del País",
"country": "País",
"countryRequired": "Por favor seleccione un país",
"selectCountry": "Seleccionar un país",
"year": "Año",
"import": "Importar",
"holidaysImported": "{{count}} días festivos importados exitosamente",
"errorImportingHolidays": "Error al importar días festivos",
"addCustomHoliday": "Agregar Día Festivo Personalizado",
"officialHolidaysFrom": "Días festivos oficiales de",
"workingDay": "Día Laboral",
"holiday": "Día Festivo",
"today": "Hoy",
"cannotEditOfficialHoliday": "No se pueden editar los días festivos oficiales",
"customHoliday": "Día Festivo Personalizado",
"officialHoliday": "Día Festivo Oficial",
"delete": "Eliminar",
"deleteHolidayConfirm": "¿Está seguro de que desea eliminar este día festivo?",
"yes": "Sí",
"no": "No"
}

View File

@@ -13,5 +13,21 @@
"sunday": "Domingo",
"saveButton": "Guardar",
"saved": "Configuración guardada exitosamente",
"errorSaving": "Error al guardar la configuración"
"errorSaving": "Error al guardar la configuración",
"holidaySettings": "Configuración de días festivos",
"country": "País",
"countryRequired": "Por favor seleccione un país",
"selectCountry": "Seleccionar país",
"state": "Estado/Provincia",
"selectState": "Seleccionar estado/provincia (opcional)",
"autoSyncHolidays": "Sincronizar automáticamente los días festivos oficiales",
"saveHolidaySettings": "Guardar configuración de días festivos",
"holidaySettingsSaved": "Configuración de días festivos guardada exitosamente",
"errorSavingHolidaySettings": "Error al guardar la configuración de días festivos",
"addCustomHoliday": "Agregar Día Festivo Personalizado",
"officialHolidaysFrom": "Días festivos oficiales de",
"workingDay": "Día Laboral",
"holiday": "Día Festivo",
"today": "Hoy",
"cannotEditOfficialHoliday": "No se pueden editar los días festivos oficiales"
}

View File

@@ -28,5 +28,45 @@
"manDaysCalculationDescription": "Todos os custos do projeto serão calculados usando dias homem estimados × taxas diárias",
"calculationMethodTooltip": "Esta configuração se aplica a todos os projetos em sua organização",
"calculationMethodUpdated": "Método de cálculo da organização atualizado com sucesso",
"calculationMethodUpdateError": "Erro ao atualizar o método de cálculo"
"calculationMethodUpdateError": "Erro ao atualizar o método de cálculo",
"holidayCalendar": "Calendário de Feriados",
"addHoliday": "Adicionar Feriado",
"editHoliday": "Editar Feriado",
"holidayName": "Nome do Feriado",
"holidayNameRequired": "Por favor, digite o nome do feriado",
"description": "Descrição",
"date": "Data",
"dateRequired": "Por favor, selecione uma data",
"holidayType": "Tipo de Feriado",
"holidayTypeRequired": "Por favor, selecione um tipo de feriado",
"recurring": "Recorrente",
"save": "Salvar",
"update": "Atualizar",
"cancel": "Cancelar",
"holidayCreated": "Feriado criado com sucesso",
"holidayUpdated": "Feriado atualizado com sucesso",
"holidayDeleted": "Feriado excluído com sucesso",
"errorCreatingHoliday": "Erro ao criar feriado",
"errorUpdatingHoliday": "Erro ao atualizar feriado",
"errorDeletingHoliday": "Erro ao excluir feriado",
"importCountryHolidays": "Importar Feriados do País",
"country": "País",
"countryRequired": "Por favor, selecione um país",
"selectCountry": "Selecionar um país",
"year": "Ano",
"import": "Importar",
"holidaysImported": "{{count}} feriados importados com sucesso",
"errorImportingHolidays": "Erro ao importar feriados",
"addCustomHoliday": "Adicionar Feriado Personalizado",
"officialHolidaysFrom": "Feriados oficiais de",
"workingDay": "Dia de Trabalho",
"holiday": "Feriado",
"today": "Hoje",
"cannotEditOfficialHoliday": "Não é possível editar feriados oficiais",
"customHoliday": "Feriado Personalizado",
"officialHoliday": "Feriado Oficial",
"delete": "Excluir",
"deleteHolidayConfirm": "Tem certeza de que deseja excluir este feriado?",
"yes": "Sim",
"no": "Não"
}

View File

@@ -13,5 +13,21 @@
"sunday": "Domingo",
"saveButton": "Salvar",
"saved": "Configurações salvas com sucesso",
"errorSaving": "Erro ao salvar configurações"
"errorSaving": "Erro ao salvar configurações",
"holidaySettings": "Configurações de feriados",
"country": "País",
"countryRequired": "Por favor, selecione um país",
"selectCountry": "Selecionar país",
"state": "Estado/Província",
"selectState": "Selecionar estado/província (opcional)",
"autoSyncHolidays": "Sincronizar automaticamente feriados oficiais",
"saveHolidaySettings": "Salvar configurações de feriados",
"holidaySettingsSaved": "Configurações de feriados salvas com sucesso",
"errorSavingHolidaySettings": "Erro ao salvar configurações de feriados",
"addCustomHoliday": "Adicionar Feriado Personalizado",
"officialHolidaysFrom": "Feriados oficiais de",
"workingDay": "Dia de Trabalho",
"holiday": "Feriado",
"today": "Hoje",
"cannotEditOfficialHoliday": "Não é possível editar feriados oficiais"
}

View File

@@ -28,5 +28,45 @@
"manDaysCalculationDescription": "所有项目成本将使用估算人天数 × 日费率计算",
"calculationMethodTooltip": "此设置适用于您组织中的所有项目",
"calculationMethodUpdated": "组织计算方法更新成功",
"calculationMethodUpdateError": "更新计算方法失败"
"calculationMethodUpdateError": "更新计算方法失败",
"holidayCalendar": "假期日历",
"addHoliday": "添加假期",
"editHoliday": "编辑假期",
"holidayName": "假期名称",
"holidayNameRequired": "请输入假期名称",
"description": "描述",
"date": "日期",
"dateRequired": "请选择日期",
"holidayType": "假期类型",
"holidayTypeRequired": "请选择假期类型",
"recurring": "循环",
"save": "保存",
"update": "更新",
"cancel": "取消",
"holidayCreated": "假期创建成功",
"holidayUpdated": "假期更新成功",
"holidayDeleted": "假期删除成功",
"errorCreatingHoliday": "创建假期时出错",
"errorUpdatingHoliday": "更新假期时出错",
"errorDeletingHoliday": "删除假期时出错",
"importCountryHolidays": "导入国家假期",
"country": "国家",
"countryRequired": "请选择国家",
"selectCountry": "选择国家",
"year": "年份",
"import": "导入",
"holidaysImported": "成功导入{{count}}个假期",
"errorImportingHolidays": "导入假期时出错",
"addCustomHoliday": "添加自定义假期",
"officialHolidaysFrom": "官方假期来自",
"workingDay": "工作日",
"holiday": "假期",
"today": "今天",
"cannotEditOfficialHoliday": "无法编辑官方假期",
"customHoliday": "自定义假期",
"officialHoliday": "官方假期",
"delete": "删除",
"deleteHolidayConfirm": "您确定要删除这个假期吗?",
"yes": "是",
"no": "否"
}

View File

@@ -13,5 +13,21 @@
"sunday": "星期日",
"saveButton": "保存",
"saved": "设置保存成功",
"errorSaving": "保存设置时出错"
"errorSaving": "保存设置时出错",
"holidaySettings": "假期设置",
"country": "国家",
"countryRequired": "请选择一个国家",
"selectCountry": "选择国家",
"state": "州/省",
"selectState": "选择州/省(可选)",
"autoSyncHolidays": "自动同步官方假期",
"saveHolidaySettings": "保存假期设置",
"holidaySettingsSaved": "假期设置保存成功",
"errorSavingHolidaySettings": "保存假期设置时出错",
"addCustomHoliday": "添加自定义假期",
"officialHolidaysFrom": "官方假期来自",
"workingDay": "工作日",
"holiday": "假期",
"today": "今天",
"cannotEditOfficialHoliday": "无法编辑官方假期"
}

View File

@@ -19,63 +19,51 @@ import {
const rootUrl = `${API_BASE_URL}/holidays`;
export const holidayApiService = {
// Holiday types - PLACEHOLDER with Sri Lankan specific types
// Holiday types
getHolidayTypes: async (): Promise<IServerResponse<IHolidayType[]>> => {
// Return holiday types including Sri Lankan specific types
const holidayTypes = [
{ id: '1', name: 'Public Holiday', color_code: '#DC143C' },
{ id: '2', name: 'Religious Holiday', color_code: '#4ecdc4' },
{ id: '3', name: 'National Holiday', color_code: '#45b7d1' },
{ id: '4', name: 'Company Holiday', color_code: '#f9ca24' },
{ id: '5', name: 'Personal Holiday', color_code: '#6c5ce7' },
{ id: '6', name: 'Bank Holiday', color_code: '#4682B4' },
{ id: '7', name: 'Mercantile Holiday', color_code: '#32CD32' },
{ id: '8', name: 'Poya Day', color_code: '#8B4513' },
];
return {
done: true,
body: holidayTypes,
} as IServerResponse<IHolidayType[]>;
const response = await apiClient.get<IServerResponse<IHolidayType[]>>(
`${rootUrl}/types`
);
return response.data;
},
// Organization holidays - PLACEHOLDER until backend implements
// Organization holidays
getOrganizationHolidays: async (
year?: number
): Promise<IServerResponse<IOrganizationHoliday[]>> => {
// Return empty array for now to prevent 404 errors
return {
done: true,
body: [],
} as IServerResponse<IOrganizationHoliday[]>;
const params = year ? { year } : {};
const response = await apiClient.get<IServerResponse<IOrganizationHoliday[]>>(
`${rootUrl}/organization`,
{ params }
);
return response.data;
},
// Holiday CRUD operations - PLACEHOLDER until backend implements
// Holiday CRUD operations
createOrganizationHoliday: async (data: ICreateHolidayRequest): Promise<IServerResponse<any>> => {
// Return success for now to prevent UI errors
return {
done: true,
body: { id: Date.now().toString(), ...data },
} as IServerResponse<any>;
const response = await apiClient.post<IServerResponse<any>>(
`${rootUrl}/organization`,
data
);
return response.data;
},
updateOrganizationHoliday: async (
id: string,
data: IUpdateHolidayRequest
): Promise<IServerResponse<any>> => {
// Return success for now to prevent UI errors
return {
done: true,
body: { id, ...data },
} as IServerResponse<any>;
const response = await apiClient.put<IServerResponse<any>>(
`${rootUrl}/organization/${id}`,
data
);
return response.data;
},
deleteOrganizationHoliday: async (id: string): Promise<IServerResponse<any>> => {
// Return success for now to prevent UI errors
return {
done: true,
body: {},
} as IServerResponse<any>;
const response = await apiClient.delete<IServerResponse<any>>(
`${rootUrl}/organization/${id}`
);
return response.data;
},
// Country holidays - PLACEHOLDER with all date-holidays supported countries
@@ -200,11 +188,15 @@ export const holidayApiService = {
countryCode: string,
year?: number
): Promise<IServerResponse<ICountryHoliday[]>> => {
// Return empty array for now
return {
done: true,
body: [],
} as IServerResponse<ICountryHoliday[]>;
const params: any = { country_code: countryCode };
if (year) {
params.year = year;
}
const response = await apiClient.get<IServerResponse<ICountryHoliday[]>>(
`${rootUrl}/countries/${countryCode}`,
{ params }
);
return response.data;
},
importCountryHolidays: async (
@@ -229,35 +221,35 @@ export const holidayApiService = {
} as IServerResponse<IHolidayCalendarEvent[]>;
},
// Organization holiday settings - PLACEHOLDER until backend implements
// Organization holiday settings
getOrganizationHolidaySettings: async (): Promise<
IServerResponse<IOrganizationHolidaySettings>
> => {
// Return default settings for now
return {
done: true,
body: {
country_code: undefined,
state_code: undefined,
auto_sync_holidays: false,
},
} as IServerResponse<IOrganizationHolidaySettings>;
const response = await apiClient.get<IServerResponse<IOrganizationHolidaySettings>>(
`${API_BASE_URL}/admin-center/organization/holiday-settings`
);
return response.data;
},
updateOrganizationHolidaySettings: async (
data: IOrganizationHolidaySettings
): Promise<IServerResponse<any>> => {
// Just return success for now
return {
done: true,
body: {},
} as IServerResponse<any>;
const response = await apiClient.put<IServerResponse<any>>(
`${API_BASE_URL}/admin-center/organization/holiday-settings`,
data
);
return response.data;
},
// Countries with states - PLACEHOLDER with date-holidays supported countries
// Countries with states
getCountriesWithStates: async (): Promise<IServerResponse<ICountryWithStates[]>> => {
// Return comprehensive list of countries supported by date-holidays library
const supportedCountries = [
const response = await apiClient.get<IServerResponse<ICountryWithStates[]>>(
`${API_BASE_URL}/admin-center/countries-with-states`
);
return response.data;
// Fallback to static data if API fails
/*const supportedCountries = [
{ code: 'AD', name: 'Andorra' },
{ code: 'AE', name: 'United Arab Emirates' },
{ code: 'AG', name: 'Antigua & Barbuda' },
@@ -696,41 +688,85 @@ export const holidayApiService = {
return {
done: true,
body: supportedCountries,
} as IServerResponse<ICountryWithStates[]>;
} as IServerResponse<ICountryWithStates[]>;*/
},
// Combined holidays (official + custom) - Database-driven approach for Sri Lanka
// Combined holidays (official + custom) - Database-driven approach
getCombinedHolidays: async (
params: ICombinedHolidaysRequest & { country_code?: string }
): Promise<IServerResponse<IHolidayCalendarEvent[]>> => {
try {
console.log('🔍 getCombinedHolidays called with params:', params);
const year = new Date(params.from_date).getFullYear();
let allHolidays: IHolidayCalendarEvent[] = [];
// Handle Sri Lankan holidays from database
if (params.country_code === 'LK' && year === 2025) {
// Import Sri Lankan holiday data
const { sriLankanHolidays2025 } = await import('@/data/sri-lanka-holidays-2025');
// Get official holidays - handle Sri Lanka specially, others from API
if (params.country_code) {
console.log(`🌐 Fetching official holidays for country: ${params.country_code}, year: ${year}`);
const sriLankanHolidays = sriLankanHolidays2025
.filter(h => h.date >= params.from_date && h.date <= params.to_date)
.map(h => ({
id: `lk-${h.date}-${h.name.replace(/\s+/g, '-').toLowerCase()}`,
name: h.name,
description: h.description,
date: h.date,
is_recurring: h.is_recurring,
holiday_type_name: h.type,
color_code: h.color_code,
source: 'official' as const,
is_editable: false,
}));
allHolidays.push(...sriLankanHolidays);
// Handle Sri Lankan holidays from static data
if (params.country_code === 'LK' && year === 2025) {
try {
console.log('🇱🇰 Loading Sri Lankan holidays from static data...');
const { sriLankanHolidays2025 } = await import('@/data/sri-lanka-holidays-2025');
const sriLankanHolidays = sriLankanHolidays2025
.filter(h => h.date >= params.from_date && h.date <= params.to_date)
.map(h => ({
id: `lk-${h.date}-${h.name.replace(/\\s+/g, '-').toLowerCase()}`,
name: h.name,
description: h.description,
date: h.date,
is_recurring: h.is_recurring,
holiday_type_name: h.type,
color_code: h.color_code,
source: 'official' as const,
is_editable: false,
}));
console.log(`✅ Found ${sriLankanHolidays.length} Sri Lankan holidays`);
allHolidays.push(...sriLankanHolidays);
} catch (error) {
console.error('❌ Error loading Sri Lankan holidays:', error);
}
} else {
// Handle other countries from API
try {
const countryHolidaysRes = await holidayApiService.getCountryHolidays(params.country_code, year);
console.log('📅 Country holidays response:', countryHolidaysRes);
if (countryHolidaysRes.done && countryHolidaysRes.body) {
const officialHolidays = countryHolidaysRes.body
.filter((h: any) => h.date >= params.from_date && h.date <= params.to_date)
.map((h: any) => ({
id: `${params.country_code}-${h.id}`,
name: h.name,
description: h.description,
date: h.date,
is_recurring: h.is_recurring,
holiday_type_name: 'Official Holiday',
color_code: h.color_code || '#1890ff',
source: 'official' as const,
is_editable: false,
}));
console.log(`✅ Found ${officialHolidays.length} official holidays from API`);
allHolidays.push(...officialHolidays);
} else {
console.log('⚠️ No official holidays returned from API');
}
} catch (error) {
console.error('❌ Error fetching official holidays from API:', error);
}
}
} else {
console.log('⚠️ No country code provided, skipping official holidays');
}
// Get organization holidays from database (includes both custom and country-specific)
console.log(`🏢 Fetching organization holidays for year: ${year}`);
const customRes = await holidayApiService.getOrganizationHolidays(year);
console.log('🏢 Organization holidays response:', customRes);
if (customRes.done && customRes.body) {
const customHolidays = customRes.body
@@ -747,13 +783,17 @@ export const holidayApiService = {
is_editable: h.is_editable !== false, // Default to true unless explicitly false
}));
// Filter out duplicates (in case Sri Lankan holidays are already in DB)
// Filter out duplicates (in case official holidays are already in DB)
const existingDates = new Set(allHolidays.map(h => h.date));
const uniqueCustomHolidays = customHolidays.filter((h: any) => !existingDates.has(h.date));
console.log(`✅ Found ${customHolidays.length} organization holidays (${uniqueCustomHolidays.length} unique)`);
allHolidays.push(...uniqueCustomHolidays);
} else {
console.log('⚠️ No organization holidays returned from API');
}
console.log(`🎉 Total holidays combined: ${allHolidays.length}`, allHolidays);
return {
done: true,
body: allHolidays,
@@ -798,12 +838,9 @@ export const holidayApiService = {
} as IServerResponse<{ working_days: number; total_days: number; holidays_count: number }>;
},
// Populate holidays - PLACEHOLDER until backend implements (deprecated - keeping for backward compatibility)
// Populate holidays - Populate the database with official holidays for various countries
populateCountryHolidays: async (): Promise<IServerResponse<any>> => {
// Return success for now
return {
done: true,
body: { message: 'Holidays populated successfully' },
} as IServerResponse<any>;
const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/populate`);
return response.data;
},
};

View File

@@ -4,20 +4,37 @@
.holiday-calendar .ant-picker-calendar {
background: transparent;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.holiday-calendar.dark .ant-picker-calendar {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.holiday-calendar .ant-picker-calendar-header {
padding: 12px 0;
padding: 20px 24px;
background: linear-gradient(135deg, #fafbfc 0%, #f1f3f4 100%);
border-bottom: 1px solid #e8eaed;
border-radius: 12px 12px 0 0;
}
.holiday-calendar.dark .ant-picker-calendar-header {
background: linear-gradient(135deg, #1f1f1f 0%, #262626 100%);
border-bottom-color: #303030;
}
.holiday-calendar .ant-picker-calendar-date {
position: relative;
height: 80px;
padding: 4px 8px;
height: 85px;
padding: 6px 10px;
border: 1px solid #f0f0f0;
border-radius: 6px;
transition: all 0.3s;
border-radius: 10px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
}
.holiday-calendar.dark .ant-picker-calendar-date {
@@ -25,18 +42,161 @@
background: #1f1f1f;
}
/* Calendar cell wrapper */
.calendar-cell {
position: relative;
height: 100%;
width: 100%;
}
/* Working day styles */
.calendar-cell.working-day {
background: #ffffff;
border: 1px solid #d9d9d9;
position: relative;
}
.calendar-cell.working-day::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(82, 196, 26, 0.04);
border-radius: inherit;
pointer-events: none;
transition: opacity 0.3s ease;
}
.holiday-calendar.dark .calendar-cell.working-day {
background: #1f1f1f;
border: 1px solid #434343;
}
.holiday-calendar.dark .calendar-cell.working-day::before {
background: rgba(82, 196, 26, 0.06);
}
/* Non-working day styles */
.calendar-cell.non-working-day {
background: #fafafa;
border: 1px solid #f0f0f0;
opacity: 0.65;
}
.holiday-calendar.dark .calendar-cell.non-working-day {
background: #141414;
border: 1px solid #303030;
opacity: 0.65;
}
/* Today styles */
.calendar-cell.today {
border: 2px solid #1890ff;
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15), 0 4px 12px rgba(24, 144, 255, 0.1);
animation: todayPulse 3s ease-in-out infinite;
}
.holiday-calendar.dark .calendar-cell.today {
border: 2px solid #177ddc;
box-shadow: 0 0 0 2px rgba(23, 125, 220, 0.3);
}
/* Other month styles */
.calendar-cell.other-month {
opacity: 0.4;
}
.holiday-calendar.dark .calendar-cell.other-month {
opacity: 0.3;
}
.holiday-calendar .ant-picker-calendar-date:hover {
background: #f5f5f5;
transform: translateY(-1px) scale(1.02);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.04);
border-color: #d9d9d9;
}
@media (hover: none) and (pointer: coarse) {
.holiday-calendar .ant-picker-calendar-date:hover {
transform: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
border-color: inherit;
}
.holiday-cell .ant-tag:hover {
transform: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
}
.holiday-calendar.dark .ant-picker-calendar-date:hover {
background: #2a2a2a;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
/* Working day indicator */
.working-day-indicator {
position: absolute;
top: 2px;
right: 2px;
z-index: 2;
}
.working-day-badge {
width: 16px;
height: 16px;
border-radius: 3px;
background: #52c41a;
color: #ffffff;
font-size: 9px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 3px rgba(82, 196, 26, 0.3);
transition: all 0.3s ease;
border: 1px solid #d9f7be;
}
.holiday-calendar.dark .working-day-badge {
background: #52c41a;
box-shadow: 0 1px 3px rgba(82, 196, 26, 0.4);
border-color: #237804;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.3);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes todayPulse {
0%, 100% {
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15), 0 4px 12px rgba(24, 144, 255, 0.1);
}
50% {
box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.25), 0 6px 16px rgba(24, 144, 255, 0.15);
}
}
.holiday-calendar .ant-picker-calendar-date-value {
font-size: 12px;
font-weight: 500;
color: #262626;
font-size: 14px;
font-weight: 600;
color: #1f1f1f;
margin-bottom: 6px;
line-height: 1.2;
text-align: center;
}
.holiday-calendar.dark .ant-picker-calendar-date-value {
@@ -44,16 +204,22 @@
}
.holiday-calendar .ant-picker-calendar-date-content {
height: 100%;
height: calc(100% - 20px);
overflow: hidden;
margin-top: 4px;
}
.holiday-cell {
position: absolute;
bottom: 2px;
left: 2px;
right: 2px;
bottom: 6px;
left: 6px;
right: 6px;
z-index: 1;
max-height: 65%;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 2px;
}
.holiday-cell .ant-tag {
@@ -61,14 +227,27 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border: none;
border: 1px solid rgba(255, 255, 255, 0.2);
font-weight: 500;
cursor: pointer;
font-size: 10px;
line-height: 1.3;
padding: 2px 6px;
border-radius: 6px;
backdrop-filter: blur(4px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.holiday-cell .ant-tag:hover {
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transform: scale(1.05) translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-color: rgba(255, 255, 255, 0.4);
z-index: 10;
overflow: visible;
white-space: normal;
text-overflow: unset;
max-width: 200px;
position: relative;
}
.holiday-calendar .ant-picker-calendar-date-today {
@@ -253,20 +432,243 @@
/* Tag styles */
.holiday-calendar .ant-tag {
border-radius: 4px;
border-radius: 6px;
font-size: 10px;
line-height: 1.2;
padding: 1px 4px;
line-height: 1.3;
padding: 2px 6px;
margin: 0;
border: none;
border: 1px solid rgba(255, 255, 255, 0.2);
font-weight: 500;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Holiday tag specific styles */
.holiday-tag.official-holiday {
background: #e6f7ff !important;
border-color: #91d5ff !important;
color: #1890ff !important;
}
.holiday-tag.custom-holiday {
background: #f6ffed !important;
border-color: #b7eb8f !important;
color: #52c41a !important;
}
.holiday-calendar.dark .holiday-tag.official-holiday {
background: #111b26 !important;
border-color: #13334c !important;
color: #69c0ff !important;
}
.holiday-calendar.dark .holiday-tag.custom-holiday {
background: #162312 !important;
border-color: #274916 !important;
color: #95de64 !important;
}
/* Holiday tag icons */
.custom-holiday-icon {
font-size: 8px;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.2));
}
.official-holiday-icon {
font-size: 8px;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.2));
}
/* Calendar container and legend */
.calendar-container {
position: relative;
}
.calendar-legend {
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
margin-top: 16px;
padding: 12px 16px;
background: #fafafa;
border-radius: 6px;
border: 1px solid #d9d9d9;
}
.holiday-calendar.dark .calendar-legend {
background: #1f1f1f;
border-color: #434343;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
}
.holiday-calendar.dark .legend-item {
color: rgba(255, 255, 255, 0.65);
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.legend-dot.working-day-dot {
background: #52c41a;
box-shadow: 0 0 0 1px rgba(82, 196, 26, 0.2);
}
.legend-dot.holiday-dot {
background: #ff4d4f;
box-shadow: 0 0 0 1px rgba(255, 77, 79, 0.2);
}
.legend-dot.today-dot {
background: #1890ff;
box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.2);
}
.holiday-calendar.dark .legend-dot.working-day-dot {
background: #52c41a;
box-shadow: 0 0 0 1px rgba(82, 196, 26, 0.3);
}
.holiday-calendar.dark .legend-dot.holiday-dot {
background: #ff4d4f;
box-shadow: 0 0 0 1px rgba(255, 77, 79, 0.3);
}
.holiday-calendar.dark .legend-dot.today-dot {
background: #1890ff;
box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.3);
}
/* Legend badge and tag styles */
.legend-badge {
width: 16px;
height: 16px;
border-radius: 3px;
color: white;
font-size: 9px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.legend-badge.working-day-badge {
background: #52c41a;
border: 1px solid #d9f7be;
color: #ffffff;
}
.holiday-calendar.dark .legend-badge.working-day-badge {
background: #52c41a;
border: 1px solid #237804;
color: #ffffff;
}
.legend-tag {
display: flex;
align-items: center;
gap: 2px;
padding: 2px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: 500;
border: 1px solid;
}
.legend-tag-text {
font-size: 8px;
line-height: 1;
}
.custom-holiday-legend {
background: #f6ffed;
border: 1px solid #b7eb8f;
color: #52c41a;
}
.official-holiday-legend {
background: #e6f7ff;
border: 1px solid #91d5ff;
color: #1890ff;
}
.holiday-calendar.dark .custom-holiday-legend {
background: #162312;
border: 1px solid #274916;
color: #95de64;
}
.holiday-calendar.dark .official-holiday-legend {
background: #111b26;
border: 1px solid #13334c;
color: #69c0ff;
}
.legend-tag .custom-holiday-icon,
.legend-tag .official-holiday-icon {
font-size: 7px;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
}
/* Responsive design */
@media (max-width: 768px) {
.holiday-calendar .ant-picker-calendar-date {
height: 60px;
padding: 2px 4px;
height: 70px;
padding: 4px 6px;
border-radius: 8px;
}
.holiday-calendar .ant-picker-calendar-date-value {
font-size: 12px;
margin-bottom: 4px;
}
.holiday-cell .ant-tag {
font-size: 9px;
padding: 1px 4px;
border-radius: 4px;
}
.calendar-legend {
flex-direction: column;
gap: 12px;
align-items: flex-start;
padding: 10px 12px;
}
.working-day-badge {
width: 14px;
height: 14px;
font-size: 8px;
}
.legend-item {
font-size: 11px;
}
.holiday-calendar .ant-picker-calendar-header {
padding: 16px 18px;
}
.holiday-calendar .ant-picker-calendar-date:hover {
transform: scale(1.01);
}
}
@media (max-width: 480px) {
.holiday-calendar .ant-picker-calendar-date {
height: 65px;
padding: 3px 4px;
}
.holiday-calendar .ant-picker-calendar-date-value {
@@ -274,7 +676,47 @@
}
.holiday-cell .ant-tag {
font-size: 9px;
padding: 0 2px;
font-size: 8px;
padding: 1px 3px;
}
.working-day-badge {
width: 12px;
height: 12px;
font-size: 7px;
}
.custom-holiday-icon,
.official-holiday-icon {
font-size: 7px;
}
.calendar-legend {
gap: 8px;
padding: 8px 10px;
}
.legend-item {
font-size: 10px;
}
.legend-badge {
width: 14px;
height: 14px;
font-size: 8px;
}
.legend-tag {
padding: 1px 4px;
font-size: 8px;
}
.legend-tag-text {
font-size: 7px;
}
.legend-tag .custom-holiday-icon,
.legend-tag .official-holiday-icon {
font-size: 6px;
}
}

View File

@@ -14,8 +14,8 @@ import {
Tag,
Popconfirm,
message,
} from 'antd';
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
} from '@/shared/antd-imports';
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
import dayjs, { Dayjs } from 'dayjs';
import { holidayApiService } from '@/api/holiday/holiday.api.service';
@@ -28,7 +28,7 @@ import {
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { RootState } from '@/app/store';
import { fetchHolidays } from '@/features/admin-center/admin-center.slice';
import { fetchHolidays, clearHolidaysCache } from '@/features/admin-center/admin-center.slice';
import logger from '@/utils/errorLogger';
import './holiday-calendar.css';
@@ -38,9 +38,10 @@ const { TextArea } = Input;
interface HolidayCalendarProps {
themeMode: string;
workingDays?: string[];
}
const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDays = [] }) => {
const { t } = useTranslation('admin-center/overview');
const dispatch = useAppDispatch();
const { holidays, loadingHolidays, holidaySettings } = useAppSelector(
@@ -66,10 +67,32 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
}
};
const fetchHolidaysForDateRange = () => {
const populateHolidaysIfNeeded = async () => {
// Check if we have holiday settings with a country code but no holidays
if (holidaySettings?.country_code && holidays.length === 0) {
try {
console.log('🔄 No holidays found, attempting to populate official holidays...');
const populateRes = await holidayApiService.populateCountryHolidays();
if (populateRes.done) {
console.log('✅ Official holidays populated successfully');
// Refresh holidays after population
fetchHolidaysForDateRange(true);
}
} catch (error) {
console.warn('⚠️ Could not populate official holidays:', error);
}
}
};
const fetchHolidaysForDateRange = (forceRefresh = false) => {
const startOfYear = currentDate.startOf('year');
const endOfYear = currentDate.endOf('year');
// If forceRefresh is true, clear the cached holidays first
if (forceRefresh) {
dispatch(clearHolidaysCache());
}
dispatch(
fetchHolidays({
from_date: startOfYear.format('YYYY-MM-DD'),
@@ -84,6 +107,11 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
fetchHolidaysForDateRange();
}, [currentDate.year()]);
// Check if we need to populate holidays when holiday settings are loaded
useEffect(() => {
populateHolidaysIfNeeded();
}, [holidaySettings, holidays.length]);
const customHolidays = useMemo(() => {
return holidays.filter(holiday => holiday.source === 'custom');
}, [holidays]);
@@ -103,7 +131,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
message.success(t('holidayCreated'));
setModalVisible(false);
form.resetFields();
fetchHolidaysForDateRange();
fetchHolidaysForDateRange(true);
}
} catch (error) {
logger.error('Error creating holiday', error);
@@ -133,7 +161,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
setEditModalVisible(false);
editForm.resetFields();
setSelectedHoliday(null);
fetchHolidaysForDateRange();
fetchHolidaysForDateRange(true);
}
} catch (error) {
logger.error('Error updating holiday', error);
@@ -146,7 +174,12 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
const res = await holidayApiService.deleteOrganizationHoliday(holidayId);
if (res.done) {
message.success(t('holidayDeleted'));
fetchHolidaysForDateRange();
// Close the edit modal and reset form
setEditModalVisible(false);
editForm.resetFields();
setSelectedHoliday(null);
// Refresh holidays data
fetchHolidaysForDateRange(true);
}
} catch (error) {
logger.error('Error deleting holiday', error);
@@ -174,31 +207,55 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
const getHolidayDateCellRender = (date: Dayjs) => {
const dateHolidays = holidays.filter(h => dayjs(h.date).isSame(date, 'day'));
const dayName = date.format('dddd');
// Check if this day is in the working days array from API response
const isWorkingDay = workingDays && workingDays.length > 0 ? workingDays.includes(dayName) : false;
const isToday = date.isSame(dayjs(), 'day');
const isCurrentMonth = date.isSame(currentDate, 'month');
if (dateHolidays.length > 0) {
return (
<div className="holiday-cell">
{dateHolidays.map((holiday, index) => (
<Tag
key={`${holiday.id}-${index}`}
color={holiday.color_code || (holiday.source === 'official' ? '#1890ff' : '#f37070')}
style={{
fontSize: '10px',
padding: '1px 4px',
margin: '1px 0',
borderRadius: '2px',
display: 'block',
opacity: holiday.source === 'official' ? 0.8 : 1,
}}
title={`${holiday.name}${holiday.source === 'official' ? ' (Official)' : ' (Custom)'}`}
>
{holiday.name}
</Tag>
))}
</div>
);
}
return null;
return (
<div className={`calendar-cell ${isWorkingDay ? 'working-day' : 'non-working-day'} ${isToday ? 'today' : ''} ${!isCurrentMonth ? 'other-month' : ''}`}>
{dateHolidays.length > 0 && (
<div className="holiday-cell">
{dateHolidays.map((holiday, index) => {
const isOfficial = holiday.source === 'official';
const isCustom = holiday.source === 'custom';
return (
<Tag
key={`${holiday.id}-${index}`}
color={holiday.color_code || (isOfficial ? '#1890ff' : '#52c41a')}
className={`holiday-tag ${isOfficial ? 'official-holiday' : 'custom-holiday'}`}
style={{
fontSize: '10px',
padding: '2px 6px',
margin: '1px 0',
borderRadius: '4px',
display: 'block',
fontWeight: isCustom ? 600 : 500,
border: isCustom ? '1px solid rgba(82, 196, 26, 0.6)' : '1px solid rgba(24, 144, 255, 0.4)',
position: 'relative',
}}
title={`${holiday.name}${isOfficial ? ' (Official Holiday)' : ' (Custom Holiday)'}`}
>
{isCustom && (
<span className="custom-holiday-icon" style={{ marginRight: '2px' }}></span>
)}
{isOfficial && (
<span className="official-holiday-icon" style={{ marginRight: '2px' }}>🏛</span>
)}
{holiday.name}
</Tag>
);
})}
</div>
)}
{isWorkingDay && (
<div className="working-day-indicator" title={`${dayName} - Working Day`}>
<div className="working-day-badge">W</div>
</div>
)}
</div>
);
};
const onPanelChange = (value: Dayjs) => {
@@ -227,40 +284,83 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
alignItems: 'flex-start',
marginBottom: 18,
padding: '4px 0',
}}
>
<Title level={5} style={{ margin: 0 }}>
{t('holidayCalendar')}
</Title>
<Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setModalVisible(true)}
size="small"
>
{t('addCustomHoliday') || 'Add Custom Holiday'}
</Button>
<div>
<Title level={4} style={{ margin: '0 0 4px 0', fontWeight: 600 }}>
{t('holidayCalendar')}
</Title>
{holidaySettings?.country_code && (
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
<Typography.Text type="secondary" style={{ fontSize: '13px', fontWeight: 500 }}>
{t('officialHolidaysFrom') || 'Official holidays from'}:{' '}
{holidaySettings.country_code}
{holidaySettings.state_code && ` (${holidaySettings.state_code})`}
<span style={{ color: themeMode === 'dark' ? '#40a9ff' : '#1890ff' }}>
{holidaySettings.country_code}
{holidaySettings.state_code && ` (${holidaySettings.state_code})`}
</span>
</Typography.Text>
)}
</Space>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setModalVisible(true)}
style={{
borderRadius: '8px',
fontWeight: 500,
boxShadow: '0 2px 8px rgba(24, 144, 255, 0.2)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(24, 144, 255, 0.3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(24, 144, 255, 0.2)';
}}
>
{t('addCustomHoliday') || 'Add Custom Holiday'}
</Button>
</div>
<Calendar
value={currentDate}
onPanelChange={onPanelChange}
onSelect={onDateSelect}
dateCellRender={getHolidayDateCellRender}
className={`holiday-calendar ${themeMode}`}
loading={loadingHolidays}
/>
<div className="calendar-container">
<Calendar
value={currentDate}
onPanelChange={onPanelChange}
onSelect={onDateSelect}
dateCellRender={getHolidayDateCellRender}
className={`holiday-calendar ${themeMode}`}
/>
{/* Calendar Legend */}
<div className="calendar-legend">
<div className="legend-item">
<div className="legend-badge working-day-badge">W</div>
<span>{t('workingDay') || 'Working Day'}</span>
</div>
<div className="legend-item">
<div className="legend-tag custom-holiday-legend">
<span className="custom-holiday-icon"></span>
<span className="legend-tag-text">Custom</span>
</div>
<span>{t('customHoliday') || 'Custom Holiday'}</span>
</div>
<div className="legend-item">
<div className="legend-tag official-holiday-legend">
<span className="official-holiday-icon">🏛</span>
<span className="legend-tag-text">Official</span>
</div>
<span>{t('officialHoliday') || 'Official Holiday'}</span>
</div>
<div className="legend-item">
<div className="legend-dot today-dot"></div>
<span>{t('today') || 'Today'}</span>
</div>
</div>
</div>
{/* Create Holiday Modal */}
<Modal
@@ -417,6 +517,18 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
>
{t('cancel')}
</Button>
{selectedHoliday && selectedHoliday.source === 'custom' && selectedHoliday.is_editable && (
<Popconfirm
title={t('deleteHolidayConfirm') || 'Are you sure you want to delete this holiday?'}
onConfirm={() => handleDeleteHoliday(selectedHoliday.id)}
okText={t('yes') || 'Yes'}
cancelText={t('no') || 'No'}
>
<Button type="primary" danger icon={<DeleteOutlined />}>
{t('delete') || 'Delete'}
</Button>
</Popconfirm>
)}
</Space>
</Form.Item>
</Form>

View File

@@ -20,16 +20,6 @@ export interface SriLankanHolidayData {
export const sriLankanHolidays2025: SriLankanHolidayData[] = [
// January
{
name: "New Year's Day",
date: "2025-01-01",
type: "Public",
description: "Celebration of the first day of the Gregorian calendar year",
is_recurring: true,
is_poya: false,
country_code: "LK",
color_code: "#DC143C"
},
{
name: "Duruthu Full Moon Poya Day",
date: "2025-01-13",

View File

@@ -105,7 +105,6 @@ export const updateHolidaySettings = createAsyncThunk(
async (settings: IOrganizationHolidaySettings) => {
const { holidayApiService } = await import('@/api/holiday/holiday.api.service');
await holidayApiService.updateOrganizationHolidaySettings(settings);
await adminCenterApiService.updateOrganizationHolidaySettings(settings);
return settings;
}
);
@@ -164,6 +163,10 @@ const adminCenterSlice = createSlice({
? (state.isUpgradeModalOpen = false)
: (state.isUpgradeModalOpen = true);
},
clearHolidaysCache: state => {
state.holidays = [];
state.holidaysDateRange = null;
},
},
extraReducers: builder => {
builder.addCase(fetchBillingInfo.pending, (state, action) => {
@@ -264,7 +267,7 @@ const adminCenterSlice = createSlice({
},
});
export const { toggleRedeemCodeDrawer, toggleUpgradeModal } = adminCenterSlice.actions;
export const { toggleRedeemCodeDrawer, toggleUpgradeModal, clearHolidaysCache } = adminCenterSlice.actions;
// Selectors for optimized access
export const selectHolidaysByDateRange = createSelector(

View File

@@ -12,17 +12,14 @@ import {
message,
Select,
Switch,
Divider,
} from '@/shared/antd-imports';
import { PageHeader } from '@ant-design/pro-components';
import { useTranslation } from 'react-i18next';
import { IOrganization, IOrganizationAdmin } from '@/types/admin-center/admin-center.types';
import logger from '@/utils/errorLogger';
import { scheduleAPIService } from '@/api/schedule/schedule.api.service';
import { Settings } from '@/types/schedule/schedule-v2.types';
import OrganizationCalculationMethod from '@/components/admin-center/overview/organization-calculation-method/organization-calculation-method';
import HolidayCalendar from '@/components/admin-center/overview/holiday-calendar/holiday-calendar';
import { holidayApiService } from '@/api/holiday/holiday.api.service';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { RootState } from '@/app/store';
@@ -38,11 +35,7 @@ const SettingsPage: React.FC = () => {
const dispatch = useAppDispatch();
const {
organization,
organizationAdmins,
loadingOrganization,
loadingOrganizationAdmins,
holidaySettings,
loadingHolidaySettings,
countriesWithStates,
loadingCountries,
} = useAppSelector((state: RootState) => state.adminCenterReducer);
@@ -241,7 +234,7 @@ const SettingsPage: React.FC = () => {
}}
showSearch
filterOption={(input, option) =>
(option?.children as string)?.toLowerCase().includes(input.toLowerCase())
(option?.children as unknown as string)?.toLowerCase().includes(input.toLowerCase())
}
>
{countriesWithStates.map(country => (
@@ -260,7 +253,7 @@ const SettingsPage: React.FC = () => {
disabled={!holidayForm.getFieldValue('country_code')}
showSearch
filterOption={(input, option) =>
(option?.children as string)?.toLowerCase().includes(input.toLowerCase())
(option?.children as unknown as string)?.toLowerCase().includes(input.toLowerCase())
}
>
{getSelectedCountryStates().map(state => (
@@ -287,7 +280,7 @@ const SettingsPage: React.FC = () => {
</Form>
</Card>
<HolidayCalendar themeMode={themeMode} />
<HolidayCalendar themeMode={themeMode} workingDays={workingDays} />
</Space>
</div>
);