I spent some time on these requirements and through Telerik's blog, forum, demo, API documentation, and submitting tickets to their support, I have been able to accomplish all of these. They were harder than they should have been because of their placement in the custom editor for the scheduler which is a partial view and because of their reliance on fired events from the scheduler itself. Most of the information you need can be passed through events from the Scheduler, but the difficulty is finding documentation on them.
I have 2 DateTimePickers, start and end times, in the editor:
<div data-container-for="start" class="k-edit-field">
@(Html.Kendo().DateTimePickerFor(model => model.Start)
.Name("start")
.HtmlAttributes(generateDatePickerAttributes("startDateTime", "start", "value:start"))
)
<span data-for="start" class="k-invalid-msg"></span>
</div>
<div class="k-edit-label">
@(Html.Label("End Time*"))
</div>
<div data-container-for="end" class="k-edit-field">
@(Html.Kendo().DateTimePickerFor(model => model.End)
.Name("end")
.HtmlAttributes(generateDatePickerAttributes(
"endDateTime",
"end",
"value:end",
new Dictionary<string, object>() { { "data-dateCompare-msg", "End date should be greater than or equal to the start date" } }))
)
<span data-for="end" class="k-invalid-msg"></span>
</div>
They are given appropriate Kendo attributes with this method:
@functions{
public Dictionary<string, object> generateDatePickerAttributes(
string elementId,
string fieldName,
string dataBindAttribute,
Dictionary<string, object> additionalAttributes = null)
{
Dictionary<string, object> datePickerAttributes = additionalAttributes != null ? new Dictionary<string, object>(additionalAttributes) : new Dictionary<string, object>();
datePickerAttributes["id"] = elementId;
datePickerAttributes["name"] = fieldName;
datePickerAttributes["data-bind"] = dataBindAttribute;
datePickerAttributes["required"] = "required";
datePickerAttributes["style"] = "z-index: inherit;";
datePickerAttributes["data-role"] = "datetimepicker";
return datePickerAttributes;
}
}
To make the DateTimePicker reflect business hours I subscribed to the Scheduler's Edit event
(don't forget to reference this in the Scheduler Events specification) and then modified the widget using the setOptions()
method:
function onEdit(e) {//When we open or add an event
var min = new Date(kendo.date.getDate(new Date()).getTime() + (kendo.date.MS_PER_HOUR * 9));
var max = new Date(kendo.date.getDate(new Date()).getTime() + (kendo.date.MS_PER_HOUR * 21));
e.container.find("[data-role=datetimepicker]").each(function () {
var widget = $(this).getKendoDateTimePicker();
widget.timeView.setOptions({
max: max,
min: min
});
});
}
You can bind an onChange()
function to the DateTimePicker itself to force the end time to always reflect >= the start time with the following code, also in the onEdit()
method of the scheduler. It also handles the situation of changing the actual day (not just time) in the picker and having it stay within business hours. Without this bit, the min time is still whatever you initially set it to meaning that on the next day it will show all 24 hours again.:
e.container.find("[name=start][data-role=datetimepicker]").bind("change", function () {
var widget = $(this).getKendoDateTimePicker();
widget.timeView.setOptions({
max: max,
min: min
});
var endTimePicker = $("#endDateTime").data("kendoDateTimePicker");
endTimePicker.value(widget.value());
endPicker = e.container.find("[name=end][data-role=datetimepicker]").getKendoDateTimePicker();
endPicker.timeView.setOptions({
max: max,
min: min
});
});
e.container.find("[name=end][data-role=datetimepicker]").bind("change", function () {
var widget = $(this).getKendoDateTimePicker();
widget.timeView.setOptions({
max: max,
min: min
});
});
Checking for double bookings isn't as complicated as you might assume because it doesn't require any server calls, getting all appointments, checking time ranges, and returning. Telerik has a method for checking appointments in the scheduler. It is a method they conveniently do not document well: occurrencesInRange()
. I found it in one of the forum demos. You can just get all the occurrences within the time range you would like to use and if no one else is using it, book the event. If they are, you can just show an alert. Subscribe to the scheduler's Save event
:
function scheduler_save(e) {
if (!checkAvailability(e.event.start, e.event.end, e.event)) {
alert("This appointment slot is taken. Schedule a different time.");
e.preventDefault();
}
}
function checkAvailability(start, end, event) {
var scheduler = $("#scheduler").getKendoScheduler();
var occurrences = scheduler.occurrencesInRange(start, end);
var idx = occurrences.indexOf(event);
if (idx > -1) {
occurrences.splice(idx, 1);
}
return !occurrences.length;
}
Another problem you may run into is that of having the DateTimePicker contain only business hours, but the default value being 00:00:00 of that day, which of course, is outside your range. This was surprisingly difficult to default to business hour start, but you can do the following using the other very helpful method event.isNew()
:
if (e.event.isNew()) {
var startdate = e.event.start;
startdate.setHours(9);
var start = e.container.find("[name=start][data-role=datetimepicker]");
var end = e.container.find("[name=end][data-role=datetimepicker]");
$(start).data("kendoDateTimePicker").value(startdate);
$(end).data("kendoDateTimePicker").value(startdate);
}
This is the final set of JavaScript methods:
function onEdit(e) {//When we open or add an event
var min = new Date(kendo.date.getDate(new Date()).getTime() + (kendo.date.MS_PER_HOUR * 9));
var max = new Date(kendo.date.getDate(new Date()).getTime() + (kendo.date.MS_PER_HOUR * 21));
if (e.event.isNew()) {
var startdate = e.event.start;
startdate.setHours(9);
var start = e.container.find("[name=start][data-role=datetimepicker]");
var end = e.container.find("[name=end][data-role=datetimepicker]");
$(start).data("kendoDateTimePicker").value(startdate);
$(end).data("kendoDateTimePicker").value(startdate);
}
e.container.find("[data-role=datetimepicker]").each(function () {
var widget = $(this).getKendoDateTimePicker();
widget.timeView.setOptions({
max: max,
min: min
});
});
e.container.find("[name=start][data-role=datetimepicker]").bind("change", function () {
var widget = $(this).getKendoDateTimePicker();
widget.timeView.setOptions({
max: max,
min: min
});
var endTimePicker = $("#endDateTime").data("kendoDateTimePicker");
endTimePicker.value(widget.value());
endPicker = e.container.find("[name=end][data-role=datetimepicker]").getKendoDateTimePicker();
endPicker.timeView.setOptions({
max: max,
min: min
});
});
e.container.find("[name=end][data-role=datetimepicker]").bind("change", function () {
var widget = $(this).getKendoDateTimePicker();
widget.timeView.setOptions({
max: max,
min: min
});
});
}
function scheduler_save(e) {
if (!checkAvailability(e.event.start, e.event.end, e.event)) {
alert("This appointment slot is taken. Schedule a different time.");
e.preventDefault();
}
}
function checkAvailability(start, end, event) {
var scheduler = $("#scheduler").getKendoScheduler();
var occurrences = scheduler.occurrencesInRange(start, end);
var idx = occurrences.indexOf(event);
if (idx > -1) {
occurrences.splice(idx, 1);
}
return !occurrences.length;
}
I hope this saves someone else the 20 hours it took me.
Sources: http://www.telerik.com/forums/set-default-time-in-scheduler-editor-window-when-adding-task-in-month-view
http://docs.telerik.com/KENDO-UI/api/javascript/ui/datetimepicker#events
http://docs.telerik.com/KENDO-UI/api/aspnet-mvc/kendo.mvc.ui.fluent/schedulerbuilder
http://www.telerik.com/forums/how-to-check-for-double-reservations-overlap-(repeating-events)
Vladimir Illiev, Telerick DateTimePicker Specialist