Skip to content

Commit 5b2c117

Browse files
authored
Make startOfWeek setting work properly alongside different locales (#1433)
* Fix startOfWeek user setting not being respected * Update WeekPicker computed locales after language change
1 parent c1937e3 commit 5b2c117

File tree

8 files changed

+118
-23
lines changed

8 files changed

+118
-23
lines changed

frontend/src/keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type DayjsType = ((_?: ConfigType) => Dayjs) & {
1414
max: (...dayjs: Dayjs[]) => Dayjs | null,
1515
min: (...dayjs: Dayjs[]) => Dayjs | null,
1616
duration: CreateDurationType,
17+
locale: (preset?: string) => string,
1718
} & ((objToParse: any, format: string) => Dayjs);
1819
export const dayjsKey = Symbol('dayjs') as InjectionKey<DayjsType>;
1920
export const isoWeekdaysKey = Symbol('isoWeekdays') as InjectionKey<IsoWeekday[]>;

frontend/src/utils.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,46 @@ import {
1717
} from '@/models';
1818
import { BookingStatus } from './definitions';
1919

20+
/**
21+
* Convert ISO weekday (1=Monday, 7=Sunday) to dayjs weekday (0=Sunday, 1=Monday, etc.)
22+
* @param isoDay - ISO weekday number (1-7)
23+
* @returns dayjs weekday number (0-6)
24+
*/
25+
export const isoWeekdayToDayjs = (isoDay: number): number => {
26+
return isoDay === 7 ? 0 : isoDay;
27+
};
28+
29+
/**
30+
* Calculate the start of week for a given date based on user's preferred start of week.
31+
* @param date - Dayjs date object
32+
* @param startOfWeekIso - ISO weekday format: 1=Monday, 2=Tuesday, ..., 7=Sunday
33+
* @returns Dayjs object representing the start of the week
34+
*/
35+
export const getStartOfWeek = (date: Dayjs, startOfWeekIso: number): Dayjs => {
36+
// Convert ISO format (1=Monday, 7=Sunday) to dayjs format (0=Sunday, 1=Monday)
37+
const startOfWeekDayjs = isoWeekdayToDayjs(startOfWeekIso);
38+
39+
// Get current day of week (0=Sunday, 1=Monday, ..., 6=Saturday)
40+
const currentDay = date.day();
41+
42+
// Calculate the difference to get to the start of week
43+
const diff = (currentDay - startOfWeekDayjs + 7) % 7;
44+
45+
return date.subtract(diff, 'day').startOf('day');
46+
};
47+
48+
/**
49+
* Calculate the end of week for a given date based on user's preferred start of week.
50+
* @param date - Dayjs date object
51+
* @param startOfWeekIso - ISO weekday format: 1=Monday, 2=Tuesday, ..., 7=Sunday
52+
* @returns Dayjs object representing the end of the week
53+
*/
54+
export const getEndOfWeek = (date: Dayjs, startOfWeekIso: number): Dayjs => {
55+
// Get the start of week and add 6 days to get the end of week
56+
const weekStart = getStartOfWeek(date, startOfWeekIso);
57+
return weekStart.add(6, 'day').endOf('day');
58+
};
59+
2060
/**
2161
* Lowercases the first character of a string
2262
*/
@@ -457,4 +497,7 @@ export default {
457497
compareAvailabilityStart,
458498
deepClone,
459499
isUnconfirmed,
500+
isoWeekdayToDayjs,
501+
getStartOfWeek,
502+
getEndOfWeek,
460503
};

frontend/src/views/BookerView/components/BookingViewSlotSelection.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import { PhGlobe } from '@phosphor-icons/vue';
77
88
import { useBookingViewStore } from '@/stores/booking-view-store';
99
import { useCalendarStore } from '@/stores/calendar-store';
10+
import { useUserStore } from '@/stores/user-store';
1011
import { Slot, TimeFormatted } from '@/models';
1112
import { dayjsKey } from '@/keys';
13+
import { getStartOfWeek, getEndOfWeek } from '@/utils';
1214
1315
import WeekPicker from '@/views/DashboardView/components/WeekPicker.vue';
1416
import WeekCalendar from '@/views/DashboardView/components/WeekCalendar.vue';
@@ -17,14 +19,18 @@ import SlotSelectionHeader from './SlotSelectionHeader.vue';
1719
1820
const { t } = useI18n();
1921
const calendarStore = useCalendarStore();
22+
const userStore = useUserStore();
2023
const { appointment, activeDate, selectedEvent } = storeToRefs(useBookingViewStore());
2124
const dj = inject(dayjsKey);
2225
2326
// current selected date, defaults to now
24-
const activeDateRange = computed(() => ({
25-
start: activeDate.value.startOf('week').format('YYYY-MM-DD'),
26-
end: activeDate.value.endOf('week').format('YYYY-MM-DD'),
27-
}));
27+
const activeDateRange = computed(() => {
28+
const startOfWeek = userStore.data.settings.startOfWeek ?? 7;
29+
return {
30+
start: getStartOfWeek(activeDate.value, startOfWeek).format('YYYY-MM-DD'),
31+
end: getEndOfWeek(activeDate.value, startOfWeek).format('YYYY-MM-DD'),
32+
};
33+
});
2834
2935
const timezone = computed(() => dj.tz.guess());
3036

frontend/src/views/DashboardView/components/WeekCalendar.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ const shortestDuration = computed(() => {
174174
const weekdays = computed(() => {
175175
const { start, end } = props.activeDateRange;
176176
177+
// Access language setting to trigger recomputation when locale changes
178+
// The actual locale is set globally on dayjs, but we need this reactive dependency
179+
void userStore.data.settings.language;
180+
177181
// Parse the start and end dates using dayjs's automatic parsing
178182
const startDate = dj(start);
179183
const endDate = dj(end);

frontend/src/views/DashboardView/components/WeekPicker.vue

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import { useI18n } from 'vue-i18n';
44
import { dayjsKey } from '@/keys';
55
import { PhCaretLeft, PhCaretRight } from '@phosphor-icons/vue';
66
import { TimeFormatted } from '@/models';
7+
import { useUserStore } from '@/stores/user-store';
8+
import { getStartOfWeek, getEndOfWeek } from '@/utils';
79
810
const dj = inject(dayjsKey);
11+
const userStore = useUserStore();
912
1013
const props = defineProps<{
1114
onDateChange: (dateObj: TimeFormatted) => void,
@@ -21,24 +24,34 @@ const { t } = useI18n();
2124
const dateInputRef = ref<HTMLInputElement>();
2225
2326
// Computed values for accessibility
27+
// Access language setting to trigger recomputation when locale changes
2428
const currentWeekLabel = computed(() => {
29+
void userStore.data.settings.language;
2530
const startDate = dj(props.activeDateRange.start).format('L');
2631
const endDate = dj(props.activeDateRange.end).format('L');
2732
return startDate + '' + endDate;
2833
});
2934
3035
const previousWeekLabel = computed(() => {
36+
void userStore.data.settings.language;
3137
const prevStart = dj(props.activeDateRange.start).subtract(7, 'day').format('L');
3238
const prevEnd = dj(props.activeDateRange.start).subtract(1, 'day').format('L');
3339
return t('label.previousWeek') + ': ' + prevStart + '' + prevEnd;
3440
});
3541
3642
const nextWeekLabel = computed(() => {
43+
void userStore.data.settings.language;
3744
const nextStart = dj(props.activeDateRange.end).add(1, 'day').format('L');
3845
const nextEnd = dj(props.activeDateRange.end).add(7, 'day').format('L');
3946
return t('label.nextWeek') + ': ' + nextStart + '' + nextEnd;
4047
});
4148
49+
// Week picker button display text
50+
const weekPickerButtonText = computed(() => {
51+
void userStore.data.settings.language;
52+
return `${dj(props.activeDateRange.start).format('MMMM DD')} – ${dj(props.activeDateRange.end).format('MMMM DD')}`;
53+
});
54+
4255
function onPreviousWeekButtonClicked() {
4356
props.onDateChange({
4457
start: dj(props.activeDateRange.start).subtract(7, 'day').toString(),
@@ -63,12 +76,13 @@ function onDateSelected(event: Event) {
6376
6477
if (target.value) {
6578
const selectedDate = dj(target.value);
66-
const startOfWeek = selectedDate.startOf('week');
67-
const endOfWeek = selectedDate.endOf('week');
79+
const startOfWeekSetting = userStore.data.settings.startOfWeek ?? 7;
80+
const weekStart = getStartOfWeek(selectedDate, startOfWeekSetting);
81+
const weekEnd = getEndOfWeek(selectedDate, startOfWeekSetting);
6882
6983
props.onDateChange({
70-
start: startOfWeek.format('YYYY-MM-DD'),
71-
end: endOfWeek.format('YYYY-MM-DD'),
84+
start: weekStart.format('YYYY-MM-DD'),
85+
end: weekEnd.format('YYYY-MM-DD'),
7286
});
7387
}
7488
}
@@ -116,7 +130,7 @@ function onKeyDown(event: KeyboardEvent) {
116130
:aria-label="t('label.selectWeek') + ': ' + currentWeekLabel"
117131
:title="t('label.selectWeek')"
118132
>
119-
{{ dj(activeDateRange.start).format('MMMM DD') }} – {{ dj(activeDateRange.end).format('MMMM DD') }}
133+
{{ weekPickerButtonText }}
120134
</button>
121135

122136
<button

frontend/src/views/DashboardView/index.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useRoute } from 'vue-router';
66
import { storeToRefs } from 'pinia';
77
import { dayjsKey, refreshKey } from '@/keys';
88
import { TimeFormatted } from '@/models';
9+
import { getStartOfWeek, getEndOfWeek } from '@/utils';
910
import QuickActionsSideBar from './components/QuickActionsSideBar.vue';
1011
import WeekPicker from './components/WeekPicker.vue';
1112
import UserCalendarSync from './components/UserCalendarSync.vue';
@@ -14,22 +15,27 @@ import WeekCalendar from './components/WeekCalendar.vue';
1415
// stores
1516
import { useCalendarStore } from '@/stores/calendar-store';
1617
import { useAppointmentStore } from '@/stores/appointment-store';
18+
import { useUserStore } from '@/stores/user-store';
1719
1820
const route = useRoute();
1921
const dj = inject(dayjsKey);
2022
const refresh = inject(refreshKey);
2123
2224
const calendarStore = useCalendarStore();
2325
const appointmentStore = useAppointmentStore();
26+
const userStore = useUserStore();
2427
const { remoteEvents } = storeToRefs(calendarStore);
2528
const { pendingAppointments } = storeToRefs(appointmentStore);
2629
2730
// current selected date, defaults to now
2831
const activeDate = ref(dj());
29-
const activeDateRange = computed(() => ({
30-
start: activeDate.value.startOf('week').format('YYYY-MM-DD'),
31-
end: activeDate.value.endOf('week').format('YYYY-MM-DD'),
32-
}));
32+
const activeDateRange = computed(() => {
33+
const startOfWeek = userStore.data.settings.startOfWeek ?? 7;
34+
return {
35+
start: getStartOfWeek(activeDate.value, startOfWeek).format('YYYY-MM-DD'),
36+
end: getEndOfWeek(activeDate.value, startOfWeek).format('YYYY-MM-DD'),
37+
};
38+
});
3339
3440
async function onDateChange(dateObj: TimeFormatted) {
3541
const start = dj(dateObj.start);

frontend/src/views/SettingsView/components/Preferences.vue

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
<script setup lang="ts">
22
import { computed, inject } from 'vue';
33
import { useI18n } from 'vue-i18n';
4-
import { isoWeekdaysKey } from '@/keys';
4+
import { dayjsKey } from '@/keys';
55
import { ColourSchemes } from '@/definitions';
66
import { BubbleSelect, SegmentedControl, SelectInput } from '@thunderbirdops/services-ui';
77
import { useSettingsStore } from '@/stores/settings-store';
8+
import { useUserStore } from '@/stores/user-store';
89
import { storeToRefs } from 'pinia';
910
1011
const { t, availableLocales } = useI18n();
11-
const isoWeekdays = inject(isoWeekdaysKey);
12+
const dj = inject(dayjsKey);
13+
const userStore = useUserStore();
1214
1315
const settingsStore = useSettingsStore();
1416
const { currentState } = storeToRefs(settingsStore);
@@ -71,13 +73,28 @@ const timeFormat = computed({
7173
})
7274
7375
// Start of Week
74-
// TODO: As long as we use Qalendar, we can only support Sunday and Monday as start of week
75-
const availableStartOfTheWeekOptions = computed(
76-
() => isoWeekdays.filter((day) => [7,1].includes(day.iso)).map((e) => ({
77-
label: e.short,
78-
value: e.iso,
79-
}))
80-
);
76+
// Generate options dynamically using dayjs to respect current locale
77+
const availableStartOfTheWeekOptions = computed(() => {
78+
// Access language to trigger recomputation when locale changes
79+
void userStore.data.settings.language;
80+
81+
// ISO weekday values: 1=Monday, 2=Tuesday, ..., 7=Sunday
82+
// dayjs weekday values: 0=Sunday, 1=Monday, ..., 6=Saturday
83+
const allDays = [
84+
{ iso: 7, dayjsDay: 0 }, // Sunday
85+
{ iso: 1, dayjsDay: 1 }, // Monday
86+
{ iso: 2, dayjsDay: 2 }, // Tuesday
87+
{ iso: 3, dayjsDay: 3 }, // Wednesday
88+
{ iso: 4, dayjsDay: 4 }, // Thursday
89+
{ iso: 5, dayjsDay: 5 }, // Friday
90+
{ iso: 6, dayjsDay: 6 }, // Saturday
91+
];
92+
93+
return allDays.map((day) => ({
94+
label: dj().day(day.dayjsDay).format('ddd'),
95+
value: day.iso,
96+
}));
97+
});
8198
8299
const startOfWeek = computed({
83100
get: () => {

frontend/src/views/SettingsView/index.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useI18n } from 'vue-i18n';
77
import { storeToRefs } from 'pinia';
88
import { PrimaryButton, LinkButton, NoticeBar, NoticeBarTypes, IconButton } from '@thunderbirdops/services-ui';
99
import { enumToObject } from '@/utils';
10-
import { callKey } from '@/keys';
10+
import { callKey, dayjsKey } from '@/keys';
1111
import { SettingsSections, ColourSchemes } from '@/definitions';
1212
import { Alert, SubscriberResponse } from '@/models';
1313
import { useUserStore } from '@/stores/user-store';
@@ -23,6 +23,7 @@ import ConnectedApplications from './components/ConnectedApplications.vue';
2323
2424
// component constants
2525
const call = inject(callKey);
26+
const dj = inject(dayjsKey);
2627
const { t, locale } = useI18n({ useScope: 'global' });
2728
const route = useRoute();
2829
const router = useRouter();
@@ -116,6 +117,9 @@ async function updatePreferences() {
116117
// Update i18n locale to change language on the page without page refresh
117118
locale.value = currentState.value.language;
118119
120+
// Update dayjs locale to match the new language
121+
dj.locale(currentState.value.language);
122+
119123
// Update the userStore internal state with fresh backend values
120124
await userStore.profile();
121125
}

0 commit comments

Comments
 (0)