Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RadzenScheduler - Resource Grouping #1994

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

paulo-rico
Copy link
Contributor

Hi all

Easiest way to follow through this is to begin at RadzenDayView.razor (week view and multi day work the same).

If there are no resources or you have elected to not show grouping (ShowResourceGrouping) it will output the same as it did before. The slight difference there is that, along with CascadingParameter for Scheduler, it also passes down itself (SchedulerViewBase) for the DropableViewBase. It uses this as a root for holding both DragStarted and DraggedAppointment variables. They had to be moved to a "parent" component in order to move appointments between multiple "diaries" (the bit at the bottom of each header that shows the slots and appointments).

The recursion loop then begins until there is no "child" resource, and then it renders the "diary" portion. So the rendering order would be something like this, where the integer is the resource and the decimal is the resource data (assuming two data items per resource)

1.1
2.1
3.1
Diary
1.1
2.1
3.2
Diary
1.1
2.2
3.1
Diary
e.t.c.

At each level, we have access to Property and Value so we can traverse back up the tree to build a FilterDescription[] in order to pass these filteredAppointments on to the "diary".

Although there are a great deal of file changes, the majority of these are to accommodate ResourceFilterList in the Scheduler events (which is defaulted to avoid breaking change). This is an IEnumerable<string Property, string Value> to pass back to identify which resource(s) are relavent to the "Click" or "Move", e.t.c.

Hope this is moving in the right direction.

Regards

Paul

@@ -7,13 +7,54 @@
public override RenderFragment Render()
{
var appointments = Scheduler.GetAppointmentsInRange(StartDate, EndDate).ToList();
var resources = Scheduler.ActiveResourceList;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is ActiveResourceList needed? What is an active resource? What is inactive resource?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone. Related to combined Views.

}
next++;
}
var currentResource = resourceDictionary.FirstOrDefault().Key;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the linked list (which is done via Dictionary ??)? I would just pass the current resource and the whole resource collection. DayViewResourceHeader can determine if it first or last based on the current index and collection.

/// Gets whether the resource is active in grouping
/// </summary>
/// <value>The visibility.</value>
bool IsActive { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is an "active" resource? I don't think this is really a requirement. Dynamic resources can be generated via @foreach:

<Resources>
      @foreach (var schedulerResourceData in resourceData.Where(d => d.Enabled))
       {
             <RadzenSchedulerResource ... />
       }
    </Resources>
    ```

/// Gets the order of grouping
/// </summary>
/// <value>The order.</value>
int Order { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not so sure order is needed either. People can define the resources in the order they need them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone

/// <summary>
/// List of resource filters.
/// </summary>
public IList<(string Property, string Value)> ResourceFilters { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tuples are not good for a public API. Should be a class instead - SchedulerResourceFilter or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now SchedulerResourceFilter

AppointmentMove=OnAppointmentMove />
</CascadingValue>
;
if (resources.Count > 0 && ShowResourceGrouping)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any use case for using resources and having ShowResourceGrouping as false? I don't think of any.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This disappears with having separate Views. When it was one View, it gave the option to ignore any resources and render as normal.

var currentResource = resourceDictionary.FirstOrDefault().Key;

return
@<div class="rz-resource-scheduler">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is RadzenDayView rendering the resource header? I think RadzeResourceDayView should do that and RadzenDayView should stay mostly untouched. The same goes for the other views.

For example I think RadzenResourceDayView should:

  1. Render the resource headers
  2. Render RadzenDayView for every resource.

@akorchev
Copy link
Collaborator

akorchev commented Feb 21, 2025

Hi @paulo-rico,

I think there may have been a misunderstanding judging by your latest commits. I still think we should have new Resource based views - RadzenResourceDayView, RadzenResourceWeekView etc. Those should render the resource headers and then render the children views passing them appointments filtered by data.

Maybe RadzenResourceDayView can inherit from RadzenDayView to avoid duplicating all parameters.

font-weight: 600;
}

.rz-resource-scheduler-diary {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still don't know what "diary" is. We call them "views"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed it to "view".

I used "diary" in order to differentiate between the view and the "diary" portion (for want of a better word). Originally, all we had was a Scheduler that contained a View. With the resource views, We have a Scheduler that contains a View that comprises a series of "Headers" and a series of "diaries".

{
draggedAppointment.Start = draggedAppointment.Start + args.TimeSpan;
draggedAppointment.End = draggedAppointment.End + args.TimeSpan;
if(args.ResourceFilters!=null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look as a good public API. I don't understand what it is doing.

/// Mark that a drag has started.
/// </summary>
/// <value>Has a drag started?</value>
public bool DragStarted { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are those needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to do with tracking the Started and Appointment variables when we're using multiple views in one. As it was, these variables were stored in the DropableViewBase, so you couldn't drag between the views. They would all have there own local variables. I've moved those tracking variables up the hierarchy so that all the views share these and we can drag-drop appointments between them.

}
}

public IList<(string Field, string Value)> resourceFilterList
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Properties must follow PascalCase naming convention.

@akorchev
Copy link
Collaborator

A few things popped up:

  1. Resource hierarchy. Right now we rely on the order of resources to denote hierarchy - every resource is a child of the previous one. I am not 100% sure this is the correct approach. Would this be more clear:
<RadzenSchedulerResource Title="Location" Property="Building" Data="@(Buildings)">
         <RadzenSchedulerResource IsActive=@showRoom Title="Room" Property="Room" Data="@(Rooms)">
               <RadzenSchedulerResource IsActive=@showAttendee Title="Attendee" Property="Person" Data="@(People)" />
         </RadzenSchedulerResource>
</RadzenSchedulerResource>

Has the drawback of defining multiple children at the same level (which we won't support) but shows the hierarchy in a better way.

  1. We should probably split replace Property with TextProperty and ValueProperty to make it more real-life:
    <RadzenSchedulerResource Title="Location" ValueProperty="BuildingId" TextProperty="Name" Data="@([new Building { BuildingId = 1, Name = "Building One" }])">

  2. The code which renders the resource headers should be identical in all resource views. You can extract it in its own component.

What do you think?

@paulo-rico
Copy link
Contributor Author

We should probably split replace Property with TextProperty and ValueProperty to make it more real-life:
<RadzenSchedulerResource Title="Location" ValueProperty="BuildingId" TextProperty="Name" Data="@([new Building { BuildingId = 1, Name = "Building One" }])">

I'm assuming this means we store the reference (Id) in the Appointment record as opposed to the Text.
I'm positive it is, but want to be 100% sure before making the changes :)

@akorchev
Copy link
Collaborator

I'm positive it is, but want to be 100% sure before making the changes :)

Yes. That's it. I am thinking of what is the easiest way to use this from a GUI IDE when you want the resources to come from a database. One would usually have some foreign key relationship between appointments and resources. And we need a way to convey that.

About the nested resources - the more I think about it the less I like it. It may cause more harm than good. Let's scrap it for now.

@paulo-rico
Copy link
Contributor Author

paulo-rico commented Feb 25, 2025

The nesting gave me some thoughts. Not to actually nest the resources, but to link them in some way.

My thoughts are along these lines.

As it stands, we are outputting all headers for all "parent" headers, i.e. basically, nested foreach without any filtering (on the output of the resource headers).

But, as a real world example, this isn't always the best approach. Take a hotel chain, for example.

A chain has many hotels. A hotel has many rooms, these rooms have bookings. As it stands with the current grouping method, it will output "Hotel 1" and then output all the rooms in the database under this header. It will then render "Hotel 2" and all the rooms again. Sometimes, like this scenario, there needs to be a link to "filter" child group headers (rooms) for it to make sense in real life. Not output rooms for a hotel that don't exist.

Also in the above scenario, Appointment records in a database may not (or shouldn't, if following standard design practices) have the hotel reference as part of it. It would gather the hotel from the hotel reference of the room record, if need be. So this is making me think that not all resource records used for grouping will take part in the filter process of the appointment records, as it does now.

An initial idea is as follows. We have the following properties as part of each resource record -

  • Title - the Title of the resource
  • ValueProperty - The Id property of the Resource record. Example RoomId in a "Room" Resource record
  • TextProperty - The text property of the Resource record, used primarily for the header title. Example RoomTitle in a "Room" Resource record
  • ResourceFilterProperty - The property of the Resource record that links to the ValueProperty of the Parent resource. Example HotelRef in a "Room" Resource. If left blank or null all resource values will be output as it does now. Otherwise, filter the resource data records based on the parent resource and just output those headers (Rooms).
  • AppointmentFilterProperty - The property of the Appointment record that holds the reference to this Resource data's record Id (ValueProperty). Example RoomRef in the Appointment record. If this is left blank or null, this Resource is not used to filter Appointment records.

Example Resource and Appointment components / data -

<RadzenSchedulerResource Title="Hotel" ValueProperty="HotelId" TextProperty="HotelName" 
        Data="@([new Hotel { HotelId = 1, HotelName = "The Dorchester" }, 
                 new Hotel { HotelId = 2, HotelName = "The Grand" }])" />
<RadzenSchedulerResource Title="Room" ValueProperty="RoomId" TextProperty="RoomName" 
        ResourceFilterProperty="HotelRef" 
        AppointmentFilterProperty = "RoomReference" 
        Data="@([new Room { RoomId = 1, RoomName = "101", HotelRef = 1 }, 
                         new Room { RoomId = 2, RoomName = "102", HotelRef = 1 }, 
                         new Room { RoomId = 3, RoomName = "103", HotelRef = 1 }, 
                         new Room { RoomId = 4, RoomName = "Presidential", HotelRef = 2 }, 
                         new Room { RoomId = 5, RoomName = "Penthouse", HotelRef = 2 }, 
                         new Room { RoomId = 6, RoomName = "Executive", HotelRef = 2 }])" />

IList<Appointment> appointments = new List<Appointment>
    {
        new Appointment { Start = DateTime.Today, End = DateTime.Today, Text = "Mr Smith", RoomReference = 4 },
        new Appointment { Start = DateTime.Today, End = DateTime.Today, Text = "Mrs Jones", RoomReference = 5 },
        new Appointment { Start = DateTime.Today, End = DateTime.Today, Text = "Mrs Beech", RoomReference = 1 },
        new Appointment { Start = DateTime.Today, End = DateTime.Today, Text = "Mrs Chapman", RoomReference = 2 },
        new Appointment { Start = DateTime.Today, End = DateTime.Today, Text = "Mr Hunter", RoomReference = 6 }
    };

So, in the above example, it will output all the Hotels as header. When it comes to rendering the Rooms, it will see that ResourceFilterProperty as a value so will only output Room headers where the ResourceFilterProperty is equal to the Parent ValueProperty.

When it comes to filtering the Appointments, it will traverse the Resources and apply filters to only the properties where a Resource has the AppointmentFilterProperty declared.

We're still not nesting. The "Parent" is the preceding Resource.

I'm sure this offers the flexibility required for this type of view.

What do you think? Would this work in the GUI IDE?

@akorchev
Copy link
Collaborator

We may be overcomplicating things. Having to set three or four properties per resource seems to be too much. And I got confused again by the hotel / room analogy. I should better check again the API of the scheduler component we developed back in the days. It had cleaner API that wasn't hard to follow and support all kinds of scenarios.

@akorchev
Copy link
Collaborator

Found an example that seems flexible enough: https://demos.telerik.com/kendo-ui/scheduler/resources-grouping-hierarchical

  1. Resources are independent from each other. Rooms and attendees don't have a relationship.
  2. The "hierarchy" is determined by the "group" setting - it specifies the order of resources to group by.
  3. Resources may repeat - as attendees do. Also an appointment can have multiple resources from the same type (multiple people can attend the same meeting).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants