Skip to content

Commit b2cae91

Browse files
Internal: Implements timezone handling based on user and platform configuration - refs BT#22706
1 parent 7ad7058 commit b2cae91

File tree

7 files changed

+80
-38
lines changed

7 files changed

+80
-38
lines changed

assets/vue/composables/formatDate.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import { DateTime } from "luxon"
22
import { useLocale } from "./locale"
3+
import { usePlatformConfig } from "../store/platformConfig"
4+
import { useSecurityStore } from "../store/securityStore"
35

46
export function useFormatDate() {
57
const { appParentLocale } = useLocale()
8+
const platformConfigStore = usePlatformConfig()
9+
const securityStore = useSecurityStore()
10+
11+
function getCurrentTimezone() {
12+
const allowUserTimezone = platformConfigStore.getSetting("profile.use_users_timezone") === "true"
13+
const userTimezone = securityStore.user?.timezone
14+
const platformTimezone = platformConfigStore.getSetting("platform.timezone")
15+
16+
if (allowUserTimezone && userTimezone) {
17+
return userTimezone
18+
}
19+
20+
if (platformTimezone && platformTimezone !== "false") {
21+
return platformTimezone
22+
}
23+
24+
return Intl.DateTimeFormat().resolvedOptions().timeZone
25+
}
626

7-
/**
8-
* @param {Date|string} datetime
9-
* @returns {DateTime|null}
10-
*/
1127
function getDateTimeObject(datetime) {
1228
if (!datetime) {
1329
return null
@@ -16,16 +32,18 @@ export function useFormatDate() {
1632
let dt
1733

1834
if (typeof datetime === "string") {
19-
dt = DateTime.fromISO(datetime)
20-
} else if (typeof datetime === "object") {
21-
dt = DateTime.fromJSDate(datetime)
35+
dt = DateTime.fromISO(datetime, { zone: "utc" })
36+
} else if (datetime instanceof Date) {
37+
dt = DateTime.fromJSDate(datetime, { zone: "utc" })
38+
} else {
39+
return null
2240
}
2341

2442
if (!dt.isValid) {
2543
return null
2644
}
2745

28-
return dt.setLocale(appParentLocale.value)
46+
return dt.setZone(getCurrentTimezone()).setLocale(appParentLocale.value)
2947
}
3048

3149
const abbreviatedDatetime = (datetime) =>
@@ -34,10 +52,12 @@ export function useFormatDate() {
3452
month: "long",
3553
})
3654

37-
const relativeDatetime = (datetime) => getDateTimeObject(datetime)?.toRelative()
55+
const relativeDatetime = (datetime) =>
56+
getDateTimeObject(datetime)?.toRelative()
3857

3958
return {
4059
abbreviatedDatetime,
4160
relativeDatetime,
61+
getCurrentTimezone,
4262
}
4363
}

assets/vue/utils/dates.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1-
const { DateTime } = require("luxon")
1+
import { DateTime } from "luxon"
2+
import { useFormatDate } from "../composables/formatDate"
23

3-
const formatDateTime = function (date) {
4-
if (!date) return null
4+
const { getCurrentTimezone } = useFormatDate()
55

6-
return DateTime(date).format("DD/MM/YYYY")
6+
/**
7+
* Format a JS Date object to string using the current or provided timezone.
8+
* @param {Date} date - JavaScript Date object
9+
* @param {string} [timezone] - Optional timezone (e.g. "America/Lima")
10+
* @returns {string}
11+
*/
12+
const formatDateTime = function (date, timezone) {
13+
if (!date) return ""
14+
const tz = timezone || getCurrentTimezone()
15+
return DateTime.fromJSDate(date, { zone: "utc" }).setZone(tz).toFormat("dd/LL/yyyy HH:mm")
716
}
817

9-
const formatDateTimeFromISO = function (dateStr) {
18+
/**
19+
* Format an ISO string to readable string using the current or provided timezone.
20+
* @param {string} dateStr - ISO date string (e.g. "2025-06-17T14:00:00Z")
21+
* @param {string} [timezone] - Optional timezone
22+
* @returns {string}
23+
*/
24+
const formatDateTimeFromISO = function (dateStr, timezone) {
1025
if (!dateStr) return ""
11-
12-
return DateTime.fromISO(dateStr).toFormat("dd/LL/yyyy HH:mm")
26+
const tz = timezone || getCurrentTimezone()
27+
return DateTime.fromISO(dateStr, { zone: "utc" }).setZone(tz).toFormat("dd/LL/yyyy HH:mm")
1328
}
1429

1530
export { formatDateTime, formatDateTimeFromISO }

assets/vue/views/ccalendarevent/CCalendarEventList.vue

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,15 @@ import { useCalendarEvent } from "../../composables/calendar/calendarEvent"
150150
import resourceLinkService from "../../services/resourceLinkService"
151151
import { useSecurityStore } from "../../store/securityStore"
152152
import { useCourseSettings } from "../../store/courseSettingStore"
153+
import { DateTime } from "luxon"
153154
154155
const store = useStore()
155156
const securityStore = useSecurityStore()
156157
const confirm = useConfirm()
157158
const cidReqStore = useCidReqStore()
158159
159160
const { course, session, group } = storeToRefs(cidReqStore)
160-
161-
const { abbreviatedDatetime } = useFormatDate()
162-
161+
const { abbreviatedDatetime, getCurrentTimezone } = useFormatDate()
163162
const { showAddButton } = useCalendarActionButtons()
164163
165164
const { isEditableByUser, allowSubscribeToEvent, allowUnsubscribeToEvent } = useCalendarEvent()
@@ -235,13 +234,15 @@ async function getCalendarEvents({ start, end }) {
235234
const calendarEvents = await cCalendarEventService.findAll({ params }).then((response) => response.json())
236235
237236
return calendarEvents["hydra:member"].map((event) => {
238-
let color = event.color || "#007BFF"
237+
const timezone = getCurrentTimezone()
238+
const start = DateTime.fromISO(event.startDate, { zone: "utc" }).setZone(timezone)
239+
const end = DateTime.fromISO(event.endDate, { zone: "utc" }).setZone(timezone)
239240
240241
return {
241242
...event,
242-
start: event.startDate,
243-
end: event.endDate,
244-
color,
243+
start: start.toString(),
244+
end: end.toString(),
245+
color: event.color || "#007BFF",
245246
}
246247
})
247248
}
@@ -257,16 +258,9 @@ const showAddEventDialog = () => {
257258
258259
dialog.value = true
259260
}
260-
261-
const goToMyStudentsSchedule = () => {
262-
window.location.href = "/main/calendar/planification.php"
263-
}
264-
265-
const goToSessionPanning = () => {
266-
window.location.href = "/main/my_space/calendar_plan.php"
267-
}
268-
261+
const timezone = getCurrentTimezone()
269262
const calendarOptions = ref({
263+
timeZone: timezone,
270264
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
271265
locales: allLocales,
272266
locale: calendarLocale?.code ?? "en-GB",

public/main/inc/lib/extra_field.lib.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class ExtraField extends Model
2020
public const FIELD_TYPE_DOUBLE_SELECT = 8;
2121
public const FIELD_TYPE_DIVIDER = 9;
2222
public const FIELD_TYPE_TAG = 10;
23+
public const FIELD_TYPE_TIMEZONE = 11;
2324
public const FIELD_TYPE_SOCIAL_PROFILE = 12;
2425
public const FIELD_TYPE_CHECKBOX = 13;
2526
public const FIELD_TYPE_MOBILE_PHONE_NUMBER = 14;
@@ -1572,6 +1573,16 @@ public function set_extra_fields_in_form(
15721573
}
15731574
}
15741575

1576+
break;
1577+
case self::FIELD_TYPE_TIMEZONE:
1578+
$form->addSelect(
1579+
'extra_'.$variable,
1580+
$field_details['display_text'],
1581+
api_get_timezones(),
1582+
);
1583+
if ($freezeElement) {
1584+
$form->freeze('extra_'.$variable);
1585+
}
15751586
break;
15761587
case self::FIELD_TYPE_SOCIAL_PROFILE:
15771588
// get the social network's favicon
@@ -1984,6 +1995,7 @@ public static function get_extra_fields_by_handler($handler)
19841995
$types[self::FIELD_TYPE_DOUBLE_SELECT] = get_lang('Double select');
19851996
$types[self::FIELD_TYPE_DIVIDER] = get_lang('Visual divider');
19861997
$types[self::FIELD_TYPE_TAG] = get_lang('User tag');
1998+
$types[self::FIELD_TYPE_TIMEZONE] = get_lang('Timezone');
19871999
$types[self::FIELD_TYPE_SOCIAL_PROFILE] = get_lang('Social network link');
19882000
$types[self::FIELD_TYPE_MOBILE_PHONE_NUMBER] = get_lang('Mobile phone number');
19892001
$types[self::FIELD_TYPE_CHECKBOX] = get_lang('Checkbox');

src/CoreBundle/Controller/PlatformConfigurationController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public function list(SettingsManager $settingsManager): Response
112112
'course.show_courses_descriptions_in_catalog',
113113
'session.session_automatic_creation_user_id',
114114
'session.session_list_view_remaining_days',
115+
'profile.use_users_timezone',
115116
];
116117

117118
$user = $this->userHelper->getCurrent();

src/CoreBundle/Controller/SocialController.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -552,13 +552,12 @@ private function getExtraFieldBlock(
552552
ExtraField::USER_FIELD_TYPE
553553
);
554554
if (!empty($extraFieldOptions)) {
555-
$fieldValue = implode(
556-
', ',
557-
array_map(
558-
fn (ExtraFieldOptions $opt) => $opt->getDisplayText(),
559-
$extraFieldOptions
560-
)
555+
$optionTexts = array_map(
556+
fn (ExtraFieldOptions $option) => $option['display_text'],
557+
$extraFieldOptions
561558
);
559+
$fieldValue = implode(', ', $optionTexts);
560+
$fieldValue = $extraFieldOptions->getDisplayText();
562561
}
563562

564563
break;

src/CoreBundle/Form/ExtraFieldType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
9393
switch ($extraField->getValueType()) {
9494
case \ExtraField::FIELD_TYPE_DOUBLE_SELECT:
9595
case \ExtraField::FIELD_TYPE_DIVIDER:
96+
case \ExtraField::FIELD_TYPE_TIMEZONE:
9697
case \ExtraField::FIELD_TYPE_FILE_IMAGE:
9798
case \ExtraField::FIELD_TYPE_FILE:
9899
case \ExtraField::FIELD_TYPE_LETTERS_SPACE:

0 commit comments

Comments
 (0)