Browse Source

Merge branch 'equipment_planner_changes' into kenric

Kenric Nugteren 10 months ago
parent
commit
1d175601dc

+ 12 - 20
prs.desktop/Dashboards/Equipment/EquipmentSchedulesDashboard.xaml

@@ -39,8 +39,9 @@
                 <ItemsControl.ItemTemplate>
                     <DataTemplate DataType="local:EquipmentScheduleViewModel">
                         <Border BorderThickness="0,0,0,1" BorderBrush="LightGray"
-                            Background="WhiteSmoke">
-                            <Grid>
+                                Background="WhiteSmoke"
+                                Tag="{Binding ID}" ContextMenuOpening="Grid_ContextMenuOpening">
+                            <Grid x:Name="EquipmentGrid">
                                 <Grid.ColumnDefinitions>
                                     <ColumnDefinition Width="250"/>
                                     <ColumnDefinition Width="*"/>
@@ -49,7 +50,9 @@
                                 <Border Grid.Column="0" Background="White"
                                         BorderThickness="0,0,1,0" BorderBrush="LightGray">
                                     <Border BorderBrush="Gray" BorderThickness="1" CornerRadius="3"
-                                            Padding="5" Margin="2.5">
+                                            Padding="5" Margin="2.5"
+                                            Tag="{Binding ID}"
+                                            ContextMenuOpening="Border_ContextMenuOpening">
                                         <Grid>
                                             <Grid.ColumnDefinitions>
                                                 <ColumnDefinition Width="Auto"/>
@@ -92,11 +95,6 @@
                                             </ToolTip>
                                         </Border.ToolTip>
                                     </Border>
-                                    <Border.ContextMenu>
-                                        <ContextMenu>
-                                            <MenuItem Tag="{Binding ID}" Header="View Equipment" Click="ViewEquipment_Click"/>
-                                        </ContextMenu>
-                                    </Border.ContextMenu>
                                 </Border>
 
                                 <ItemsControl Grid.Column="1" ItemsSource="{Binding Schedules}">
@@ -110,7 +108,9 @@
                                             <Border Background="{Binding DueDate, Converter={StaticResource scheduleBackgroundConverter}}"
                                                     BorderBrush="Gray" BorderThickness="1" CornerRadius="3"
                                                     Padding="5" Margin="5,2.5,0,2.5"
-                                                    Width="150">
+                                                    Width="150"
+                                                    Tag="{Binding ID}"
+                                                    ContextMenuOpening="Schedule_ContextMenuOpening">
                                                 <Grid>
                                                     <Grid.ColumnDefinitions>
                                                         <ColumnDefinition Width="Auto"/>
@@ -118,7 +118,9 @@
                                                     </Grid.ColumnDefinitions>
                                                     <Border Width="30" Height="30" CornerRadius="15"
                                                             Margin="0,0,5,0"
-                                                            BorderThickness="1" BorderBrush="Black">
+                                                            BorderThickness="1" BorderBrush="Black"
+                                                            Tag="{Binding EmployeeID}"
+                                                            ContextMenuOpening="Employee_ContextMenuOpening">
                                                         <Border.Background>
                                                             <ImageBrush ImageSource="{Binding EmployeeImage}" Stretch="UniformToFill"/>
                                                         </Border.Background>
@@ -131,11 +133,6 @@
                                                                 </StackPanel>
                                                             </ToolTip>
                                                         </Border.ToolTip>
-                                                        <Border.ContextMenu>
-                                                            <ContextMenu Style="{StaticResource employeeMenuStyle}">
-                                                                <MenuItem Tag="{Binding EmployeeID}" Header="View Employee" Click="ViewEmployee_Click"/>
-                                                            </ContextMenu>
-                                                        </Border.ContextMenu>
                                                     </Border>
                                                     <StackPanel Grid.Column="1">
                                                         <TextBlock Text="{Binding Title}"
@@ -164,11 +161,6 @@
                                                         </Grid>
                                                     </ToolTip>
                                                 </Border.ToolTip>
-                                                <Border.ContextMenu>
-                                                    <ContextMenu>
-                                                        <MenuItem Header="View Schedule" Tag="{Binding ID}" Click="ViewSchedule_Click"/>
-                                                    </ContextMenu>
-                                                </Border.ContextMenu>
                                             </Border>
 
                                         </DataTemplate>

+ 408 - 282
prs.desktop/Dashboards/Equipment/EquipmentSchedulesDashboard.xaml.cs

@@ -20,373 +20,499 @@ using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using InABox.Configuration;
 using Client = InABox.Clients.Client;
+using System.Windows;
+using InABox.Wpf;
 
-namespace PRSDesktop.Dashboards
+namespace PRSDesktop.Dashboards;
+
+public class EquipmentSchedulesDashboardProperties : IUserConfigurationSettings, IDashboardProperties
 {
-    public class EquipmentSchedulesDashboardProperties : IUserConfigurationSettings, IDashboardProperties
-    {
+    public bool OnlyShowSchedules { get; set; } = true;
 
-    }
+    public DynamicGridSelectedFilterSettings Filters { get; set; } = new();
+}
 
-    public class ScheduleViewModel : INotifyPropertyChanged
-    {
-        private Guid employeeID;
-        private string? employeeName;
-        private string? employeeMobile;
-        private string? employeeEmail;
-        private BitmapImage? employeeImage;
+public class ScheduleViewModel : INotifyPropertyChanged
+{
+    private Guid employeeID;
+    private string? employeeName;
+    private string? employeeMobile;
+    private string? employeeEmail;
+    private BitmapImage? employeeImage;
 
-        public bool HasEmployee => EmployeeID != Guid.Empty;
+    public bool HasEmployee => EmployeeID != Guid.Empty;
 
-        public Guid ID { get; set; }
+    public Guid ID { get; set; }
 
-        public string Title { get; set; }
+    public string Title { get; set; }
 
-        public DateTime DueDate { get; set; }
+    public DateTime DueDate { get; set; }
 
-        public BitmapImage? EmployeeImage
+    public BitmapImage? EmployeeImage
+    {
+        get => employeeImage;
+        set
         {
-            get => employeeImage;
-            set
-            {
-                employeeImage = value;
-                NotifyPropertyChanged();
-            }
+            employeeImage = value;
+            NotifyPropertyChanged();
         }
+    }
 
-        public Guid EmployeeID
+    public Guid EmployeeID
+    {
+        get => employeeID;
+        set
         {
-            get => employeeID;
-            set
-            {
-                employeeID = value;
-                NotifyPropertyChanged(nameof(HasEmployee));
-            }
+            employeeID = value;
+            NotifyPropertyChanged(nameof(HasEmployee));
         }
+    }
 
-        public string? EmployeeName
+    public string? EmployeeName
+    {
+        get => employeeName;
+        set
         {
-            get => employeeName;
-            set
-            {
-                employeeName = value;
-                NotifyPropertyChanged();
-            }
+            employeeName = value;
+            NotifyPropertyChanged();
         }
+    }
 
-        public string? EmployeeMobile
+    public string? EmployeeMobile
+    {
+        get => employeeMobile;
+        set
         {
-            get => employeeMobile;
-            set
-            {
-                employeeMobile = value;
-                NotifyPropertyChanged();
-                NotifyPropertyChanged(nameof(EmployeeContact));
-            }
+            employeeMobile = value;
+            NotifyPropertyChanged();
+            NotifyPropertyChanged(nameof(EmployeeContact));
         }
+    }
 
-        public string? EmployeeEmail
+    public string? EmployeeEmail
+    {
+        get => employeeEmail;
+        set
         {
-            get => employeeEmail;
-            set
-            {
-                employeeEmail = value;
-                NotifyPropertyChanged();
-                NotifyPropertyChanged(nameof(EmployeeContact));
-            }
+            employeeEmail = value;
+            NotifyPropertyChanged();
+            NotifyPropertyChanged(nameof(EmployeeContact));
         }
+    }
 
-        public string? EmployeeContact
+    public string? EmployeeContact
+    {
+        get
         {
-            get
+            var str = "";
+            if (EmployeeEmail is not null) str = EmployeeEmail;
+            if(EmployeeMobile is not null)
             {
-                var str = "";
-                if (EmployeeEmail is not null) str = EmployeeEmail;
-                if(EmployeeMobile is not null)
+                if (string.IsNullOrWhiteSpace(str))
                 {
-                    if (string.IsNullOrWhiteSpace(str))
-                    {
-                        str = EmployeeMobile;
-                    }
-                    else
-                    {
-                        str += $", {EmployeeMobile}";
-                    }
+                    str = EmployeeMobile;
+                }
+                else
+                {
+                    str += $", {EmployeeMobile}";
                 }
-
-                return string.IsNullOrWhiteSpace(str) ? null : str;
             }
+
+            return string.IsNullOrWhiteSpace(str) ? null : str;
         }
+    }
 
-        public event PropertyChangedEventHandler? PropertyChanged;
+    public event PropertyChangedEventHandler? PropertyChanged;
 
-        private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
-        {
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
-        }
+    private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+}
+
+public class EquipmentScheduleViewModel
+{
+    public bool HasLocation { get; set; }
+
+    public Guid ID { get; set; }
+
+    public string Code { get; set; }
+
+    public string Description { get; set; }
+
+    public List<ScheduleViewModel> Schedules { get; set; }
+}
+
+[ValueConversion(typeof(DateTime), typeof(SolidColorBrush))]
+class ScheduleBackgroundConverter : IValueConverter
+{
+    public Color GetColor(DateTime date)
+    {
+        var diff = date - DateTime.Today;
+        if (diff < TimeSpan.Zero)
+            return Colors.Salmon;
+        else if (diff.TotalDays <= 7)
+            return Colors.Orange;
+        else if (diff.Days <= 30)
+            return Colors.LightYellow;
+        else
+            return Colors.LightGreen;
     }
 
-    public class EquipmentScheduleViewModel
+    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     {
-        public bool HasLocation { get; set; }
+        var date = (DateTime)value;
+        var color = GetColor(date);
+        return new SolidColorBrush(color);
+    }
 
-        public Guid ID { get; set; }
+    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        throw new NotImplementedException();
+    }
+}
+
+/// <summary>
+/// Interaction logic for EquipmentSchedulesDashboard.xaml
+/// </summary>
+public partial class EquipmentSchedulesDashboard : UserControl, IDashboardWidget<EquipmentDashboardGroup, EquipmentSchedulesDashboardProperties>, IHeaderDashboard, IBasePanel
+{
+    public EquipmentSchedulesDashboardProperties Properties { get; set; } = null!;
 
-        public string Code { get; set; }
+    public DashboardHeader Header { get; set; } = new();
 
-        public string Description { get; set; }
+    public event LoadSettings<EquipmentSchedulesDashboardProperties>? LoadSettings;
+    public event SaveSettings<EquipmentSchedulesDashboardProperties>? SaveSettings;
+    public event DataModelUpdateEvent? OnUpdateDataModel;
 
-        public List<ScheduleViewModel> Schedules { get; set; }
+    private Dictionary<Guid, Equipment> Equipment = new();
+
+    private Dictionary<Guid, BitmapImage?> EmployeeImages = new();
+
+    private Dictionary<Guid, Employee> Employees = new();
+
+    private List<EquipmentScheduleViewModel> Models = new();
+
+    public EquipmentSchedulesDashboard()
+    {
+        InitializeComponent();
     }
 
-    [ValueConversion(typeof(DateTime), typeof(SolidColorBrush))]
-    class ScheduleBackgroundConverter : IValueConverter
+    public void Setup()
     {
-        public Color GetColor(DateTime date)
-        {
-            var diff = date - DateTime.Today;
-            if (diff < TimeSpan.Zero)
-                return Colors.Salmon;
-            else if (diff.TotalDays <= 7)
-                return Colors.Orange;
-            else if (diff.Days <= 30)
-                return Colors.LightYellow;
-            else
-                return Colors.LightGreen;
-        }
+        LoadEmployeeImages();
 
-        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            var date = (DateTime)value;
-            var color = GetColor(date);
-            return new SolidColorBrush(color);
-        }
+        SetupHeader();
+    }
 
-        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            throw new NotImplementedException();
-        }
+    #region IBasePanel
+
+    public string SectionName => "EquipmentSchedules";
+    
+    public DataModel DataModel(Selection selection)
+    {
+        return new AutoDataModel<Equipment>(new Filter<Equipment>().All());
+    }
+    
+    public bool IsReady { get; set; }
+    
+    public void CreateToolbarButtons(IPanelHost host)
+    {
     }
 
-    /// <summary>
-    /// Interaction logic for EquipmentSchedulesDashboard.xaml
-    /// </summary>
-    public partial class EquipmentSchedulesDashboard : UserControl, IDashboardWidget<EquipmentDashboardGroup, EquipmentSchedulesDashboardProperties>
+    public Dictionary<string, object[]> Selected()
     {
-        public EquipmentSchedulesDashboardProperties Properties { get; set; } = null!;
-        public event LoadSettings<EquipmentSchedulesDashboardProperties>? LoadSettings;
-        public event SaveSettings<EquipmentSchedulesDashboardProperties>? SaveSettings;
+        return new();
+    }
 
-        private Dictionary<Guid, Equipment> Equipment = new();
+    public void Heartbeat(TimeSpan time)
+    {
+        
+    }
 
-        private Dictionary<Guid, BitmapImage?> EmployeeImages = new();
+    #endregion
 
-        private Dictionary<Guid, Employee> Employees = new();
+    #region Header
 
-        private List<EquipmentScheduleViewModel> Models = new();
+    private CheckBox ShowSchedulesBox = null!;
+    private FilterButton<Equipment> FilterBtn = null!;
 
-        public EquipmentSchedulesDashboard()
+    public void SetupHeader()
+    {
+        ShowSchedulesBox = new CheckBox
         {
-            InitializeComponent();
-        }
-
-        public void Setup()
+            Content = "Only show schedules?",
+            IsChecked = Properties.OnlyShowSchedules,
+            VerticalAlignment = VerticalAlignment.Center
+        };
+        ShowSchedulesBox.Checked += ShowSchedulesBox_Checked;
+        ShowSchedulesBox.Unchecked += ShowSchedulesBox_Checked;
+
+        FilterBtn = new FilterButton<Equipment>(
+            new GlobalConfiguration<CoreFilterDefinitions>(nameof(EquipmentSchedulesDashboard)),
+            new UserConfiguration<CoreFilterDefinitions>(nameof(EquipmentSchedulesDashboard)))
         {
-            LoadEmployeeImages();
-        }
+            Width = 25,
+            Height = 25,
+            Padding = new()
+        };
+        FilterBtn.BuiltInFilters.Add(new("Exclude Disposed", () => new Filter<Equipment>(x => x.Disposed).IsEqualTo(null)));
+        FilterBtn.SetSettings(Properties.Filters, false);
+        FilterBtn.OnFiltersSelected += FilterBtn_OnFiltersSelected;
+        FilterBtn.OnFilterRefresh += FilterBtn_OnFilterRefresh;
+
+        Header.BeginUpdate()
+            .Clear()
+            .Add(ShowSchedulesBox)
+            .AddRight(FilterBtn)
+            .EndUpdate();
+    }
+
+    private void ShowSchedulesBox_Checked(object sender, RoutedEventArgs e)
+    {
+        Properties.OnlyShowSchedules = ShowSchedulesBox.IsChecked == true;
+        Refresh();
+    }
+
+    private void FilterBtn_OnFilterRefresh()
+    {
+        Refresh();
+    }
 
-        private void LoadEmployeeImages()
+    private void FilterBtn_OnFiltersSelected(DynamicGridSelectedFilterSettings filters)
+    {
+        Properties.Filters = filters;
+    }
+
+    #endregion
+
+    private void LoadEmployeeImages()
+    {
+        Task.Run(() =>
         {
-            Task.Run(() =>
-            {
-                var employees = new Client<Employee>()
-                    .Query(
-                        LookupFactory.DefineFilter<Employee>(),
-                        Columns.None<Employee>().Add(
-                            x => x.ID,
-                            x => x.Thumbnail.ID,
-                            x => x.Name,
-                            x => x.Mobile,
-                            x => x.Email))
-                    .ToObjects<Employee>().ToList();
-                var documents = new Client<Document>()
-                    .Query(
-                        new Filter<Document>(x => x.ID).InList(employees.Select(x => x.Thumbnail.ID).ToArray()),
-                        Columns.None<Document>().Add(x => x.ID).Add(x => x.Data))
-                    .ToDictionary<Document, Guid, byte[]>(x => x.ID, x => x.Data);
-                return new { Employees = employees, Documents = documents };
-            }).ContinueWith((task) =>
+            var employees = new Client<Employee>()
+                .Query(
+                    LookupFactory.DefineFilter<Employee>(),
+                    Columns.None<Employee>().Add(
+                        x => x.ID,
+                        x => x.Thumbnail.ID,
+                        x => x.Name,
+                        x => x.Mobile,
+                        x => x.Email))
+                .ToObjects<Employee>().ToList();
+            var documents = new Client<Document>()
+                .Query(
+                    new Filter<Document>(x => x.ID).InList(employees.Select(x => x.Thumbnail.ID).ToArray()),
+                    Columns.None<Document>().Add(x => x.ID).Add(x => x.Data))
+                .ToDictionary<Document, Guid, byte[]>(x => x.ID, x => x.Data);
+            return new { Employees = employees, Documents = documents };
+        }).ContinueWith((task) =>
+        {
+            EmployeeImages = task.Result.Employees.ToDictionary(
+                x => x.ID,
+                x =>
+                {
+                    var document = task.Result.Documents.GetValueOrDefault(x.Thumbnail.ID);
+                    if (document is null)
+                        return null;
+
+                    return ImageUtils.BitmapImageFromBytes(document);
+                });
+            Employees = task.Result.Employees.ToDictionary(x => x.ID, x => x);
+            lock (Models)
             {
-                EmployeeImages = task.Result.Employees.ToDictionary(
-                    x => x.ID,
-                    x =>
-                    {
-                        var document = task.Result.Documents.GetValueOrDefault(x.Thumbnail.ID);
-                        if (document is null)
-                            return null;
-
-                        return ImageUtils.BitmapImageFromBytes(document);
-                    });
-                Employees = task.Result.Employees.ToDictionary(x => x.ID, x => x);
-                lock (Models)
+                var anonymous = PRSDesktop.Resources.anonymous.AsBitmapImage();
+                foreach (var model in Models)
                 {
-                    var anonymous = PRSDesktop.Resources.anonymous.AsBitmapImage();
-                    foreach (var model in Models)
+                    foreach (var schedule in model.Schedules)
                     {
-                        foreach (var schedule in model.Schedules)
-                        {
-                            var employee = Employees?.GetValueOrDefault(schedule.EmployeeID);
-                            schedule.EmployeeImage = EmployeeImages?.GetValueOrDefault(schedule.EmployeeID) ?? anonymous;
-                            schedule.EmployeeMobile = employee?.Mobile;
-                            schedule.EmployeeName = employee?.Name;
-                            schedule.EmployeeEmail = employee?.Email;
-                        }
+                        var employee = Employees?.GetValueOrDefault(schedule.EmployeeID);
+                        schedule.EmployeeImage = EmployeeImages?.GetValueOrDefault(schedule.EmployeeID) ?? anonymous;
+                        schedule.EmployeeMobile = employee?.Mobile;
+                        schedule.EmployeeName = employee?.Name;
+                        schedule.EmployeeEmail = employee?.Email;
                     }
                 }
-            }, TaskScheduler.FromCurrentSynchronizationContext());
-        }
+            }
+        }, TaskScheduler.FromCurrentSynchronizationContext());
+    }
 
-        public void Refresh()
+    public void Refresh()
+    {
+        var eqFilter = FilterBtn.GetFilter();
+
+        var results = Client.QueryMultiple(
+            new KeyedQueryDef<Equipment>(
+                eqFilter,
+                Columns.None<Equipment>().Add(x => x.ID)
+                    .Add(x => x.Code)
+                    .Add(x => x.Description)
+                    .Add(x => x.TrackerLink.ID)
+                    .Add(x => x.TrackerLink.Location.Latitude)
+                    .Add(x => x.TrackerLink.Location.Longitude)
+                    .Add(x => x.TrackerLink.Location.Timestamp)),
+            new KeyedQueryDef<Schedule>(
+                new Filter<Schedule>(x => x.DocumentClass).IsEqualTo(typeof(Equipment).EntityName())
+                    .And(x => x.IncludeInAggregate).IsEqualTo(true),
+                Columns.None<Schedule>().Add(x => x.ID)
+                    .Add(x => x.Title)
+                    .Add(x => x.DueDate)
+                    .Add(x => x.DocumentID)
+                    .Add(x => x.EmployeeLink.ID)));
+
+        var equipmentItems = results.Get<Equipment>().Rows.Select(x => x.ToObject<Equipment>());
+        Equipment = equipmentItems.ToDictionary(x => x.ID, x => x);
+        var schedules = results.Get<Schedule>().Rows
+            .Select(x => x.ToObject<Schedule>())
+            .GroupBy(x => x.DocumentID)
+            .ToDictionary(x => x.Key, x => x.OrderBy(x => x.DueDate).ToList());
+
+        var anonymous = PRSDesktop.Resources.anonymous.AsBitmapImage();
+
+        lock (Models)
         {
-            var results = Client.QueryMultiple(
-                new KeyedQueryDef<Equipment>(
-                    new Filter<Equipment>().All(),
-                    Columns.None<Equipment>().Add(x => x.ID)
-                        .Add(x => x.Code)
-                        .Add(x => x.Description)
-                        .Add(x => x.TrackerLink.ID)
-                        .Add(x => x.TrackerLink.Location.Latitude)
-                        .Add(x => x.TrackerLink.Location.Longitude)
-                        .Add(x => x.TrackerLink.Location.Timestamp)),
-                new KeyedQueryDef<Schedule>(
-                    new Filter<Schedule>(x => x.DocumentClass).IsEqualTo(typeof(Equipment).EntityName())
-                        .And(x => x.IncludeInAggregate).IsEqualTo(true),
-                    Columns.None<Schedule>().Add(x => x.ID)
-                        .Add(x => x.Title)
-                        .Add(x => x.DueDate)
-                        .Add(x => x.DocumentID)
-                        .Add(x => x.EmployeeLink.ID)));
-
-            var equipmentItems = results.Get<Equipment>().Rows.Select(x => x.ToObject<Equipment>());
-            Equipment = equipmentItems.ToDictionary(x => x.ID, x => x);
-            var schedules = results.Get<Schedule>().Rows
-                .Select(x => x.ToObject<Schedule>())
-                .GroupBy(x => x.DocumentID)
-                .ToDictionary(x => x.Key, x => x.OrderBy(x => x.DueDate).ToList());
-
-            var anonymous = PRSDesktop.Resources.anonymous.AsBitmapImage();
-
-            lock (Models)
+            Models = new List<EquipmentScheduleViewModel>();
+            foreach (var equipmentItem in equipmentItems)
             {
-                Models = new List<EquipmentScheduleViewModel>();
-                foreach (var equipmentItem in equipmentItems)
+                var equipmentSchedules = schedules.GetValueOrDefault(equipmentItem.ID);
+                if(equipmentSchedules is null && Properties.OnlyShowSchedules)
+                {
+                    continue;
+                }
+
+                var model = new EquipmentScheduleViewModel
                 {
-                    var equipmentSchedules = schedules.GetValueOrDefault(equipmentItem.ID);
-                    if (equipmentSchedules is not null)
+                    Code = equipmentItem.Code,
+                    Description = equipmentItem.Description,
+                    ID = equipmentItem.ID,
+                    HasLocation = equipmentItem.TrackerLink.ID != Guid.Empty
+                };
+
+                model.Schedules = (equipmentSchedules ?? Enumerable.Empty<Schedule>())
+                    .Select(x =>
                     {
-                        var model = new EquipmentScheduleViewModel
+                        var employee = Employees.GetValueOrDefault(x.EmployeeLink.ID);
+                        return new ScheduleViewModel
                         {
-                            Code = equipmentItem.Code,
-                            Description = equipmentItem.Description,
-                            ID = equipmentItem.ID,
-                            HasLocation = equipmentItem.TrackerLink.ID != Guid.Empty
+                            Title = x.Title,
+                            DueDate = x.DueDate,
+                            ID = x.ID,
+                            EmployeeImage = EmployeeImages.GetValueOrDefault(x.EmployeeLink.ID) ?? anonymous,
+                            EmployeeID = x.EmployeeLink.ID,
+                            EmployeeEmail = employee?.Email,
+                            EmployeeMobile = employee?.Mobile,
+                            EmployeeName = employee?.Name
                         };
+                    })
+                    .ToList() ?? [];
 
-                        model.Schedules = equipmentSchedules
-                            .Select(x =>
-                            {
-                                var employee = Employees.GetValueOrDefault(x.EmployeeLink.ID);
-                                return new ScheduleViewModel
-                                {
-                                    Title = x.Title,
-                                    DueDate = x.DueDate,
-                                    ID = x.ID,
-                                    EmployeeImage = EmployeeImages.GetValueOrDefault(x.EmployeeLink.ID) ?? anonymous,
-                                    EmployeeID = x.EmployeeLink.ID,
-                                    EmployeeEmail = employee?.Email,
-                                    EmployeeMobile = employee?.Mobile,
-                                    EmployeeName = employee?.Name
-                                };
-                            })
-                            .ToList() ?? new List<ScheduleViewModel>();
-
-                        Models.Add(model);
-                    }
-                }
+                Models.Add(model);
             }
-            EquipmentList.ItemsSource = Models;
         }
+        EquipmentList.ItemsSource = Models;
+    }
+
+    public void Shutdown(CancelEventArgs? cancel)
+    {
+    }
+
+    private void Border_ContextMenuOpening(object sender, ContextMenuEventArgs e)
+    {
+        if (sender is not FrameworkElement element || element.Tag is not Guid equipmentID) return;
+
+        var menu = new ContextMenu();
+        menu.AddItem("View Equipment", null, equipmentID, ViewEquipment_Click);
+        menu.AddItem("Add Schedule", null, equipmentID, CreateSchedule_Click);
+        menu.IsOpen = true;
+
+        e.Handled = true;
+    }
+
+    private void CreateSchedule_Click(Guid equipmentID)
+    {
+        var schedule = new Schedule();
+        schedule.DocumentClass = typeof(Equipment).EntityName();
+        schedule.DocumentID = equipmentID;
 
-        public void Shutdown(CancelEventArgs? cancel)
+        if (DynamicGridUtils.EditEntity(schedule))
         {
+            Refresh();
         }
+    }
 
-        private void ViewEquipment_Click(object sender, System.Windows.RoutedEventArgs e)
-        {
-            var equipmentID = (Guid)(sender as MenuItem)!.Tag;
+    private void Grid_ContextMenuOpening(object sender, ContextMenuEventArgs e)
+    {
+        if (sender is not FrameworkElement element || element.Tag is not Guid equipmentID) return;
 
-            var equipment = new Client<Equipment>().Load(
-                new Filter<Equipment>(x => x.ID).IsEqualTo(equipmentID)).FirstOrDefault();
-            if(equipment != null)
-            {
-                var grid = DynamicGridUtils.CreateDynamicGrid(typeof(DynamicDataGrid<>), typeof(Equipment));
-                if(grid.EditItems(new[] { equipment }))
-                {
-                    new Client<Equipment>().Save(equipment, "Edited by user from schedules dashboard");
-                    Refresh();
-                }
-            }
-        }
+        var menu = new ContextMenu();
+        menu.AddItem("Add Schedule", null, equipmentID, CreateSchedule_Click);
+        menu.IsOpen = true;
 
-        private void ViewSchedule_Click(object sender, System.Windows.RoutedEventArgs e)
-        {
-            var scheduleID = (Guid)(sender as MenuItem)!.Tag;
+        e.Handled = true;
+    }
 
-            var schedule = new Client<Schedule>().Load(
-                new Filter<Schedule>(x => x.ID).IsEqualTo(scheduleID)).FirstOrDefault();
-            if (schedule != null)
-            {
-                var grid = DynamicGridUtils.CreateDynamicGrid(typeof(DynamicDataGrid<>), typeof(Schedule));
-                if (grid.EditItems(new[] { schedule }))
-                {
-                    new Client<Schedule>().Save(schedule, "Edited by user from schedules dashboard");
-                    Refresh();
-                }
-            }
-        }
+    private void Schedule_ContextMenuOpening(object sender, ContextMenuEventArgs e)
+    {
+        if (sender is not FrameworkElement element || element.Tag is not Guid scheduleID) return;
 
-        private void ViewEmployee_Click(object sender, System.Windows.RoutedEventArgs e)
-        {
-            var employeeID = (Guid)(sender as MenuItem)!.Tag;
+        var menu = new ContextMenu();
+        menu.AddItem("View Schedule", null, scheduleID, ViewSchedule_Click);
+        menu.IsOpen = true;
 
-            var employee = new Client<Employee>().Load(
-                new Filter<Employee>(x => x.ID).IsEqualTo(employeeID)).FirstOrDefault();
-            if (employee != null)
-            {
-                var grid = DynamicGridUtils.CreateDynamicGrid(typeof(DynamicDataGrid<>), typeof(Employee));
-                if (grid.EditItems(new[] { employee }))
-                {
-                    new Client<Employee>().Save(employee, "Edited by user from schedules dashboard");
-                    Refresh();
-                }
-            }
-        }
+        e.Handled = true;
+    }
 
-        private void EquipmentLocation_Click(object sender, System.Windows.RoutedEventArgs e)
+    private void Employee_ContextMenuOpening(object sender, ContextMenuEventArgs e)
+    {
+        if (sender is not FrameworkElement element || element.Tag is not Guid employeeID) return;
+
+        if (employeeID == Guid.Empty) return;
+
+        var menu = new ContextMenu();
+        menu.AddItem("View Employee", null, employeeID, ViewEmployee_Click);
+        menu.IsOpen = true;
+
+        e.Handled = true;
+    }
+
+    private void ViewEquipment_Click(Guid equipmentID)
+    {
+        if (DynamicGridUtils.EditEntity<Equipment>(equipmentID))
         {
-            var equipmentID = (Guid)(sender as Button)!.Tag;
+            Refresh();
+        }
+    }
 
-            if (!Equipment.TryGetValue(equipmentID, out var equipment))
-                return;
+    private void ViewSchedule_Click(Guid scheduleID)
+    {
+        if (DynamicGridUtils.EditEntity<Schedule>(scheduleID))
+        {
+            Refresh();
+        }
+    }
 
-            var form = new MapForm(
-                equipment.TrackerLink.Location.Latitude,
-                equipment.TrackerLink.Location.Longitude,
-                equipment.TrackerLink.Location.Timestamp);
-            form.ShowDialog();
+    private void ViewEmployee_Click(Guid employeeID)
+    {
+        if (DynamicGridUtils.EditEntity<Employee>(employeeID))
+        {
+            Refresh();
         }
     }
 
-    public class EquipmentSchedulesDashboardElement : 
-        DashboardElement<EquipmentSchedulesDashboard, EquipmentDashboardGroup, EquipmentSchedulesDashboardProperties> { }
+    private void EquipmentLocation_Click(object sender, System.Windows.RoutedEventArgs e)
+    {
+        var equipmentID = (Guid)(sender as Button)!.Tag;
+
+        if (!Equipment.TryGetValue(equipmentID, out var equipment))
+            return;
+
+        var form = new MapForm(
+            equipment.TrackerLink.Location.Latitude,
+            equipment.TrackerLink.Location.Longitude,
+            equipment.TrackerLink.Location.Timestamp);
+        form.ShowDialog();
+    }
 }
+
+public class EquipmentSchedulesDashboardElement : 
+    DashboardElement<EquipmentSchedulesDashboard, EquipmentDashboardGroup, EquipmentSchedulesDashboardProperties> { }

+ 23 - 11
prs.desktop/MainWindow.xaml.cs

@@ -2793,6 +2793,10 @@ public partial class MainWindow : IPanelHostControl
             }
             menu.AddSeparatorIfNeeded();
             CreateTaskMenu(menu, "(No Task Selected)", Guid.Empty);
+            if(_kanbantrackingassignment is not null && _kanbantrackingassignment.Task.ID != Guid.Empty)
+            {
+                menu.AddItem("View Task", null, _kanbantrackingassignment.Task.ID, ViewTrackingKanban_Click);
+            }
             menu.AddSeparatorIfNeeded();
 
             var filterItem = menu.AddItem(TrackingKanbanFilterComponent.Text, TrackingKanbanFilterComponent.Image, null);
@@ -2803,6 +2807,17 @@ public partial class MainWindow : IPanelHostControl
         _trackingKanbanMenu = menu;
     }
 
+    private void ViewTrackingKanban_Click(Guid guid)
+    {
+        var item = Client.Query(
+            new Filter<Kanban>(x => x.ID).IsEqualTo(guid),
+            DynamicGridUtils.LoadEditorColumns<Kanban>())
+            .ToObjects<Kanban>().FirstOrDefault();
+        if (item is null) return;
+
+        DynamicGridUtils.EditEntity(item);
+    }
+
     private Assignment? _kanbantrackingassignment = null;
     private CoreFilterDefinitions? _kanbanTrackingFilter = null;
 
@@ -2852,18 +2867,15 @@ public partial class MainWindow : IPanelHostControl
 
     private void CreateTaskMenu(ItemsControl menu, string title, Guid id)
     {
-        var item = new MenuItem()
-        {
-            Header = title,
-            Tag = id
-        };
-        item.Click += (o, args) =>
-        {
-            if (o is not MenuItem menu) return;
+        menu.AddCheckMenuItem(title, (title, id), TrackingKanbanMenuItem_Click, isChecked: _kanbantrackingassignment?.Task.ID == id);
+    }
 
-            SetTrackingKanban((Guid)item.Tag, (menu.Header as string) ?? "");
-        };
-        menu.Items.Add(item);
+    private void TrackingKanbanMenuItem_Click((string title, Guid id) tuple, bool isChecked)
+    {
+        if (isChecked)
+        {
+            SetTrackingKanban(tuple.id, tuple.title);
+        }
     }
 
     #endregion

+ 3 - 26
prs.desktop/Panels/Equipment/EquipmentMaintenancePanel.cs

@@ -5,34 +5,11 @@ using InABox.Core;
 using InABox.Wpf;
 using PRSDesktop.Dashboards;
 using Comal.Classes;
+using PRSDesktop.WidgetGroups;
 
 namespace PRSDesktop;
 
-public class EquipmentMaintenancePanel : EquipmentSchedulesDashboard, IPanel<Equipment>
+public class EquipmentMaintenancePanel : DashboardContainerPanel<EquipmentSchedulesDashboard, EquipmentDashboardGroup, EquipmentSchedulesDashboardProperties>,
+    IPanel<Equipment>
 {
-    
-    public string SectionName => "EquipmentSchedules";
-    
-    public DataModel DataModel(Selection selection)
-    {
-        return new AutoDataModel<Equipment>(new Filter<Equipment>().All());
-    }
-
-    public event DataModelUpdateEvent? OnUpdateDataModel;
-    
-    public bool IsReady { get; set; }
-    
-    public void CreateToolbarButtons(IPanelHost host)
-    {
-    }
-
-    public Dictionary<string, object[]> Selected()
-    {
-        return new();
-    }
-
-    public void Heartbeat(TimeSpan time)
-    {
-        
-    }
 }

+ 211 - 212
prs.shared/Grids/ScheduleItemGrid.cs

@@ -16,277 +16,276 @@ using MessageBox = System.Windows.MessageBox;
 using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
 using SaveFileDialog = Microsoft.Win32.SaveFileDialog;
 
-namespace PRS.Shared
+namespace PRS.Shared;
+
+internal class ScheduleItemGrid : DynamicDataGrid<Schedule>//, IDefaultGrid
 {
-    internal class ScheduleItemGrid : DynamicDataGrid<Schedule>
+    private readonly BitmapImage disabled = PRS.Shared.Resources.disabled.AsBitmapImage();
+
+    private readonly BitmapImage tick = PRS.Shared.Resources.tick.AsBitmapImage();
+
+    protected override void Init()
+    {
+        base.Init();
+        ActionColumns.Add(new DynamicTickColumn<Schedule, bool>(x => x.Active, tick, tick, disabled, CheckClick));
+        HiddenColumns.Add(x => x.Active);
+        HiddenColumns.Add(x => x.Title);
+        HiddenColumns.Add(x => x.DocumentClass);
+
+        AddButton("Export", PRS.Shared.Resources.download.AsBitmapImage(), SaveSchedules);
+        AddButton("Import", PRS.Shared.Resources.upload.AsBitmapImage(), LoadSchedules);
+    }
+
+    protected override void DoReconfigure(DynamicGridOptions options)
     {
-        private readonly BitmapImage disabled = PRS.Shared.Resources.disabled.AsBitmapImage();
+        base.DoReconfigure(options);
+        options.RecordCount = true;
+        options.SelectColumns = true;
+        options.MultiSelect = true;
+    }
 
-        private readonly BitmapImage tick = PRS.Shared.Resources.tick.AsBitmapImage();
+    public CoreTable Schedules { get; set; }
 
-        protected override void Init()
-        {
-            base.Init();
-            ActionColumns.Add(new DynamicTickColumn<Schedule, bool>(x => x.Active, tick, tick, disabled, CheckClick));
-            HiddenColumns.Add(x => x.Active);
-            HiddenColumns.Add(x => x.Title);
-            HiddenColumns.Add(x => x.DocumentClass);
-
-            AddButton("Export", PRS.Shared.Resources.download.AsBitmapImage(), SaveSchedules);
-            AddButton("Import", PRS.Shared.Resources.upload.AsBitmapImage(), LoadSchedules);
-        }
+    public Type DocumentType { get; set; }
+    public Guid DocumentID { get; set; }
 
-        protected override void DoReconfigure(DynamicGridOptions options)
+    private bool LoadSchedules(Button sender, CoreRow[] rows)
+    {
+        if (rows.Length != 1)
         {
-            base.DoReconfigure(options);
-            options.RecordCount = true;
-            options.SelectColumns = true;
-            options.MultiSelect = true;
+            MessageBox.Show("Please select only one row to process");
+            return false;
         }
 
-        public CoreTable Schedules { get; set; }
-
-        public Type DocumentType { get; set; }
-        public Guid DocumentID { get; set; }
+        var row = rows.First();
 
-        private bool LoadSchedules(Button sender, CoreRow[] rows)
+        var dlg = new OpenFileDialog();
+        dlg.Filter = "PRS Schedule Files (*.schedule)|*.schedule";
+        if (dlg.ShowDialog() == true)
         {
-            if (rows.Length != 1)
+            Progress.Show("");
+            var json = File.ReadAllText(dlg.FileName);
+            Schedule[] schedules = { };
+            try
             {
-                MessageBox.Show("Please select only one row to process");
-                return false;
+                schedules = Serialization.Deserialize<Schedule[]>(json);
             }
-
-            var row = rows.First();
-
-            var dlg = new OpenFileDialog();
-            dlg.Filter = "PRS Schedule Files (*.schedule)|*.schedule";
-            if (dlg.ShowDialog() == true)
+            catch
             {
-                Progress.Show("");
-                var json = File.ReadAllText(dlg.FileName);
-                Schedule[] schedules = { };
-                try
-                {
-                    schedules = Serialization.Deserialize<Schedule[]>(json);
-                }
-                catch
-                {
-                    Progress.Close();
-                    MessageBox.Show("[" + Path.GetFileName(dlg.FileName) + "] is not a valid schedule file!");
-                    return false;
-                }
-
-                if (!schedules.Any())
-                {
-                    Progress.Close();
-                    MessageBox.Show("[" + Path.GetFileName(dlg.FileName) + "] does not contain any schedules!");
-                    return false;
-                }
-
-                foreach (var schedule in schedules)
-                {
-                    schedule.DocumentID = DocumentID;
-                    schedule.DocumentClass = DocumentType.EntityName();
-                    schedule.ID = Guid.Empty;
-                    schedule.Active = false;
-                    schedule.DueDate = DateTime.MinValue;
-                }
-
-                new Client<Schedule>().Save(schedules, "Imported from [" + Path.GetFileName(dlg.FileName) + "]");
                 Progress.Close();
-                MessageBox.Show(string.Format("{0} schedules loaded from [{1}]", schedules.Length, Path.GetFileName(dlg.FileName)));
+                MessageBox.Show("[" + Path.GetFileName(dlg.FileName) + "] is not a valid schedule file!");
+                return false;
             }
 
-            return true;
-        }
-
-        private bool SaveSchedules(Button sender, CoreRow[] rows)
-        {
-            if (rows.Any())
+            if (!schedules.Any())
             {
-                MessageBox.Show("Please select at least one schedule before Exporting!");
+                Progress.Close();
+                MessageBox.Show("[" + Path.GetFileName(dlg.FileName) + "] does not contain any schedules!");
                 return false;
             }
 
-            var names = new List<string>();
-            rows.ForEach(r => names.Add(r.Get<Schedule, string>(c => c.Title)));
-            var filename = DocumentType.Name + " (" + string.Join(" + ", names) + ")";
-            Path.GetInvalidFileNameChars().ForEach(c => filename = filename.Replace(c.ToString(), ""));
-            Path.GetInvalidPathChars().ForEach(c => filename = filename.Replace(c.ToString(), ""));
-
-            var dlg = new SaveFileDialog();
-            dlg.Filter = "PRS Schedule Files (*.schedule)|*.schedule";
-            dlg.FileName = filename + ".schedule";
-            dlg.AddExtension = false;
-            if (dlg.ShowDialog() == true)
+            foreach (var schedule in schedules)
             {
-                Progress.Show("");
-
-                Filter<Schedule> filter = null;
-                foreach (var schedule in SelectedRows)
-                    if (filter == null)
-                        filter = new Filter<Schedule>(x => x.ID).IsEqualTo(schedule.Get<Schedule, Guid>(x => x.ID));
-                    else
-                        filter = filter.Or(x => x.ID).IsEqualTo(schedule.Get<Schedule, Guid>(x => x.ID));
-
-                var schedules = new Client<Schedule>().Load(filter);
-                foreach (var schedule in schedules)
-                {
-                    schedule.DocumentID = DocumentID;
-                    schedule.DocumentClass = DocumentType.EntityName();
-                    schedule.ID = Guid.Empty;
-                    schedule.Active = false;
-                    schedule.DueDate = DateTime.MinValue;
-                }
-
-                var json = Serialization.Serialize(schedules);
-                File.WriteAllText(dlg.FileName + ".schedule", json);
-                Progress.Close();
-                MessageBox.Show(string.Format("{0} schedules saved to [{1}]", SelectedRows.Length, Path.GetFileName(dlg.FileName)));
+                schedule.DocumentID = DocumentID;
+                schedule.DocumentClass = DocumentType.EntityName();
+                schedule.ID = Guid.Empty;
+                schedule.Active = false;
+                schedule.DueDate = DateTime.MinValue;
             }
 
+            new Client<Schedule>().Save(schedules, "Imported from [" + Path.GetFileName(dlg.FileName) + "]");
+            Progress.Close();
+            MessageBox.Show(string.Format("{0} schedules loaded from [{1}]", schedules.Length, Path.GetFileName(dlg.FileName)));
+        }
+
+        return true;
+    }
+
+    private bool SaveSchedules(Button sender, CoreRow[] rows)
+    {
+        if (rows.Any())
+        {
+            MessageBox.Show("Please select at least one schedule before Exporting!");
             return false;
         }
 
-        private bool CheckClick(CoreRow row)
+        var names = new List<string>();
+        rows.ForEach(r => names.Add(r.Get<Schedule, string>(c => c.Title)));
+        var filename = DocumentType.Name + " (" + string.Join(" + ", names) + ")";
+        Path.GetInvalidFileNameChars().ForEach(c => filename = filename.Replace(c.ToString(), ""));
+        Path.GetInvalidPathChars().ForEach(c => filename = filename.Replace(c.ToString(), ""));
+
+        var dlg = new SaveFileDialog();
+        dlg.Filter = "PRS Schedule Files (*.schedule)|*.schedule";
+        dlg.FileName = filename + ".schedule";
+        dlg.AddExtension = false;
+        if (dlg.ShowDialog() == true)
         {
-            var Due = row.Get<Schedule, DateTime>(x => x.DueDate);
-            if (Due.Equals(DateTime.MinValue))
-            {
-                MessageBox.Show("Schedule must have a due date!");
-                return false;
-            }
+            Progress.Show("");
 
-            using (var client = new Client<Schedule>())
+            Filter<Schedule> filter = null;
+            foreach (var schedule in SelectedRows)
+                if (filter == null)
+                    filter = new Filter<Schedule>(x => x.ID).IsEqualTo(schedule.Get<Schedule, Guid>(x => x.ID));
+                else
+                    filter = filter.Or(x => x.ID).IsEqualTo(schedule.Get<Schedule, Guid>(x => x.ID));
+
+            var schedules = new Client<Schedule>().Load(filter);
+            foreach (var schedule in schedules)
             {
-                var schedule = client.Query(
-                    new Filter<Schedule>(x => x.ID).IsEqualTo(row.Get<Schedule, Guid>(x => x.ID)),
-                    Columns.Required<Schedule>().Add(x => x.Active))
-                    .ToObjects<Schedule>().First();
-                schedule.Active = !schedule.Active;
-                client.Save(schedule, schedule.Active ? "Activated Schedule" : "Disabled Schedule");
+                schedule.DocumentID = DocumentID;
+                schedule.DocumentClass = DocumentType.EntityName();
+                schedule.ID = Guid.Empty;
+                schedule.Active = false;
+                schedule.DueDate = DateTime.MinValue;
             }
 
-            return true;
+            var json = Serialization.Serialize(schedules);
+            File.WriteAllText(dlg.FileName + ".schedule", json);
+            Progress.Close();
+            MessageBox.Show(string.Format("{0} schedules saved to [{1}]", SelectedRows.Length, Path.GetFileName(dlg.FileName)));
         }
 
-        protected override void Reload(
-        	Filters<Schedule> criteria, Columns<Schedule> columns, ref SortOrder<Schedule>? sort,
-        	CancellationToken token, Action<CoreTable?, Exception?> action)
+        return false;
+    }
+
+    private bool CheckClick(CoreRow row)
+    {
+        var Due = row.Get<Schedule, DateTime>(x => x.DueDate);
+        if (Due.Equals(DateTime.MinValue))
         {
-            criteria.Add(new Filter<Schedule>(x => x.DocumentID).IsEqualTo(DocumentID));
-            sort = new SortOrder<Schedule>(x => x.DueDate);
-            base.Reload(criteria, columns, ref sort, token, action);
+            MessageBox.Show("Schedule must have a due date!");
+            return false;
         }
 
-        public override Schedule CreateItem()
+        using (var client = new Client<Schedule>())
         {
-            if(DocumentType is null)
-            {
-                throw new Exception("Cannot create item when DocumentType is null.");
-            }
-
-            var schedule = base.CreateItem();
-            schedule.DocumentClass = DocumentType.EntityName();
-            schedule.DocumentID = DocumentID;
-            if (DocumentType == typeof(CustomModule) || DocumentType == typeof(ScheduledScript))
-                schedule.ScheduleType = ScheduleType.None;
-            return schedule;
+            var schedule = client.Query(
+                new Filter<Schedule>(x => x.ID).IsEqualTo(row.Get<Schedule, Guid>(x => x.ID)),
+                Columns.Required<Schedule>().Add(x => x.Active))
+                .ToObjects<Schedule>().First();
+            schedule.Active = !schedule.Active;
+            client.Save(schedule, schedule.Active ? "Activated Schedule" : "Disabled Schedule");
         }
 
-        public override void SaveItem(Schedule item)
+        return true;
+    }
+
+    protected override void Reload(
+    	Filters<Schedule> criteria, Columns<Schedule> columns, ref SortOrder<Schedule>? sort,
+    	CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        criteria.Add(new Filter<Schedule>(x => x.DocumentID).IsEqualTo(DocumentID));
+        sort = new SortOrder<Schedule>(x => x.DueDate);
+        base.Reload(criteria, columns, ref sort, token, action);
+    }
+
+    public override Schedule CreateItem()
+    {
+        if(DocumentType is null)
         {
-            if(DocumentType is not null)
-            {
-                item.DocumentClass = DocumentType.EntityName();
-            }
-            else
-            {
-                if (string.IsNullOrWhiteSpace(item.DocumentClass))
-                {
-                    throw new Exception("Cannot save item when DocumentType is null.");
-                }
-            }
-            base.SaveItem(item);
+            throw new Exception("Cannot create item when DocumentType is null.");
         }
 
-        protected override void DoReconfigureEditors(DynamicEditorGrid grid, Schedule[] items)
-        {
-            base.DoReconfigureEditors(grid, items);
+        var schedule = base.CreateItem();
+        schedule.DocumentClass = DocumentType.EntityName();
+        schedule.DocumentID = DocumentID;
+        if (DocumentType == typeof(CustomModule) || DocumentType == typeof(ScheduledScript))
+            schedule.ScheduleType = ScheduleType.None;
+        return schedule;
+    }
 
-            var frequency = grid.FindEditor("Frequency");
-            if (frequency != null)
+    public override void SaveItem(Schedule item)
+    {
+        if(DocumentType is not null)
+        {
+            item.DocumentClass = DocumentType.EntityName();
+        }
+        else
+        {
+            if (string.IsNullOrWhiteSpace(item.DocumentClass))
             {
-                var freq = (int)frequency.GetValue("Frequency");
-                var period = grid.FindEditor("Period");
-                period?.SetEnabled(freq > 0);
-                var due = grid.FindEditor("DueDate");
-                due?.SetEnabled(freq > 0);
+                throw new Exception("Cannot save item when DocumentType is null.");
             }
+        }
+        base.SaveItem(item);
+    }
 
-            var threshold = grid.FindEditor("Threshold");
-            if (threshold != null)
-            {
-                var thresh = (int)threshold.GetValue("Threshold");
-                var trigger = grid.FindEditor("Trigger");
-                trigger?.SetEnabled(thresh > 0);
-                var next = grid.FindEditor("DueThreshold");
-                next?.SetEnabled(thresh > 0);
-            }
+    protected override void DoReconfigureEditors(DynamicEditorGrid grid, Schedule[] items)
+    {
+        base.DoReconfigureEditors(grid, items);
 
-            var scheduleTypeEditor = grid.FindEditor("ScheduleType");
-            if(scheduleTypeEditor != null)
-            {
-                var scheduleType = (ScheduleType)scheduleTypeEditor.GetValue("ScheduleType");
-                var taskTypeEditor = grid.FindEditor(nameof(Schedule.KanbanType));
-                taskTypeEditor?.SetEnabled(scheduleType == ScheduleType.Task);
-            }
+        var frequency = grid.FindEditor("Frequency");
+        if (frequency != null)
+        {
+            var freq = (int)frequency.GetValue("Frequency");
+            var period = grid.FindEditor("Period");
+            period?.SetEnabled(freq > 0);
+            var due = grid.FindEditor("DueDate");
+            due?.SetEnabled(freq > 0);
         }
 
-        protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
+        var threshold = grid.FindEditor("Threshold");
+        if (threshold != null)
         {
-            var types = new List<Type> { typeof(CustomModule), typeof(ScheduledScript), typeof(Employee), typeof(Equipment) };
+            var thresh = (int)threshold.GetValue("Threshold");
+            var trigger = grid.FindEditor("Trigger");
+            trigger?.SetEnabled(thresh > 0);
+            var next = grid.FindEditor("DueThreshold");
+            next?.SetEnabled(thresh > 0);
+        }
 
-            var columns = new List<string> { "ScheduleType" };
-            var schedule = (Schedule)item;
+        var scheduleTypeEditor = grid.FindEditor("ScheduleType");
+        if(scheduleTypeEditor != null)
+        {
+            var scheduleType = (ScheduleType)scheduleTypeEditor.GetValue("ScheduleType");
+            var taskTypeEditor = grid.FindEditor(nameof(Schedule.KanbanType));
+            taskTypeEditor?.SetEnabled(scheduleType == ScheduleType.Task);
+        }
+    }
 
-            var documentType = schedule.DocumentType();
-            if (documentType == typeof(CustomModule) || documentType == typeof(ScheduledScript))
-                columns.AddRange(new[]
-                {
-                    "Description", "LeadTime", "EmployeeLink.ID", "ManagerLink.ID", "Report.ID", "Threshold", "Trigger", "DueThreshold", "Rollover",
-                    "QAForm"
-                });
+    protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
+    {
+        var types = new List<Type> { typeof(CustomModule), typeof(ScheduledScript), typeof(Employee), typeof(Equipment) };
 
-            else if (documentType == typeof(Employee))
-                columns.AddRange(new[] { "Description", "EmployeeLink.ID", "Threshold", "Trigger", "DueThreshold" });
+        var columns = new List<string> { "ScheduleType" };
+        var schedule = (Schedule)item;
 
-            else if (documentType == typeof(Customer))
-                columns.AddRange(new[] { "Threshold", "Trigger", "DueThreshold" });
+        var documentType = schedule.DocumentType();
+        if (documentType == typeof(CustomModule) || documentType == typeof(ScheduledScript))
+            columns.AddRange(new[]
+            {
+                "Description", "LeadTime", "EmployeeLink.ID", "ManagerLink.ID", "Report.ID", "Threshold", "Trigger", "DueThreshold", "Rollover",
+                "QAForm"
+            });
 
+        else if (documentType == typeof(Employee))
+            columns.AddRange(new[] { "Description", "EmployeeLink.ID", "Threshold", "Trigger", "DueThreshold" });
 
-            if (types.Contains(documentType) && columns.Contains(column.ColumnName))
-                return new NullEditor();
-            return base.GetEditor(item, column);
-        }
+        else if (documentType == typeof(Customer))
+            columns.AddRange(new[] { "Threshold", "Trigger", "DueThreshold" });
 
-        public override DynamicEditorPages LoadEditorPages(Schedule item)
-        {
-            var pages = base.LoadEditorPages(item);
-            foreach (var page in pages.ToArray())
-            {
-                if (page is IDynamicOneToManyGrid<Schedule, Kanban> && item.ScheduleType != ScheduleType.Task)
-                    pages = new DynamicEditorPages(pages.Where(x => x != page));
-                else if (page is IDynamicOneToManyGrid<Schedule, Job> && item.ScheduleType != ScheduleType.Job)
-                    pages = new DynamicEditorPages(pages.Where(x => x != page));
-            }
-            return pages;
-        }
 
-        protected override void DefineLookups(ILookupEditorControl sender, Schedule[] items, bool async = true)
+        if (types.Contains(documentType) && columns.Contains(column.ColumnName))
+            return new NullEditor();
+        return base.GetEditor(item, column);
+    }
+
+    public override DynamicEditorPages LoadEditorPages(Schedule item)
+    {
+        var pages = base.LoadEditorPages(item);
+        foreach (var page in pages.ToArray())
         {
-            base.DefineLookups(sender, items, async);
+            if (page is IDynamicOneToManyGrid<Schedule, Kanban> && item.ScheduleType != ScheduleType.Task)
+                pages = new DynamicEditorPages(pages.Where(x => x != page));
+            else if (page is IDynamicOneToManyGrid<Schedule, Job> && item.ScheduleType != ScheduleType.Job)
+                pages = new DynamicEditorPages(pages.Where(x => x != page));
         }
+        return pages;
+    }
+
+    protected override void DefineLookups(ILookupEditorControl sender, Schedule[] items, bool async = true)
+    {
+        base.DefineLookups(sender, items, async);
     }
 }