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:
@@ -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);
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "否"
|
||||
}
|
||||
|
||||
@@ -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": "无法编辑官方假期"
|
||||
}
|
||||
|
||||
@@ -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,26 +688,32 @@ 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
|
||||
// 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}`);
|
||||
|
||||
// Handle Sri Lankan holidays from static data
|
||||
if (params.country_code === 'LK' && year === 2025) {
|
||||
// Import Sri Lankan holiday data
|
||||
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()}`,
|
||||
id: `lk-${h.date}-${h.name.replace(/\\s+/g, '-').toLowerCase()}`,
|
||||
name: h.name,
|
||||
description: h.description,
|
||||
date: h.date,
|
||||
@@ -726,11 +724,49 @@ export const holidayApiService = {
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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={`calendar-cell ${isWorkingDay ? 'working-day' : 'non-working-day'} ${isToday ? 'today' : ''} ${!isCurrentMonth ? 'other-month' : ''}`}>
|
||||
{dateHolidays.length > 0 && (
|
||||
<div className="holiday-cell">
|
||||
{dateHolidays.map((holiday, index) => (
|
||||
{dateHolidays.map((holiday, index) => {
|
||||
const isOfficial = holiday.source === 'official';
|
||||
const isCustom = holiday.source === 'custom';
|
||||
return (
|
||||
<Tag
|
||||
key={`${holiday.id}-${index}`}
|
||||
color={holiday.color_code || (holiday.source === 'official' ? '#1890ff' : '#f37070')}
|
||||
color={holiday.color_code || (isOfficial ? '#1890ff' : '#52c41a')}
|
||||
className={`holiday-tag ${isOfficial ? 'official-holiday' : 'custom-holiday'}`}
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
padding: '1px 4px',
|
||||
padding: '2px 6px',
|
||||
margin: '1px 0',
|
||||
borderRadius: '2px',
|
||||
borderRadius: '4px',
|
||||
display: 'block',
|
||||
opacity: holiday.source === 'official' ? 0.8 : 1,
|
||||
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}${holiday.source === 'official' ? ' (Official)' : ' (Custom)'}`}
|
||||
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>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const onPanelChange = (value: Dayjs) => {
|
||||
@@ -227,41 +284,84 @@ 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 }}>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: '0 0 4px 0', fontWeight: 600 }}>
|
||||
{t('holidayCalendar')}
|
||||
</Title>
|
||||
<Space>
|
||||
{holidaySettings?.country_code && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '13px', fontWeight: 500 }}>
|
||||
{t('officialHolidaysFrom') || 'Official holidays from'}:{' '}
|
||||
<span style={{ color: themeMode === 'dark' ? '#40a9ff' : '#1890ff' }}>
|
||||
{holidaySettings.country_code}
|
||||
{holidaySettings.state_code && ` (${holidaySettings.state_code})`}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setModalVisible(true)}
|
||||
size="small"
|
||||
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>
|
||||
{holidaySettings?.country_code && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{t('officialHolidaysFrom') || 'Official holidays from'}:{' '}
|
||||
{holidaySettings.country_code}
|
||||
{holidaySettings.state_code && ` (${holidaySettings.state_code})`}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="calendar-container">
|
||||
<Calendar
|
||||
value={currentDate}
|
||||
onPanelChange={onPanelChange}
|
||||
onSelect={onDateSelect}
|
||||
dateCellRender={getHolidayDateCellRender}
|
||||
className={`holiday-calendar ${themeMode}`}
|
||||
loading={loadingHolidays}
|
||||
/>
|
||||
|
||||
{/* 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
|
||||
title={t('addHoliday')}
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user