Просмотр исходного кода

avalonia: Assignments View now has a Month View as well.

Kenric Nugteren 4 дней назад
Родитель
Сommit
6ae6ae3ac2

+ 83 - 50
PRS.Avalonia/PRS.Avalonia/Modules/Assignments/AssignmentsView.axaml

@@ -10,72 +10,105 @@
              x:DataType="viewModels:AssignmentsViewModel"
              Name="View">
     <Grid RowDefinitions="Auto,*"
-          ColumnDefinitions="*,Auto,Auto,Auto">
+          ColumnDefinitions="Auto,*,Auto,Auto,Auto">
         <Button Classes="Standard"
                 Grid.Row="0" Grid.Column="0"
+                Command="{Binding MenuCommand}">
+            <Image Classes="Small" Source="{SvgImage /Images/lines.svg}"/>
+        </Button>
+        <Button Classes="Standard"
+                Grid.Row="0" Grid.Column="1"
                 Content="{Binding EmployeeName}"
                 Command="{Binding SelectEmployeesCommand}"/>
         <Button Classes="Standard"
-                Grid.Row="0" Grid.Column="1"
+                Grid.Row="0" Grid.Column="2"
                 Command="{Binding PreviousDayCommand}">
             <Image Classes="Small" Source="{SvgImage /Images/arrow_white_left.svg}"/>
         </Button>
         <Button Classes="Standard"
-                Grid.Row="0" Grid.Column="2"
+                Grid.Row="0" Grid.Column="3"
                 Command="{Binding SelectDayCommand}">
             <Image Classes="Small" Source="{SvgImage /Images/schedule.svg}"/>
         </Button>
         <Button Classes="Standard"
-                Grid.Row="0" Grid.Column="3"
+                Grid.Row="0" Grid.Column="4"
                 Command="{Binding NextDayCommand}">
             <Image Classes="Small" Source="{SvgImage /Images/arrow_white_right.svg}"/>
         </Button>
-        <components:CalendarView Classes="Standard"
-                                 ItemsSource="{Binding Model.Items}"
-                                 Columns="{Binding EmployeeIDs}"
-                                 MinimumColumnWidth="150"
-                                 StartTimeMapping="{Binding StartTime, Converter={x:Static viewModels:AssignmentsView.DateTimeToTimeSpanConverter}}"
-                                 EndTimeMapping="{Binding EndTime, Converter={x:Static viewModels:AssignmentsView.DateTimeToTimeSpanConverter}}"
-                                 ColumnMapping="{Binding EmployeeID}"
-                                 ShowColumns="{Binding ShowColumns}"
-                                 BlockClicked="Calendar_BlockClicked"
-                                 BlockHeld="Calendar_BlockHeld"
-                                 Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4">
-            <components:CalendarView.HeaderTemplate>
-                <DataTemplate>
-                    <Border Background="White">
-                        <TextBlock TextAlignment="Center"
-                                   Padding="5">
-                            <TextBlock.Text>
-                                <MultiBinding Converter="{x:Static viewModels:AssignmentsView.ColumnHeaderConverter}">
-                                    <Binding/>
-                                    <Binding ElementName="View"/>
-                                </MultiBinding>
-                            </TextBlock.Text>
-                        </TextBlock>
-                    </Border>
-                </DataTemplate>
-            </components:CalendarView.HeaderTemplate>
-            <components:CalendarView.ItemTemplate>
-                <DataTemplate>
-                    <Border Classes="Standard"
-                            Background="{Binding Background}">
-                        <Grid RowDefinitions="Auto,*" ColumnDefinitions="*,Auto">
-                            <TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
+        <TabControl Classes="Standard HideHeader"
+                    TabStripPlacement="Bottom"
+                    SelectedIndex="{Binding SelectedTab}"
+                    Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="5">
+            <TabItem Header="Day">
+                <components:CalendarView Classes="Standard"
+                                         ItemsSource="{Binding DayItems}"
+                                         Columns="{Binding EmployeeIDs}"
+                                         MinimumColumnWidth="150"
+                                         StartTimeMapping="{Binding StartTime, Converter={x:Static viewModels:AssignmentsView.DateTimeToTimeSpanConverter}}"
+                                         EndTimeMapping="{Binding EndTime, Converter={x:Static viewModels:AssignmentsView.DateTimeToTimeSpanConverter}}"
+                                         ColumnMapping="{Binding EmployeeID}"
+                                         ShowColumns="{Binding ShowColumns}"
+                                         BlockClicked="Calendar_BlockClicked"
+                                         BlockHeld="Calendar_BlockHeld">
+                    <components:CalendarView.HeaderTemplate>
+                        <DataTemplate>
+                            <Border Background="White">
+                                <TextBlock TextAlignment="Center"
+                                           Padding="5">
+                                    <TextBlock.Text>
+                                        <MultiBinding Converter="{x:Static viewModels:AssignmentsView.ColumnHeaderConverter}">
+                                            <Binding/>
+                                            <Binding ElementName="View"/>
+                                        </MultiBinding>
+                                    </TextBlock.Text>
+                                </TextBlock>
+                            </Border>
+                        </DataTemplate>
+                    </components:CalendarView.HeaderTemplate>
+                    <components:CalendarView.ItemTemplate>
+                        <DataTemplate>
+                            <Border Classes="Standard"
+                                    Background="{Binding Background}">
+                                <Grid RowDefinitions="Auto,*" ColumnDefinitions="*,Auto">
+                                    <TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
+                                               Classes="ExtraSmall Bold"
+                                               Text="{Binding Subject}"/>
+                                    <TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
+                                               Classes="ExtraSmall"
+                                               Text="{Binding Description}"/>
+                                    <Image Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2"
+                                           Source="{SvgImage /Images/tick.svg}"
+                                           Classes="Small"
+                                           IsVisible="{Binding Completed, Converter={x:Static converters:DateTimeToBooleanConverter.Instance}}"
+                                           HorizontalAlignment="Right" VerticalAlignment="Top"/>
+                                </Grid>
+                            </Border>
+                        </DataTemplate>
+                    </components:CalendarView.ItemTemplate>
+                </components:CalendarView>
+            </TabItem>
+            <TabItem Header="Month">
+                <components:MonthView Classes="Standard"
+                                      ItemsSource="{Binding MonthItems}"
+                                      CurrentDate="{Binding Date}"
+                                      StartDateMapping="{Binding Date}"
+                                      EndDateMapping="{Binding Date}"
+                                      ColourMapping="{Binding Background}"
+                                      DateRangeChanged="MonthView_DateRangeChanged"
+                                      BlockClicked="MonthView_Block_Clicked"
+                                      BlockHeld="MonthView_Block_Held"
+                                      CellClicked="MonthView_Cell_Clicked"
+                                      CellHeld="MonthView_Cell_Held">
+                    <components:MonthView.ItemTemplate>
+                        <DataTemplate>
+                            <TextBlock Foreground="{Binding TextColor}"
                                        Classes="ExtraSmall Bold"
-                                       Text="{Binding Subject}"/>
-                            <TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
-                                       Classes="ExtraSmall"
-                                       Text="{Binding Description}"/>
-                            <Image Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2"
-                                   Source="{SvgImage /Images/tick.svg}"
-                                   Classes="Small"
-                                   IsVisible="{Binding Completed, Converter={x:Static converters:DateTimeToBooleanConverter.Instance}}"
-                                   HorizontalAlignment="Right" VerticalAlignment="Top"/>
-                        </Grid>
-                    </Border>
-                </DataTemplate>
-            </components:CalendarView.ItemTemplate>
-        </components:CalendarView>
+                                       Text="{Binding ShortDisplay}"
+                                       VerticalAlignment="Center"/>
+                        </DataTemplate>
+                    </components:MonthView.ItemTemplate>
+                </components:MonthView>
+            </TabItem>
+        </TabControl>
     </Grid>
 </UserControl>

+ 46 - 0
PRS.Avalonia/PRS.Avalonia/Modules/Assignments/AssignmentsView.axaml.cs

@@ -58,4 +58,50 @@ public partial class AssignmentsView : UserControl
             contextMenu.Open(sender as Control);
         }
     }
+
+    private void MonthView_Block_Clicked(object sender, MonthViewBlockEventArgs e)
+    {
+        if (DataContext is not AssignmentsViewModel model) return;
+        if(e.Value is AssignmentShell assignment)
+        {
+            model.BlockClicked(assignment).ErrorIfFail();
+        }
+    }
+
+    private void MonthView_Block_Held(object sender, MonthViewBlockEventArgs e)
+    {
+        if (DataContext is not AssignmentsViewModel model) return;
+        if(e.Value is AssignmentShell assignment)
+        {
+            var menu = new CoreMenu<IImage>();
+            model.BlockHeld(assignment, menu);
+            if (menu.Items.Count == 0) return;
+            var contextMenu = menu.Build();
+            contextMenu.Open(sender as Control);
+        }
+    }
+
+    private void MonthView_Cell_Clicked(object sender, MonthViewCellEventArgs e)
+    {
+        if (DataContext is not AssignmentsViewModel model) return;
+        model.EmptyMonthBlockClicked(e.Date);
+    }
+
+    private void MonthView_Cell_Held(object sender, MonthViewCellEventArgs e)
+    {
+        if (DataContext is not AssignmentsViewModel model) return;
+
+        var menu = new CoreMenu<IImage>();
+        model.EmptyMonthBlockHeld(e.Date, menu);
+        if (menu.Items.Count == 0) return;
+        var contextMenu = menu.Build();
+        contextMenu.Open(sender as Control);
+    }
+
+    private void MonthView_DateRangeChanged(object sender, MonthViewDateRangeEventArgs e)
+    {
+        if (DataContext is not AssignmentsViewModel model) return;
+
+        model.DateRange = (e.StartDate, e.EndDate);
+    }
 }

+ 206 - 9
PRS.Avalonia/PRS.Avalonia/Modules/Assignments/AssignmentsViewModel.cs

@@ -2,9 +2,11 @@
 using Avalonia.Data.Converters;
 using Avalonia.Media;
 using Avalonia.Threading;
+using BruTile.Extensions;
 using Comal.Classes;
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.Input;
+using DynamicData.Binding;
 using InABox.Avalonia;
 using InABox.Avalonia.Components;
 using InABox.Avalonia.Components.DateSelector;
@@ -13,7 +15,10 @@ using InABox.Clients;
 using InABox.Configuration;
 using InABox.Core;
 using PRS.Avalonia.Components;
+using ReactiveUI;
 using System;
+using System.Collections.Generic;
+using System.Data.Common;
 using System.Linq;
 using System.Threading.Tasks;
 
@@ -24,6 +29,14 @@ public class AssignmentsModuleSettings : ILocalConfigurationSettings
     public DateTime Date { get; set; }
 
     public Guid[] Employees { get; set; }
+
+    public AssignmentsViewType ViewType { get; set; }
+}
+
+public enum AssignmentsViewType
+{
+    Day,
+    Month
 }
 
 public partial class AssignmentsViewModel : ModuleViewModel
@@ -57,6 +70,19 @@ public partial class AssignmentsViewModel : ModuleViewModel
     [ObservableProperty]
     private bool _showColumns;
 
+    [ObservableProperty]
+    private int _selectedTab;
+
+    [ObservableProperty]
+    private AssignmentsViewType _viewType;
+
+    [ObservableProperty]
+    private (DateTime Start, DateTime End) _dateRange;
+
+    public IEnumerable<AssignmentShell> MonthItems => ViewType == AssignmentsViewType.Month ? Model.Items : [];
+
+    public IEnumerable<AssignmentShell> DayItems => ViewType == AssignmentsViewType.Day ? Model.Items : [];
+
     public AssignmentsViewModel()
     {
         Settings = LocalConfiguration.Load<AssignmentsModuleSettings>();
@@ -65,14 +91,30 @@ public partial class AssignmentsViewModel : ModuleViewModel
             Settings.Employees = [Repositories.Me.ID];
         }
         _date = Settings.Date == DateTime.MinValue ? DateTime.Today : Settings.Date;
-        UpdateTitle(_date.ToString("dd MMMM yyyy"));
+        ViewType = Settings.ViewType;
+        UpdateTitle();
 
         Model = new AssignmentModel(DataAccess,
-            () => Filter<Assignment>.Where(x => x.Date).IsEqualTo(Date)
+            () => GetDateFilter()
                 .And(x => x.EmployeeLink.ID).InList(EmployeeIDs));
         EmployeeModel = Repositories.Employees();
         ActivityModel = new ActivityModel(DataAccess,
             () => Filter<EmployeeActivity>.Where(x => x.Employee.ID).InList(EmployeeIDs));
+
+        Model.WhenValueChanged(x => x.Items)
+            .Subscribe(x =>
+            {
+                OnPropertyChanged(nameof(DayItems));
+                OnPropertyChanged(nameof(MonthItems));
+            });
+    }
+
+    private Filter<Assignment> GetDateFilter()
+    {
+        return ViewType == AssignmentsViewType.Day
+            ? Filter<Assignment>.Where(x => x.Date).IsEqualTo(Date)
+            : Filter<Assignment>.Where(x => x.Date).IsGreaterThanOrEqualTo(DateRange.Start)
+                .And(x => x.Date).IsLessThanOrEqualTo(DateRange.End);
     }
 
     protected override async Task<TimeSpan> OnRefresh()
@@ -85,6 +127,41 @@ public partial class AssignmentsViewModel : ModuleViewModel
         return TimeSpan.Zero;
     }
 
+    partial void OnDateRangeChanged((DateTime Start, DateTime End) value)
+    {
+        if(ViewType == AssignmentsViewType.Month)
+        {
+            Model.RefreshAsync(true).ErrorIfFail();
+        }
+    }
+
+    private void UpdateTitle()
+    {
+        UpdateTitle(Date.ToString(ViewType == AssignmentsViewType.Day ? "dd MMMM yyyy" : "MMMM yyyy"));
+    }
+
+    private bool _holdRefresh = false;
+
+    partial void OnViewTypeChanged(AssignmentsViewType value)
+    {
+        SelectedTab = value == AssignmentsViewType.Day ? 0 : 1;
+        if(value != Settings.ViewType)
+        {
+            Settings.ViewType = value;
+            LocalConfiguration.Save(Settings);
+        }
+        UpdateTitle();
+        if(Model is not null && !_holdRefresh)
+        {
+            Model.RefreshAsync(true).ErrorIfFail();
+        }
+    }
+
+    partial void OnSelectedTabChanged(int value)
+    {
+        ViewType = value == 0 ? AssignmentsViewType.Day : AssignmentsViewType.Month;
+    }
+
     partial void OnEmployeeIDsChanged(Guid[] value)
     {
         Settings.Employees = value;
@@ -93,10 +170,13 @@ public partial class AssignmentsViewModel : ModuleViewModel
 
     partial void OnDateChanged(DateTime value)
     {
-        UpdateTitle(value.ToString("dd MMMM yyyy"));
+        UpdateTitle();
         Settings.Date = value;
         LocalConfiguration.Save(Settings);
-        Model.RefreshAsync(true).ErrorIfFail();
+        if(ViewType == AssignmentsViewType.Day && !_holdRefresh)
+        {
+            Model.RefreshAsync(true).ErrorIfFail();
+        }
     }
 
     partial void OnEmployeesChanged(EmployeeShell[] value)
@@ -116,9 +196,9 @@ public partial class AssignmentsViewModel : ModuleViewModel
 
     public void EmptyBlockHeld(TimeSpan start, TimeSpan end, Guid employeeID, CoreMenu<IImage> menu)
     {
-        menu.AddItem("New Assignment", () => NewAssignment(start, end, employeeID));
+        menu.AddItem("New Assignment", () => NewAssignment(Date, start, end, employeeID));
     }
-    public async Task<bool> NewAssignment(TimeSpan start, TimeSpan end, Guid employeeID)
+    public async Task<bool> NewAssignment(DateTime date, TimeSpan start, TimeSpan end, Guid employeeID)
     {
         var activity = (await SelectionViewModel.ExecutePopup<ActivityShell>(model =>
         {
@@ -144,7 +224,7 @@ public partial class AssignmentsViewModel : ModuleViewModel
         if (activity is null) return false;
 
         var assignment = Model.CreateItem();
-        assignment.Date = Date;
+        assignment.Date = date;
         assignment.Title = "New Assignment";
 
         assignment.BookedStart = start;
@@ -165,6 +245,98 @@ public partial class AssignmentsViewModel : ModuleViewModel
         return true;
     }
 
+    private Dictionary<Guid, EmployeeRosterItem[]> _rosters = new();
+
+    public async Task<bool> NewAssignment(DateTime date)
+    {
+        EmployeeShell? employee;
+        if(Employees.Length == 0)
+        {
+            await MessageDialog.ShowMessage("No employees selected.");
+            return false;
+        }
+        else if(Employees.Length == 1)
+        {
+            employee = Employees[0];
+        }
+        else
+        {
+            employee = (await SelectionViewModel.ExecutePopup<EmployeeShell>(model =>
+            {
+                model.Columns.BeginUpdate()
+                    .Add(new AvaloniaDataGridTextColumn<EmployeeShell>
+                    {
+                        Column = x => x.Code,
+                        Width = GridLength.Auto,
+                        Alignment = TextAlignment.Center
+                    })
+                    .Add(new AvaloniaDataGridTextColumn<EmployeeShell>
+                    {
+                        Column = x => x.Name,
+                        Width = GridLength.Star,
+                        Alignment = TextAlignment.Left
+                    })
+                    .EndUpdate();
+            }, args =>
+            {
+                return Employees;
+            }))?.FirstOrDefault();
+        }
+        if (employee is null) return false;
+
+        var assignments = Model.Items.Where(x => x.Date == date).ToList();
+        var last = assignments.MaxBy(x => x.EndTime.TimeOfDay);
+        if(last is not null)
+        {
+            var start = last.EndTime.TimeOfDay;
+            var end = start + TimeSpan.FromHours(1);
+            if(end > TimeSpan.FromHours(24))
+            {
+                end = TimeSpan.FromHours(24);
+            }
+            return await NewAssignment(date, start, end, employee.ID);
+        }
+        else
+        {
+            if(!_rosters.TryGetValue(employee.ID, out var rosterItems))
+            {
+                rosterItems = Client.Query(
+                    Filter<EmployeeRosterItem>.Where(x => x.Employee.ID).IsEqualTo(employee.ID),
+                    Columns.None<EmployeeRosterItem>()
+                        .Add(x => x.Start)
+                        .Add(x => x.Finish)
+                        .Add(x => x.Enabled),
+                    new SortOrder<EmployeeRosterItem>(x => x.Day))
+                    .ToArray<EmployeeRosterItem>();
+                _rosters[employee.ID] = rosterItems;
+            }
+
+            var roster = RosterUtils.GetRoster(rosterItems, employee.RosterStart, date)!;
+            if (!roster.Enabled)
+            {
+                return await NewAssignment(date, TimeSpan.FromHours(9), TimeSpan.FromHours(10), employee.ID);
+            }
+            else
+            {
+                return await NewAssignment(date, roster.Start, roster.Finish, employee.ID);
+            }
+        }
+    }
+
+    public void EmptyMonthBlockClicked(DateTime date)
+    {
+        _holdRefresh = true;
+        ViewType = AssignmentsViewType.Day;
+        Date = date;
+        _holdRefresh = false;
+        Model.RefreshAsync(true).ErrorIfFail();
+    }
+
+    public void EmptyMonthBlockHeld(DateTime date, CoreMenu<IImage> menu)
+    {
+        menu.AddItem("New Assignment", () => NewAssignment(date));
+    }
+
     public async Task BlockClicked(AssignmentShell assignment)
     {
         await OpenAssignmentClick(assignment);
@@ -247,10 +419,28 @@ public partial class AssignmentsViewModel : ModuleViewModel
         });
     }
 
+    [RelayCommand]
+    private void Menu()
+    {
+        ViewType = ViewType switch
+        {
+            AssignmentsViewType.Day => AssignmentsViewType.Month,
+            AssignmentsViewType.Month => AssignmentsViewType.Day,
+            _ => AssignmentsViewType.Day
+        };
+    }
+
     [RelayCommand]
     private void PreviousDay()
     {
-        Date = Date.AddDays(-1);
+        if(ViewType == AssignmentsViewType.Day)
+        {
+            Date = Date.AddDays(-1);
+        }
+        else
+        {
+            Date = Date.AddMonths(-1);
+        }
     }
 
     [RelayCommand]
@@ -269,6 +459,13 @@ public partial class AssignmentsViewModel : ModuleViewModel
     [RelayCommand]
     private void NextDay()
     {
-        Date = Date.AddDays(1);
+        if(ViewType == AssignmentsViewType.Day)
+        {
+            Date = Date.AddDays(1);
+        }
+        else
+        {
+            Date = Date.AddMonths(1);
+        }
     }
 }

+ 21 - 0
PRS.Avalonia/PRS.Avalonia/Repositories/Assignment/AssignmentShell.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.ObjectModel;
+using System.Linq;
 using Avalonia.Media;
 using Comal.Classes;
 using InABox.Avalonia;
@@ -21,6 +22,12 @@ public class AssignmentShell : Shell<AssignmentModel, Assignment>
         set => Set(value);
     }
 
+    public string EmployeeName
+    {
+        get => Get<string>();
+        set => Set(value);
+    }
+
     public int Number
     {
         get => Get<int>();
@@ -58,6 +65,7 @@ public class AssignmentShell : Shell<AssignmentModel, Assignment>
     {
         columns
             .Map(nameof(EmployeeID), x => x.EmployeeLink.ID)
+            .Map(nameof(EmployeeName), x => x.EmployeeLink.Name)
             .Map(nameof(Number), x => x.Number)
             .Map(nameof(Title), x => x.Title)
             .Map(nameof(Description), x => x.Description)
@@ -250,6 +258,19 @@ public class AssignmentShell : Shell<AssignmentModel, Assignment>
         Title
     );
 
+    public string ShortDisplay
+    {
+        get
+        {
+            var comps = EmployeeName.Split(' ');
+
+            var shortEmployee = (comps[0].Length > 0 ? comps[0][..1] : "")
+                + (comps.Length > 1 ? comps[1][..1].ToUpper() : "");
+
+            return $"{shortEmployee} {JobNumber}";
+        }
+    }
+
     public bool IsCompleted => !Completed.IsEmpty();
 
     public DateTime StartTime =>

+ 5 - 1
PRS.Avalonia/PRS.Avalonia/Repositories/Employee/EmployeeShell.cs

@@ -25,6 +25,8 @@ public class EmployeeShell : Shell<EmployeeModel, Employee>
 
     public Guid ThumbnailID => Get<Guid>();
 
+    public DateTime RosterStart => Get<DateTime>();
+
     protected override void ConfigureColumns(ShellColumns<EmployeeModel, Employee> columns)
     {
         columns
@@ -33,6 +35,8 @@ public class EmployeeShell : Shell<EmployeeModel, Employee>
             .Map(nameof(Mobile), x => x.Mobile)
             .Map(nameof(Email), x => x.Email)
             .Map(nameof(UserID), x => x.UserLink.ID)
-            .Map(nameof(ThumbnailID), x => x.Thumbnail.ID);
+            .Map(nameof(ThumbnailID), x => x.Thumbnail.ID)
+            .Map(nameof(RosterStart), x => x.RosterStart);
+            ;
     }
 }

+ 6 - 0
prs.classes/Utilities/RosterUtils.cs

@@ -51,6 +51,12 @@ namespace Comal.Classes
             return result.ToArray();
         }
 
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <remarks>
+        /// Returns <see langword="null"/> if and only if <paramref name="startdate"/> > <paramref name="currentdate"/>
+        /// </remarks>
         public static EmployeeRosterItem? GetRoster(IList<EmployeeRosterItem>? roster, DateTime startdate, DateTime currentdate)
         {
             if (startdate > currentdate)