Skip to content

Commit 4a02f82

Browse files
authored
Merge branch 'develop' into fix/issue-1352‑not_found_on_plus_icon
2 parents d068b16 + a3cc88c commit 4a02f82

6 files changed

Lines changed: 138 additions & 50 deletions

File tree

Web/css/schedule.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,10 @@ ul.jqtree-tree .jqtree-toggler {
348348
top: 10%;
349349
}
350350

351-
#make_default {
352-
padding-right: 10px;
351+
.schedule-style.active {
352+
border-bottom: 2px solid var(--primary);
353+
padding-bottom: 6px;
354+
margin-bottom: 2px;
353355
}
354356

355357
.table-cell-wrapper {

Web/scripts/date-helper.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,41 @@ var dateHelper = (function () {
4646
};
4747
}
4848

49+
// Pads a number to 2 digits with leading zero
50+
const pad = (n) => n.toString().padStart(2, '0');
51+
52+
/**
53+
* Formats a Date object as 'YYYY-MM-DD' or 'YYYY-MM-DD HH:mm'.
54+
*
55+
* By default, uses local time components (getFullYear, getMonth, getDate, etc.).
56+
* Set useUtc=true to use UTC components (getUTCFullYear, ...).
57+
*
58+
* WARNING: For Date objects created from 'YYYY-MM-DD' (e.g., via parseYMDDate),
59+
* using toISOString() or UTC methods will shift the day if your local timezone is behind UTC.
60+
* Always use the default (local) mode for calendar and reservation logic unless you explicitly need UTC.
61+
*
62+
* Examples:
63+
* formatDate(new Date(2026, 3, 22)) // '2026-04-22' (local)
64+
* formatDate(new Date(2026, 3, 22, 15, 30), true) // '2026-04-22 15:30' (local)
65+
* formatDate(new Date(Date.UTC(2026, 3, 22)), false, true) // '2026-04-22' (UTC)
66+
*
67+
* @param {Date} date
68+
* @param {boolean} withTime - If true, includes ' HH:mm'
69+
* @param {boolean} useUtc - If true, uses UTC components (default: false)
70+
* @returns {string}
71+
*/
72+
function formatDate(date, withTime = false, useUtc = false) {
73+
if (!(date instanceof Date) || isNaN(date)) return '';
74+
const y = useUtc ? date.getUTCFullYear() : date.getFullYear();
75+
const m = useUtc ? date.getUTCMonth() + 1 : date.getMonth() + 1;
76+
const d = useUtc ? date.getUTCDate() : date.getDate();
77+
const ymd = y + '-' + pad(m) + '-' + pad(d);
78+
if (!withTime) return ymd;
79+
const hh = useUtc ? date.getUTCHours() : date.getHours();
80+
const mm = useUtc ? date.getUTCMinutes() : date.getMinutes();
81+
return ymd + ' ' + pad(hh) + ':' + pad(mm);
82+
}
83+
4984
/**
5085
* Parses a time string into a Date object (today's date).
5186
*
@@ -93,7 +128,6 @@ var dateHelper = (function () {
93128
* text: string // Human-readable representation according to format
94129
* }}
95130
*/
96-
const pad = (n) => n.toString().padStart(2, '0');
97131

98132
function formatTime(hour24, minute, format) {
99133
const value = `${pad(hour24)}:${pad(minute)}`; // internal value always 24h
@@ -191,7 +225,26 @@ var dateHelper = (function () {
191225
}
192226
}
193227

228+
/**
229+
* Parses a 'YYYY-MM-DD' string into a Date object (local time).
230+
*
231+
* @param {string} ymd - Date string in 'YYYY-MM-DD' format
232+
* @returns {Date|null}
233+
*/
234+
function parseYMDDate(ymd) {
235+
if (!ymd || typeof ymd !== 'string') return null;
236+
var parts = ymd.split('-');
237+
if (parts.length !== 3) return null;
238+
var year = parseInt(parts[0], 10);
239+
var month = parseInt(parts[1], 10) - 1;
240+
var day = parseInt(parts[2], 10);
241+
if (isNaN(year) || isNaN(month) || isNaN(day)) return null;
242+
return new Date(year, month, day);
243+
}
244+
194245
return {
246+
formatDate,
247+
parseYMDDate,
195248
MoreThanOneDayBetweenBeginAndEnd: function (beginDateElement, beginTimeElement, endDateElement, endTimeElement) {
196249
var begin = this.GetDate(beginDateElement, beginTimeElement);
197250
var end = this.GetDate(endDateElement, endTimeElement);

Web/scripts/reservation.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function Reservation(opts) {
8585

8686
Reservation.prototype.init = function (ownerId, startDateString, endDateString) {
8787
_ownerId = ownerId;
88-
_startDate = moment(startDateString, 'YYYY-MM-DD HH:mm');
88+
_startDate = new Date(startDateString.replace(' ', 'T'));
8989
participation.addedUsers.push(ownerId);
9090

9191
SetUpAdHocEmail();
@@ -639,8 +639,8 @@ function Reservation(opts) {
639639
$(v).prop('checked', false);
640640
});
641641

642-
var date = moment(elements.beginDate.val() + 'T' + elements.beginTime.val());
643-
var checkbox = $('#repeatDay' + date.day());
642+
var date = dateHelper.GetDate(elements.beginDate, elements.beginTime);
643+
var checkbox = $('#repeatDay' + date.getDay());
644644
checkbox.prop('checked', true);
645645
checkbox.parent().addClass('active');
646646
};
@@ -839,21 +839,25 @@ function Reservation(opts) {
839839
});
840840

841841
var previousDateEndsAtMidnight = function (scheduleId, date) {
842-
var currDate = moment(date, 'YYYY-MM-DD');
843-
currDate.subtract(1, 'days');
844-
var weekday = currDate.day();
842+
var currDate = dateHelper.parseYMDDate(date);
843+
if (!currDate) return false;
844+
// Subtract one day to view the layout of the previous day.
845+
var prevDate = new Date(currDate.getTime());
846+
prevDate.setDate(prevDate.getDate() - 1);
847+
var weekday = prevDate.getDay();
845848

846849
if (layoutCache[weekday] == null) {
847-
getLayoutItems(scheduleId, currDate.format('Y-M-D'));
850+
getLayoutItems(scheduleId, dateHelper.formatDate(prevDate));
848851
}
849852

850853
var lastPeriod = _.last(layoutCache[weekday]);
851-
return lastPeriod.isReservable == true && lastPeriod.end == '00:00:00';
854+
return lastPeriod && lastPeriod.isReservable == true && lastPeriod.end == '00:00:00';
852855
};
853856

854857
var getLayoutItems = function (scheduleId, date) {
855-
var currDate = moment(date, 'YYYY-MM-DD');
856-
var weekday = currDate.day();
858+
var currDate = dateHelper.parseYMDDate(date);
859+
if (!currDate) return [];
860+
var weekday = currDate.getDay();
857861

858862
if (layoutCache[weekday] != null) {
859863
return layoutCache[weekday];
@@ -1076,8 +1080,8 @@ function Reservation(opts) {
10761080
if (autoReleaseMinutes != '') {
10771081
var interval;
10781082
var updateAutoReleaseMinutes = function () {
1079-
var ms = _startDate.diff(moment());
1080-
var releaseMinutesText = Math.max(0, Math.ceil(moment.duration(ms).asMinutes()) + autoReleaseMinutes);
1083+
var ms = _startDate.getTime() - Date.now();
1084+
var releaseMinutesText = Math.max(0, Math.ceil(ms / 60000) + autoReleaseMinutes);
10811085
$('.autoReleaseMinutes').text(releaseMinutesText);
10821086

10831087
if (releaseMinutesText <= 0) {

Web/scripts/schedule.js

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -843,27 +843,38 @@ function Schedule(opts, resourceGroups) {
843843
});
844844
};
845845

846-
this.initUserDefaultSchedule = function (anonymous) {
847-
var makeDefaultButton = $('#make_default');
848-
if (anonymous) {
849-
makeDefaultButton.hide();
850-
return;
851-
}
846+
this.initUserDefaultSchedule = function () {
847+
const makeDefaultButton = document.getElementById('make_default');
848+
const scheduleInput = document.getElementById('scheduleId');
852849

853-
makeDefaultButton.show();
850+
if (!makeDefaultButton) return;
854851

855-
var defaultSetMessage = $('#defaultSetMessage');
856-
makeDefaultButton.click(function (e) {
852+
makeDefaultButton.addEventListener('click', function (e) {
857853
e.preventDefault();
858-
var scheduleId = $('#scheduleId').val();
859-
var changeDefaultUrl = options.setDefaultScheduleUrl.replace('[scheduleId]', scheduleId);
860854

861-
$.ajax({
862-
url: changeDefaultUrl,
863-
success: function (data) {
864-
defaultSetMessage.show().delay(5000).fadeOut();
865-
},
866-
});
855+
const scheduleId = scheduleInput.value;
856+
const changeDefaultUrl = options.setDefaultScheduleUrl.replace('[scheduleId]', scheduleId);
857+
858+
fetch(changeDefaultUrl)
859+
.then((response) => {
860+
if (!response.ok) {
861+
throw new Error(
862+
`Request failed with status ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`
863+
);
864+
}
865+
return response.json().catch(() => null);
866+
})
867+
.then(() => {
868+
// Toast to display a success message when changing the default schedule
869+
const toastEl = document.getElementById('defaultSetToast');
870+
if (toastEl && window.bootstrap && window.bootstrap.Toast) {
871+
toastEl.classList.remove('d-none');
872+
window.bootstrap.Toast.getOrCreateInstance(toastEl).show();
873+
}
874+
})
875+
.catch((err) => {
876+
console.error(err);
877+
});
867878
});
868879
};
869880

tpl/Reservation/create.tpl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,6 @@
528528
{control type="DatePickerSetupControl" ControlId="EndRepeat" DefaultDate=$RepeatTerminationDate MinDate=$StartDate MaxDate=$AvailabilityEnd FirstDay=$FirstWeekday}
529529
{control type="DatePickerSetupControl" ControlId="RepeatDate" MaxDate=$AvailabilityEnd FirstDay=$FirstWeekday MinDate=Date::Now()->ToTimezone($Timezone) Multiple=false}
530530

531-
{vendor_js src="moment/2.13.0/js/moment.min.js"}
532531
{jsfile src="resourcePopup.js"}
533532
{jsfile src="userPopup.js"}
534533
{jsfile src="date-helper.js"}

tpl/Schedule/schedule.tpl

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,17 @@
7474
{/if}
7575

7676
{if $IsAccessible}
77-
<div id="defaultSetMessage" class="alert alert-success d-none">
78-
{translate key=DefaultScheduleSet}
77+
<div class="toast-container position-fixed bottom-0 end-0 p-3">
78+
<div id="defaultSetToast" class="toast align-items-center bg-primary text-white border-0 d-none" role="alert"
79+
aria-live="assertive" aria-atomic="true">
80+
<div class="d-flex">
81+
<div class="toast-body">
82+
<i class="bi bi-check-circle-fill me-2"></i>{translate key=DefaultScheduleSet}
83+
</div>
84+
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
85+
aria-label="{translate key=Close}"></button>
86+
</div>
87+
</div>
7988
</div>
8089
{block name="schedule_control"}
8190
<div class="row">
@@ -85,31 +94,40 @@
8594
<div id="schedule-actions" class="col-sm-3 col-12">
8695
{block name="actions"}
8796
<div class="d-flex align-items-center mb-2">
88-
<a href="#" id="print_schedule" class="link-primary me-1" title="{translate key=Print}">
89-
<span class="bi bi-printer schedule_icon"></span>
90-
</a>
91-
<a href="#" id="make_default" class="link-primary me-2" style="display:none;"
92-
title="{translate key='MakeDefaultSchedule'}">
93-
<i class="bi bi-star-fill schedule_icon"></i>
94-
</a>
95-
<a href="#" class="schedule-style me-2 d-flex align-items-center" id="schedule_standard"
96-
schedule-display="{ScheduleStyle::Standard->value}"
97+
<div class="me-4 d-flex align-items-center gap-2">
98+
<a href="#" id="print_schedule" class="link-primary me-1" title="{translate key=Print}">
99+
<i class="bi bi-printer schedule_icon"></i>
100+
</a>
101+
{if $LoggedIn}
102+
<a href="#" id="make_default" class="link-primary" title="{translate key='MakeDefaultSchedule'}">
103+
<i class="bi bi-star-fill schedule_icon"></i>
104+
</a>
105+
{/if}
106+
</div>
107+
<a href="#"
108+
class="schedule-style me-2 d-inline-flex align-items-center{if $ScheduleStyle == ScheduleStyle::Standard->value} active{/if}"
109+
id="schedule_standard" schedule-display="{ScheduleStyle::Standard->value}"
97110
title="{translate key='StandardScheduleDisplay'}">
98111
<img class="schedule_icon shadow-sm" src="img/table.png"
99112
alt="{translate key='StandardScheduleDisplay'}" />
100113
</a>
101-
<a href="#" class="schedule-style me-2 d-flex align-items-center" id="schedule_tall"
102-
schedule-display="{ScheduleStyle::Tall->value}" title="{translate key='TallScheduleDisplay'}">
114+
<a href="#"
115+
class="schedule-style me-2 d-inline-flex align-items-center{if $ScheduleStyle == ScheduleStyle::Tall->value} active{/if}"
116+
id="schedule_tall" schedule-display="{ScheduleStyle::Tall->value}"
117+
title="{translate key='TallScheduleDisplay'}">
103118
<img class="schedule_icon shadow-sm" src="img/table-tall.png"
104119
alt="{translate key='TallScheduleDisplay'}" />
105120
</a>
106-
<a href="#" class="schedule-style d-none d-md-flex me-2 align-items-center" id="schedule_wide"
107-
schedule-display="{ScheduleStyle::Wide->value}" title="{translate key='WideScheduleDisplay'}">
121+
<a href="#"
122+
class="schedule-style d-none d-md-inline-flex me-2 align-items-center{if $ScheduleStyle == ScheduleStyle::Wide->value} active{/if}"
123+
id="schedule_wide" schedule-display="{ScheduleStyle::Wide->value}"
124+
title="{translate key='WideScheduleDisplay'}">
108125
<img class="schedule_icon shadow-sm" src="img/table-wide.png"
109126
alt="{translate key='WideScheduleDisplay'}" />
110127
</a>
111-
<a href="#" class="schedule-style d-none d-md-flex align-items-center" id="schedule_week"
112-
schedule-display="{ScheduleStyle::CondensedWeek->value}"
128+
<a href="#"
129+
class="schedule-style d-none d-md-inline-flex align-items-center{if $ScheduleStyle == ScheduleStyle::CondensedWeek->value} active{/if}"
130+
id="schedule_week" schedule-display="{ScheduleStyle::CondensedWeek->value}"
113131
title="{translate key='CondensedWeekScheduleDisplay'}">
114132
<img class="schedule_icon shadow-sm" src="img/table-week.png"
115133
alt="{translate key='CondensedWeekScheduleDisplay'}" />
@@ -478,6 +496,7 @@
478496
$(document).ready(function() {
479497
const schedule = new Schedule(scheduleOpts, {$ResourceGroupsAsJson});
480498
schedule.init();
499+
481500
});
482501
483502
$('#schedules').select2({

0 commit comments

Comments
 (0)