diff --git a/__test__/features/Settings/WorkingDays.test.tsx b/__test__/features/Settings/WorkingDays.test.tsx
new file mode 100644
index 0000000..a1a85ef
--- /dev/null
+++ b/__test__/features/Settings/WorkingDays.test.tsx
@@ -0,0 +1,467 @@
+import SettingsPage from "@/features/Settings/SettingsPage";
+import settingsReducer from "@/features/Settings/SettingsSlice";
+import userReducer, {
+ getOpenPaasUserDataAsync,
+} from "@/features/User/userSlice";
+import { api } from "@/utils/apiUtils";
+import { configureStore } from "@reduxjs/toolkit";
+import "@testing-library/jest-dom";
+import { fireEvent, screen, waitFor } from "@testing-library/react";
+import { renderWithProviders } from "../../utils/Renderwithproviders";
+
+jest.mock("@/utils/apiUtils");
+
+const basePreloadedState = {
+ user: {
+ userData: {
+ sub: "test",
+ email: "test@test.com",
+ family_name: "Doe",
+ name: "John",
+ sid: "mockSid",
+ openpaasId: "667037022b752d0026472254",
+ },
+ organiserData: null,
+ tokens: null,
+ coreConfig: {
+ language: "en",
+ datetime: { timeZone: "UTC" },
+ },
+ alarmEmailsEnabled: false,
+ loading: false,
+ error: null,
+ },
+ settings: {
+ language: "en",
+ timeZone: "UTC",
+ isBrowserDefaultTimeZone: false,
+ view: "settings",
+ businessHours: {
+ start: "8:0",
+ end: "19:0",
+ daysOfWeek: [1, 2, 3, 4, 5],
+ },
+ workingDays: false,
+ },
+};
+
+describe("Working Days and Business Hours Settings", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (api.patch as jest.Mock).mockResolvedValue({ status: 204 });
+ });
+
+ describe("Initial rendering", () => {
+ it("renders the working days section with day selector", () => {
+ renderWithProviders(, basePreloadedState);
+
+ expect(
+ screen.getByText("settings.chooseWorkingDays")
+ ).toBeInTheDocument();
+ });
+
+ it("renders the show only working days toggle", () => {
+ renderWithProviders(, basePreloadedState);
+
+ expect(
+ screen.getByRole("switch", { name: /settings.showOnlyWorkingDays/i })
+ ).toBeInTheDocument();
+ });
+
+ it("shows working days toggle as unchecked when workingDays is false", () => {
+ renderWithProviders(, basePreloadedState);
+
+ const toggle = screen.getByRole("switch", {
+ name: /settings.showOnlyWorkingDays/i,
+ }) as HTMLInputElement;
+ expect(toggle.checked).toBe(false);
+ });
+
+ it("shows working days toggle as checked when workingDays is true", () => {
+ renderWithProviders(, {
+ ...basePreloadedState,
+ settings: { ...basePreloadedState.settings, workingDays: true },
+ });
+
+ const toggle = screen.getByRole("switch", {
+ name: /settings.showOnlyWorkingDays/i,
+ }) as HTMLInputElement;
+ expect(toggle.checked).toBe(true);
+ });
+
+ it("shows working days toggle as unchecked when workingDays is null", () => {
+ renderWithProviders(, {
+ ...basePreloadedState,
+ settings: { ...basePreloadedState.settings, workingDays: null },
+ });
+
+ const toggle = screen.getByRole("switch", {
+ name: /settings.showOnlyWorkingDays/i,
+ }) as HTMLInputElement;
+ expect(toggle.checked).toBe(false);
+ });
+ });
+
+ describe("workingDays toggle", () => {
+ it("updates workingDays immediately (optimistic update) and calls API", async () => {
+ const { store } = renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ const toggle = screen.getByRole("switch", {
+ name: /settings.showOnlyWorkingDays/i,
+ });
+ fireEvent.click(toggle);
+
+ await waitFor(() => {
+ expect(store.getState().settings.workingDays).toBe(true);
+ });
+
+ await waitFor(() => {
+ expect(api.patch).toHaveBeenCalledWith(
+ "api/configurations?scope=user",
+ expect.objectContaining({
+ json: expect.arrayContaining([
+ expect.objectContaining({
+ name: "linagora.esn.calendar",
+ configurations: expect.arrayContaining([
+ expect.objectContaining({
+ name: "workingDays",
+ value: true,
+ }),
+ ]),
+ }),
+ ]),
+ })
+ );
+ });
+ });
+
+ it("rolls back workingDays if API call fails", async () => {
+ (api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
+
+ const { store } = renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ const toggle = screen.getByRole("switch", {
+ name: /settings.showOnlyWorkingDays/i,
+ });
+ fireEvent.click(toggle);
+
+ await waitFor(
+ () => {
+ expect(store.getState().settings.workingDays).toBe(false);
+ },
+ { timeout: 3000 }
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("settings.workingDaysUpdateError")
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("toggles workingDays from true to false", async () => {
+ const { store } = renderWithProviders(, {
+ ...basePreloadedState,
+ settings: { ...basePreloadedState.settings, workingDays: true },
+ });
+
+ const toggle = screen.getByRole("switch", {
+ name: /settings.showOnlyWorkingDays/i,
+ });
+ fireEvent.click(toggle);
+
+ await waitFor(() => {
+ expect(store.getState().settings.workingDays).toBe(false);
+ });
+
+ await waitFor(() => {
+ expect(api.patch).toHaveBeenCalledWith(
+ "api/configurations?scope=user",
+ expect.objectContaining({
+ json: expect.arrayContaining([
+ expect.objectContaining({
+ name: "linagora.esn.calendar",
+ configurations: expect.arrayContaining([
+ expect.objectContaining({
+ name: "workingDays",
+ value: false,
+ }),
+ ]),
+ }),
+ ]),
+ })
+ );
+ });
+ });
+ });
+
+ describe("businessHours day selector", () => {
+ it("updates businessHours in Redux immediately when a day is clicked", async () => {
+ const { store } = renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ // Sunday (0) is not in initial daysOfWeek [1,2,3,4,5], click it to add
+ const sundayButton = screen.getByRole("button", {
+ name: /sunday/i,
+ });
+ fireEvent.click(sundayButton);
+
+ await waitFor(() => {
+ expect(store.getState().settings.businessHours?.daysOfWeek).toContain(
+ 0
+ );
+ });
+ });
+
+ it("removes a day from businessHours when an already selected day is clicked", async () => {
+ const { store } = renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ // Monday (1) is in initial daysOfWeek, click to remove
+ const mondayButton = screen.getByRole("button", { name: /monday/i });
+ fireEvent.click(mondayButton);
+
+ await waitFor(() => {
+ expect(
+ store.getState().settings.businessHours?.daysOfWeek
+ ).not.toContain(1);
+ });
+ });
+
+ it("debounces API call when multiple days are clicked rapidly", async () => {
+ jest.useFakeTimers();
+
+ renderWithProviders(, basePreloadedState);
+
+ const sundayButton = screen.getByRole("button", { name: /sunday/i });
+ const saturdayButton = screen.getByRole("button", { name: /saturday/i });
+
+ fireEvent.click(sundayButton);
+ fireEvent.click(saturdayButton);
+ fireEvent.click(sundayButton);
+
+ // API should not have been called yet
+ expect(api.patch).not.toHaveBeenCalled();
+
+ // Advance timers past debounce threshold
+ jest.runAllTimers();
+
+ await waitFor(() => {
+ expect(api.patch).toHaveBeenCalledTimes(1);
+ });
+
+ jest.useRealTimers();
+ });
+
+ it("sends businessHours as array to API", async () => {
+ jest.useFakeTimers();
+
+ renderWithProviders(, basePreloadedState);
+
+ const sundayButton = screen.getByRole("button", { name: /sunday/i });
+ fireEvent.click(sundayButton);
+
+ jest.runAllTimers();
+
+ await waitFor(() => {
+ expect(api.patch).toHaveBeenCalledWith(
+ "api/configurations?scope=user",
+ expect.objectContaining({
+ json: expect.arrayContaining([
+ expect.objectContaining({
+ name: "core",
+ configurations: expect.arrayContaining([
+ expect.objectContaining({
+ name: "businessHours",
+ value: expect.arrayContaining([
+ expect.objectContaining({
+ start: "8:0",
+ end: "19:0",
+ daysOfWeek: expect.arrayContaining([0]),
+ }),
+ ]),
+ }),
+ ]),
+ }),
+ ]),
+ })
+ );
+ });
+
+ jest.useRealTimers();
+ });
+
+ it("rolls back businessHours if API call fails", async () => {
+ jest.useFakeTimers();
+ (api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
+
+ const { store } = renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ const sundayButton = screen.getByRole("button", { name: /sunday/i });
+ fireEvent.click(sundayButton);
+
+ jest.runAllTimers();
+
+ await waitFor(
+ () => {
+ expect(
+ store.getState().settings.businessHours?.daysOfWeek
+ ).not.toContain(0);
+ },
+ { timeout: 3000 }
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("settings.workingDaysUpdateError")
+ ).toBeInTheDocument();
+ });
+
+ jest.useRealTimers();
+ });
+ });
+
+ describe("getOpenPaasUserDataAsync integration", () => {
+ it("populates businessHours from core module configurations", async () => {
+ const store = configureStore({
+ reducer: { user: userReducer, settings: settingsReducer },
+ preloadedState: {
+ user: {
+ userData: {
+ sub: "test",
+ email: "test@test.com",
+ family_name: "Doe",
+ name: "John",
+ sid: "mockSid",
+ openpaasId: "667037022b752d0026472254",
+ },
+ organiserData: null,
+ tokens: null,
+ coreConfig: { language: "en", datetime: { timeZone: "UTC" } },
+ alarmEmailsEnabled: null,
+ loading: false,
+ error: null,
+ },
+ settings: {
+ language: "en",
+ timeZone: "UTC",
+ isBrowserDefaultTimeZone: false,
+ view: "calendar",
+ businessHours: null,
+ workingDays: null,
+ },
+ },
+ });
+
+ const mockResponse = {
+ id: "667037022b752d0026472254",
+ firstname: "John",
+ lastname: "Doe",
+ preferredEmail: "test@test.com",
+ configurations: {
+ modules: [
+ {
+ name: "core",
+ configurations: [
+ {
+ name: "businessHours",
+ value: [
+ { start: "8:0", end: "19:0", daysOfWeek: [1, 2, 3, 4, 5] },
+ ],
+ },
+ ],
+ },
+ {
+ name: "linagora.esn.calendar",
+ configurations: [{ name: "workingDays", value: true }],
+ },
+ ],
+ },
+ };
+
+ await store.dispatch(
+ getOpenPaasUserDataAsync.fulfilled(mockResponse, "", undefined)
+ );
+
+ const state = store.getState();
+ expect(state.settings.businessHours).toEqual({
+ start: "8:0",
+ end: "19:0",
+ daysOfWeek: [1, 2, 3, 4, 5],
+ });
+ expect(state.settings.workingDays).toBe(true);
+ });
+
+ it("sets businessHours to null when not present in API response", async () => {
+ const store = configureStore({
+ reducer: { user: userReducer, settings: settingsReducer },
+ preloadedState: {
+ user: {
+ userData: {
+ sub: "test",
+ email: "test@test.com",
+ family_name: "Doe",
+ name: "John",
+ sid: "mockSid",
+ openpaasId: "667037022b752d0026472254",
+ },
+ organiserData: null,
+ tokens: null,
+ coreConfig: { language: "en", datetime: { timeZone: "UTC" } },
+ alarmEmailsEnabled: null,
+ loading: false,
+ error: null,
+ },
+ settings: {
+ language: "en",
+ timeZone: "UTC",
+ isBrowserDefaultTimeZone: false,
+ view: "calendar",
+ businessHours: null,
+ workingDays: null,
+ },
+ },
+ });
+
+ const mockResponse = {
+ id: "667037022b752d0026472254",
+ firstname: "John",
+ lastname: "Doe",
+ preferredEmail: "test@test.com",
+ configurations: {
+ modules: [
+ {
+ name: "core",
+ configurations: [],
+ },
+ {
+ name: "linagora.esn.calendar",
+ configurations: [{ name: "workingDays", value: null }],
+ },
+ ],
+ },
+ };
+
+ await store.dispatch(
+ getOpenPaasUserDataAsync.fulfilled(mockResponse, "", undefined)
+ );
+
+ const state = store.getState();
+ expect(state.settings.businessHours).toBeNull();
+ expect(state.settings.workingDays).toBeNull();
+ });
+ });
+});
diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx
index 1e9f585..4c8b413 100644
--- a/src/components/Calendar/Calendar.tsx
+++ b/src/components/Calendar/Calendar.tsx
@@ -78,9 +78,21 @@ export default function CalendarApp({
const theme = useTheme();
const view = useAppSelector((state) => state.settings.view);
const userData = useAppSelector((state) => state.user.userData);
+ const workingDays = useAppSelector(
+ (state) => state.settings.businessHours?.daysOfWeek
+ );
+ const hideWorkingDays = useAppSelector((state) => state.settings.workingDays);
+
const hideDeclinedEvents = useAppSelector(
(state) => state.settings.hideDeclinedEvents
);
+ const hiddenDays = useMemo(() => {
+ if (!hideWorkingDays || !workingDays || workingDays.length === 0) return [];
+ const validWorkingDays = workingDays.filter((d) => d >= 0 && d <= 6);
+ if (validWorkingDays.length === 0) return [];
+ return [0, 1, 2, 3, 4, 5, 6].filter((d) => !validWorkingDays.includes(d));
+ }, [hideWorkingDays, workingDays]);
+
const calendars = useAppSelector((state) => state.calendars.list);
const isPending = useAppSelector((state) => state.calendars.pending);
const displayWeekNumbers = useAppSelector(
@@ -413,6 +425,7 @@ export default function CalendarApp({
{menubarProps?.isIframe && }
{view === "calendar" && (
{
if (ref) {
calendarRef.current = ref.getApi();
@@ -428,6 +441,7 @@ export default function CalendarApp({
firstDay={1}
editable={true}
locale={localeMap[lang]}
+ hiddenDays={hiddenDays}
selectable={true}
timeZone={timezone}
height={"100%"}
diff --git a/src/components/Event/EventRepeat.tsx b/src/components/Event/EventRepeat.tsx
index fce821a..d72b40e 100644
--- a/src/components/Event/EventRepeat.tsx
+++ b/src/components/Event/EventRepeat.tsx
@@ -23,6 +23,7 @@ import "dayjs/locale/vi";
import { useI18n } from "twake-i18n";
import { ReadOnlyDateField } from "./components/ReadOnlyPickerField";
import { LONG_DATE_FORMAT } from "./utils/dateTimeFormatters";
+import { FC_DAYS, WeekDaySelector } from "./WeekDaySelector";
export default function RepeatEvent({
repetition,
@@ -57,31 +58,6 @@ export default function RepeatEvent({
const defaultEndDate = dayjs(eventStart).add(1, "day").format("YYYY-MM-DD");
- const handleDayChange = (dayCode: string) => {
- const currentDays = repetition.byday || [];
- const updatedDays = currentDays.includes(dayCode)
- ? currentDays.filter((d) => d !== dayCode)
- : [...currentDays, dayCode];
-
- setRepetition({
- ...repetition,
- byday: updatedDays.length > 0 ? updatedDays : null,
- });
- };
-
- const getDayLabel = (day: string) => {
- const dayMap: { [key: string]: string } = {
- MO: t("event.repeat.days.monday"),
- TU: t("event.repeat.days.tuesday"),
- WE: t("event.repeat.days.wednesday"),
- TH: t("event.repeat.days.thursday"),
- FR: t("event.repeat.days.friday"),
- SA: t("event.repeat.days.saturday"),
- SU: t("event.repeat.days.sunday"),
- };
- return dayMap[day] || day;
- };
-
return (
@@ -150,51 +126,21 @@ export default function RepeatEvent({
{/* Weekly selection */}
{repetition.freq === "weekly" && (
-
- {days.map((dayCode) => {
- const isSelected = repetition.byday?.includes(dayCode) ?? false;
- const fullLabel = getDayLabel(dayCode);
- const label = fullLabel.charAt(0);
-
- return (
- {
- if (!isOwn) return;
- handleDayChange(dayCode);
- }}
- sx={{
- width: 40,
- height: 40,
- borderRadius: "4px",
- border: "1px solid",
- borderColor: isSelected ? "primary.main" : "#AEAEC0",
- color: isSelected ? "#fff" : "#8C9CAF",
- fontSize: 16,
- fontWeight: 400,
- lineHeight: "24px",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- textAlign: "center",
- bgcolor: isSelected ? "primary.main" : "transparent",
- cursor: isOwn ? "pointer" : "default",
- "&:hover": isOwn
- ? {
- borderColor: "primary.main",
- bgcolor: "primary.main",
- color: "#fff",
- }
- : undefined,
- }}
- >
- {label}
-
- );
- })}
-
+ FC_DAYS.find((d) => d.ics === ics)?.fc ?? -1)
+ .filter((d) => d !== -1)}
+ onChange={(fcDays) => {
+ const icsDays = fcDays
+ .map((fc) => FC_DAYS.find((d) => d.fc === fc)?.ics ?? "")
+ .filter(Boolean);
+ setRepetition({
+ ...repetition,
+ byday: icsDays.length > 0 ? icsDays : null,
+ });
+ }}
+ disabled={!isOwn}
+ />
)}
diff --git a/src/components/Event/WeekDaySelector.tsx b/src/components/Event/WeekDaySelector.tsx
new file mode 100644
index 0000000..66714c2
--- /dev/null
+++ b/src/components/Event/WeekDaySelector.tsx
@@ -0,0 +1,92 @@
+import { Box } from "@linagora/twake-mui";
+import { useI18n } from "twake-i18n";
+
+interface WeekDaySelectorProps {
+ selectedDays: number[]; // FullCalendar format: 0=Sun, 1=Mon...
+ onChange: (days: number[]) => void;
+ disabled?: boolean;
+}
+
+export const FC_DAYS = [
+ { fc: 1, ics: "MO" },
+ { fc: 2, ics: "TU" },
+ { fc: 3, ics: "WE" },
+ { fc: 4, ics: "TH" },
+ { fc: 5, ics: "FR" },
+ { fc: 6, ics: "SA" },
+ { fc: 0, ics: "SU" },
+];
+
+export function WeekDaySelector({
+ selectedDays,
+ onChange,
+ disabled,
+}: WeekDaySelectorProps) {
+ const { t } = useI18n();
+
+ const getDayLabel = (ics: string) => {
+ const dayMap: Record = {
+ MO: t("event.repeat.days.monday"),
+ TU: t("event.repeat.days.tuesday"),
+ WE: t("event.repeat.days.wednesday"),
+ TH: t("event.repeat.days.thursday"),
+ FR: t("event.repeat.days.friday"),
+ SA: t("event.repeat.days.saturday"),
+ SU: t("event.repeat.days.sunday"),
+ };
+ return dayMap[ics] || ics;
+ };
+
+ const handleToggle = (fcDay: number) => {
+ if (disabled) return;
+ const updated = selectedDays.includes(fcDay)
+ ? selectedDays.filter((d) => d !== fcDay)
+ : [...selectedDays, fcDay];
+ onChange(updated);
+ };
+
+ return (
+
+ {FC_DAYS.map(({ fc, ics }) => {
+ const isSelected = selectedDays.includes(fc);
+ const fullLabel = getDayLabel(ics);
+
+ return (
+ handleToggle(fc)}
+ disabled={disabled}
+ sx={{
+ width: 40,
+ height: 40,
+ borderRadius: "4px",
+ border: "1px solid",
+ borderColor: isSelected ? "primary.main" : "#AEAEC0",
+ color: isSelected ? "#fff" : "#8C9CAF",
+ fontSize: 16,
+ fontWeight: 400,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ bgcolor: isSelected ? "primary.main" : "transparent",
+ cursor: disabled ? "default" : "pointer",
+ "&:hover": !disabled
+ ? {
+ borderColor: "primary.main",
+ bgcolor: "primary.main",
+ color: "#fff",
+ }
+ : undefined,
+ }}
+ >
+ {fullLabel.charAt(0)}
+
+ );
+ })}
+
+ );
+}
diff --git a/src/features/Settings/GeneralSettings.tsx b/src/features/Settings/GeneralSettings.tsx
new file mode 100644
index 0000000..f8a7058
--- /dev/null
+++ b/src/features/Settings/GeneralSettings.tsx
@@ -0,0 +1,339 @@
+import { useAppDispatch, useAppSelector } from "@/app/hooks";
+import { useTimeZoneList } from "@/components/Calendar/TimezoneSelector";
+import { WeekDaySelector } from "@/components/Event/WeekDaySelector";
+import { TimezoneAutocomplete } from "@/components/Timezone/TimezoneAutocomplete";
+import { browserDefaultTimeZone, getTimezoneOffset } from "@/utils/timezone";
+import {
+ Box,
+ FormControl,
+ FormControlLabel,
+ MenuItem,
+ Select,
+ SelectChangeEvent,
+ Switch,
+ Typography,
+} from "@linagora/twake-mui";
+import { useRef, useCallback, useEffect } from "react";
+import { useI18n } from "twake-i18n";
+import {
+ setLanguage as setUserLanguage,
+ setTimezone as setUserTimeZone,
+ updateUserConfigurationsAsync,
+} from "../User/userSlice";
+import { AVAILABLE_LANGUAGES } from "./constants";
+import {
+ BusinessHour,
+ setBusinessHours,
+ setDisplayWeekNumbers,
+ setHideDeclinedEvents,
+ setIsBrowserDefaultTimeZone,
+ setLanguage as setSettingsLanguage,
+ setTimeZone as setSettingsTimeZone,
+ setWorkingDays,
+} from "./SettingsSlice";
+
+interface GeneralSettingsProps {
+ onLanguageError: () => void;
+ onTimeZoneError: () => void;
+ onHideDeclinedEventsError: () => void;
+ onDisplayWeekNumbersError: () => void;
+ onWorkingDaysError: () => void;
+}
+
+export function GeneralSettings({
+ onLanguageError,
+ onTimeZoneError,
+ onHideDeclinedEventsError,
+ onDisplayWeekNumbersError,
+ onWorkingDaysError,
+}: GeneralSettingsProps) {
+ const dispatch = useAppDispatch();
+ const { t } = useI18n();
+
+ const previousConfig = useAppSelector((state) => state.user.coreConfig);
+ const userLanguage = useAppSelector(
+ (state) => state.user?.coreConfig.language
+ );
+ const settingsLanguage = useAppSelector((state) => state.settings?.language);
+ const currentLanguage = userLanguage || settingsLanguage || "en";
+
+ const timezoneList = useTimeZoneList();
+ const userTimeZone = useAppSelector(
+ (state) => state.user?.coreConfig?.datetime?.timeZone
+ );
+ const settingTimeZone = useAppSelector((state) => state.settings?.timeZone);
+ const currentTimeZone =
+ userTimeZone ?? settingTimeZone ?? browserDefaultTimeZone;
+ const isBrowserDefault = useAppSelector(
+ (state) => state.settings.isBrowserDefaultTimeZone
+ );
+
+ const hideDeclinedEvents = useAppSelector(
+ (state) => state.settings?.hideDeclinedEvents
+ );
+ const displayWeekNumbers = useAppSelector(
+ (state) => state.settings?.displayWeekNumbers
+ );
+ const workingDays = useAppSelector((state) => state.settings.workingDays);
+ const businessHours = useAppSelector((state) => state.settings.businessHours);
+ const pendingBusinessHoursRef = useRef(null);
+ const businessHoursTimeoutRef = useRef | null>(
+ null
+ );
+
+ const handleLanguageChange = (event: SelectChangeEvent) => {
+ const newLanguage = event.target.value;
+ const previousLanguage = currentLanguage;
+ dispatch(setUserLanguage(newLanguage));
+ dispatch(setSettingsLanguage(newLanguage));
+ dispatch(updateUserConfigurationsAsync({ language: newLanguage }))
+ .unwrap()
+ .catch(() => {
+ dispatch(setUserLanguage(previousLanguage));
+ dispatch(setSettingsLanguage(previousLanguage));
+ onLanguageError();
+ });
+ };
+
+ const handleTimeZoneChange = (newTimeZone: string) => {
+ const previousTimeZone = currentTimeZone;
+ dispatch(setUserTimeZone(newTimeZone));
+ dispatch(setSettingsTimeZone(newTimeZone));
+ dispatch(
+ updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
+ )
+ .unwrap()
+ .catch(() => {
+ dispatch(setUserTimeZone(previousTimeZone));
+ dispatch(setSettingsTimeZone(previousTimeZone));
+ onTimeZoneError();
+ });
+ };
+
+ const handleTimeZoneDefaultChange = (isDefault: boolean) => {
+ const previousTimeZone = currentTimeZone;
+ dispatch(setIsBrowserDefaultTimeZone(isDefault));
+ if (isDefault) {
+ dispatch(setUserTimeZone(null));
+ dispatch(setSettingsTimeZone(browserDefaultTimeZone));
+ dispatch(
+ updateUserConfigurationsAsync({ timezone: null, previousConfig })
+ )
+ .unwrap()
+ .catch(() => {
+ dispatch(setUserTimeZone(previousTimeZone));
+ dispatch(setSettingsTimeZone(previousTimeZone));
+ dispatch(setIsBrowserDefaultTimeZone(!isDefault));
+ onTimeZoneError();
+ });
+ }
+ };
+
+ const handleHideDeclinedEvents = (value: boolean) => {
+ dispatch(setHideDeclinedEvents(value));
+ dispatch(updateUserConfigurationsAsync({ hideDeclinedEvents: value }))
+ .unwrap()
+ .catch(() => {
+ dispatch(setHideDeclinedEvents(!value));
+ onHideDeclinedEventsError();
+ });
+ };
+
+ const handleDisplayWeekNumbers = (value: boolean) => {
+ dispatch(setDisplayWeekNumbers(value));
+ dispatch(updateUserConfigurationsAsync({ displayWeekNumbers: value }))
+ .unwrap()
+ .catch(() => {
+ dispatch(setDisplayWeekNumbers(!value));
+ onDisplayWeekNumbersError();
+ });
+ };
+
+ const handleBusinessHour = useCallback(
+ ({ days }: { days: number[] }) => {
+ const previousHours = businessHours;
+ const value: BusinessHour | null = businessHours
+ ? { ...businessHours, daysOfWeek: days }
+ : null;
+
+ dispatch(setBusinessHours(value));
+ pendingBusinessHoursRef.current = value;
+
+ if (businessHoursTimeoutRef.current) {
+ clearTimeout(businessHoursTimeoutRef.current);
+ }
+
+ businessHoursTimeoutRef.current = setTimeout(() => {
+ dispatch(
+ updateUserConfigurationsAsync({
+ businessHours: pendingBusinessHoursRef.current,
+ })
+ )
+ .unwrap()
+ .catch(() => {
+ dispatch(setBusinessHours(previousHours));
+ onWorkingDaysError();
+ });
+ }, 500);
+ },
+ [businessHours, dispatch, onWorkingDaysError]
+ );
+
+ useEffect(() => {
+ return () => {
+ if (businessHoursTimeoutRef.current) {
+ clearTimeout(businessHoursTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ const handleWorkingDays = (value: boolean) => {
+ dispatch(setWorkingDays(value));
+ dispatch(updateUserConfigurationsAsync({ workingDays: value }))
+ .unwrap()
+ .catch(() => {
+ dispatch(setWorkingDays(!value));
+ onWorkingDaysError();
+ });
+ };
+
+ return (
+
+ {/* Language */}
+
+
+ {t("settings.language") || "Language"}
+
+
+ {t("settings.languageDescription") ||
+ "This will be the language used in your Twake Calendar"}
+
+
+
+
+
+
+ {/* Timezone */}
+
+
+ {t("settings.timeZone")}
+
+
+
+
+ handleTimeZoneDefaultChange(!isBrowserDefault)
+ }
+ aria-label={t("settings.timeZoneBrowserDefault")}
+ />
+ }
+ label={t("settings.timeZoneBrowserDefault")}
+ labelPlacement="start"
+ sx={{
+ minWidth: 400,
+ justifyContent: "space-between",
+ marginLeft: 0,
+ mb: 2,
+ }}
+ />
+ {!isBrowserDefault && (
+
+ )}
+
+
+
+
+ {/* Working */}
+
+
+ {t("settings.chooseWorkingDays")}
+
+ handleBusinessHour({ days })}
+ />
+
+ handleWorkingDays(!workingDays)}
+ />
+ }
+ label={t("settings.showOnlyWorkingDays")}
+ labelPlacement="start"
+ sx={{
+ minWidth: 400,
+ justifyContent: "space-between",
+ marginLeft: 0,
+ }}
+ />
+
+
+
+ {/* Calendar & Events */}
+
+
+ {t("settings.calAndEvent")}
+
+
+ handleHideDeclinedEvents(!hideDeclinedEvents)}
+ aria-label={t("settings.showDeclinedEvent")}
+ />
+ }
+ label={t("settings.showDeclinedEvent")}
+ labelPlacement="start"
+ sx={{
+ minWidth: 400,
+ justifyContent: "space-between",
+ marginLeft: 0,
+ }}
+ />
+ handleDisplayWeekNumbers(!displayWeekNumbers)}
+ aria-label={t("settings.displayWeekNumbers")}
+ />
+ }
+ label={t("settings.displayWeekNumbers")}
+ labelPlacement="start"
+ sx={{
+ minWidth: 400,
+ justifyContent: "space-between",
+ marginLeft: 0,
+ }}
+ />
+
+
+
+ );
+}
diff --git a/src/features/Settings/NotificationSettings.tsx b/src/features/Settings/NotificationSettings.tsx
new file mode 100644
index 0000000..b2bd96e
--- /dev/null
+++ b/src/features/Settings/NotificationSettings.tsx
@@ -0,0 +1,55 @@
+import { useAppDispatch, useAppSelector } from "@/app/hooks";
+import { FormControlLabel, Switch, Typography, Box } from "@linagora/twake-mui";
+import React from "react";
+import { useI18n } from "twake-i18n";
+import {
+ setAlarmEmails,
+ updateUserConfigurationsAsync,
+} from "../User/userSlice";
+
+interface NotificationsSettingsProps {
+ onAlarmEmailsError: () => void;
+}
+
+export function NotificationsSettings({
+ onAlarmEmailsError,
+}: NotificationsSettingsProps) {
+ const dispatch = useAppDispatch();
+ const { t } = useI18n();
+ const alarmEmailsEnabled = useAppSelector(
+ (state) => state.user?.alarmEmailsEnabled ?? true
+ );
+
+ const handleAlarmEmailsToggle = (
+ event: React.ChangeEvent
+ ) => {
+ const newValue = event.target.checked;
+ const previousValue = alarmEmailsEnabled;
+ dispatch(setAlarmEmails(newValue));
+ dispatch(updateUserConfigurationsAsync({ alarmEmails: newValue }))
+ .unwrap()
+ .catch(() => {
+ dispatch(setAlarmEmails(previousValue));
+ onAlarmEmailsError();
+ });
+ };
+
+ return (
+
+
+ {t("settings.notifications.deliveryMethod")}
+
+
+ }
+ label={t("settings.notifications.email")}
+ labelPlacement="start"
+ sx={{ minWidth: 400, justifyContent: "space-between", marginLeft: 0 }}
+ />
+
+ );
+}
diff --git a/src/features/Settings/SettingsPage.tsx b/src/features/Settings/SettingsPage.tsx
index 71f5898..e338e17 100644
--- a/src/features/Settings/SettingsPage.tsx
+++ b/src/features/Settings/SettingsPage.tsx
@@ -1,45 +1,24 @@
-import { useAppDispatch, useAppSelector } from "@/app/hooks";
-import { useTimeZoneList } from "@/components/Calendar/TimezoneSelector";
-import { TimezoneAutocomplete } from "@/components/Timezone/TimezoneAutocomplete";
-import { browserDefaultTimeZone, getTimezoneOffset } from "@/utils/timezone";
+import { useAppDispatch } from "@/app/hooks";
import {
Box,
- FormControl,
- FormControlLabel,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
- MenuItem,
- Select,
- SelectChangeEvent,
Snackbar,
- Switch,
Tab,
Tabs,
Typography,
} from "@linagora/twake-mui";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import SettingsIcon from "@mui/icons-material/Settings";
-import React, { useState } from "react";
+import { useState } from "react";
import { useI18n } from "twake-i18n";
-import {
- setAlarmEmails,
- setLanguage as setUserLanguage,
- setTimezone as setUserTimeZone,
- updateUserConfigurationsAsync,
-} from "../User/userSlice";
-import { AVAILABLE_LANGUAGES } from "./constants";
+import { GeneralSettings } from "./GeneralSettings";
+import { NotificationsSettings } from "./NotificationSettings";
import "./SettingsPage.styl";
-import {
- setDisplayWeekNumbers,
- setHideDeclinedEvents,
- setIsBrowserDefaultTimeZone,
- setLanguage as setSettingsLanguage,
- setTimeZone as setSettingsTimeZone,
- setView,
-} from "./SettingsSlice";
+import { setView } from "./SettingsSlice";
type SidebarNavItem = "settings" | "sync";
type SettingsSubTab = "settings" | "notifications";
@@ -47,39 +26,12 @@ type SettingsSubTab = "settings" | "notifications";
export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
const dispatch = useAppDispatch();
const { t } = useI18n();
- const previousConfig = useAppSelector((state) => state.user.coreConfig);
- const userLanguage = useAppSelector(
- (state) => state.user?.coreConfig.language
- );
- const settingsLanguage = useAppSelector((state) => state.settings?.language);
- const currentLanguage = userLanguage || settingsLanguage || "en";
-
- const timezoneList = useTimeZoneList();
- const userTimeZone = useAppSelector(
- (state) => state.user?.coreConfig?.datetime?.timeZone
- );
- const settingTimeZone = useAppSelector((state) => state.settings?.timeZone);
- const currentTimeZone =
- userTimeZone ?? settingTimeZone ?? browserDefaultTimeZone;
- const isBrowserDefault = useAppSelector(
- (state) => state.settings.isBrowserDefaultTimeZone
- );
- const alarmEmailsEnabled = useAppSelector(
- (state) => state.user?.alarmEmailsEnabled ?? true
- );
-
- const hideDeclinedEvents = useAppSelector(
- (state) => state.settings?.hideDeclinedEvents
- );
-
- const displayWeekNumbers = useAppSelector(
- (state) => state.settings?.displayWeekNumbers
- );
const [activeNavItem, setActiveNavItem] =
useState("settings");
const [activeSettingsSubTab, setActiveSettingsSubTab] =
useState("settings");
+
const [languageErrorOpen, setLanguageErrorOpen] = useState(false);
const [timeZoneErrorOpen, setTimeZoneErrorOpen] = useState(false);
const [alarmEmailsErrorOpen, setAlarmEmailsErrorOpen] = useState(false);
@@ -87,6 +39,8 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
useState(false);
const [displayWeekNumbersErrorOpen, setDisplayWeekNumbersErrorOpen] =
useState(false);
+ const [workingDaysErrorOpen, setWorkingDaysErrorOpen] = useState(false);
+
const handleBackClick = () => {
dispatch(setView("calendar"));
};
@@ -104,143 +58,18 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
) => {
setActiveSettingsSubTab(newValue);
};
-
- const handleLanguageChange = (event: SelectChangeEvent) => {
- const newLanguage = event.target.value;
- const previousLanguage = currentLanguage;
-
- // Optimistic update - update UI immediately
- dispatch(setUserLanguage(newLanguage));
- dispatch(setSettingsLanguage(newLanguage));
-
- // Call API in background, don't wait for it
- dispatch(updateUserConfigurationsAsync({ language: newLanguage }))
- .unwrap()
- .catch((error) => {
- console.error("Failed to update language:", error);
- // Rollback on error
- dispatch(setUserLanguage(previousLanguage));
- dispatch(setSettingsLanguage(previousLanguage));
- setLanguageErrorOpen(true);
- });
- };
-
const handleLanguageErrorClose = () => {
setLanguageErrorOpen(false);
};
-
- const handleTimeZoneChange = (newTimeZone: string) => {
- const previousTimeZone = currentTimeZone;
-
- // Optimistic update - update UI immediately
- dispatch(setUserTimeZone(newTimeZone));
- dispatch(setSettingsTimeZone(newTimeZone));
-
- // Call API in background, don't wait for it
- dispatch(
- updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
- )
- .unwrap()
- .catch((error) => {
- console.error("Failed to update TimeZone:", error);
- // Rollback on error
- dispatch(setUserTimeZone(previousTimeZone));
- dispatch(setSettingsTimeZone(previousTimeZone));
- setTimeZoneErrorOpen(true);
- });
- };
-
- const handleTimeZoneDefaultChange = (isDefault: boolean) => {
- const previousTimeZone = currentTimeZone;
-
- // Optimistic update - update UI immediately
- dispatch(setIsBrowserDefaultTimeZone(isDefault));
- if (isDefault) {
- dispatch(setUserTimeZone(null));
- dispatch(setSettingsTimeZone(browserDefaultTimeZone));
-
- // Call API in background, don't wait for it
- dispatch(
- updateUserConfigurationsAsync({ timezone: null, previousConfig })
- )
- .unwrap()
- .catch((error) => {
- console.error("Failed to update TimeZone:", error);
- // Rollback on error
- dispatch(setUserTimeZone(previousTimeZone));
- dispatch(setSettingsTimeZone(previousTimeZone));
- dispatch(setIsBrowserDefaultTimeZone(!isDefault));
- setTimeZoneErrorOpen(true);
- });
- }
- };
-
const handleTimeZoneErrorClose = () => {
setTimeZoneErrorOpen(false);
};
-
- const handleHideDeclinedEvents = (doHideDeclinedEvents: boolean) => {
- // Optimistic update - update UI immediately
- dispatch(setHideDeclinedEvents(doHideDeclinedEvents));
-
- // Call API in background, don't wait for it
- dispatch(
- updateUserConfigurationsAsync({
- hideDeclinedEvents: doHideDeclinedEvents,
- })
- )
- .unwrap()
- .catch((error) => {
- console.error("Failed to update hide declined event:", error);
- dispatch(setHideDeclinedEvents(!doHideDeclinedEvents));
- setHideDeclinedEventsErrorOpen(true);
- });
- };
const handleHideDeclinedEventsErrorClose = () => {
setHideDeclinedEventsErrorOpen(false);
};
-
- const handleAlarmEmailsToggle = (
- event: React.ChangeEvent
- ) => {
- const newValue = event.target.checked;
- const previousValue = alarmEmailsEnabled;
-
- // Optimistic update - update UI immediately
- dispatch(setAlarmEmails(newValue));
-
- // Call API in background, don't wait for it
- dispatch(updateUserConfigurationsAsync({ alarmEmails: newValue }))
- .unwrap()
- .catch((error) => {
- console.error("Failed to update alarm emails:", error);
- // Rollback on error
- dispatch(setAlarmEmails(previousValue));
- setAlarmEmailsErrorOpen(true);
- });
- };
-
const handleAlarmEmailsErrorClose = () => {
setAlarmEmailsErrorOpen(false);
};
-
- const handleDisplayWeekNumbers = (doDisplayWeekNumbers: boolean) => {
- // Optimistic update - update UI immediately
- dispatch(setDisplayWeekNumbers(doDisplayWeekNumbers));
-
- // Call API in background, don't wait for it
- dispatch(
- updateUserConfigurationsAsync({
- displayWeekNumbers: doDisplayWeekNumbers,
- })
- )
- .unwrap()
- .catch((error) => {
- console.error("Failed to update the week number setting:", error);
- dispatch(setDisplayWeekNumbers(!doDisplayWeekNumbers));
- setDisplayWeekNumbersErrorOpen(true);
- });
- };
const handleDisplayWeekNumbersErrorClose = () => {
setDisplayWeekNumbersErrorOpen(false);
};
@@ -300,157 +129,33 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
{activeNavItem === "settings" && (
<>
{activeSettingsSubTab === "settings" && (
-
-
-
- {t("settings.language") || "Language"}
-
-
- {t("settings.languageDescription") ||
- "This will be the language used in your Twake Calendar"}
-
-
-
-
-
-
-
- {t("settings.timeZone")}
-
-
-
-
- handleTimeZoneDefaultChange(!isBrowserDefault)
- }
- aria-label={t("settings.timeZoneBrowserDefault")}
- />
- }
- label={t("settings.timeZoneBrowserDefault")}
- labelPlacement="start"
- sx={{
- minWidth: 400,
- justifyContent: "space-between",
- marginLeft: 0,
- mb: 2,
- }}
- />
- {!isBrowserDefault && (
-
- )}
-
-
-
-
-
- {t("settings.calAndEvent")}
-
-
-
- handleHideDeclinedEvents(!hideDeclinedEvents)
- }
- aria-label={t("settings.showDeclinedEvent")}
- />
- }
- label={t("settings.showDeclinedEvent")}
- labelPlacement="start"
- sx={{
- minWidth: 400,
- justifyContent: "space-between",
- marginLeft: 0,
- }}
- />
-
- handleDisplayWeekNumbers(!displayWeekNumbers)
- }
- aria-label={t("settings.displayWeekNumbers")}
- />
- }
- label={t("settings.displayWeekNumbers")}
- labelPlacement="start"
- sx={{
- minWidth: 400,
- justifyContent: "space-between",
- marginLeft: 0,
- }}
- />
-
-
-
+ setLanguageErrorOpen(true)}
+ onTimeZoneError={() => setTimeZoneErrorOpen(true)}
+ onHideDeclinedEventsError={() =>
+ setHideDeclinedEventsErrorOpen(true)
+ }
+ onDisplayWeekNumbersError={() =>
+ setDisplayWeekNumbersErrorOpen(true)
+ }
+ onWorkingDaysError={() => setWorkingDaysErrorOpen(true)}
+ />
)}
{activeSettingsSubTab === "notifications" && (
-
-
- {t("settings.notifications.deliveryMethod") ||
- "Delivery method"}
-
-
- }
- label={t("settings.notifications.email") || "Email"}
- labelPlacement="start"
- sx={{
- minWidth: 400,
- justifyContent: "space-between",
- marginLeft: 0,
- }}
- />
-
+ setAlarmEmailsErrorOpen(true)}
+ />
)}
>
)}
- {activeNavItem === "sync" && (
-
-
- {t("settings.sync.empty") || "Sync settings coming soon"}
-
-
- )}
+ {activeNavItem === "sync" && (
+
+
+ {t("settings.sync.empty") || "Sync settings coming soon"}
+
+
+ )}
-
+ setWorkingDaysErrorOpen(false)}
+ message={t("settings.workingDaysUpdateError")}
+ />
);
}
diff --git a/src/features/Settings/SettingsSlice.ts b/src/features/Settings/SettingsSlice.ts
index 6341ea9..75e2c83 100644
--- a/src/features/Settings/SettingsSlice.ts
+++ b/src/features/Settings/SettingsSlice.ts
@@ -10,8 +10,14 @@ export interface SettingsState {
hideDeclinedEvents: boolean | null;
displayWeekNumbers: boolean;
view: "calendar" | "settings" | "search";
+ businessHours: BusinessHour | null;
+ workingDays: boolean | null;
+}
+export interface BusinessHour {
+ start: string;
+ end: string;
+ daysOfWeek: number[];
}
-
const savedLang = localStorage.getItem("lang");
const defaultLang = savedLang ?? window.LANG ?? "en";
@@ -27,6 +33,8 @@ const initialState: SettingsState = {
hideDeclinedEvents: null,
displayWeekNumbers: true,
view: "calendar",
+ businessHours: null,
+ workingDays: null,
};
export const settingsSlice = createSlice({
@@ -56,6 +64,12 @@ export const settingsSlice = createSlice({
) => {
state.view = action.payload;
},
+ setBusinessHours: (state, action: PayloadAction) => {
+ state.businessHours = action.payload ?? null;
+ },
+ setWorkingDays: (state, action: PayloadAction) => {
+ state.workingDays = action.payload;
+ },
},
extraReducers: (builder) => {
builder.addCase(getOpenPaasUserDataAsync.fulfilled, (state, action) => {
@@ -90,6 +104,23 @@ export const settingsSlice = createSlice({
? hideDeclinedEventsConfig.value
: null;
+ const businessHoursConfig = coreModule?.configurations?.find(
+ (config: ConfigurationItem) => config.name === "businessHours"
+ );
+ state.businessHours =
+ Array.isArray(businessHoursConfig?.value) &&
+ businessHoursConfig?.value?.[0]
+ ? businessHoursConfig.value[0]
+ : null;
+
+ // From esnCalendarModule (alongside hideDeclinedEvents)
+ const workingDaysConfig = esnCalendarModule?.configurations?.find(
+ (config: ConfigurationItem) => config.name === "workingDays"
+ );
+ state.workingDays =
+ typeof workingDaysConfig?.value === "boolean"
+ ? workingDaysConfig.value
+ : null;
const calendarModule = action.payload.configurations?.modules?.find(
(module: ModuleConfiguration) => module.name === "calendar"
);
@@ -112,5 +143,7 @@ export const {
setIsBrowserDefaultTimeZone,
setHideDeclinedEvents,
setDisplayWeekNumbers,
+ setBusinessHours,
+ setWorkingDays,
} = settingsSlice.actions;
export default settingsSlice.reducer;
diff --git a/src/features/User/userAPI.ts b/src/features/User/userAPI.ts
index 6e649c4..f6ed783 100644
--- a/src/features/User/userAPI.ts
+++ b/src/features/User/userAPI.ts
@@ -1,5 +1,6 @@
import { User } from "@/components/Attendees/PeopleSearch";
import { api } from "@/utils/apiUtils";
+import { BusinessHour } from "../Settings/SettingsSlice";
import { OpenPaasUserData } from "./type/OpenPaasUserData";
import {
ConfigurationItem,
@@ -49,6 +50,8 @@ export interface UserConfigurationUpdates {
previousConfig?: Record;
alarmEmails?: boolean;
hideDeclinedEvents?: boolean;
+ workingDays?: boolean;
+ businessHours?: BusinessHour | null;
}
export async function updateUserConfigurations(
@@ -76,22 +79,34 @@ export async function updateUserConfigurations(
},
});
}
+ if (updates.businessHours !== undefined) {
+ coreConfigs.push({
+ name: "businessHours",
+ value: updates.businessHours ? [updates.businessHours] : [],
+ });
+ }
if (updates.alarmEmails !== undefined) {
calendarConfigs.push({
name: "alarmEmails",
value: updates.alarmEmails,
});
}
+ if (updates.displayWeekNumbers !== undefined) {
+ calendarConfigs.push({
+ name: "displayWeekNumbers",
+ value: updates.displayWeekNumbers,
+ });
+ }
if (updates.hideDeclinedEvents !== undefined) {
esnCalendarConfigs.push({
name: "hideDeclinedEvents",
value: updates.hideDeclinedEvents,
});
}
- if (updates.displayWeekNumbers !== undefined) {
- calendarConfigs.push({
- name: "displayWeekNumbers",
- value: updates.displayWeekNumbers,
+ if (updates.workingDays !== undefined) {
+ esnCalendarConfigs.push({
+ name: "workingDays",
+ value: updates.workingDays,
});
}
diff --git a/src/locales/en.json b/src/locales/en.json
index 8111161..38c9027 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -257,7 +257,10 @@
"calAndEvent": "Calendar and events",
"showDeclinedEvent": "Show declined events",
"hideDeclinedEventsUpdateError": "Failed to update hide declined event.",
- "back": "Back to calendar"
+ "back": "Back to calendar",
+ "chooseWorkingDays": "Choose your working days",
+ "showOnlyWorkingDays": "Show only working days",
+ "workingDaysUpdateError": "Failed to update working days setting"
},
"eventPreview": {
"emailAttendees": "Email attendees",
diff --git a/src/locales/fr.json b/src/locales/fr.json
index faab80c..9bbe057 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -258,7 +258,10 @@
"calAndEvent": "Calendrier et événements",
"showDeclinedEvent": "Afficher les événements déclinés",
"hideDeclinedEventsUpdateError": "Échec de la mise à jour du masquage des événements déclinés.",
- "back": "Retour au calendrier"
+ "back": "Retour au calendrier",
+ "chooseWorkingDays": "Choisissez vos jours ouvrés",
+ "showOnlyWorkingDays": "Afficher uniquement les jours sélectionnés",
+ "workingDaysUpdateError": "Échec de la mise à jour des jours ouvrés"
},
"eventPreview": {
"emailAttendees": "Envoyer un e-mail aux participants",
diff --git a/src/locales/ru.json b/src/locales/ru.json
index 69b86f6..ef63987 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -258,7 +258,10 @@
"calAndEvent": "Календарь и мероприятия",
"showDeclinedEvent": "Показать отклонённые мероприятия",
"hideDeclinedEventsUpdateError": "Не удалось обновить скрытие отклонённых событий.",
- "back": "Вернуться к календарю"
+ "back": "Вернуться к календарю",
+ "chooseWorkingDays": "Выберите ваши рабочие дни",
+ "showOnlyWorkingDays": "Показывать только рабочие дни",
+ "workingDaysUpdateError": "Не удалось обновить настройку рабочих дней"
},
"eventPreview": {
"emailAttendees": "Написать участникам",
diff --git a/src/locales/vi.json b/src/locales/vi.json
index 2a92d53..1fb7a2e 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -256,7 +256,10 @@
"calAndEvent": "Lịch và sự kiện",
"showDeclinedEvent": "Hiển thị các sự kiện đã từ chối",
"hideDeclinedEventsUpdateError": "Không cập nhật được các sự kiện ẩn bị từ chối.",
- "back": "Quay lại lịch"
+ "back": "Quay lại lịch",
+ "chooseWorkingDays": "Chọn ngày làm việc của bạn",
+ "showOnlyWorkingDays": "Chỉ hiển thị ngày làm việc",
+ "workingDaysUpdateError": "Không thể cập nhật cài đặt ngày làm việc"
},
"eventPreview": {
"emailAttendees": "Gửi email cho người tham gia",