Can we do resource and/or grouping with the scheduler component?
Something like this:
Resources and grouping are not available in RadzenScheduler.
Do you think it will be an added feature in the future?
This won't be available in the near future for sure.
@korchev - Can you define "near future"? I am building a case for implementing Radzen, and this is a bit of a stumbling block as it's needed for our requirements and some of your competitors already have this. If it were 6 months, we may be able to wait, but much longer and we would have to think about it.
Thanks in advance, love your work, by the way!
Hi @SkinnyPete63,
We don't plan to work on resources and grouping this year.
Hi!
Is this on the cards in the near future?
No, this isn't part of our short term plans. We would gladly accept a pull request implementing that feature though!
@korchev Can you offer any guidance as to the best approach to implement this feature? I need it for my project as well and may be able to create a PR.
Anyone coming to this thread looking for Resource Scheduling, here is one method you could adopt using the RadzenScheduler
component.
Utilizing the Employees
table from the Radzen Components demo site, and generating some random appointments on the fly, this code will run unmodified in the Editor tab.
The code has quite a few comments running through it, so it should give you some idea of how you could put something together to accomodate your own needs.
Regards
Paul
@using Radzen.Blazor.Rendering
@using RadzenBlazorDemos.Data
@using RadzenBlazorDemos.Models.Northwind
@using Microsoft.EntityFrameworkCore
@using System.Globalization
@inherits DbContextPage
@inject TooltipService TooltipService
@inject DialogService DialogService
@inject IJSRuntime JSRuntime
<style>
.highlight-slot {
background-color: var(--rz-info-light) !important;
}
.employee-rotated-text {
transform: rotate(-90deg);
width: 190px;
position: relative;
left: -90px;
max-width: 150px;
text-align: left;
top: 24px;
}
.employee-small-image {
width: 28px;
height: 28px;
position: relative;
left: -30px;
text-align: center;
top: 188px;
border-radius: 50%;
}
.rz-slider-horizontal .rz-slider-handle {
top: 50%;
transform: translateY(-50%);
margin-inline-start: -4px;
width: 5px;
height: var(--rz-slider-handle-height);
border-radius: 0;
}
</style>
@* Custom control panel. Modified version of the existing control panel in the RadzenScheduler component *@
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.SpaceBetween" Gap="0.5rem" class="rz-p-4 rz-mb-6 rz-border-radius-1" Style="border: var(--rz-grid-cell-border);height:4rem;">
<div class="rz-scheduler-nav" style="width: -webkit-fill-available;">
<div class="rz-scheduler-nav-prev-next">
<button tabindex="0" class="rz-button rz-prev" @onclick=@(() => OnPrev()) title="PrevText"><RadzenIcon Icon="chevron_left" /></button>
<button tabindex="0" class="rz-button rz-next" @onclick=@(() => OnNext()) title="NextText"><RadzenIcon Icon="chevron_right" /></button>
<button tabindex="0" class="rz-button rz-today" @onclick=@OnToday title="TodayText">@(referenceScheduler == null ? "Today" : referenceScheduler.TodayText)</button>
</div>
<div class="rz-scheduler-nav-title">@schedulerTitle</div>
<RadzenStack Visible="@(selectedIndex==1)" JustifyContent="JustifyContent.SpaceAround" AlignItems="AlignItems.Center">
<RadzenLabel Style="font-size:smaller;" Text="@($"Multi-day: Show {multiDayCount} days")" />
<RadzenSlider Min="2" Max="6" @bind-Value=@multiDayCount TValue="int"
Change="@(() => schedulerTitle=($"{CurrentDate.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern)} - {CurrentDate.AddDays(multiDayCount).ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern)}"))" />
</RadzenStack>
<div class="rz-scheduler-nav-views">
@foreach (var view in Views)
{
<RadzenButton Click=@(args => OnChangeView(view)) Icon=@view.Icon Text=@view.Text class="@($"{(IsSelected(view) ? " rz-state-active" : "")}")" />
}
</div>
</div>
</RadzenStack>
@* Using the employees table, we iterate through each record and render a header and RadzenScheduler control *@
<RadzenStack Orientation="Orientation.Horizontal" Gap="10">
@foreach (var emp in employees)
{
<RadzenStack Orientation="Orientation.Vertical" Gap="0" Style="@($"width: -webkit-fill-available; max-width: {(emp.ReportsTo == 350 ? "none" : "30px")};")">
<RadzenButton Click="@(() => emp.ReportsTo = (emp.ReportsTo==350 ? 30 : 350))" Text="@($"{(emp.ReportsTo == 350 ? "Collapse" : "")}")" Icon="@(emp.ReportsTo==350 ? "chevron_left" : "chevron_right")"
Size="ButtonSize.Medium" Variant="Variant.Text" Shade="Shade.Dark" ButtonStyle="ButtonStyle.Info" />
@if (emp.ReportsTo == 350)
{
<RadzenCard Style="@($"height: 170px;width: {(emp.ReportsTo==350 ? "auto" : "30px")}; min-width: {(emp.ReportsTo==350 ? "350px" : "30px")}; margin-bottom: 8px;")">
<RadzenStack Orientation="Orientation.Horizontal" JustifyContent="JustifyContent.Start" Gap="1rem" class="rz-p-4">
<RadzenImage Path="@emp.Photo" Style="width: 100px; height: 100px; border-radius: 50%;" />
<RadzenStack Gap="0">
<RadzenText TextStyle="TextStyle.Overline" class="rz-display-flex rz-mt-2 rz-my-0">Employee</RadzenText>
<RadzenText TextStyle="TextStyle.Body1"><b>@(emp.FirstName + " " + emp.LastName)</b></RadzenText>
<RadzenText TextStyle="TextStyle.Overline" class="rz-display-flex rz-mt-4 rz-mb-0">Department</RadzenText>
<RadzenText TextStyle="TextStyle.Body1"><b>@emp.Title</b></RadzenText>
</RadzenStack>
</RadzenStack>
</RadzenCard>
}
else
{
<RadzenCard Style="@($"height: 260px;width: {emp.ReportsTo}px; margin-bottom: 8px;")">
<RadzenStack Orientation="Orientation.Vertical" JustifyContent="JustifyContent.Start" AlignItems="AlignItems.Start" Gap="1rem" class="rz-p-4">
<RadzenImage class="employee-small-image" Path="@emp.Photo" />
<text class="employee-rotated-text"><b>@(emp.FirstName + " " + emp.LastName)</b></text>
</RadzenStack>
</RadzenCard>
}
<RadzenScheduler Date="@CurrentDate" @ref=@scheduler[emp.EmployeeID]
Style="@($"width:{(emp.ReportsTo == 350 ? "-webkit-fill-available" : "30px")}; visibility:{(emp.ReportsTo == 350 ? "visible" : "hidden")};height: 613px")" TItem="AppointmentWithResource"
Data=@(appointments.Where(a=>a.ResourceId==emp.EmployeeID)) StartProperty="Start" EndProperty="End" ShowHeader=@showHeader
TextProperty="Text" SelectedIndex="@selectedIndex"
SlotRender=@OnSlotRender[emp.EmployeeID]
SlotSelect=@OnSlotSelect[emp.EmployeeID]
AppointmentSelect=@OnAppointmentSelect
AppointmentRender=@OnAppointmentRender
AppointmentMouseEnter=@OnAppointmentMouseEnter
AppointmentMouseLeave=@OnAppointmentMouseLeave>
<Template Context="data">
<RadzenText TextStyle="TextStyle.Caption">@data.Text</RadzenText>
</Template>
<ChildContent>
<RadzenDayView StartTime="@(new TimeSpan(8,0,0))" EndTime="@(new TimeSpan(20,0,0))" />
<RadzenMultiDayView StartTime="@(new TimeSpan(8,0,0))" EndTime="@(new TimeSpan(20,0,0))" NumberOfDays="@multiDayCount" AdvanceDays="1" />
<RadzenWeekView StartTime="@(new TimeSpan(8,0,0))" EndTime="@(new TimeSpan(20,0,0))" />
<RadzenMonthView />
</ChildContent>
</RadzenScheduler>
</RadzenStack>
}
</RadzenStack>
<EventConsole @ref=@console />
@code {
// a Dictionary of the rendered RadzenScheduler components. One for each employee
Dictionary<int, RadzenScheduler<AppointmentWithResource>> scheduler;
// a Dictionary of SlotRender event handlers. One for each employee
Dictionary<int, Action<SchedulerSlotRenderEventArgs>> OnSlotRender;
// a Dictionary of SlotSelect event handlers. One for each employee
Dictionary<int, Action<SchedulerSlotSelectEventArgs>> OnSlotSelect;
// set to the first rendered RadzenScheduler and used for calling methods and functions like OnNext and OnPrev, e.t.c.
RadzenScheduler<AppointmentWithResource> referenceScheduler;
EventConsole console;
IList<Employee> employees;
string schedulerTitle;
AppointmentWithResource draggedItem;
// how many employees do we want to see in the demo (there are nine in the Employees table)
const int EMPLOYEE_COUNT = 9;
// we do not want to show any headers for the individually rendered RadzenScheduler components
bool showHeader = false;
// used to hold the index of the Scheduler current view
private int selectedIndex { get; set; } = 0;
// used by the progress bar in the header to indicate how many days to display in the MUlti-day view
int multiDayCount = 4;
// we keep a copy of the current date being rendered
public DateTime CurrentDate { get; set; } = DateTime.Now.Date;
// these are used as a simple way of building the custom header and other stuff.
// Must marry with the views that are being rendered for each RadzenScheduler component.
IList<ISchedulerView> Views = new List<ISchedulerView>()
{
new RadzenDayView(),
new RadzenMultiDayView(),
new RadzenWeekView(),
new RadzenMonthView(),
};
// this list is used in the random generation of the appointments
IList<AppointmentTextColor> appointmentTextColor = new List<AppointmentTextColor>()
{
new AppointmentTextColor() {Id=1, Text="Client Meeting", Color="PaleGoldenrod"},
new AppointmentTextColor() {Id=2, Text="Conference Call", Color="lightblue"},
new AppointmentTextColor() {Id=3, Text="Team Meeting", Color="#cdceff"},
new AppointmentTextColor() {Id=4, Text="Sales Call", Color="violet"},
new AppointmentTextColor() {Id=5, Text="Department Meeting", Color="salmon"},
new AppointmentTextColor() {Id=6, Text="Management Meeting", Color="lightpink"},
new AppointmentTextColor() {Id=7, Text="Office Training", Color="palegreen"},
new AppointmentTextColor() {Id=8, Text="Progress Report", Color="paleturquoise"},
new AppointmentTextColor() {Id=9, Text="IT Training", Color="#bbccff"},
new AppointmentTextColor() {Id=10, Text="Research", Color="peru"},
};
// how many random appointments to generate
const int APPOINTMENT_COUNT = 1200;
// the following generates a bunch of (APPOINTMENT_COUNT) appointments, spread over nine employees and 60 days
IList<AppointmentWithResource> appointments = new List<AppointmentWithResource>();
(DateTime start, DateTime end, string text, int res) GetRandomAppointments()
{
Random random = new Random();
int appDay = random.Next(APPOINTMENT_COUNT / 15) - (APPOINTMENT_COUNT / 45);
int startHour = 8 + random.Next(9);
int endMinutes = (random.Next(6) + 1) * 30;
int rnum = (random.Next(10) + 1);
string text = appointmentTextColor.Where(a => a.Id == rnum).First().Text;
int resource = random.Next(EMPLOYEE_COUNT) + 1;
return (DateTime.Now.Date.AddDays(appDay).AddHours(startHour).AddMinutes(30), DateTime.Now.Date.AddDays(appDay).AddHours(startHour).AddMinutes(endMinutes), text, resource);
}
void SetupAppointments()
{
(DateTime start, DateTime end, string Text, int res) calcTimes;
for (int i = 0; i < 900; i++)
{
calcTimes = GetRandomAppointments();
appointments.Add(new AppointmentWithResource() { Start = calcTimes.start, End = calcTimes.end, Text = calcTimes.Text, ResourceId = calcTimes.res });
}
}
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
SetupAppointments();
schedulerTitle = CurrentDate.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern);
employees = dbContext.Employees.Where(e => e.EmployeeID <= EMPLOYEE_COUNT).OrderBy(o => o.LastName).ToList();
// for the purpose of this demo, I'm arbitrarily using an unused field in Employees table to hold it's column width
foreach (var e in employees)
{
e.ReportsTo = 350;
}
scheduler = new Dictionary<int, RadzenScheduler<AppointmentWithResource>>();
OnSlotRender = new Dictionary<int, Action<SchedulerSlotRenderEventArgs>>();
OnSlotSelect = new Dictionary<int, Action<SchedulerSlotSelectEventArgs>>();
// here we populate the Dictionary of event handlers for the RadzenScheduler's SlotRender and SlotSelect events
foreach (var e in employees)
{
OnSlotRender[e.EmployeeID] = (args) =>
{
// Highlight working hours (9-18)
if ((args.View.Text == "Week" || args.View.Text == "Day" || args.View.Text == "Multi-Day") && args.Start.Hour > 8 && args.Start.Hour < 18)
{
args.Attributes["style"] = "background: var(--rz-scheduler-highlight-background-color, rgba(255,220,40,.2));";
}
args.Attributes.Add("ondragover", "event.preventDefault();event.target.classList.add('highlight-slot')");
args.Attributes.Add("ondragleave", "event.target.classList.remove('highlight-slot')");
args.Attributes.Add("ondrop", EventCallback.Factory.Create<DragEventArgs>(this, () =>
{
var empRecordFrom = employees.Where(er => er.EmployeeID == draggedItem.ResourceId).FirstOrDefault();
var empRecordTo = employees.Where(er => er.EmployeeID == e.EmployeeID).FirstOrDefault();
console.Log($"Moved {draggedItem.Text} from {empRecordFrom?.FirstName} {empRecordFrom?.LastName} to {empRecordTo?.FirstName} {empRecordTo?.LastName}");
draggedItem.ResourceId = e.EmployeeID;
draggedItem.End = args.Start.Add(draggedItem.End - draggedItem.Start);
draggedItem.Start = args.Start;
ReloadDiary();
JSRuntime.InvokeVoidAsync("eval", $"document.querySelector('.highlight-slot').classList.remove('highlight-slot')");
}));
};
OnSlotSelect[e.EmployeeID] = async (args) =>
{
console.Log($"SlotSelect: Start={args.Start} End={args.End}");
if (args.View.Text != "Year")
{
RadzenBlazorDemos.Appointment data = await DialogService.OpenAsync<AddAppointmentPage>("Add Appointment",
new Dictionary<string, object> { { "Start", args.Start }, { "End", args.End } });
if (data != null)
{
var dataWithResource = new AppointmentWithResource() { ResourceId = e.EmployeeID, Start = data.Start, End = data.End, Text = data.Text };
appointments.Add(dataWithResource);
// Either call the Reload method or reassign the Data property of the Scheduler
await ReloadDiary();
}
}
};
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// setup the reference to the first RadzenScheduler component
if (firstRender && referenceScheduler == null && scheduler.Count > 0)
{
referenceScheduler = scheduler.First().Value;
}
}
void OnAppointmentRender(SchedulerAppointmentRenderEventArgs<AppointmentWithResource> args)
{
// Never call StateHasChanged in AppointmentRender - would lead to infinite loop
string style = "";
var colorRecord = appointmentTextColor.Where(a => a.Text == args.Data.Text).FirstOrDefault();
style = $"background: {(colorRecord == null ? "lightgrey" : colorRecord.Color)};";
style += " cursor:grab";
args.Attributes.Add("style", style);
args.Attributes.Add("draggable", "true");
args.Attributes.Add("ondragstart", EventCallback.Factory.Create<DragEventArgs>(this, () =>
{
TooltipService.Close();
draggedItem = args.Data;
}));
}
async Task OnAppointmentSelect(SchedulerAppointmentSelectEventArgs<AppointmentWithResource> args)
{
console.Log($"AppointmentSelect: Appointment={args.Data.Text}");
var copy = new AppointmentWithResource
{
Start = args.Data.Start,
End = args.Data.End,
Text = args.Data.Text
};
var data = await DialogService.OpenAsync<EditAppointmentPage>("Edit Appointment", new Dictionary<string, object> { { "Appointment", copy } });
if (data != null)
{
// Update the appointment
args.Data.Start = data.Start;
args.Data.End = data.End;
args.Data.Text = data.Text;
}
await ReloadDiary();
}
void OnAppointmentMouseEnter(SchedulerAppointmentMouseEventArgs<AppointmentWithResource> args)
{
var employee = employees.Where(e => e.EmployeeID == args.Data.ResourceId).FirstOrDefault();
TooltipService.Open(args.Element, ts =>
@<RadzenStack Orientation="Orientation.Vertical" Gap="0" class="rz-p-6" Style="min-width: 250px; max-width: 500px;">
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Start" Gap="10px" Style="margin-bottom: 30px; min-width: 250px; max-width: 500px;">
<RadzenImage Path="@employee.Photo" Style="width: 40px; height: 40px; border-radius: 50%;" />
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-4" Style="margin-top: 15px; color: var(--rz-tooltip-color);">
@($"{employee.FirstName} {employee.LastName}")
</RadzenText>
</RadzenStack>
<RadzenText TextStyle="TextStyle.H3" class="rz-mb-4" Style="color: var(--rz-tooltip-color);">
@args.Data.Text
</RadzenText>
<RadzenStack Orientation="Orientation.Horizontal" Gap="4px">
<RadzenText TextStyle="TextStyle.Body2" Style="color: var(--rz-tooltip-color); width: 44px;">
<strong>Start</strong>
</RadzenText>
<RadzenText TextStyle="TextStyle.Body2" Style="color: var(--rz-tooltip-color);">
@args.Data.Start.ToString("hh:mm ⋅ dddd, MMMM d")
</RadzenText>
</RadzenStack>
<RadzenStack Orientation="Orientation.Horizontal" Gap="4px">
<RadzenText TextStyle="TextStyle.Body2" Style="color: var(--rz-tooltip-color); width: 44px;">
<strong>End</strong>
</RadzenText>
<RadzenText TextStyle="TextStyle.Body2" Style="color: var(--rz-tooltip-color);">
@args.Data.End.ToString("hh:mm ⋅ dddd, MMMM d")
</RadzenText>
</RadzenStack>
</RadzenStack>, new TooltipOptions { Position = TooltipPosition.Bottom, Duration = null });
}
void OnAppointmentMouseLeave(SchedulerAppointmentMouseEventArgs<AppointmentWithResource> args)
{
TooltipService.Close();
}
// the following functions mainly deal with the custom header control panel that mimics one RadzenScheduler and propogates the changes to the rest
public bool IsSelected(ISchedulerView view)
{
return selectedIndex == Views.IndexOf(view);
}
public ISchedulerView SelectedView
{
get
{
return Views.ElementAtOrDefault(selectedIndex);
}
}
async Task OnPrev(bool reload = true)
{
if (referenceScheduler != null)
{
CurrentDate = referenceScheduler.SelectedView.Prev();
foreach (var sched in scheduler)
{
sched.Value.CurrentDate = CurrentDate;
}
if (reload) await ReloadDiary();
}
}
async Task OnToday()
{
CurrentDate = DateTime.Now.Date;
foreach (var sched in scheduler)
{
sched.Value.CurrentDate = CurrentDate;
}
await ReloadDiary();
}
async Task OnNext(bool reload = true)
{
if (referenceScheduler != null)
{
CurrentDate = referenceScheduler.SelectedView.Next();
foreach (var sched in scheduler)
{
sched.Value.CurrentDate = CurrentDate;
}
if (reload) await ReloadDiary();
}
}
async Task OnChangeView(ISchedulerView view)
{
var temp = CurrentDate;
selectedIndex = Views.IndexOf(view);
StateHasChanged();
await Task.Yield();
// due to component refesh order and such, changing the view doesn't update the Title of the header. We have to force this by jumping Prev then Next.
await OnPrev(false);
await OnNext();
CurrentDate = temp;
}
async Task ReloadDiary()
{
schedulerTitle = referenceScheduler?.SelectedView?.Title;
StateHasChanged();
foreach (var s in scheduler)
{
await s.Value.Reload();
}
}
// custom classes used by the demo
public class AppointmentWithResource : RadzenBlazorDemos.Appointment
{
public int ResourceId { get; set; }
}
public class AppointmentTextColor
{
public int Id { get; set; }
public string Text { get; set; }
public string Color { get; set; }
}
}
Tanks Paul this work great for me.