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)); 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/calculation-method", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOrganizationCalculationMethod));
adminCenterApiRouter.put("/organization/owner/contact-number", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOwnerContactNumber)); 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 // users
adminCenterApiRouter.get("/organization/users", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationUsers)); 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", "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", "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", "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ë", "sunday": "E Dielë",
"saveButton": "Ruaj", "saveButton": "Ruaj",
"saved": "Cilësimet u ruajtën me sukses", "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", "manDaysCalculationDescription": "Alle Projektkosten werden anhand geschätzter Mann-Tage × Tagessätze berechnet",
"calculationMethodTooltip": "Diese Einstellung gilt für alle Projekte in Ihrer Organisation", "calculationMethodTooltip": "Diese Einstellung gilt für alle Projekte in Ihrer Organisation",
"calculationMethodUpdated": "Organisations-Berechnungsmethode erfolgreich aktualisiert", "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", "sunday": "Sonntag",
"saveButton": "Speichern", "saveButton": "Speichern",
"saved": "Einstellungen erfolgreich gespeichert", "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", "year": "Year",
"import": "Import", "import": "Import",
"holidaysImported": "Successfully imported {{count}} holidays", "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", "sunday": "Sunday",
"saveButton": "Save", "saveButton": "Save",
"saved": "Settings saved successfully", "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", "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", "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", "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", "sunday": "Domingo",
"saveButton": "Guardar", "saveButton": "Guardar",
"saved": "Configuración guardada exitosamente", "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", "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", "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", "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", "sunday": "Domingo",
"saveButton": "Salvar", "saveButton": "Salvar",
"saved": "Configurações salvas com sucesso", "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": "所有项目成本将使用估算人天数 × 日费率计算", "manDaysCalculationDescription": "所有项目成本将使用估算人天数 × 日费率计算",
"calculationMethodTooltip": "此设置适用于您组织中的所有项目", "calculationMethodTooltip": "此设置适用于您组织中的所有项目",
"calculationMethodUpdated": "组织计算方法更新成功", "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": "星期日", "sunday": "星期日",
"saveButton": "保存", "saveButton": "保存",
"saved": "设置保存成功", "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`; const rootUrl = `${API_BASE_URL}/holidays`;
export const holidayApiService = { export const holidayApiService = {
// Holiday types - PLACEHOLDER with Sri Lankan specific types // Holiday types
getHolidayTypes: async (): Promise<IServerResponse<IHolidayType[]>> => { getHolidayTypes: async (): Promise<IServerResponse<IHolidayType[]>> => {
// Return holiday types including Sri Lankan specific types const response = await apiClient.get<IServerResponse<IHolidayType[]>>(
const holidayTypes = [ `${rootUrl}/types`
{ id: '1', name: 'Public Holiday', color_code: '#DC143C' }, );
{ id: '2', name: 'Religious Holiday', color_code: '#4ecdc4' }, return response.data;
{ 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[]>;
}, },
// Organization holidays - PLACEHOLDER until backend implements // Organization holidays
getOrganizationHolidays: async ( getOrganizationHolidays: async (
year?: number year?: number
): Promise<IServerResponse<IOrganizationHoliday[]>> => { ): Promise<IServerResponse<IOrganizationHoliday[]>> => {
// Return empty array for now to prevent 404 errors const params = year ? { year } : {};
return { const response = await apiClient.get<IServerResponse<IOrganizationHoliday[]>>(
done: true, `${rootUrl}/organization`,
body: [], { params }
} as IServerResponse<IOrganizationHoliday[]>; );
return response.data;
}, },
// Holiday CRUD operations - PLACEHOLDER until backend implements // Holiday CRUD operations
createOrganizationHoliday: async (data: ICreateHolidayRequest): Promise<IServerResponse<any>> => { createOrganizationHoliday: async (data: ICreateHolidayRequest): Promise<IServerResponse<any>> => {
// Return success for now to prevent UI errors const response = await apiClient.post<IServerResponse<any>>(
return { `${rootUrl}/organization`,
done: true, data
body: { id: Date.now().toString(), ...data }, );
} as IServerResponse<any>; return response.data;
}, },
updateOrganizationHoliday: async ( updateOrganizationHoliday: async (
id: string, id: string,
data: IUpdateHolidayRequest data: IUpdateHolidayRequest
): Promise<IServerResponse<any>> => { ): Promise<IServerResponse<any>> => {
// Return success for now to prevent UI errors const response = await apiClient.put<IServerResponse<any>>(
return { `${rootUrl}/organization/${id}`,
done: true, data
body: { id, ...data }, );
} as IServerResponse<any>; return response.data;
}, },
deleteOrganizationHoliday: async (id: string): Promise<IServerResponse<any>> => { deleteOrganizationHoliday: async (id: string): Promise<IServerResponse<any>> => {
// Return success for now to prevent UI errors const response = await apiClient.delete<IServerResponse<any>>(
return { `${rootUrl}/organization/${id}`
done: true, );
body: {}, return response.data;
} as IServerResponse<any>;
}, },
// Country holidays - PLACEHOLDER with all date-holidays supported countries // Country holidays - PLACEHOLDER with all date-holidays supported countries
@@ -200,11 +188,15 @@ export const holidayApiService = {
countryCode: string, countryCode: string,
year?: number year?: number
): Promise<IServerResponse<ICountryHoliday[]>> => { ): Promise<IServerResponse<ICountryHoliday[]>> => {
// Return empty array for now const params: any = { country_code: countryCode };
return { if (year) {
done: true, params.year = year;
body: [], }
} as IServerResponse<ICountryHoliday[]>; const response = await apiClient.get<IServerResponse<ICountryHoliday[]>>(
`${rootUrl}/countries/${countryCode}`,
{ params }
);
return response.data;
}, },
importCountryHolidays: async ( importCountryHolidays: async (
@@ -229,35 +221,35 @@ export const holidayApiService = {
} as IServerResponse<IHolidayCalendarEvent[]>; } as IServerResponse<IHolidayCalendarEvent[]>;
}, },
// Organization holiday settings - PLACEHOLDER until backend implements // Organization holiday settings
getOrganizationHolidaySettings: async (): Promise< getOrganizationHolidaySettings: async (): Promise<
IServerResponse<IOrganizationHolidaySettings> IServerResponse<IOrganizationHolidaySettings>
> => { > => {
// Return default settings for now const response = await apiClient.get<IServerResponse<IOrganizationHolidaySettings>>(
return { `${API_BASE_URL}/admin-center/organization/holiday-settings`
done: true, );
body: { return response.data;
country_code: undefined,
state_code: undefined,
auto_sync_holidays: false,
},
} as IServerResponse<IOrganizationHolidaySettings>;
}, },
updateOrganizationHolidaySettings: async ( updateOrganizationHolidaySettings: async (
data: IOrganizationHolidaySettings data: IOrganizationHolidaySettings
): Promise<IServerResponse<any>> => { ): Promise<IServerResponse<any>> => {
// Just return success for now const response = await apiClient.put<IServerResponse<any>>(
return { `${API_BASE_URL}/admin-center/organization/holiday-settings`,
done: true, data
body: {}, );
} as IServerResponse<any>; return response.data;
}, },
// Countries with states - PLACEHOLDER with date-holidays supported countries // Countries with states
getCountriesWithStates: async (): Promise<IServerResponse<ICountryWithStates[]>> => { getCountriesWithStates: async (): Promise<IServerResponse<ICountryWithStates[]>> => {
// Return comprehensive list of countries supported by date-holidays library const response = await apiClient.get<IServerResponse<ICountryWithStates[]>>(
const supportedCountries = [ `${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: 'AD', name: 'Andorra' },
{ code: 'AE', name: 'United Arab Emirates' }, { code: 'AE', name: 'United Arab Emirates' },
{ code: 'AG', name: 'Antigua & Barbuda' }, { code: 'AG', name: 'Antigua & Barbuda' },
@@ -696,41 +688,85 @@ export const holidayApiService = {
return { return {
done: true, done: true,
body: supportedCountries, 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 ( getCombinedHolidays: async (
params: ICombinedHolidaysRequest & { country_code?: string } params: ICombinedHolidaysRequest & { country_code?: string }
): Promise<IServerResponse<IHolidayCalendarEvent[]>> => { ): Promise<IServerResponse<IHolidayCalendarEvent[]>> => {
try { try {
console.log('🔍 getCombinedHolidays called with params:', params);
const year = new Date(params.from_date).getFullYear(); const year = new Date(params.from_date).getFullYear();
let allHolidays: IHolidayCalendarEvent[] = []; let allHolidays: IHolidayCalendarEvent[] = [];
// Handle Sri Lankan holidays from database // Get official holidays - handle Sri Lanka specially, others from API
if (params.country_code === 'LK' && year === 2025) { if (params.country_code) {
// Import Sri Lankan holiday data console.log(`🌐 Fetching official holidays for country: ${params.country_code}, year: ${year}`);
const { sriLankanHolidays2025 } = await import('@/data/sri-lanka-holidays-2025');
const sriLankanHolidays = sriLankanHolidays2025 // Handle Sri Lankan holidays from static data
.filter(h => h.date >= params.from_date && h.date <= params.to_date) if (params.country_code === 'LK' && year === 2025) {
.map(h => ({ try {
id: `lk-${h.date}-${h.name.replace(/\s+/g, '-').toLowerCase()}`, console.log('🇱🇰 Loading Sri Lankan holidays from static data...');
name: h.name, const { sriLankanHolidays2025 } = await import('@/data/sri-lanka-holidays-2025');
description: h.description,
date: h.date, const sriLankanHolidays = sriLankanHolidays2025
is_recurring: h.is_recurring, .filter(h => h.date >= params.from_date && h.date <= params.to_date)
holiday_type_name: h.type, .map(h => ({
color_code: h.color_code, id: `lk-${h.date}-${h.name.replace(/\\s+/g, '-').toLowerCase()}`,
source: 'official' as const, name: h.name,
is_editable: false, description: h.description,
})); date: h.date,
is_recurring: h.is_recurring,
allHolidays.push(...sriLankanHolidays); 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) // 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); const customRes = await holidayApiService.getOrganizationHolidays(year);
console.log('🏢 Organization holidays response:', customRes);
if (customRes.done && customRes.body) { if (customRes.done && customRes.body) {
const customHolidays = 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 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 existingDates = new Set(allHolidays.map(h => h.date));
const uniqueCustomHolidays = customHolidays.filter((h: any) => !existingDates.has(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); allHolidays.push(...uniqueCustomHolidays);
} else {
console.log('⚠️ No organization holidays returned from API');
} }
console.log(`🎉 Total holidays combined: ${allHolidays.length}`, allHolidays);
return { return {
done: true, done: true,
body: allHolidays, body: allHolidays,
@@ -798,12 +838,9 @@ export const holidayApiService = {
} as IServerResponse<{ working_days: number; total_days: number; holidays_count: number }>; } 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>> => { populateCountryHolidays: async (): Promise<IServerResponse<any>> => {
// Return success for now const response = await apiClient.post<IServerResponse<any>>(`${rootUrl}/populate`);
return { return response.data;
done: true,
body: { message: 'Holidays populated successfully' },
} as IServerResponse<any>;
}, },
}; };

View File

@@ -4,20 +4,37 @@
.holiday-calendar .ant-picker-calendar { .holiday-calendar .ant-picker-calendar {
background: transparent; 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 { .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 { .holiday-calendar .ant-picker-calendar-date {
position: relative; position: relative;
height: 80px; height: 85px;
padding: 4px 8px; padding: 6px 10px;
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
border-radius: 6px; border-radius: 10px;
transition: all 0.3s; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer; cursor: pointer;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
} }
.holiday-calendar.dark .ant-picker-calendar-date { .holiday-calendar.dark .ant-picker-calendar-date {
@@ -25,18 +42,161 @@
background: #1f1f1f; 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 { .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 { .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 { .holiday-calendar .ant-picker-calendar-date-value {
font-size: 12px; font-size: 14px;
font-weight: 500; font-weight: 600;
color: #262626; color: #1f1f1f;
margin-bottom: 6px;
line-height: 1.2;
text-align: center;
} }
.holiday-calendar.dark .ant-picker-calendar-date-value { .holiday-calendar.dark .ant-picker-calendar-date-value {
@@ -44,16 +204,22 @@
} }
.holiday-calendar .ant-picker-calendar-date-content { .holiday-calendar .ant-picker-calendar-date-content {
height: 100%; height: calc(100% - 20px);
overflow: hidden; overflow: hidden;
margin-top: 4px;
} }
.holiday-cell { .holiday-cell {
position: absolute; position: absolute;
bottom: 2px; bottom: 6px;
left: 2px; left: 6px;
right: 2px; right: 6px;
z-index: 1; z-index: 1;
max-height: 65%;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 2px;
} }
.holiday-cell .ant-tag { .holiday-cell .ant-tag {
@@ -61,14 +227,27 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
border: none; border: 1px solid rgba(255, 255, 255, 0.2);
font-weight: 500; font-weight: 500;
cursor: pointer; 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 { .holiday-cell .ant-tag:hover {
transform: scale(1.05); transform: scale(1.05) translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 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 { .holiday-calendar .ant-picker-calendar-date-today {
@@ -253,20 +432,243 @@
/* Tag styles */ /* Tag styles */
.holiday-calendar .ant-tag { .holiday-calendar .ant-tag {
border-radius: 4px; border-radius: 6px;
font-size: 10px; font-size: 10px;
line-height: 1.2; line-height: 1.3;
padding: 1px 4px; padding: 2px 6px;
margin: 0; margin: 0;
border: none; border: 1px solid rgba(255, 255, 255, 0.2);
font-weight: 500; 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 */ /* Responsive design */
@media (max-width: 768px) { @media (max-width: 768px) {
.holiday-calendar .ant-picker-calendar-date { .holiday-calendar .ant-picker-calendar-date {
height: 60px; height: 70px;
padding: 2px 4px; 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 { .holiday-calendar .ant-picker-calendar-date-value {
@@ -274,7 +676,47 @@
} }
.holiday-cell .ant-tag { .holiday-cell .ant-tag {
font-size: 9px; font-size: 8px;
padding: 0 2px; 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, Tag,
Popconfirm, Popconfirm,
message, message,
} from 'antd'; } from '@/shared/antd-imports';
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'; import { PlusOutlined, DeleteOutlined, EditOutlined } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { holidayApiService } from '@/api/holiday/holiday.api.service'; import { holidayApiService } from '@/api/holiday/holiday.api.service';
@@ -28,7 +28,7 @@ import {
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { RootState } from '@/app/store'; 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 logger from '@/utils/errorLogger';
import './holiday-calendar.css'; import './holiday-calendar.css';
@@ -38,9 +38,10 @@ const { TextArea } = Input;
interface HolidayCalendarProps { interface HolidayCalendarProps {
themeMode: string; themeMode: string;
workingDays?: string[];
} }
const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => { const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode, workingDays = [] }) => {
const { t } = useTranslation('admin-center/overview'); const { t } = useTranslation('admin-center/overview');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { holidays, loadingHolidays, holidaySettings } = useAppSelector( 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 startOfYear = currentDate.startOf('year');
const endOfYear = currentDate.endOf('year'); const endOfYear = currentDate.endOf('year');
// If forceRefresh is true, clear the cached holidays first
if (forceRefresh) {
dispatch(clearHolidaysCache());
}
dispatch( dispatch(
fetchHolidays({ fetchHolidays({
from_date: startOfYear.format('YYYY-MM-DD'), from_date: startOfYear.format('YYYY-MM-DD'),
@@ -84,6 +107,11 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
fetchHolidaysForDateRange(); fetchHolidaysForDateRange();
}, [currentDate.year()]); }, [currentDate.year()]);
// Check if we need to populate holidays when holiday settings are loaded
useEffect(() => {
populateHolidaysIfNeeded();
}, [holidaySettings, holidays.length]);
const customHolidays = useMemo(() => { const customHolidays = useMemo(() => {
return holidays.filter(holiday => holiday.source === 'custom'); return holidays.filter(holiday => holiday.source === 'custom');
}, [holidays]); }, [holidays]);
@@ -103,7 +131,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
message.success(t('holidayCreated')); message.success(t('holidayCreated'));
setModalVisible(false); setModalVisible(false);
form.resetFields(); form.resetFields();
fetchHolidaysForDateRange(); fetchHolidaysForDateRange(true);
} }
} catch (error) { } catch (error) {
logger.error('Error creating holiday', error); logger.error('Error creating holiday', error);
@@ -133,7 +161,7 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
setEditModalVisible(false); setEditModalVisible(false);
editForm.resetFields(); editForm.resetFields();
setSelectedHoliday(null); setSelectedHoliday(null);
fetchHolidaysForDateRange(); fetchHolidaysForDateRange(true);
} }
} catch (error) { } catch (error) {
logger.error('Error updating holiday', error); logger.error('Error updating holiday', error);
@@ -146,7 +174,12 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
const res = await holidayApiService.deleteOrganizationHoliday(holidayId); const res = await holidayApiService.deleteOrganizationHoliday(holidayId);
if (res.done) { if (res.done) {
message.success(t('holidayDeleted')); 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) { } catch (error) {
logger.error('Error deleting holiday', error); logger.error('Error deleting holiday', error);
@@ -174,31 +207,55 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
const getHolidayDateCellRender = (date: Dayjs) => { const getHolidayDateCellRender = (date: Dayjs) => {
const dateHolidays = holidays.filter(h => dayjs(h.date).isSame(date, 'day')); 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 (
return ( <div className={`calendar-cell ${isWorkingDay ? 'working-day' : 'non-working-day'} ${isToday ? 'today' : ''} ${!isCurrentMonth ? 'other-month' : ''}`}>
<div className="holiday-cell"> {dateHolidays.length > 0 && (
{dateHolidays.map((holiday, index) => ( <div className="holiday-cell">
<Tag {dateHolidays.map((holiday, index) => {
key={`${holiday.id}-${index}`} const isOfficial = holiday.source === 'official';
color={holiday.color_code || (holiday.source === 'official' ? '#1890ff' : '#f37070')} const isCustom = holiday.source === 'custom';
style={{ return (
fontSize: '10px', <Tag
padding: '1px 4px', key={`${holiday.id}-${index}`}
margin: '1px 0', color={holiday.color_code || (isOfficial ? '#1890ff' : '#52c41a')}
borderRadius: '2px', className={`holiday-tag ${isOfficial ? 'official-holiday' : 'custom-holiday'}`}
display: 'block', style={{
opacity: holiday.source === 'official' ? 0.8 : 1, fontSize: '10px',
}} padding: '2px 6px',
title={`${holiday.name}${holiday.source === 'official' ? ' (Official)' : ' (Custom)'}`} margin: '1px 0',
> borderRadius: '4px',
{holiday.name} display: 'block',
</Tag> fontWeight: isCustom ? 600 : 500,
))} border: isCustom ? '1px solid rgba(82, 196, 26, 0.6)' : '1px solid rgba(24, 144, 255, 0.4)',
</div> position: 'relative',
); }}
} title={`${holiday.name}${isOfficial ? ' (Official Holiday)' : ' (Custom Holiday)'}`}
return null; >
{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) => { const onPanelChange = (value: Dayjs) => {
@@ -227,40 +284,83 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
style={{ style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'flex-start',
marginBottom: 16, marginBottom: 18,
padding: '4px 0',
}} }}
> >
<Title level={5} style={{ margin: 0 }}> <div>
{t('holidayCalendar')} <Title level={4} style={{ margin: '0 0 4px 0', fontWeight: 600 }}>
</Title> {t('holidayCalendar')}
<Space> </Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setModalVisible(true)}
size="small"
>
{t('addCustomHoliday') || 'Add Custom Holiday'}
</Button>
{holidaySettings?.country_code && ( {holidaySettings?.country_code && (
<Typography.Text type="secondary" style={{ fontSize: '12px' }}> <Typography.Text type="secondary" style={{ fontSize: '13px', fontWeight: 500 }}>
{t('officialHolidaysFrom') || 'Official holidays from'}:{' '} {t('officialHolidaysFrom') || 'Official holidays from'}:{' '}
{holidaySettings.country_code} <span style={{ color: themeMode === 'dark' ? '#40a9ff' : '#1890ff' }}>
{holidaySettings.state_code && ` (${holidaySettings.state_code})`} {holidaySettings.country_code}
{holidaySettings.state_code && ` (${holidaySettings.state_code})`}
</span>
</Typography.Text> </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> </div>
<Calendar <div className="calendar-container">
value={currentDate} <Calendar
onPanelChange={onPanelChange} value={currentDate}
onSelect={onDateSelect} onPanelChange={onPanelChange}
dateCellRender={getHolidayDateCellRender} onSelect={onDateSelect}
className={`holiday-calendar ${themeMode}`} dateCellRender={getHolidayDateCellRender}
loading={loadingHolidays} 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 */} {/* Create Holiday Modal */}
<Modal <Modal
@@ -417,6 +517,18 @@ const HolidayCalendar: React.FC<HolidayCalendarProps> = ({ themeMode }) => {
> >
{t('cancel')} {t('cancel')}
</Button> </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> </Space>
</Form.Item> </Form.Item>
</Form> </Form>

View File

@@ -20,16 +20,6 @@ export interface SriLankanHolidayData {
export const sriLankanHolidays2025: SriLankanHolidayData[] = [ export const sriLankanHolidays2025: SriLankanHolidayData[] = [
// January // 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", name: "Duruthu Full Moon Poya Day",
date: "2025-01-13", date: "2025-01-13",

View File

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

View File

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