Sfoglia il codice sorgente

avalonia: Added filters to InOut board and improved in/out board display

Kenric Nugteren 1 mese fa
parent
commit
6784c9938f

+ 19 - 9
PRS.Avalonia/PRS.Avalonia/Modules/InOut/InOutView.axaml

@@ -7,13 +7,23 @@
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PRS.Avalonia.Modules.InOutView"
 			 x:DataType="local:InOutViewModel">
-	<components:AvaloniaDataGrid ItemsSource="{Binding ItemsSource}"
-								 Columns="{Binding Columns}"
-								 Margin="5"
-								 RefreshRequested="Grid_OnRefreshRequested"
-								 SelectionMode="AlwaysSelected"
-								 ShowRecordCount="True"
-								 CanSearch="True"
-								 RefreshVisible="True"
-								 LastUpdated="{Binding LastUpdated}"/>
+	<Grid>
+		<Grid.RowDefinitions>
+			<RowDefinition Height="Auto"/>
+			<RowDefinition Height="*"/>
+		</Grid.RowDefinitions>
+		<components:ButtonStrip Name="Filters" Grid.Row="0" ItemsSource="{Binding FilterButtons}" SelectedCommand="{Binding FilterSelectedCommand}"/>
+		<components:AvaloniaDataGrid Grid.Row="1"
+									 ItemsSource="{Binding ItemsSource}"
+									 Columns="{Binding Columns}"
+									 Margin="5"
+									 RefreshRequested="Grid_OnRefreshRequested"
+									 Focusable="False"
+									 SelectionMode="None"
+									 ShowRecordCount="True"
+									 CanSearch="True"
+									 RefreshVisible="True"
+									 LastUpdated="{Binding LastUpdated}"
+									 LoadingRow="Grid_LoadingRow"/>
+	</Grid>
 </UserControl>

+ 5 - 0
PRS.Avalonia/PRS.Avalonia/Modules/InOut/InOutView.axaml.cs

@@ -16,4 +16,9 @@ public partial class InOutView : UserControl
     {
         Model.RefreshCommand.Execute(null);
     }
+    
+    private void Grid_LoadingRow(object? sender, DataGridRowEventArgs e)
+    {
+        Model.LoadRowCommand.Execute(e.Row);
+    }
 }

+ 54 - 14
PRS.Avalonia/PRS.Avalonia/Modules/InOut/InOutViewModel.cs

@@ -3,16 +3,23 @@ using Avalonia.Media;
 using Comal.Classes;
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.Input;
+using DynamicData;
+using InABox.Avalonia;
 using InABox.Avalonia.Components;
 using InABox.Core;
 using System;
 using System.Collections;
+using System.Collections.ObjectModel;
+using System.Linq;
 using System.Threading.Tasks;
 
 namespace PRS.Avalonia.Modules;
 
 public partial class InOutViewModel : ModuleViewModel
 {
+    const string AllFilter = "All";
+    const string NotInFilter = "Not In";
+
     public override string Title => "In/Out";
 
     [ObservableProperty]
@@ -21,6 +28,9 @@ public partial class InOutViewModel : ModuleViewModel
     [ObservableProperty]
     private IEnumerable? _itemsSource;
 
+    [ObservableProperty]
+    private ObservableCollection<object?> _filterButtons;
+
     [ObservableProperty]
     private DateTime _lastUpdated;
 
@@ -32,27 +42,25 @@ public partial class InOutViewModel : ModuleViewModel
         ProgressVisible = true;
 
         Columns = new AvaloniaDataGridColumns().BeginUpdate();
-        if (!Security.IsAllowed<CanViewMobileInOutBoardDetails>())
-        {
-            Columns.Add(new AvaloniaDataGridImageColumn<InOutShell>()
-            {
-                Column = x => x.In,
-                Header = Images.circle_gray,
-                 Margin = 6,
-                Width = new GridLength(30),
-            });
-        }
+        // Columns.Add(new AvaloniaDataGridImageColumn<InOutShell>()
+        // {
+        //     Column = x => x.In,
+        //     Header = Images.circle_gray,
+        //      Margin = 6,
+        //     Width = new GridLength(30),
+        // });
         Columns.Add(new AvaloniaDataGridTextColumn<InOutShell>
         {
             Column = x => x.Name,
             Alignment = TextAlignment.Start,
             Width = GridLength.Star
         });
-        if (Security.IsAllowed<CanViewMobileInOutBoardDetails>())
+        Columns.Add(new AvaloniaDataGridTextColumn<InOutShell>
         {
-            Columns.Add(new AvaloniaDataGridTimeColumn<InOutShell> { Column = x => x.Start, Width = new(50) });
-            Columns.Add(new AvaloniaDataGridTimeColumn<InOutShell> { Column = x => x.Finish, Width = new(50) });
-        }
+            Column = x => x.Status,
+            Alignment = TextAlignment.Start,
+            Width = GridLength.Star
+        });
 
         Columns.Add(new AvaloniaDataGridImageColumn<InOutShell>()
         {
@@ -70,6 +78,8 @@ public partial class InOutViewModel : ModuleViewModel
                 .Add(LookupFactory.DefineFilter<Employee>())
                 .Add(new Filter<Employee>(x => x.ID).IsNotEqualTo(Repositories.Me.ID).And(x => x.ShowOnInOutBoard).IsEqualTo(true))
                 .Combine() ?? new Filter<Employee>().All());
+
+        FilterButtons = [AllFilter, NotInFilter, .. Model.AvailableFilters];
     }
 
     protected override async Task<TimeSpan> OnRefresh()
@@ -90,4 +100,34 @@ public partial class InOutViewModel : ModuleViewModel
         LastUpdated = Model.LastUpdated;
         ProgressVisible = false;
     }
+
+    [RelayCommand]
+    private void LoadRow(DataGridRow row)
+    {
+        if (row.DataContext is not InOutShell shell) return;
+
+        row.Background = new SolidColorBrush(shell.Colour);
+    }
+
+    [RelayCommand]
+    private void FilterSelected(object? filter)
+    {
+        if(filter is string stringFilter)
+        {
+            Model.SelectFilter(null);
+            if(stringFilter == AllFilter)
+            {
+                Model.Search(null);
+            }
+            else if(stringFilter == NotInFilter)
+            {
+                Model.Search(x => !x.IsClockedOn);
+            }
+        }
+        else if(filter is CoreRepositoryFilter coreFilter)
+        {
+            Model.SelectFilter(coreFilter.Name);
+            Model.Search(null);
+        }
+    }
 }

+ 169 - 18
PRS.Avalonia/PRS.Avalonia/Repositories/InOut/InOutModel.cs

@@ -1,5 +1,7 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
+using Avalonia.Media;
 using Comal.Classes;
 using InABox.Avalonia;
 using InABox.Core;
@@ -8,49 +10,198 @@ namespace PRS.Avalonia;
 
 public class InOutModel : CoreRepository<InOutModel, InOutShell, Employee>
 {
-    private Tuple<Guid, TimeSpan, TimeSpan>[] _statuses;
+    private Dictionary<Guid, TimeSheet[]> _timeSheets = [];
+    private StandardLeave[] _standardLeave = [];
+    private Dictionary<Guid, LeaveRequest[]> _leaveRequests = [];
+    private Dictionary<Guid, Activity> _activities = [];
+    private Dictionary<Guid, EmployeeRosterItem[]> _rosters = [];
 
     public InOutModel(IModelHost host, Func<Filter<Employee>> filter, Func<string>? filename = null) : base(host, filter, filename)
     {
     }
 
-    protected override void Initialize()
+    public class InOutStatus(string status, Color colour, bool clockedOn)
     {
-        base.Initialize();
-        _statuses = new Tuple<Guid, TimeSpan, TimeSpan>[] { };
-    }
+        public string Status { get; set; } = status;
 
-    public bool IsClockedOn(Guid id)
-    {
-        return _statuses.Any(x => x.Item1.Equals(id) && x.Item3.Equals(TimeSpan.Zero));
-    }
+        public Color Colour { get; set; } = colour;
 
-    public TimeSpan StartTime(Guid id)
-    {
-        return _statuses.FirstOrDefault(x => x.Item1.Equals(id))?.Item2 ?? TimeSpan.Zero;
+        public bool ClockedOn { get; set; } = clockedOn;
     }
 
-    public TimeSpan FinishTime(Guid id)
+    public InOutStatus StatusObject(InOutShell shell)
     {
-        return _statuses.FirstOrDefault(x => x.Item1.Equals(id))?.Item3 ?? TimeSpan.Zero;
+        var time = DateTime.Now.TimeOfDay;
+
+        TimeSheet? closedTimeSheet = null;
+        if(_timeSheets.TryGetValue(shell.ID, out var sheets))
+        {
+            var openTimeSheet = sheets.FirstOrDefault(x => x.Finish == TimeSpan.Zero || x.Finish > time);
+            if(openTimeSheet is not null)
+            {
+                if(_activities.TryGetValue(openTimeSheet.ActivityLink.ID, out var activity))
+                {
+                    return new(openTimeSheet.Address, Color.TryParse(activity.Color, out var colour) ? colour : Colors.LightGreen, true);
+                }
+                else
+                {
+                    return new(openTimeSheet.Address, Colors.LightGreen, true);
+                }
+            }
+            else
+            {
+                closedTimeSheet = sheets?.MaxBy(x => x.Start);
+                // Check leave and rosters before giving a result.
+            }
+        }
+        
+        if(_standardLeave.Length > 0)
+        {
+            var leave = _standardLeave.FirstOrDefault(x =>
+                (x.From < DateTime.Today || x.FromTime <= time) &&
+                (x.To > DateTime.Today || x.ToTime >= time));
+            if(leave is not null)
+            {
+                var activity = _activities.GetValueOrDefault(leave.LeaveType.ID);
+                return new(
+                    activity?.Description ?? "Leave",
+                    activity is not null && Color.TryParse(activity.Color, out var colour) ? colour : Colors.Gainsboro,
+                    false);
+            }
+        }
+
+        if(_leaveRequests.TryGetValue(shell.ID, out var leaveRequests))
+        {
+            var leave = leaveRequests.FirstOrDefault(x =>
+                (x.From < DateTime.Today || x.FromTime <= time) &&
+                (x.To > DateTime.Today || x.ToTime >= time));
+            if(leave is not null)
+            {
+                var activity = _activities.GetValueOrDefault(leave.LeaveType.ID);
+                return new(
+                    activity?.Description ?? "Leave",
+                    activity is not null && Color.TryParse(activity.Color, out var colour) ? colour : Colors.Gainsboro,
+                    false);
+            }
+        }
+
+        if(_rosters.TryGetValue(shell.ID, out var rosters))
+        {
+            var roster = RosterUtils.GetRoster(rosters, shell.RosterStart, DateTime.Today);
+            if(roster is null)
+            {
+                if (closedTimeSheet is not null)
+                {
+                    return new("Finished", Colors.Gainsboro, false);
+                }
+                else
+                {
+                    return new("Not rostered on", Colors.Gainsboro, false);
+                }
+            }
+            else
+            {
+                var block = RosterUtils.GetBlocks(roster, DateTime.Today, TimeSpan.MinValue, TimeSpan.MaxValue)
+                    .FirstOrDefault(x => x.Start <= time && time <= x.Finish);
+                if(block is null)
+                {
+                    if (closedTimeSheet is not null)
+                    {
+                        return new("Finished", Colors.Gainsboro, false);
+                    }
+                    else
+                    {
+                        return new("Not rostered on", Colors.Gainsboro, false);
+                    }
+                }
+                else if(closedTimeSheet is not null)
+                {
+                    if(closedTimeSheet.Finish >= block.Start)
+                    {
+                        return new("Finished early", Colors.LightSalmon, false);
+                    }
+                    else
+                    {
+                        return new("Overdue", Colors.LightSalmon, false);
+                    }
+                }
+                else
+                {
+                    return new("Overdue", Colors.LightSalmon, false);
+                }
+            }
+        }
+
+        if (closedTimeSheet is not null)
+        {
+            return new("Finished", Colors.Gainsboro, false);
+        }
+        else
+        {
+            return new("Not rostered on", Colors.Gainsboro, false);
+        }
     }
 
     protected override void BeforeLoad(MultiQuery query)
     {
         base.BeforeLoad(query);
-        query.Add<TimeSheet>(
+        query.Add(
             new Filter<TimeSheet>(x => x.Date).IsEqualTo(DateTime.Today),
             new Columns<TimeSheet>(ColumnTypeFlags.None).Add(x => x.EmployeeLink.ID)
                 .Add(x => x.Start)
                 .Add(x => x.Finish)
+                .Add(x => x.Address)
+                .Add(x => x.ActivityLink.ID)
+        );
+        query.Add(
+            new Filter<StandardLeave>(x => x.From).IsLessThanOrEqualTo(DateTime.Today)
+                .And(x => x.To).IsGreaterThanOrEqualTo(DateTime.Today),
+            Columns.None<StandardLeave>().Add(x => x.From)
+                .Add(x => x.FromTime)
+                .Add(x => x.To)
+                .Add(x => x.ToTime)
+                .Add(x => x.LeaveType.ID)
+        );
+        
+        query.Add(
+            new Filter<LeaveRequest>(x => x.From).IsLessThanOrEqualTo(DateTime.Today)
+                .And(x => x.To).IsGreaterThanOrEqualTo(DateTime.Today)
+                .And(x => x.Status).IsNotEqualTo(LeaveRequestStatus.Rejected),
+            Columns.None<LeaveRequest>().Add(x => x.EmployeeLink.ID)
+                .Add(x => x.From)
+                .Add(x => x.FromTime)
+                .Add(x => x.To)
+                .Add(x => x.ToTime)
+                .Add(x => x.LeaveType.ID)
         );
+        query.Add(
+            new Filter<EmployeeRosterItem>(x => x.Employee.ID).InQuery(EffectiveFilter(), x => x.ID),
+            sort: new SortOrder<EmployeeRosterItem>(x => x.Day));
+        query.Add(
+            columns: Columns.None<Activity>()
+                .Add(x => x.ID)
+                .Add(x => x.Color)
+                .Add(x => x.Description));
     }
 
     protected override void AfterLoad(MultiQuery query)
     {
         base.AfterLoad(query);
-        _statuses = query.Get<TimeSheet>()
-            .ToTuples<TimeSheet, Guid, TimeSpan, TimeSpan>(x => x.EmployeeLink.ID, x => x.Start, x => x.Finish)
-            .ToArray();
+        _timeSheets = query.Get<TimeSheet>()
+            .ToObjects<TimeSheet>()
+            .GroupBy(x => x.EmployeeLink.ID)
+            .ToDictionary(x => x.Key, x => x.ToArray());
+        _standardLeave = query.Get<StandardLeave>()
+            .ToObjects<StandardLeave>().ToArray();
+        _leaveRequests = query.Get<LeaveRequest>()
+            .ToObjects<LeaveRequest>()
+            .GroupBy(x => x.EmployeeLink.ID)
+            .ToDictionary(x => x.Key, x => x.ToArray());
+        _rosters = query.Get<EmployeeRosterItem>()
+            .ToObjects<EmployeeRosterItem>()
+            .GroupBy(x => x.Employee.ID)
+            .ToDictionary(x => x.Key, x => x.ToArray());
+        _activities = query.Get<Activity>()
+            .ToObjects<Activity>().ToDictionary(x => x.ID);
     }
 }

+ 21 - 7
PRS.Avalonia/PRS.Avalonia/Repositories/InOut/InOutShell.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Media;
+using BruTile.Wms;
 using Comal.Classes;
 using InABox.Avalonia;
 
@@ -11,24 +12,37 @@ public class InOutShell : Shell<InOutModel, Employee>
     
     public string Mobile => Get<string>();
 
+    public string Status => StatusObject.Status;
+
+    public Color Colour => StatusObject.Colour;
+
     public IImage? Call => string.IsNullOrWhiteSpace(Mobile)
         ? null
         : Images.phone;
-
-    public TimeSpan Start => Parent.StartTime(ID);
-
-    public TimeSpan Finish => Parent.FinishTime(ID);
     
-    public bool IsClockedOn => Parent.IsClockedOn(ID);
+    public bool IsClockedOn => StatusObject.ClockedOn;
 
-    public IImage? In => Parent.IsClockedOn(ID)
+    public IImage? In => StatusObject.ClockedOn
         ? Images.circle_green
         : Images.circle_red;
 
+    public DateTime RosterStart => Get<DateTime>();
+
+    private InOutModel.InOutStatus? _statusObject;
+    private InOutModel.InOutStatus StatusObject
+    {
+        get
+        {
+            _statusObject ??= Parent.StatusObject(this);
+            return _statusObject;
+        }
+    }
+
     protected override void ConfigureColumns(ShellColumns<InOutModel, Employee> columns)
     {
         columns
             .Map(nameof(Name), x => x.Name)
-            .Map(nameof(Mobile), x => x.Mobile);
+            .Map(nameof(Mobile), x => x.Mobile)
+            .Map(nameof(RosterStart), x => x.RosterStart);
     }
 }

+ 1 - 0
prs.desktop/Components/Calendar/Appointments/LeaveRequestAppointment.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Windows.Media.Imaging;
+using Comal.Classes;
 using InABox.WPF;
 using PRS.Shared;
 

+ 1 - 0
prs.desktop/Components/Calendar/Appointments/StandardLeaveAppointment.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Windows.Media.Imaging;
+using Comal.Classes;
 using InABox.WPF;
 using PRS.Shared;