Browse Source

Prototype Equipment Planner

frogsoftware 1 year ago
parent
commit
d65ec45e38

+ 11 - 1
prs.classes/Entities/Activity/ActivityLookups.cs

@@ -2,7 +2,7 @@
 
 namespace Comal.Classes
 {
-    public class ActivityLookups : EntityLookup<Activity>
+    public class ActivityLookups : EntityLookup<Activity>, ILookupDefinition<Activity,EquipmentActivity>
     {
         public override Columns<Activity> DefineColumns()
         {
@@ -22,5 +22,15 @@ namespace Comal.Classes
         {
             return new SortOrder<Activity>(x => x.Code);
         }
+
+        public Filter<Activity> DefineFilter(EquipmentActivity[] items)
+        {
+            return new Filter<Activity>(x => x.IsLeave).IsEqualTo(false);
+        }
+
+        public Columns<EquipmentActivity> DefineFilterColumns()
+            => new Columns<EquipmentActivity>(x => x.ID)
+                .Add(x => x.Activity.Code)
+                .Add(x => x.Activity.Description);
     }
 }

+ 14 - 22
prs.classes/Entities/Assignment/Assignment.cs

@@ -4,19 +4,7 @@ using InABox.Core;
 
 namespace Comal.Classes
 {
-    //public class DeliveryAssignments : CoreAggregate<Assignment, Delivery, Guid>
-    //{
-    //    public override Expression<Func<Delivery, Guid>> Aggregate => Delivery => Delivery.ID;
-    //    //public Expression<Func<Delivery, Guid>> Link => x => x.Assignment.ID;
-    //    public override AggregateCalculation Calculation => AggregateCalculation.Count;
-
-    //    public override Dictionary<Expression<Func<Delivery, object>>, Expression<Func<Assignment, object>>> Links =>
-    //        new Dictionary<Expression<Func<Delivery, object>>, Expression<Func<Assignment, object>>>
-    //        {
-    //            { Delivery => Delivery.Assignment.ID, x => x.ID }
-    //        };
-    //}
-
+    
     /// <summary>
     ///     An assignment represents an anticipated booking for an employee, much like a diary entry
     ///     <exclude />
@@ -37,26 +25,30 @@ namespace Comal.Classes
         [EntityRelationship(DeleteAction.Cascade)]
         [EditorSequence(3)]
         public EmployeeLink EmployeeLink { get; set; }
+        
+        // [EntityRelationship(DeleteAction.Cascade)]
+        // [EditorSequence(4)]
+        // public EquipmentLink Equipment { get; set; }
 
         [TextBoxEditor]
-        [EditorSequence(4)]
+        [EditorSequence(5)]
         public string Title { get; set; }
 
         [MemoEditor]
-        [EditorSequence(5)]
+        [EditorSequence(6)]
         public string Description { get; set; }
 
-        [EditorSequence(6)]
+        [EditorSequence(7)]
         public AssignmentActivityLink ActivityLink { get; set; }
 
-        [EditorSequence(7)]
+        [EditorSequence(8)]
         [EntityRelationship(DeleteAction.SetNull)]
         public KanbanLink Task { get; set; }
 
-        [EditorSequence(8)]
+        [EditorSequence(9)]
         public JobLink JobLink { get; set; }
 
-        [EditorSequence(9)]
+        [EditorSequence(10)]
         public JobITPLink ITP { get; set; }
 
         [NullEditor]
@@ -71,17 +63,17 @@ namespace Comal.Classes
         [Obsolete("Replaced with Actual.Finish", true)]
         public TimeSpan Finish { get; set; }
 
-        [EditorSequence(10)]
+        [EditorSequence(11)]
         [CoreTimeEditor]
         public TimeBlock Booked { get; set; }
 
-        [EditorSequence(11)]
+        [EditorSequence(12)]
         [CoreTimeEditor]
         public TimeBlock Actual { get; set; }
         
         [TimestampEditor]
         [RequiredColumn]
-        [EditorSequence(12)]
+        [EditorSequence(13)]
         public DateTime Completed { get; set; }
         
         [EditorSequence("Processing",1)]

+ 13 - 0
prs.classes/Entities/Equipment/EquipmentActivity.cs

@@ -0,0 +1,13 @@
+using InABox.Core;
+
+namespace Comal.Classes
+{
+    public class EquipmentActivity : Entity, IRemotable, IPersistent, IOneToMany<Equipment>
+    {
+        [NullEditor]
+        public EquipmentLink Equipment { get; set; }
+        
+        [EditorSequence(1)]
+        public ActivityLink Activity { get; set; }
+    }
+}

+ 109 - 0
prs.classes/Entities/Equipment/EquipmentAssignment/EquipmentAssignment.cs

@@ -0,0 +1,109 @@
+using System;
+using System.Linq.Expressions;
+using InABox.Core;
+
+namespace Comal.Classes
+{
+    /// <summary>
+    ///     An EquipmentAssignment represents an anticipated booking for an equipment item, much like a diary entry
+    ///     <exclude />
+    /// </summary>
+    [UserTracking("Assignments")]
+    [Caption("Assignments")]
+    public class EquipmentAssignment : Entity, IPersistent, IRemotable, INumericAutoIncrement<EquipmentAssignment>, IOneToMany<Equipment>, IOneToMany<Job>,
+        ILicense<SchedulingControlLicense>, IOneToMany<Invoice>, IJobScopedItem
+    {
+        [IntegerEditor(Editable = Editable.Hidden)]
+        [EditorSequence(1)]
+        public int Number { get; set; }
+
+        [DateEditor]
+        [EditorSequence(2)]
+        public DateTime Date { get; set; }
+
+        [EntityRelationship(DeleteAction.Cascade)]
+        [EditorSequence(3)]
+        public EquipmentLink Equipment { get; set; }
+        
+        [MemoEditor]
+        [EditorSequence(6)]
+        public string Description { get; set; }
+        
+        [EditorSequence(9)]
+        public JobLink JobLink { get; set; }
+        
+        [EditorSequence(10)]
+        public JobScopeLink JobScope { get; set; }
+        
+        [EditorSequence(11)]
+        [CoreTimeEditor]
+        public TimeBlock Booked { get; set; }
+
+        [EditorSequence(12)]
+        [CoreTimeEditor]
+        public TimeBlock Actual { get; set; }
+        
+        [TimestampEditor]
+        [RequiredColumn]
+        [EditorSequence(13)]
+        public DateTime Completed { get; set; }
+        
+        [EditorSequence("Processing",1)]
+        public ActualCharge Charge { get; set; }
+        
+        [TimestampEditor]
+        [EditorSequence("Processing",2)]
+        public DateTime Processed { get; set; }
+        
+        [NullEditor]
+        [EntityRelationship(DeleteAction.SetNull)]
+        public InvoiceLink Invoice { get; set; }
+        
+        
+        public Expression<Func<EquipmentAssignment, int>> AutoIncrementField()
+        {
+            return x => x.Number;
+        }
+
+        public Filter<EquipmentAssignment> AutoIncrementFilter()
+        {
+            return null;
+        }
+        
+        public static TimeSpan EffectiveTime(TimeSpan actual, TimeSpan booked) => actual.Ticks != 0L ? actual : booked; 
+
+        public TimeSpan EffectiveStartTime()
+        {
+            return EffectiveTime(Actual.Start, Booked.Start);
+        }
+        public DateTime EffectiveStart()
+        {
+            return Date.Add(EffectiveStartTime());
+        }
+
+        public TimeSpan EffectiveFinishTime()
+        {
+            // If we have an actual finish, always use that
+            // otherwise use EffectiveStart() + booked.duration
+            return EffectiveTime(
+                    Actual.Finish,
+                    EffectiveTime(Actual.Start, Booked.Start)
+                        .Add(Booked.Duration)
+                );
+        }
+        public DateTime EffectiveFinish()
+        {
+            return Date.Add(
+                EffectiveFinishTime()
+            );
+        }
+        
+        static EquipmentAssignment()
+        {
+            //LinkedProperties.Register<EquipmentAssignment, ActivityCharge, bool>(ass => ass.Equi.Charge, chg => chg.Chargeable, ass => ass.Charge.Chargeable);
+            Classes.JobScope.LinkScopeProperties<EquipmentAssignment>();
+        }
+        
+    }
+
+}

+ 47 - 0
prs.classes/Entities/Equipment/EquipmentAssignment/EquipmentAssignmentLink.cs

@@ -0,0 +1,47 @@
+using System;
+using System.ComponentModel;
+using InABox.Core;
+
+namespace Comal.Classes
+{
+    /// <summary>
+    ///     Allows other entities to link to an EquipmentAssignment
+    /// </summary>
+    public class EquipmentAssignmentLink : EntityLink<Assignment>
+    {
+        /// <summary>
+        ///     The ID of the linked EquipmentAssignment
+        /// </summary>
+        [EditorSequence(1)]
+        [LookupEditor(typeof(EquipmentAssignment))]
+        public override Guid ID { get; set; }
+        
+        /// <summary>
+        ///     The Number of the linked EquipmentAssignment
+        /// </summary>
+        [EditorSequence(2)]
+        [IntegerEditor(Editable = Editable.Hidden)]
+        public int Number { get; set; }
+
+        /// <summary>
+        ///     The date of the EquipmentAssignment
+        /// </summary>
+        [EditorSequence(3)]
+        [DateTimeEditor(Editable = Editable.Hidden)]
+        public DateTime Date { get; set; }
+
+        /// <summary>
+        ///     The booked time of the EquipmentAssignment
+        ///     At this stage, we're not going to deal with actual versus booked times
+        ///     The booked time is assumed to be the actual time
+        /// </summary>
+        [EditorSequence(4)]
+        [CoreTimeEditor(Editable = Editable.Hidden)]
+        public TimeBlock Booked { get; set; }
+
+        [EditorSequence(5)]
+        [CoreTimeEditor(Editable = Editable.Hidden)]
+        public JobLink JobLink { get; set; }
+    }
+
+}

+ 32 - 0
prs.classes/Entities/Equipment/EquipmentAssignment/EquipmentAssignmentLookup.cs

@@ -0,0 +1,32 @@
+using InABox.Core;
+
+namespace Comal.Classes
+{
+    public class EquipmentAssignmentLookups : EntityLookup<EquipmentAssignment>
+    {
+        public override Columns<EquipmentAssignment> DefineColumns()
+        {
+            return new Columns<EquipmentAssignment>(
+                x => x.ID,
+                x => x.Number,
+                x => x.Equipment.ID,
+                x => x.Equipment.Code,
+                x => x.Equipment.Description,
+                x => x.Date,
+                x => x.Booked.Start,
+                x => x.Booked.Duration
+            );
+        }
+
+        public override Filter<EquipmentAssignment> DefineFilter()
+        {
+            return null;
+        }
+
+        public override SortOrder<EquipmentAssignment> DefineSortOrder()
+        {
+            return new SortOrder<EquipmentAssignment>(x => x.Date).ThenBy(x => x.Booked.Start).ThenBy(x => x.Equipment.Code);
+        }
+        
+    }
+}

+ 5 - 0
prs.classes/SecurityDescriptors/Desktop_Access.cs

@@ -268,6 +268,11 @@ namespace Comal.Classes.SecurityDescriptors
     { 
     }
 
+    [Caption("View Desktop Equipment Planner Screen")]
+    public class ViewDesktopEquipmentPlannerScreen : EnabledSecurityDescriptor<DesktopAccessLicence>
+    { 
+    }
+
     [Caption("View Desktop GPS Trackers Screen")]
     public class ViewDesktopGPSTrackersScreen : EnabledSecurityDescriptor<DesktopAccessLicence>
     { 

+ 99 - 0
prs.desktop/Components/Calendar/Models/EquipmentAssignmentModel.cs

@@ -0,0 +1,99 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Windows.Media;
+using Comal.Classes;
+using InABox.Core;
+using InABox.WPF;
+
+namespace PRSDesktop
+{
+    public class EquipmentAssignmentModel : Model<EquipmentAssignmentModel,EquipmentAssignment>
+    {
+        
+        public Guid EquipmentID { get; }
+
+        public int Number { get; set; }
+        public String? Description { get; set; }
+        public DateTime Completed { get; set; }
+
+        public DateTime Date { get; }
+        public TimeSpan BookedStart { get; set; }
+        public TimeSpan BookedFinish { get; set; }
+        public TimeSpan BookedDuration { get; }
+        
+        public TimeSpan ActualStart { get; set; }
+        public TimeSpan ActualFinish { get; set; }
+        public TimeSpan ActualDuration { get; }
+        
+        public static TimeSpan EffectiveTime(TimeSpan actual, TimeSpan booked) => actual.Ticks != 0L ? actual : booked; 
+
+        public TimeSpan EffectiveStart()
+        {
+            return EffectiveTime(ActualStart, BookedStart);
+        }
+
+        public TimeSpan EffectiveFinish()
+        {
+             // If we have an actual finish, always use that
+             // otherwise use EffectiveStart() + booked.duration
+             return EffectiveTime(
+                 ActualFinish, 
+                 EffectiveTime(ActualStart, BookedStart)
+                     .Add(BookedDuration)
+            );
+            
+        }
+        
+        public Guid JobID { get; }
+        public string? JobNumber { get; }
+        public string? JobName { get; }
+        
+        public EquipmentAssignmentModel(CoreRow row) : base(row)
+        {
+            EquipmentID = Get(x=>x.Equipment.ID);
+
+            Number = Get(x => x.Number);
+            Description = Get(x => x.Description);
+            Completed = Get(x => x.Completed);
+
+            Date = Get(x=>x.Date);
+            BookedDuration = Get(x => x.Booked.Duration);
+            BookedStart = Get(x => x.Booked.Start);
+            BookedFinish = Get(x => x.Booked.Finish);
+
+            ActualDuration = Get(x => x.Actual.Duration);
+            ActualStart = Get(x => x.Actual.Start);
+            ActualFinish = Get(x => x.Actual.Finish);
+            
+            JobID = Get(x => x.JobLink.ID);
+            JobNumber = Get(x => x.JobLink.JobNumber);
+            JobName = Get(x => x.JobLink.Name);
+            
+           
+        }
+        
+        public override Columns<EquipmentAssignment> GetColumns()
+        {
+            return new Columns<EquipmentAssignment>(x => x.ID)
+
+                .Add(x => x.Equipment.ID)
+
+                .Add(x => x.Number)
+                .Add(x => x.Description)
+                .Add(x => x.Completed)
+
+                .Add(x => x.Date)
+                .Add(x => x.Booked.Start)
+                .Add(x => x.Booked.Duration)
+                .Add(x => x.Booked.Finish)
+                .Add(x => x.Actual.Start)
+                .Add(x => x.Actual.Duration)
+                .Add(x => x.Actual.Finish)
+
+                .Add(x => x.JobLink.ID)
+                .Add(x => x.JobLink.JobNumber)
+                .Add(x => x.JobLink.Name);
+
+        }
+    }
+}

+ 59 - 0
prs.desktop/Components/EquipmentSelector/EquipmentSelector.xaml

@@ -0,0 +1,59 @@
+<UserControl x:Class="PRSDesktop.EquipmentSelector"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:local="clr-namespace:PRSDesktop"
+             xmlns:Syncfusion="http://schemas.syncfusion.com/wpf"
+             mc:Ignorable="d"
+             d:DesignHeight="600" d:DesignWidth="200">
+       <Grid x:Name="EquipmentGrid" Margin="5,0,0,0" >
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="100" x:Name="GroupListRow"/>
+                <RowDefinition Height="4" x:Name="GroupListSplitterRow"/>
+                <RowDefinition Height="*" />
+            </Grid.RowDefinitions>
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" />
+                <ColumnDefinition Width="*" />
+            </Grid.ColumnDefinitions>
+           
+            <ComboBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" x:Name="Groups" DisplayMemberPath="Value" SelectedValuePath="Key"
+                      SelectionChanged="TeamsSelectionChanged" VerticalAlignment="Center" Margin="0,0,0,4" />
+
+            <Syncfusion:CheckListBox Grid.Row="1" Grid.ColumnSpan="2" x:Name="GroupList" DisplayMemberPath="Value"
+                                     SelectedValuePath="Key" IsSelectAllEnabled="False" IsCheckOnFirstClick="True"
+                                     Margin="0,0,0,0" 
+                                     ItemChecked="SelectedGroups_ItemChecked"
+                                     SizeChanged="SelectedGroups_SizeChanged" />
+
+            <Syncfusion:SfGridSplitter Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
+                                       ResizeBehavior="PreviousAndNext" Height="4" HorizontalAlignment="Stretch"
+                                       Background="Transparent" Template="{StaticResource HorizontalSplitter}">
+
+                <Syncfusion:SfGridSplitter.PreviewStyle>
+                    <Style TargetType="Control">
+                        <Setter Property="Background" Value="Gray" />
+                        <Setter Property="Template">
+                            <Setter.Value>
+                                <ControlTemplate TargetType="Control">
+                                    <Grid x:Name="Root" Opacity="0.5">
+                                        <Rectangle Fill="{TemplateBinding Background}" />
+                                    </Grid>
+                                </ControlTemplate>
+                            </Setter.Value>
+                        </Setter>
+                    </Style>
+                </Syncfusion:SfGridSplitter.PreviewStyle>
+
+            </Syncfusion:SfGridSplitter>
+
+            <Syncfusion:CheckListBox Grid.Row="3" Grid.ColumnSpan="2" x:Name="SelectedEquipment"
+                                     DisplayMemberPath="Value" SelectedValuePath="Key" IsSelectAllEnabled="False" IsCheckOnFirstClick="True"
+                                     Margin="0,0,0,0" 
+                                     ItemChecked="SelectedEquipment_OnItemChecked" />
+            
+        </Grid>
+
+</UserControl>

+ 365 - 0
prs.desktop/Components/EquipmentSelector/EquipmentSelector.xaml.cs

@@ -0,0 +1,365 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Imaging;
+using Comal.Classes;
+using InABox.Clients;
+using InABox.Configuration;
+using InABox.Core;
+using InABox.WPF;
+using NPOI.HSSF.Util;
+using Syncfusion.Windows.Tools.Controls;
+
+namespace PRSDesktop
+{
+    public class EquipmentSelectorData
+    {
+        // if first == FullGuid, remainder are selected
+        // Otherwise, ignore the rest
+        public Guid[] Groups { get; set; }
+        
+        // if first == FullGuid, select all
+        public Guid[] Equipment { get; set; }
+        
+        public EquipmentSelectorData( Guid[] groups, Guid[] equipment)
+        {
+            Groups = groups;
+            Equipment = equipment;
+        }
+
+        public EquipmentSelectorData() : this(new[] { Guid.Empty }, new[] { CoreUtils.FullGuid })
+        {
+        }
+    }
+
+    public class EquipmentSelectorSettings
+    {
+        public double SplitPosition { get; set; }
+
+        public EquipmentSelectorSettings()
+        {
+            SplitPosition = 200.0F;
+        }
+    }
+    
+    public class EquipmentSelectorSettingsChangedArgs : EventArgs
+    {
+        public EquipmentSelectorSettings Settings { get; private set; }
+
+        public EquipmentSelectorSettingsChangedArgs(EquipmentSelectorSettings settings)
+        {
+            Settings = settings;
+        }
+    }
+    
+    public delegate void EquipmentSelectorSettingsChanged(object sender, EquipmentSelectorSettingsChangedArgs args);
+    
+    public class EquipmentSelectorSelectionChangedArgs : EventArgs
+    {
+        public EquipmentSelectorData Selection { get; private set; }
+
+        public EquipmentSelectorSelectionChangedArgs(EquipmentSelectorData selection)
+        {
+            Selection = selection;
+        }
+    }
+
+    public delegate void EquipmentSelectorSelectionChanged(object sender, EquipmentSelectorSelectionChangedArgs args);
+
+
+    public partial class EquipmentSelector : UserControl
+    {
+
+        private enum Suppress
+        {
+            This
+        }
+
+        private class ListEntry
+        {
+            public Guid Key { get; set; }
+            public String Value { get; set; }
+            public BitmapImage Image { get; set; }
+
+            public ListEntry(Guid key, String value, BitmapImage image = null)
+            {
+                Key = key;
+                Value = value;
+                Image = image;
+            }
+        }
+
+        public EquipmentSelectorSettings Settings { get; set; }
+        
+        public event EquipmentSelectorSettingsChanged SettingsChanged;
+        public event EquipmentSelectorSelectionChanged SelectionChanged;
+        
+        private CoreTable _equipment;
+        private CoreTable _activities;
+        private ListEntry[] _groups;
+        private Tuple<Guid,Guid,String, BitmapImage?>[] _equipmentgroups;
+
+        public T[] GetEquipmentData<T>(Func<CoreRow, BitmapImage?, EquipmentActivity[], T> transform)
+        {
+            List<T> result = new List<T>();
+            var selgrps = GetSelectedGroups();
+            var seleq = GetSelectedEquipment();
+            var eqids = _equipmentgroups
+                .Where(x => selgrps.Contains(x.Item1) && seleq.Contains(CoreUtils.FullGuid) || seleq.Contains(x.Item2))
+                .Select(x => x.Item2)
+                .Distinct()
+                .ToArray();
+            var rows = _equipment.Rows.Where(r => eqids.Contains(r.Get<Equipment, Guid>(c => c.ID)));
+            foreach (var row in rows)
+            {
+                var activities = _activities.Rows
+                    .Where(r => r.Get<EquipmentActivity, Guid>(c => c.Equipment.ID) ==
+                                row.Get<Equipment, Guid>(c => c.ID))
+                    .Select(r=>r.ToObject<EquipmentActivity>())
+                    .ToArray();
+                result.Add(
+                    transform.Invoke(
+                        row,
+                        _groups.FirstOrDefault(x => x.Key == row.Get<Equipment, Guid>(c => c.GroupLink.ID))?.Image,
+                        activities
+                    )
+                );
+            }
+
+            return result.ToArray();
+        }
+        
+        public EquipmentSelector()
+        {
+            Settings = new EquipmentSelectorSettings();
+            using (new EventSuppressor(Suppress.This))
+                InitializeComponent();
+        }
+        
+
+        public void Setup()
+        {
+            using (new EventSuppressor(Suppress.This))
+            {
+                var results = Client.QueryMultiple(
+                    new KeyedQueryDef<Equipment>(LookupFactory.DefineFilter<Equipment>()),
+                    new KeyedQueryDef<EquipmentActivity>(),
+                    new KeyedQueryDef<EquipmentGroup>(LookupFactory.DefineFilter<EquipmentGroup>())
+                );
+
+                _equipment = results.Get<Equipment>();
+                _activities = results.Get<EquipmentActivity>();
+                var groups = results.Get<EquipmentGroup>();
+                _groups = groups.Rows.Select(r => new ListEntry(
+                    r.Get<EquipmentGroup,Guid>(c=>c.ID), 
+                    r.Get<Team,String>(c=>c.Name)
+                )).ToArray();
+                GroupList.ItemsSource = _groups;
+                
+                var ids = groups.Rows.Select(r => r.Get<EquipmentGroup,Guid>(c=>c.Thumbnail.ID)).Distinct().Where(x => x != Guid.Empty).ToArray();
+                var Images = ids.Any()
+                    ? new Client<Document>().Load(new Filter<Document>(x=>x.ID).InList(ids))
+                    : new Document[] { };
+            
+                foreach (var row in groups.Rows)
+                {
+                    var image = Images.FirstOrDefault(x => x.ID.Equals(row.Get<EquipmentGroup,Guid>(c=>c.Thumbnail.ID)));
+                    BitmapImage img =  (image != null && image.Data != null && image.Data.Length > 0)
+                        ? ImageUtils.LoadImage(image.Data)
+                        : PRSDesktop.Resources.truck.AsBitmapImage();
+                    var grp = _groups.FirstOrDefault(x => x.Key == row.Get<EquipmentGroup, Guid>(c => c.Thumbnail.ID));
+                    if (grp != null)
+                        grp.Image = img;
+                }
+                
+                ObservableCollection<ListEntry> groupdata = new() { new ListEntry(Guid.Empty, "All Equipment", PRSDesktop.Resources.truck.AsBitmapImage()) };
+                foreach (var group in _groups)
+                    groupdata.Add(group);
+                groupdata.Add(new ListEntry(CoreUtils.FullGuid, "Multiple Groups", PRSDesktop.Resources.truck.AsBitmapImage()));
+                Groups.ItemsSource = groupdata;
+                
+                _equipmentgroups = _equipment.Rows.Select(
+                    row => new Tuple<Guid, Guid, String, BitmapImage?>(
+                        row.Get<Equipment, Guid>(c => c.GroupLink.ID),
+                        row.Get<Equipment, Guid>(c => c.ID),
+                        row.Get<Equipment, String>(c => c.Description),
+                        _groups.FirstOrDefault(x=>x.Key == row.Get<Equipment, Guid>(c => c.GroupLink.ID))?.Image
+                    )
+                ).Union(
+                    _equipment.Rows.Select(
+                        row => new Tuple<Guid,Guid,String, BitmapImage?>(
+                            Guid.Empty,
+                            row.Get<Equipment, Guid>(c => c.ID),
+                            row.Get<Equipment, String>(c => c.Description),
+                            PRSDesktop.Resources.truck.AsBitmapImage()
+                        )
+                    )    
+                ).ToArray();
+
+                Groups.SelectedValue = Guid.Empty;
+
+            }
+
+        }
+
+        private void UpdateSettings()
+        {
+            bool changed = false;
+            
+            var groups = GetSelectedGroups();
+            if (groups.Contains(CoreUtils.FullGuid) && (Math.Abs(Settings.SplitPosition - GroupListRow.Height.Value) >= 1.0F))
+            {
+                changed = true;
+                Settings.SplitPosition = GroupListRow.Height.Value;
+            }
+
+            if (changed && (!EventSuppressor.IsSet(Suppress.This)))
+                SettingsChanged?.Invoke(this, new EquipmentSelectorSettingsChangedArgs(Settings));
+        }
+        
+        private Guid[] GetSelectedGroups()
+        {
+            Guid groupid = Groups.SelectedValue != null ? (Guid)Groups.SelectedValue : Guid.Empty;
+            var teams = (groupid == CoreUtils.FullGuid)
+                ? GroupList.SelectedItems
+                    .OfType<ListEntry>()
+                    .Select(x => x.Key)
+                    .Prepend(groupid)
+                    .ToArray()
+                : new Guid[] { groupid };
+            return teams;
+        }
+
+        private Guid[] GetSelectedEquipment()
+        {
+            var emps = (SelectedEquipment.SelectedItems.Count == SelectedEquipment.Items.Count)
+                ? new Guid[] { CoreUtils.FullGuid }
+                : SelectedEquipment.SelectedItems
+                    .OfType<ListEntry>()
+                    .Select(x => x.Key)
+                    .ToArray();
+            return emps;
+        }
+
+        public EquipmentSelectorData Selection
+        {
+            get => GetSelectionData();
+            set => SelectEquipment(value);
+        }
+        
+        private EquipmentSelectorData GetSelectionData()
+        {
+            return new EquipmentSelectorData(
+                GetSelectedGroups(),
+                GetSelectedEquipment()
+            );
+        }
+        
+        private void SelectEquipment(EquipmentSelectorData selection)
+        {
+            using (new EventSuppressor(Suppress.This))
+            {
+                Groups.SelectedValue = selection.Groups.Contains(CoreUtils.FullGuid)
+                    ? CoreUtils.FullGuid
+                    : selection.Groups.FirstOrDefault();
+                LoadGroups(selection.Groups);
+                var equipment = SelectedEquipment.ItemsSource as ObservableCollection<ListEntry>;
+                var pairs = equipment.Where(x => selection.Equipment.Contains(CoreUtils.FullGuid) || selection.Equipment.Contains(x.Key));
+                SelectedEquipment.SelectedItems.Clear();
+                foreach (var pair in pairs)
+                    SelectedEquipment.SelectedItems.Add(pair);
+            }            
+        }
+        
+        private void LoadGroups(Guid[] groupids)
+        {
+            using (new EventSuppressor(Suppress.This))
+            {
+                Guid groupid = groupids.FirstOrDefault();
+                if (Guid.Equals(groupid, CoreUtils.FullGuid))
+                {
+                    GroupListRow.Height = new GridLength(Settings.SplitPosition, GridUnitType.Pixel);
+                    GroupListSplitterRow.Height = new GridLength(4, GridUnitType.Pixel);
+                    var selected = new ObservableCollection<object>(_groups.Where(x => groupids.Contains(x.Key)));
+                    GroupList.SelectedItems = selected;
+                    LoadEquipment(groupids.Skip(1).ToArray());
+                }
+                else
+                {
+                    GroupListRow.Height = new GridLength(0, GridUnitType.Pixel);
+                    GroupListSplitterRow.Height = new GridLength(0, GridUnitType.Pixel);
+                    LoadEquipment(new Guid[] { groupid });
+                }
+            }
+        }
+
+        private void LoadEquipment(Guid[] teamids)
+        {
+            using (new EventSuppressor(Suppress.This))
+            {
+                var eqs =_equipmentgroups
+                    .OrderBy(x => x.Item3)
+                    .Where(x => teamids.Contains(x.Item1))
+                    .Select(x => new ListEntry(x.Item2, x.Item3))
+                    .Distinct()
+                    .ToArray();
+                SelectedEquipment.ItemsSource = new ObservableCollection<ListEntry>(eqs);
+                SelectedEquipment.SelectedItems = new ObservableCollection<object>(eqs);
+            }
+        }
+        
+        public void SelectEmployee(Guid employeeid)
+        {
+            using (new EventSuppressor(Suppress.This))
+            {
+                LoadGroups(new Guid[] { Guid.Empty });
+                if (SelectedEquipment.ItemsSource is ObservableCollection<ListEntry> employees)
+                {
+                    var emp = employees.FirstOrDefault(x => x.Key == employeeid);
+                    if (emp != null)
+                        SelectedEquipment.SelectedItems = new ObservableCollection<object>() { emp };
+                }
+            }
+        }
+        
+        private void TeamsSelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (EventSuppressor.IsSet(Suppress.This))
+                return;
+            using (new EventSuppressor(Suppress.This))
+                LoadGroups(GetSelectedGroups());
+            UpdateSettings();
+            SelectionChanged?.Invoke(this, new EquipmentSelectorSelectionChangedArgs(GetSelectionData())); 
+        }
+        
+        private void SelectedGroups_ItemChecked(object? sender, ItemCheckedEventArgs e)
+        {
+            if (EventSuppressor.IsSet(Suppress.This))
+                return;
+            using (new EventSuppressor(Suppress.This))
+                LoadEquipment(GetSelectedGroups());
+            UpdateSettings();
+            SelectionChanged?.Invoke(this, new EquipmentSelectorSelectionChangedArgs(GetSelectionData())); 
+        }
+        
+        private void SelectedEquipment_OnItemChecked(object? sender, ItemCheckedEventArgs e)
+        {
+            if (EventSuppressor.IsSet(Suppress.This))
+                return;
+            UpdateSettings();
+            SelectionChanged?.Invoke(this, new EquipmentSelectorSelectionChangedArgs(GetSelectionData()));            
+        }
+        
+        private void SelectedGroups_SizeChanged(object sender, SizeChangedEventArgs e)
+        {
+            if (EventSuppressor.IsSet(Suppress.This))
+                return;
+            UpdateSettings();
+        }
+
+
+    }
+}

+ 3 - 0
prs.desktop/MainWindow.xaml

@@ -777,6 +777,9 @@
                     <fluent:Button x:Name="EquipmentButton" Header="Equipment List"
                                    LargeIcon="pack://application:,,,/Resources/specifications.png"
                                    Click="Equipment_Checked" MinWidth="60" />
+                    <fluent:Button x:Name="EquipmentPlannerButton" Header="Equipment Planner"
+                                   LargeIcon="pack://application:,,,/Resources/calendar.png"
+                                   Click="EquipmentPlannerButton_Click" MinWidth="60" />
                     <fluent:Button x:Name="TrackersMasterList" Header="GPS Trackers"
                                    LargeIcon="pack://application:,,,/Resources/milestone.png"
                                    Click="Trackers_Click" MinWidth="60" />

+ 16 - 2
prs.desktop/MainWindow.xaml.cs

@@ -902,14 +902,23 @@ namespace PRSDesktop
                 && Security.CanView<Equipment>()
                 && Security.IsAllowed<ViewDesktopEquipmentListScreen>());
 
+            SetVisibility(EquipmentPlannerButton, 
+                ClientFactory.IsSupported<Equipment>() 
+                && Security.CanView<Equipment>()
+                && ClientFactory.IsSupported<Assignment>() 
+                && Security.CanView<Assignment>()
+                && Security.IsAllowed<ViewDesktopEquipmentPlannerScreen>());
+
+
             SetVisibleIfEither(EquipmentTaskSeparator,
                 new FrameworkElement[]
                 {
                                 EquipmentDashboardButton, EquipmentMessagesButton, EquipmentTaskButton, EquipmentAttendanceButton, EquipmentMapButton,
                                 EquipmentDailyReportButton
-                }, new FrameworkElement[] { EquipmentButton });
+                }, new FrameworkElement[] { EquipmentButton, EquipmentPlannerButton });
+            
             SetVisibleIfAny(EquipmentActions, EquipmentDashboardButton, EquipmentMessagesButton, EquipmentTaskButton,
-                EquipmentAttendanceButton, EquipmentDailyReportButton, EquipmentButton);
+                EquipmentAttendanceButton, EquipmentDailyReportButton, EquipmentButton, EquipmentPlannerButton);
 
             SetVisibility(TrackersMasterList, Security.CanView<GPSTracker>() && Security.IsAllowed<ViewDesktopGPSTrackersScreen>());
 
@@ -2669,6 +2678,11 @@ namespace PRSDesktop
             LoadWindow<EmployeeResourcePlannerPanel>((Fluent.Button)sender);
         }
 
+        private void EquipmentPlannerButton_Click(object sender, RoutedEventArgs e)
+        {
+            LoadWindow<EquipmentPlannerPanel>((Fluent.Button)sender);
+        }
+
         private void InvoiceList_Click(object sender, RoutedEventArgs e)
         {
             LoadWindow<InvoicePanel>((Fluent.Button)sender);

+ 5 - 0
prs.desktop/PRSDesktop.csproj

@@ -865,6 +865,11 @@
         <XamlRuntime>Wpf</XamlRuntime>
         <SubType>Designer</SubType>
       </Page>
+      <Page Update="Components\EquipmentSelector\EquipmentSelector.xaml">
+        <Generator>MSBuild:Compile</Generator>
+        <XamlRuntime>Wpf</XamlRuntime>
+        <SubType>Designer</SubType>
+      </Page>
     </ItemGroup>
 
     <Import Project="..\PRS.Stores\PRSStores.projitems" Label="Shared" />

+ 1 - 1
prs.desktop/Panels/EmployeePlanner/EmployeeResourcePlanner.xaml.cs

@@ -48,7 +48,7 @@ namespace PRSDesktop
             }
         }
     }
-
+    
     public partial class EmployeeResourcePlanner : UserControl
     {
 

+ 244 - 0
prs.desktop/Panels/EquipmentPlanner/EquipmentPlanner.xaml

@@ -0,0 +1,244 @@
+<UserControl x:Class="PRSDesktop.EquipmentPlanner"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:local="clr-namespace:PRSDesktop"
+             xmlns:Syncfusion="http://schemas.syncfusion.com/wpf"
+             mc:Ignorable="d"
+             d:DesignHeight="600" d:DesignWidth="800">
+      <UserControl.Resources>
+
+        <Style x:Key="DateHeaderStyle" TargetType="{x:Type Syncfusion:GridHeaderCellControl}">
+            <Setter Property="Background" Value="LightSkyBlue"/>
+            <Setter Property="Foreground" Value="Black"/>
+            <Setter Property="BorderBrush" Value="Black"/>
+            <Setter Property="BorderThickness" Value="0.5,0.5,0.5,0.5"/>
+            <Setter Property="HorizontalContentAlignment" Value="Center"/>
+            <Setter Property="Padding" Value="5,3"/>
+            <Setter Property="FontFamily" Value="Segoe UI"/>
+            <Setter Property="FontSize" Value="14"/>
+            <Setter Property="FontWeight" Value="Normal"/>
+            <Setter Property="IsTabStop" Value="False"/>
+        </Style>
+          
+          <Style x:Key="TimeHeaderStyle" TargetType="{x:Type Syncfusion:GridHeaderCellControl}">
+              <Setter Property="Background" Value="LightSkyBlue"/>
+              <Setter Property="Foreground" Value="Black"/>
+              <Setter Property="BorderBrush" Value="Black"/>
+              <Setter Property="BorderThickness" Value="0.5,0.5,0.5,0.5"/>
+              <Setter Property="HorizontalContentAlignment" Value="Center"/>
+              <Setter Property="Padding" Value="5,3"/>
+              <Setter Property="FontFamily" Value="Segoe UI"/>
+              <Setter Property="FontSize" Value="14"/>
+              <Setter Property="FontWeight" Value="Normal"/>
+              <Setter Property="IsTabStop" Value="False"/>
+          </Style>
+
+        <Style x:Key="ContentHeaderStyle" TargetType="{x:Type Syncfusion:GridHeaderCellControl}">
+            <Setter Property="Background" Value="LightSkyBlue"/>
+            <Setter Property="Foreground" Value="Black"/>
+            <Setter Property="BorderBrush" Value="Black"/>
+            <Setter Property="BorderThickness" Value="0.5,0.5,0.5,0.5"/>
+            <Setter Property="HorizontalContentAlignment" Value="Left"/>
+            <Setter Property="Padding" Value="5,3"/>
+            <Setter Property="FontFamily" Value="Segoe UI"/>
+            <Setter Property="FontSize" Value="14"/>
+            <Setter Property="FontWeight" Value="Normal"/>
+            <Setter Property="IsTabStop" Value="False"/>
+            <Setter Property="VerticalContentAlignment" Value="Center"/>
+            <Setter Property="Template">
+                <Setter.Value>
+                    <ControlTemplate TargetType="{x:Type Syncfusion:GridHeaderCellControl}">
+                        <Grid>
+                            <!-- <Grid.LayoutTransform> -->
+                            <!--     <RotateTransform Angle="270"/> -->
+                            <!-- </Grid.LayoutTransform> -->
+                            <VisualStateManager.VisualStateGroups>
+                                <VisualStateGroup x:Name="HiddenColumnsResizingStates">
+                                    <VisualState x:Name="PreviousColumnHidden">
+                                        <Storyboard>
+                                            <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0" Storyboard.TargetProperty="BorderThickness" Storyboard.TargetName="PART_HeaderCellBorder">
+                                                <EasingThicknessKeyFrame KeyTime="0" Value="3,0,1,1"/>
+                                            </ThicknessAnimationUsingKeyFrames>
+                                        </Storyboard>
+                                    </VisualState>
+                                    <VisualState x:Name="HiddenState">
+                                        <Storyboard>
+                                            <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0" Storyboard.TargetProperty="BorderThickness" Storyboard.TargetName="PART_HeaderCellBorder">
+                                                <EasingThicknessKeyFrame KeyTime="0" Value="3,0,3,1"/>
+                                            </ThicknessAnimationUsingKeyFrames>
+                                        </Storyboard>
+                                    </VisualState>
+                                    <VisualState x:Name="NormalState"/>
+                                    <VisualState x:Name="LastColumnHidden">
+                                        <Storyboard>
+                                            <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0" Storyboard.TargetProperty="BorderThickness" Storyboard.TargetName="PART_HeaderCellBorder">
+                                                <EasingThicknessKeyFrame KeyTime="0" Value="0,0,3,1"/>
+                                            </ThicknessAnimationUsingKeyFrames>
+                                        </Storyboard>
+                                    </VisualState>
+                                </VisualStateGroup>
+                                <VisualStateGroup x:Name="CommonStates">
+                                    <VisualState x:Name="MouseOver"/>
+                                    <VisualState x:Name="Normal"/>
+                                </VisualStateGroup>
+                                <VisualStateGroup x:Name="BorderStates">
+                                    <VisualState x:Name="NormalCell"/>
+                                    <VisualState x:Name="FooterColumnCell">
+                                        <Storyboard BeginTime="0">
+                                            <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0" Storyboard.TargetProperty="BorderThickness" Storyboard.TargetName="PART_FooterCellBorder">
+                                                <EasingThicknessKeyFrame KeyTime="0" Value="1,0,1,1"/>
+                                            </ThicknessAnimationUsingKeyFrames>
+                                        </Storyboard>
+                                    </VisualState>
+                                    <VisualState x:Name="BeforeFooterColumnCell">
+                                        <Storyboard BeginTime="0">
+                                            <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0" Storyboard.TargetProperty="BorderThickness" Storyboard.TargetName="PART_FooterCellBorder">
+                                                <EasingThicknessKeyFrame KeyTime="0" Value="0,0,0,1"/>
+                                            </ThicknessAnimationUsingKeyFrames>
+                                            <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0" Storyboard.TargetProperty="BorderThickness" Storyboard.TargetName="PART_HeaderCellBorder">
+                                                <EasingThicknessKeyFrame KeyTime="0" Value="0,0,0,1"/>
+                                            </ThicknessAnimationUsingKeyFrames>
+                                        </Storyboard>
+                                    </VisualState>
+                                </VisualStateGroup>
+                            </VisualStateManager.VisualStateGroups>
+                            <Border x:Name="PART_FooterCellBorder" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}"/>
+                            <Border x:Name="PART_HeaderCellBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
+                                <Grid Margin="{TemplateBinding Padding}" SnapsToDevicePixels="True">
+                                    <Grid.ColumnDefinitions>
+                                        <ColumnDefinition Width="*"/>
+                                        <ColumnDefinition Width="Auto"/>
+                                        <ColumnDefinition Width="Auto"/>
+                                    </Grid.ColumnDefinitions>
+                                    <Grid.RowDefinitions>
+                                        <RowDefinition Height="*"/>
+                                        <RowDefinition Height="Auto"/>
+                                    </Grid.RowDefinitions>
+                                    <Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" CornerRadius="5" BorderThickness="0.75" BorderBrush="Gray" Background="WhiteSmoke"/>
+                                    <ContentPresenter Grid.Row="1" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" Focusable="False" HorizontalAlignment="Center" VerticalAlignment="Center"/>
+                                    <Grid x:Name="PART_SortButtonPresenter" Grid.Row="1" Grid.Column="1" SnapsToDevicePixels="True">
+                                        <Grid.ColumnDefinitions>
+                                            <ColumnDefinition Width="*">
+                                                <ColumnDefinition.MinWidth>
+                                                    <Binding Mode="OneWay" Path="SortDirection" RelativeSource="{RelativeSource TemplatedParent}">
+                                                        <Binding.Converter>
+                                                            <Syncfusion:SortDirectionToWidthConverter/>
+                                                        </Binding.Converter>
+                                                    </Binding>
+                                                </ColumnDefinition.MinWidth>
+                                            </ColumnDefinition>
+                                            <ColumnDefinition Width="*"/>
+                                        </Grid.ColumnDefinitions>
+                                        <TextBlock Grid.Column="1" Foreground="{TemplateBinding Foreground}" FontSize="10" Margin="0,-4,0,0" SnapsToDevicePixels="True" Text="{TemplateBinding SortNumber}" Visibility="{TemplateBinding SortNumberVisibility}" VerticalAlignment="Bottom"/>
+                                    </Grid>
+                                    <Syncfusion:FilterToggleButton x:Name="PART_FilterToggleButton" Grid.Row="1" Grid.Column="2" HorizontalAlignment="Stretch" SnapsToDevicePixels="True" Visibility="{TemplateBinding FilterIconVisiblity}" VerticalAlignment="Stretch"/>
+                                    <Border x:Name="PART_FilterPopUpPresenter"/>
+                                </Grid>
+                            </Border>
+                        </Grid>
+                    </ControlTemplate>
+                </Setter.Value>
+            </Setter>
+        </Style>
+        
+        <ControlTemplate x:Key="HorizontalSplitter">
+            <Grid Background="{TemplateBinding Background}" Height="4">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="*" />
+                    <ColumnDefinition Width="Auto" />
+                    <ColumnDefinition Width="*" />
+                </Grid.ColumnDefinitions>
+                <Button Grid.Column="0" x:Name="PART_Left" Visibility="Collapsed" />
+                <StackPanel Grid.Column="1" Margin="0" Orientation="Horizontal" HorizontalAlignment="Center"
+                            VerticalAlignment="Center">
+                    <Ellipse Fill="Silver" HorizontalAlignment="Center" Height="2" Width="2" Opacity="1"
+                             Margin="2,0,0,0" />
+                    <Ellipse Fill="Silver" HorizontalAlignment="Center" Height="2" Width="2" Opacity="1"
+                             Margin="2,0,0,0" />
+                    <Ellipse Fill="Silver" HorizontalAlignment="Center" Height="2" Width="2" Opacity="1"
+                             Margin="2,0,0,0" />
+                    <Ellipse Fill="Silver" HorizontalAlignment="Center" Height="2" Width="2" Opacity="1"
+                             Margin="2,0,0,0" />
+                    <Ellipse Fill="Silver" HorizontalAlignment="Center" Height="2" Width="2" Opacity="1"
+                             Margin="2,0,0,0" />
+                    <Ellipse Fill="Silver" HorizontalAlignment="Center" Height="2" Width="2" Opacity="1"
+                             Margin="2,0,0,0" />
+                    <Ellipse Fill="Silver" HorizontalAlignment="Center" Height="2" Width="2" Opacity="1"
+                             Margin="2,0,0,0" />
+                </StackPanel>
+                <Button Grid.Column="2" x:Name="PART_Right" Visibility="Collapsed" />
+            </Grid>
+        </ControlTemplate>
+
+    </UserControl.Resources>
+    
+    <Grid>
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="*" />
+            <ColumnDefinition Width="180" />
+        </Grid.ColumnDefinitions>
+
+        <Grid.RowDefinitions>
+            <RowDefinition Height="*" />
+        </Grid.RowDefinitions>
+
+        <Syncfusion:SfDataGrid
+            x:Name="dataGrid"
+            Grid.Row="0"
+            Grid.Column="0"
+            AutoGenerateColumns="True"
+            AutoGeneratingColumn="DataGrid_AutoGeneratingColumn"
+            RowHeight="30"
+            AllowSorting="False"
+            HeaderRowHeight="100"
+            ContextMenuOpening="DataGrid_ContextMenuOpening"
+            QueryCoveredRange="DataGrid_OnQueryCoveredRange"
+            SelectionUnit="Cell"
+            NavigationMode="Cell"
+            FrozenColumnCount="1"
+            CanMaintainScrollPosition="True"
+            SelectionMode="Extended"
+            SelectionForegroundBrush="Yellow"
+            RowSelectionBrush="Red">
+            
+            <Syncfusion:SfDataGrid.ContextMenu>
+                <ContextMenu />
+            </Syncfusion:SfDataGrid.ContextMenu>
+        </Syncfusion:SfDataGrid>
+           
+        <Grid Grid.Column="1" Grid.Row="0">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto"/>
+                <ColumnDefinition Width="*"/>
+            </Grid.ColumnDefinitions>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="*"/>
+                <RowDefinition Height="Auto"/>                
+                <RowDefinition Height="Auto"/>                
+                <RowDefinition Height="Auto"/>
+                <RowDefinition Height="Auto"/>
+            </Grid.RowDefinitions>
+ 
+            
+            <local:EquipmentSelector x:Name="EquipmentSelector" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" SettingsChanged="_equipment_OnSettingsChanged" SelectionChanged="_equipment_OnSelectionChanged"/>
+
+            <Label Content="From" Grid.Row="1" Margin="0,5,0,0" VerticalContentAlignment="Center" />
+            <Syncfusion:DateTimeEdit x:Name="FromDate" DateTimeChanged="DateTimeChanged" Grid.Row="1" Grid.Column="1"
+                                     Pattern="CustomPattern" CustomPattern="dd MMMM yy" Margin="5,5,0,0" />
+            
+            <Label Content="To" Grid.Row="2" Margin="0,5,0,0"  VerticalContentAlignment="Center" />
+            <Syncfusion:DateTimeEdit x:Name="ToDate" DateTimeChanged="DateTimeChanged"  Grid.Row="2" Grid.Column="1"
+                                     Pattern="CustomPattern" CustomPattern="dd MMMM yy" Margin="5,5,0,0" />  
+
+            <Button Content="Jobs" Grid.Row="3" Margin="5,5,0,0"  VerticalContentAlignment="Center" Click="JobFilterButton_Click" />
+            <ComboBox x:Name="JobFilter" Grid.Row="3" Grid.Column="1" Margin="5,5,0,0" 
+                      SelectionChanged="JobFilter_OnSelectionChanged" VerticalContentAlignment="Center"
+                      DisplayMemberPath="Name" />
+            
+            
+        </Grid>
+    </Grid>
+
+</UserControl>

+ 650 - 0
prs.desktop/Panels/EquipmentPlanner/EquipmentPlanner.xaml.cs

@@ -0,0 +1,650 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using Comal.Classes;
+using InABox.Clients;
+using InABox.Configuration;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.WPF;
+using Syncfusion.UI.Xaml.Grid;
+using Syncfusion.UI.Xaml.Grid.Helpers;
+using Syncfusion.Windows.Shared;
+using MultiQuery = InABox.Core.MultiQuery;
+using SelectionChangedEventArgs = System.Windows.Controls.SelectionChangedEventArgs;
+
+namespace PRSDesktop
+{
+    public class EquipmentPlannerValue
+    {
+        public Guid ID { get; set; }
+        
+        public Brush Background { get; set; }
+        public Brush Foreground { get; set; }
+        public String Text { get; set; }
+
+        private String _color = "";
+        public String Color
+        {
+            get { return _color; }
+            set
+            {
+                _color = value;
+                var color = String.IsNullOrWhiteSpace(value) ? Colors.Transparent : (Color)ColorConverter.ConvertFromString(value);
+                Background = new SolidColorBrush(color) { Opacity = 0.8 };
+                Foreground = new SolidColorBrush(ImageUtils.GetForegroundColor(color)) { Opacity = 0.8 };
+            }
+        }
+    }
+
+    public class EquipmentResourceModel : Model<EquipmentResourceModel,Equipment>
+    {
+        public String? Code { get; }
+        public BitmapImage? Image { get; set; }
+        public EquipmentActivity[] Activities { get; private set; }
+
+        public EquipmentResourceModel(CoreRow row, BitmapImage? image, EquipmentActivity[] activities) : base(row)
+        {
+            Activities = activities;
+            Code = Get(c => c.Code);
+            Image = image;
+        }
+
+        public override Columns<Equipment> GetColumns()
+        {
+            return new Columns<Equipment>(c => c.ID)
+                .Add(c => c.Code)
+                .Add(c=>c.GroupLink.ID);
+        }
+        
+    }
+
+    public class TimeSlot
+    {
+        public TimeSpan From { get; set; }
+        public TimeSpan To { get; set; }
+
+        public TimeSlot(TimeSpan from, TimeSpan to)
+        {
+            From = from;
+            To = to;
+        }
+
+        public override string ToString()
+        {
+            return $"{From:HH\\:mm}";
+        }
+    }
+    
+    public partial class EquipmentPlanner : UserControl
+    {
+
+        private enum Suppress
+        {
+            This
+        }
+        
+        private EquipmentResourceModel[] _equipment = new EquipmentResourceModel[] { };
+        private JobModel[] _jobs = new JobModel[] { };
+        
+        private CoreFilterDefinitions _jobfilters = new CoreFilterDefinitions();
+        
+        public event LoadSettings<EquipmentPlannerProperties> LoadSettings;
+        public event SaveSettings<EquipmentPlannerProperties> SaveSettings;
+
+        private void DoLoadSettings()
+        {
+            Properties = LoadSettings?.Invoke(this) ?? new EquipmentPlannerProperties();
+            _jobfilters = new GlobalConfiguration<CoreFilterDefinitions>("Job").Load();
+
+        }
+        
+        private void DoSaveSettings()
+        {
+            SaveSettings?.Invoke(this, Properties);
+        }
+        
+        public EquipmentPlannerProperties Properties { get; set; }
+
+        public EquipmentPlanner()
+        {
+            using (new EventSuppressor(Suppress.This))
+                InitializeComponent();
+        }
+
+        private Filter<Job>? GetJobFilter()
+        {
+            var jobfilter = _jobfilters.FirstOrDefault(x => String.Equals(x.Name, Properties.JobFilter));
+            return !String.IsNullOrWhiteSpace(jobfilter?.Filter)
+                ? Serialization.Deserialize<Filter<Job>>(jobfilter.Filter)
+                : LookupFactory.DefineFilter<Job>();           
+        }
+        
+        public void Setup()
+        {
+            using (new EventSuppressor(Suppress.This))
+            {
+                DoLoadSettings();
+
+                EquipmentSelector.Setup();
+                EquipmentSelector.Settings = Properties.EquipmentSettings;
+                EquipmentSelector.Selection = Properties.EquipmentSelection;
+
+                FromDate.DateTime = DateTime.Today;
+                ToDate.DateTime = DateTime.Today.AddYears(1);
+                
+                MultiQuery query = new MultiQuery();
+                
+                query.Add<Job>(
+                    GetJobFilter(), 
+                    JobModel.Columns,
+                    new SortOrder<Job>(x => x.JobNumber)
+                );
+                
+                query.Query();
+
+                _jobs = query.Get<Job>().Rows.Select(r => new JobModel(r)).ToArray();
+                JobFilter.ItemsSource = _jobfilters;
+                JobFilter.SelectedValue = _jobfilters.FirstOrDefault(x => String.Equals(x.Name, Properties.JobFilter));
+            }
+
+        }
+        
+        public void Shutdown(CancelEventArgs? cancel)
+        {
+        }
+
+        public void Refresh()
+        {
+            using (new WaitCursor())
+            {
+                _equipment = EquipmentSelector.GetEquipmentData((row, img, activities) => new EquipmentResourceModel(row, img, activities));
+                var eqids = _equipment.Select(x => x.ID).ToArray();
+
+                DateTime fromdate = FromDate.DateTime.HasValue ? FromDate.DateTime.Value.Date : DateTime.Today;
+                DateTime todate = ToDate.DateTime.HasValue ? ToDate.DateTime.Value.Date : DateTime.Today.AddYears(1);
+
+                MultiQuery query = new MultiQuery();
+                
+                query.Add<EquipmentAssignment>(
+                    new Filter<EquipmentAssignment>(x => x.Equipment.ID).InList(eqids)
+                        .And(x => x.Date).IsGreaterThanOrEqualTo(fromdate)
+                        .And(x => x.Date).IsLessThanOrEqualTo(todate),
+                    EquipmentAssignmentModel.Columns,
+                    new SortOrder<EquipmentAssignment>(x => x.Equipment.ID).ThenBy(x => x.Date).ThenBy(x => x.Booked.Duration, SortDirection.Descending)
+                );
+
+
+                query.Query();
+
+                var assignments = query.Get<EquipmentAssignment>().Rows.Select(r => new EquipmentAssignmentModel(r)).ToArray();
+
+
+                var data = new DataTable();
+                data.Columns.Add("Date", typeof(DateTime));
+                data.Columns.Add("From", typeof(TimeSpan));
+                data.Columns.Add("To", typeof(TimeSpan));
+
+                foreach (var equipment in _equipment)
+                    data.Columns.Add(equipment.ID.ToString(), typeof(object));
+
+                for (var curdate = fromdate; curdate <= todate; curdate = curdate.AddDays(1))
+                {
+                    foreach (var slot in Properties.TimeSlots)
+                    {
+                        var values = new List<object> { curdate, slot.From, slot.To };
+                        foreach (var equipment in _equipment)
+                        {
+                            var value = new EquipmentPlannerValue();
+                            var bOK = CheckAssignments(equipment, curdate, slot, assignments, value);
+                            //bOK = bOK || CheckRoster(employee, curdate, value);
+                            //bOK = bOK || CheckStandardLeave(leavevalue, value);
+                            //bOK = bOK || CheckLeaveRequest(employee, curdate, _leaverequests, value);
+
+                            values.Add(value);
+                        }
+                        data.Rows.Add(values.ToArray());
+                    }
+                }
+
+                dataGrid.ItemsSource = data;
+            }
+        }
+        
+        private bool CheckAssignments(EquipmentResourceModel equipment, DateTime curdate, TimeSlot slot, EquipmentAssignmentModel[] assignments, EquipmentPlannerValue value)
+        {
+            var assignment = assignments.FirstOrDefault(x => (x.EquipmentID == equipment.ID) && (x.Date == curdate.Date) && (x.BookedStart < slot.To) && (x.BookedFinish > slot.From));
+            
+            var bgColor = Properties.WorkDays.Contains(curdate.DayOfWeek)
+                ? System.Drawing.Color.LightYellow
+                : System.Drawing.Color.LightGray;
+
+            value.Color = ImageUtils.ColorToString(bgColor);
+            
+            if (assignment == null)
+                return false;
+
+            value.ID = assignment.ID;
+            value.Text = (value.ID != Guid.Empty) ? assignment?.JobNumber ?? "" : "XX";
+            value.Color = ImageUtils.ColorToString(System.Drawing.Color.LightGreen);
+            
+            return true;
+        }
+
+        #region AutoGenerate Columns / Styling
+        
+        private class EquipmentResourcePlannerBackgroundConverter : UtilityConverter<EquipmentPlannerValue, Brush>
+        {
+            public override Brush Convert(EquipmentPlannerValue value) => value?.Background ?? new SolidColorBrush(Colors.LightYellow)  { Opacity = 0.8 };
+        }
+
+        private class EquipmentResourcePlannerForegroundConverter : UtilityConverter<EquipmentPlannerValue, Brush>
+        {
+            public override Brush Convert(EquipmentPlannerValue value) => value?.Foreground ?? new SolidColorBrush(Colors.DimGray)  { Opacity = 0.8 };
+        }
+
+        private class EquipmentResourcePlannerFontStyleConverter : UtilityConverter<EquipmentPlannerValue, FontStyle>
+        {
+            public override FontStyle Convert(EquipmentPlannerValue value) => FontStyles.Normal;
+        }
+
+        private class EquipmentResourcePlannerFontWeightConverter : UtilityConverter<EquipmentPlannerValue, FontWeight>
+        {
+            public override FontWeight Convert(EquipmentPlannerValue value) => FontWeights.Normal;
+        }
+
+        private class EquipmentResourcePlannerContentConverter : UtilityConverter<EquipmentPlannerValue, String?>
+        {
+            public override String? Convert(EquipmentPlannerValue value) => value?.Text;
+        }
+
+        private class DateFormatConverter : UtilityConverter<DateTime, String>
+        {
+            public String Format { get; private set; }
+            
+            public override string Convert(DateTime value)
+            {
+                return String.Format($"{{0:{Format}}}", value);
+            }
+
+            public DateFormatConverter(string format)
+            {
+                Format = format;
+            }
+        }
+        
+        private void DataGrid_AutoGeneratingColumn(object? sender, AutoGeneratingColumnArgs e)
+        {
+            e.Column.TextAlignment = TextAlignment.Center;
+            e.Column.HorizontalHeaderContentAlignment = HorizontalAlignment.Center;
+            e.Column.ColumnSizer = GridLengthUnitType.None;
+
+            var value = (e.Column.ValueBinding as Binding)!;
+            if (value.Path.Path.Equals("Date")) // && e.Column is GridDateTimeColumn dt)
+            {
+                if (dataGrid.Columns.FirstOrDefault() is GridTemplateColumn)
+                    e.Cancel = true;
+                else
+                {
+                    var col = new GridTemplateColumn();
+                    col.CellTemplate = TemplateGenerator.CreateDataTemplate(() =>
+                    {
+                        var stack = new StackPanel()
+                        {
+                            Orientation = Orientation.Vertical,
+                            VerticalAlignment = VerticalAlignment.Center,
+                            HorizontalAlignment = HorizontalAlignment.Stretch
+                        };
+                        var day = new Label()
+                        {
+                            HorizontalContentAlignment = HorizontalAlignment.Center
+                        };
+                        day.SetBinding(Label.ContentProperty, new Binding("Date") { Converter = new DateFormatConverter("dddd") });
+                        stack.Children.Add(day);
+                        var date = new Label()
+                        {
+                            HorizontalContentAlignment = HorizontalAlignment.Center
+                        };
+                        date.SetBinding(Label.ContentProperty, new Binding("Date") { Converter = new DateFormatConverter("dd MMM yy") });
+                        stack.Children.Add(date);
+                        return stack;
+                    });
+                    col.Width = 70;
+                    col.HeaderStyle = Resources["DateHeaderStyle"] as Style;
+                    col.HeaderText = "Date";
+                    col.AllowFocus = false;
+                    e.Column = col;
+                }
+                //dt.Width = 70;
+                //dt.HeaderStyle = Resources["DateHeaderStyle"] as Style;
+                //dt.AllowFocus = false;
+                //dt.CustomPattern = "dd MMM yy";
+                //dt.Pattern = DateTimePattern.CustomPattern;
+            }
+            else if (value.Path.Path.Equals("From") && e.Column is GridTimeSpanColumn ts)
+            {
+                ts.Width = Properties.TimeSlots.Length > 1 ? 50 : 0;
+                ts.HeaderText = "Time";
+                ts.HeaderStyle = Resources["TimeHeaderStyle"] as Style;
+                ts.Format = "hh:mm";
+                ts.AllowFocus = false;
+            }
+            else if (value.Path.Path.Equals("To"))
+            {
+                e.Column.Width = 0;
+                e.Column.HeaderStyle = Resources["TimeHeaderStyle"] as Style;
+                e.Column.AllowFocus = false;
+            }
+            else
+            {
+                var col = new GridTemplateColumn();
+                col.CellTemplate = TemplateGenerator.CreateDataTemplate(() =>
+                {
+                    var grid = new Grid()
+                    {
+                        VerticalAlignment = VerticalAlignment.Stretch,
+                        HorizontalAlignment = HorizontalAlignment.Stretch
+                    };
+                    grid.ColumnDefinitions.Add(new ColumnDefinition() {Width = new GridLength(1, GridUnitType.Star)});
+                    grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star)});
+                    grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto)});
+                    var job = new Label()
+                    {
+                        HorizontalContentAlignment = HorizontalAlignment.Center,
+                        VerticalContentAlignment = VerticalAlignment.Center
+                    };
+                    job.SetValue(Grid.ColumnProperty,0);
+                    job.SetValue(Grid.RowProperty,0);
+                    //day.SetBinding(Label.ContentProperty, new Binding("Date") { Converter = new DateFormatConverter("dddd") });
+                    grid.Children.Add(job);
+                    return grid;
+                });
+                col.Width = 80;
+                col.HeaderStyle = Resources["ContentHeaderStyle"] as Style;
+                col.AllowFocus = true;
+                var style = new Style(typeof(GridCell));
+                style.Setters.Add(new Setter(BackgroundProperty, new Binding(value.Path.Path) { Converter = new EquipmentResourcePlannerBackgroundConverter() }));
+                style.Setters.Add(new Setter(ForegroundProperty, new Binding(value.Path.Path) { Converter = new EquipmentResourcePlannerForegroundConverter() }));
+                col.CellStyle = style;
+                
+                col.HeaderText = (Guid.TryParse(value.Path.Path, out Guid id))
+                    ? _equipment.FirstOrDefault(x => String.Equals(x.ID, id))?.Code ?? value.Path.Path
+                    : value.Path.Path;
+                
+                e.Column = col;
+
+                
+                // var style = new Style(typeof(GridCell));
+                // style.Setters.Add(new Setter(BackgroundProperty, new Binding(value.Path.Path) { Converter = new EquipmentResourcePlannerBackgroundConverter() }));
+                // style.Setters.Add(new Setter(ForegroundProperty, new Binding(value.Path.Path) { Converter = new EquipmentResourcePlannerForegroundConverter() }));
+                // style.Setters.Add(new Setter(FontStyleProperty, new Binding(value.Path.Path) { Converter = new EquipmentResourcePlannerFontStyleConverter() }));
+                // style.Setters.Add(new Setter(FontWeightProperty, new Binding(value.Path.Path) { Converter = new EquipmentResourcePlannerFontWeightConverter() }));
+                // e.Column.CellStyle = style;
+                // e.Column.Width = 80;
+                // e.Column.HeaderStyle = Resources["ContentHeaderStyle"] as Style;
+                //
+                // e.Column.HeaderText = (Guid.TryParse(value.Path.Path, out Guid id))
+                //     ? _equipment.FirstOrDefault(x => String.Equals(x.ID, id))?.Code ?? value.Path.Path
+                //     : value.Path.Path;
+                //
+                // e.Column.DisplayBinding = new Binding { Path = new PropertyPath(e.Column.MappingName), Converter = new EquipmentResourcePlannerContentConverter() };
+                // //e.Column.ValueBinding = new Binding() { Path = new PropertyPath(e.Column.MappingName), Converter = new LeaveContentConverter() };
+                // //e.Column.UseBindingValue = true;
+                // e.Column.AllowFocus = true;
+            }
+        }
+
+        #endregion
+        
+        private bool HasData()
+        {
+            foreach (var cell in dataGrid.GetSelectedCells())
+            {
+                if (!cell.IsDataRowCell)
+                    continue;
+                var propertyCollection = dataGrid.View.GetPropertyAccessProvider();
+                var cellValue = propertyCollection.GetValue(cell.RowData, cell.Column.MappingName);
+                if (cellValue is EquipmentPlannerValue val && val.ID != Guid.Empty)
+                    return true;
+            }
+
+            return false;
+        }
+        
+        private bool HasData(GridCellInfo cell)
+        {
+            if (!cell.IsDataRowCell)
+                return false;
+            var propertyCollection = dataGrid.View.GetPropertyAccessProvider();
+            var cellValue = propertyCollection.GetValue(cell.RowData, cell.Column.MappingName);
+            return cellValue is EquipmentPlannerValue val && val.ID != Guid.Empty;
+        }
+
+        private void DataGrid_ContextMenuOpening(object sender, ContextMenuEventArgs e)
+        {
+            var vc = dataGrid.GetVisualContainer();
+            var p = Mouse.GetPosition(vc);
+            var rci = vc.PointToCellRowColumnIndex(p);
+            if (rci.RowIndex < 1 || rci.ColumnIndex < 1)
+            {
+                e.Handled = true;
+                return;
+            }
+
+            dataGrid.ContextMenu.Items.Clear();
+            var bAssign = !HasData(dataGrid.CurrentCellInfo);
+            var bClear = HasData();
+
+            if (bAssign)
+            {
+                foreach (var job in _jobs)
+                {
+                    var assign = new MenuItem
+                    {
+                        Header = job.Name,
+                        Tag = job
+                    };
+                    assign.Click += AssignJobClick;
+                    dataGrid.ContextMenu.Items.Add(assign);
+                }
+            }
+            
+            
+            if (bClear && bAssign)
+                dataGrid.ContextMenu.Items.Add(new Separator());
+            
+            
+            if (bClear)
+            {
+                var clear = new MenuItem { Header = "Clear Assignments" };
+                clear.Click += ClearJobClick;
+                dataGrid.ContextMenu.Items.Add(clear);
+            }
+            
+        }
+        
+        private void GetSelectionData(out DateTime from, out DateTime to, out Guid[] employees, out Guid[] assignments)
+        {
+            var emps = new List<Guid>();
+            var items = new List<Guid>();
+            from = DateTime.MaxValue;
+            to = DateTime.MinValue;
+            foreach (var cell in dataGrid.GetSelectedCells())
+            {
+                var binding = (cell.Column.ValueBinding as Binding)!;
+                if (Guid.TryParse(binding.Path.Path, out var emp))
+                    if (!emps.Contains(emp))
+                        emps.Add(emp);
+                var row = (cell.RowData as DataRowView)!;
+                var date = (DateTime)row.Row.ItemArray.First()!;
+                var fromtime = (TimeSpan)row.Row.ItemArray.Skip(1).First()!;
+                var totime = (TimeSpan)row.Row.ItemArray.Skip(2).First()!;
+                if (date.Add(fromtime) < from)
+                    from = date.Add(fromtime);
+                if (date.Add(totime) > to)
+                    to = date.Add(totime);
+                Guid itemid = (row[binding.Path.Path] as EquipmentPlannerValue).ID;
+                if (itemid != Guid.Empty)
+                    items.Add(itemid);
+            }
+
+            employees = emps.ToArray();
+            assignments = items.ToArray();
+        }
+
+
+        private void AssignJobClick(object sender, RoutedEventArgs e)
+        {
+            JobModel? job = (sender as MenuItem)?.Tag as JobModel;
+            if (job == null)
+                return;
+            GetSelectionData(out var from, out var to, out var ids, out var assignments);
+            var updates = new List<EquipmentAssignment>();
+            foreach (var id in ids)
+            {
+                for (DateTime curdate = from.Date; curdate <= to.Date; curdate = curdate.AddDays(1))
+                {
+                    var equipment = _equipment.FirstOrDefault(x => x.ID == id);
+                    if (equipment != null)
+                    {
+                        var assign = new EquipmentAssignment();
+                        assign.Date = curdate;
+                        assign.Booked.Start = curdate == from.Date ? from.TimeOfDay : Properties.TimeSlots.FirstOrDefault()?.From ?? TimeSpan.Zero;
+                        assign.Booked.Finish = curdate == to.Date ? to.TimeOfDay : Properties.TimeSlots.LastOrDefault()?.To ?? TimeSpan.FromDays(1).Subtract(TimeSpan.FromSeconds(1));
+                        assign.JobLink.ID = job.ID;
+                        assign.Equipment.ID = id;
+                        updates.Add(assign);
+                    }
+                }
+            }
+            if (updates.Any())
+            {
+                using (new WaitCursor())
+                {
+                    new Client<EquipmentAssignment>().Save(updates, "Assigned from Employee Resource Planner");
+                    Refresh();
+                }
+            }
+        }
+
+
+        private void ClearJobClick(object sender, RoutedEventArgs e)
+        {
+            GetSelectionData(out DateTime from, out DateTime to, out Guid[] ids, out Guid[] assignments);
+            if (assignments.Any() && MessageBox.Show("Clear Assignments?", "Confirm", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+            {
+                var deletes = assignments.Select(x => new EquipmentAssignment() { ID = x }).ToArray();
+                using (new WaitCursor())
+                {
+                    new Client<EquipmentAssignment>().Delete(deletes, "Deleted from Employee Resource Planner");
+                    Refresh();
+                }
+            }
+        }
+        
+        public void Heartbeat(TimeSpan time)
+        {
+
+        }
+
+        private void _equipment_OnSettingsChanged(object sender, EquipmentSelectorSettingsChangedArgs args)
+        {
+            Properties.EquipmentSettings = args.Settings;
+            DoSaveSettings();
+        }
+
+        private void _equipment_OnSelectionChanged(object sender, EquipmentSelectorSelectionChangedArgs args)
+        {
+            Properties.EquipmentSelection = args.Selection;
+            DoSaveSettings();
+            Refresh();
+        }
+
+        private void DateTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            if (EventSuppressor.IsSet(Suppress.This))
+                return;
+            Refresh();
+        }
+        
+        private void JobFilter_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (EventSuppressor.IsSet(Suppress.This))
+                return;
+            var sel = JobFilter.SelectedValue as CoreFilterDefinition;
+            Properties.JobFilter = sel?.Name ?? "";
+            using (new WaitCursor())
+            {
+                DoSaveSettings();
+                _jobs = new Client<Job>().Query(
+                    GetJobFilter(),
+                    JobModel.Columns,
+                    new SortOrder<Job>(x => x.JobNumber)
+                ).Rows.Select(r => new JobModel(r)).ToArray();
+            }
+        }
+
+        private void JobFilterButton_Click(object sender, RoutedEventArgs e)
+        {
+            var window = new DynamicGridFilterEditor(_jobfilters, typeof(Job));
+            if (window.ShowDialog() == true)
+            {
+                new GlobalConfiguration<CoreFilterDefinitions>("Job").Save(_jobfilters);
+                JobFilter.SelectedValue = _jobfilters.FirstOrDefault(x => String.Equals(x.Name, Properties.JobFilter));
+            }
+        }
+
+        private void DataGrid_OnQueryCoveredRange(object? sender, GridQueryCoveredRangeEventArgs e)
+        {
+            if (Properties.TimeSlots.Length <= 1 || e.RowColumnIndex.RowIndex == 0)
+                return;
+            if (e.RowColumnIndex.ColumnIndex == 0)
+            {
+                var top = (((e.RowColumnIndex.RowIndex - 1) / Properties.TimeSlots.Length) * Properties.TimeSlots.Length) + 1;
+                var bottom = top + Properties.TimeSlots.Length - 1;
+                try
+                {
+                    e.Range = new CoveredCellInfo(0, 0, top, bottom);
+                }
+                catch (Exception _exception)
+                {
+                    System.Console.WriteLine(_exception);
+                    throw;
+                }
+            }
+            else if (e.Record is DataRowView drv && e.RowColumnIndex.ColumnIndex > 1 && e.RowColumnIndex.ColumnIndex < drv.Row.ItemArray.Length  && drv.Row.ItemArray[e.RowColumnIndex.ColumnIndex] is EquipmentPlannerValue epv)
+            {
+                if (epv.ID != Guid.Empty)
+                {
+                    int iMin = int.MaxValue;
+                    int iMax = int.MinValue;
+                    var rows = drv.DataView.OfType<DataRowView>().ToArray();
+                    for (int i = 0; i < drv.DataView.Count; i++)
+                    {
+                        var test = drv.DataView[i].Row.ItemArray[e.RowColumnIndex.ColumnIndex] as EquipmentPlannerValue;
+                        if (test != null && test.ID == epv.ID)
+                        {
+                            if (i < iMin)
+                                iMin = i;
+                            if (i > iMax)
+                                iMax = i;
+                        }
+                    }
+                    e.Range = new CoveredCellInfo(e.RowColumnIndex.ColumnIndex, e.RowColumnIndex.ColumnIndex, iMin+1, iMax+1);                    
+
+                }
+            }
+
+            e.Handled = true;
+        }
+    }
+    
+}

+ 40 - 0
prs.desktop/Panels/EquipmentPlanner/EquipmentPlannerPanel.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using Comal.Classes;
+using InABox.Configuration;
+using InABox.Core;
+using Microsoft.Exchange.WebServices.Data;
+
+namespace PRSDesktop
+{
+    public class EquipmentPlannerPanel : EquipmentPlanner, IPanel<Assignment>
+    {
+
+        public EquipmentPlannerPanel()
+        {
+            SectionName = nameof(EquipmentPlannerPanel);
+            LoadSettings += (sender) => new UserConfiguration<EquipmentPlannerProperties>(nameof(EquipmentPlannerPanel)).Load();
+            SaveSettings += (sender, properties) => new UserConfiguration<EquipmentPlannerProperties>(nameof(EquipmentPlannerPanel)).Save(properties);
+        }
+        
+        public string SectionName { get; }
+        
+        public DataModel DataModel(Selection selection)
+        {
+            return new AutoDataModel<Assignment>(new Filter<Assignment>(x=>x.ID).IsEqualTo(Guid.Empty));
+        }
+
+        public event DataModelUpdateEvent? OnUpdateDataModel;
+        
+        public bool IsReady { get; set; }
+        
+        public void CreateToolbarButtons(IPanelHost host)
+        {
+        }
+
+        public Dictionary<string, object[]> Selected()
+        {
+            return new Dictionary<string, object[]>();
+        }
+    }
+}

+ 42 - 0
prs.desktop/Panels/EquipmentPlanner/EquipmentPlannerProperties.cs

@@ -0,0 +1,42 @@
+using System;
+using InABox.Configuration;
+using PRSDesktop;
+
+public class EquipmentPlannerProperties : IUserConfigurationSettings, IDashboardProperties
+{
+    public EquipmentSelectorSettings EquipmentSettings { get; set; }
+        
+    public EquipmentSelectorData EquipmentSelection { get; set; }
+    public String JobFilter { get; set; }
+    
+    public DayOfWeek[] WorkDays { get; set; }
+    
+    public TimeSlot[] TimeSlots { get; set; }
+
+    public EquipmentPlannerProperties()
+    {
+        EquipmentSettings = new EquipmentSelectorSettings();
+        EquipmentSelection = new EquipmentSelectorData();
+        JobFilter = "";
+        
+        WorkDays = new[]
+        {
+            DayOfWeek.Monday,
+            DayOfWeek.Tuesday,
+            DayOfWeek.Wednesday,
+            DayOfWeek.Thursday,
+            DayOfWeek.Friday,
+        };
+        
+        TimeSlots = new[]
+        {
+            // new TimeSlot(TimeSpan.Zero,TimeSpan.FromDays(1))
+            new TimeSlot(TimeSpan.FromHours(6), TimeSpan.FromHours(8)),
+            new TimeSlot(TimeSpan.FromHours(8), TimeSpan.FromHours(10)),
+            new TimeSlot(TimeSpan.FromHours(10), TimeSpan.FromHours(12)),
+            new TimeSlot(TimeSpan.FromHours(12), TimeSpan.FromHours(14)),
+            new TimeSlot(TimeSpan.FromHours(14), TimeSpan.FromHours(16)),
+            new TimeSlot(TimeSpan.FromHours(16), TimeSpan.FromHours(18))
+        };
+    }
+}