Quellcode durchsuchen

avalonia: Added Task details editor and Task notes editor

Kenric Nugteren vor 2 Monaten
Ursprung
Commit
595875ab14

+ 13 - 0
PRS.Avalonia/PRS.Avalonia/Components/NotesPage/NotesPageView.axaml

@@ -0,0 +1,13 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:prs="using:PRS.Avalonia.Components"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PRS.Avalonia.Components.NotesPageView"
+             x:DataType="prs:NotesPageViewModel">
+    <TextBox AcceptsReturn="True"
+             VerticalContentAlignment="Top"
+             TextWrapping="Wrap"
+             Text="{Binding Text}"/>
+</UserControl>

+ 13 - 0
PRS.Avalonia/PRS.Avalonia/Components/NotesPage/NotesPageView.axaml.cs

@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PRS.Avalonia.Components;
+
+public partial class NotesPageView : UserControl
+{
+    public NotesPageView()
+    {
+        InitializeComponent();
+    }
+}

+ 31 - 0
PRS.Avalonia/PRS.Avalonia/Components/NotesPage/NotesPageViewModel.cs

@@ -0,0 +1,31 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using InABox.Avalonia;
+using InABox.Avalonia.Components;
+using PRS.Avalonia.Modules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRS.Avalonia.Components;
+
+internal partial class NotesPageViewModel : ModuleViewModel
+{
+    public override string Title => "Add Note";
+
+    [ObservableProperty]
+    private string _text = "";
+
+    [ObservableProperty]
+    private Action<string>? _confirm;
+
+    public NotesPageViewModel()
+    {
+        PrimaryMenu.Add(new AvaloniaMenuItem(Images.tick, () =>
+        {
+            Confirm?.Invoke(Text);
+            Navigation.Back();
+        }));
+    }
+}

+ 2 - 2
PRS.Avalonia/PRS.Avalonia/Components/SelectionView/SelectionView.axaml

@@ -28,8 +28,8 @@
 				</MultiBinding>
 			</Label.IsVisible>
 		</Label>
-		<components:ButtonStrip Name="Filters" Grid.Row="1" ItemsSource="{Binding FilterButtons}" SelectionChanged="Filters_OnSelectionChanged"
-								SelectedItem="{Binding SelectedFilter}"/>
+		<components:ButtonStrip Name="Filters" Grid.Row="1" ItemsSource="{Binding FilterButtons}" SelectionChanged="Filters_OnSelectionChanged" ItemSpacing="{StaticResource PrsControlSpacingAmount}"
+								SelectedItem="{Binding SelectedFilter}" Margin="0,0,0,2"/>
 		<components:AvaloniaDataGrid Name="Grid" Grid.Row="2"
 									 ItemsSource="{Binding ItemsSource}"
 									 RefreshRequested="Grid_OnRefreshRequested"

+ 1 - 1
PRS.Avalonia/PRS.Avalonia/HomePage/HomePageViewModel.cs

@@ -66,7 +66,7 @@ public partial class HomePageViewModel : ViewModelBase
         Modules.Add<ViewMobileSiteModule, SiteViewModel>("Site Module", "", Images.construction);
         Modules.Add<ViewMobileMyTasksModule, MyTasksViewModel>("My Tasks", "", Images.task, configure: model =>
         {
-            model.Model = Repositories.MyTasks;
+            model.Model = Repositories.TaskModel();
         });
         Modules.Add<ViewMobileWarehousingModule, WarehouseModuleViewModel>("Warehouse", "", Images.warehouse, isVisible: true);
         Modules.Add("Update App", "", Images.version, UpdateApp, isVisible: false);

+ 1 - 0
PRS.Avalonia/PRS.Avalonia/Images/Images.cs

@@ -59,6 +59,7 @@ public static class Images
     public static SvgImage? rotate => LoadSVG("/Images/rotate.svg");
     public static SvgImage? save => LoadSVG("/Images/save.svg");
     public static SvgImage? schedule => LoadSVG("/Images/schedule.svg");
+    public static SvgImage? share => LoadSVG("/Images/share.svg");
     public static SvgImage? shoppingcart => LoadSVG("/Images/shoppingcart.svg");
     public static SvgImage? stock => LoadSVG("/Images/stock.svg");
     public static SvgImage? stock_receive => LoadSVG("/Images/stock_receive.svg");

+ 1 - 1
PRS.Avalonia/PRS.Avalonia/Modules/DigitalForms/FormsViewModel.cs

@@ -31,7 +31,7 @@ public partial class FormsViewModel : ModuleViewModel
 
     public FormsViewModel()
     {
-        Kanbans = Repositories.MyTasks;
+        Kanbans = Repositories.TaskModel();
 
         Forms = new KanbanFormModel(DataAccess,
             () => new Filter<KanbanForm>(x => x.Parent.EmployeeLink.ID).IsEqualTo(Repositories.Me.ID)

+ 64 - 0
PRS.Avalonia/PRS.Avalonia/Modules/MyTasks/TaskEditView.axaml

@@ -3,7 +3,71 @@
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:modules="using:PRS.Avalonia.Modules"
+             xmlns:components="using:InABox.Avalonia.Components"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PRS.Avalonia.Modules.TaskEditView"
 			 x:DataType="modules:TaskEditViewModel">
+    <TabControl Classes="Standard" TabStripPlacement="Bottom"
+                SelectedIndex="{Binding SelectedTab}">
+		<TabItem Header="Info">
+			<Grid RowDefinitions="Auto,*,Auto,Auto,Auto,Auto,Auto">
+                <TextBox Watermark="Title"
+                         Text="{Binding Shell.Title}"
+                         Grid.Row="0"
+                         TextChanged="TextBox_TextChanged"/>
+                <TextBox Watermark="Task Description"
+                         Text="{Binding Shell.Description}"
+                         AcceptsReturn="True"
+                         VerticalContentAlignment="Top"
+                         IsEnabled="{Binding CanEditDescription}"
+                         Grid.Row="1"
+                         TextChanged="TextBox_TextChanged"/>
+                <Button Classes="Standard"
+                        Content="{Binding Shell.JobName, Converter={x:Static modules:TaskEditViewSelectButtonConverter.Instance}, ConverterParameter='Job'}"
+                        Command="{Binding SelectJobCommand}"
+                        Grid.Row="2"/>
+                <Button Classes="Standard"
+                        Content="{Binding Shell.TypeName, Converter={x:Static modules:TaskEditViewSelectButtonConverter.Instance}, ConverterParameter='Type'}"
+                        Command="{Binding SelectTypeCommand}"
+                        Grid.Row="3"/>
+                <components:DateSelectorButton Prefix="Due:"
+                                               Format="dd MMM yy"
+                                               Date="{Binding Shell.DueDate}"
+                                               Grid.Row="4"
+                                               DateChanged="DateSelector_DateChanged"/>
+                <Button Classes="Standard"
+                        Content="{Binding Shell.EmployeeName, Converter={x:Static modules:TaskEditViewSelectButtonConverter.Instance}, ConverterParameter='Employee'}"
+                        Command="{Binding SelectEmployeeCommand}"
+                        Grid.Row="5"/>
+                <Button Classes="Standard"
+                        Content="{Binding Shell.Status, Converter={x:Static modules:TaskEditViewSelectButtonConverter.Instance}, ConverterParameter='Status'}"
+                        Command="{Binding ChangeStatusCommand}"
+                        Grid.Row="6"/>
+            </Grid>
+        </TabItem>
+        <TabItem Header="Notes">
+            <Border Classes="Standard">
+				<Border CornerRadius="{StaticResource PrsCornerRadius}" ClipToBounds="True">
+					<ItemsControl ItemsSource="{Binding Shell.Notes}">
+						<ItemsControl.ItemTemplate>
+							<DataTemplate>
+								<StackPanel>
+                                    <TextBlock Text="{Binding}" Background="Gainsboro"
+                                               Padding="5,10"
+                                               VerticalAlignment="Center"
+                                               TextWrapping="WrapWithOverflow"/>
+									<Rectangle Height="1" Margin="0" Fill="Silver"/>
+								</StackPanel>
+							</DataTemplate>
+						</ItemsControl.ItemTemplate>
+					</ItemsControl>
+				</Border>
+            </Border>
+        </TabItem>
+        <TabItem Header="Images">
+			
+        </TabItem>
+        <TabItem Header="Forms">
+        </TabItem>
+    </TabControl>
 </UserControl>

+ 40 - 0
PRS.Avalonia/PRS.Avalonia/Modules/MyTasks/TaskEditView.axaml.cs

@@ -1,13 +1,53 @@
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using InABox.Avalonia.Components;
+using InABox.Avalonia.Converters;
+using InABox.Core;
+using System;
 
 namespace PRS.Avalonia.Modules;
 
+public class TaskEditViewSelectButtonConverter : AbstractConverter<object?, string>
+{
+    public static TaskEditViewSelectButtonConverter Instance { get; } = new TaskEditViewSelectButtonConverter();
+
+    protected override string? Convert(object? value, object? parameter = null)
+    {
+        if (value is null || (value is string str && str.IsNullOrWhiteSpace()))
+        {
+            return $"Select {parameter}";
+        }
+        else
+        {
+            return $"{parameter}: {value}";
+        }
+    }
+}
+
 public partial class TaskEditView : UserControl
 {
+    private TaskEditViewModel? ViewModel;
+
     public TaskEditView()
     {
         InitializeComponent();
     }
+
+    protected override void OnDataContextChanged(EventArgs e)
+    {
+        base.OnDataContextChanged(e);
+        
+        ViewModel = DataContext as TaskEditViewModel;
+    }
+
+    private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
+    {
+        ViewModel?.Changed();
+    }
+
+    private void DateSelector_DateChanged(object sender, DateSelectorDateChangedEventArgs e)
+    {
+        ViewModel?.Changed();
+    }
 }

+ 230 - 2
PRS.Avalonia/PRS.Avalonia/Modules/MyTasks/TaskEditViewModel.cs

@@ -1,4 +1,13 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Comal.Classes;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using InABox.Avalonia;
+using InABox.Avalonia.Components;
+using InABox.Avalonia.Dialogs;
+using InABox.Core;
+using PRS.Avalonia.Components;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -12,5 +21,224 @@ internal partial class TaskEditViewModel : ModuleViewModel
     public override string Title => "Task Details";
 
     [ObservableProperty] 
-    private IKanbanShell? _shell;
+    private IKanbanShell _shell = null!;
+
+    [ObservableProperty]
+    private bool _canEditDescription;
+
+    [ObservableProperty]
+    private KanbanTypeModel _kanbanTypes;
+
+    [ObservableProperty]
+    private EmployeeModel _employees;
+
+    [ObservableProperty]
+    private int _selectedTab;
+
+    private AvaloniaMenuItem SaveButton;
+    private AvaloniaMenuItem ShareButton;
+    private AvaloniaMenuItem AddNoteButton;
+    private AvaloniaMenuItem ImageMenuButton;
+    private AvaloniaMenuItem NewFormButton;
+
+    public TaskEditViewModel()
+    {
+        SaveButton = new AvaloniaMenuItem(Images.save, Save) { IsVisible = false };
+        ShareButton = new AvaloniaMenuItem(Images.share, () =>
+        {
+            var menu = new CoreMenu<IImage>();
+            menu.AddItem("Email", SendEmail);
+            return menu;
+        }) { IsVisible = false };
+        AddNoteButton = new AvaloniaMenuItem(Images.plus, AddNote) { IsVisible = false };
+        ImageMenuButton = new AvaloniaMenuItem(Images.plus, () =>
+        {
+            var menu = new CoreMenu<IImage>();
+            menu.AddItem("Take Photo", TakePhoto);
+            menu.AddItem("Browse Library", BrowseLibrary);
+            return menu;
+        }) { IsVisible = false };
+        NewFormButton = new AvaloniaMenuItem(Images.plus, NewForm) { IsVisible = false };
+        PrimaryMenu.Add(SaveButton);
+        PrimaryMenu.Add(ShareButton);
+        PrimaryMenu.Add(AddNoteButton);
+        PrimaryMenu.Add(ImageMenuButton);
+        PrimaryMenu.Add(NewFormButton);
+
+        KanbanTypes = new KanbanTypeModel(DataAccess,
+            () => new Filter<KanbanType>().All(),
+            () => DefaultCacheFileName<KanbanTypeShell>());
+        Employees = Repositories.Employees();
+    }
+
+    partial void OnSelectedTabChanged(int value)
+    {
+        Changed();
+    }
+
+    private async Task<bool> TakePhoto()
+    {
+        await MessageDialog.ShowMessage("Unimplemented");
+        return true;
+    }
+
+    private async Task<bool> BrowseLibrary()
+    {
+        await MessageDialog.ShowMessage("Unimplemented");
+        return true;
+    }
+
+    private async Task<bool> NewForm()
+    {
+        await MessageDialog.ShowMessage("Unimplemented");
+        return true;
+    }
+
+    private void AddNote()
+    {
+        Navigation.Navigate<NotesPageViewModel>(model =>
+        {
+            model.Confirm = note =>
+            {
+                Shell.Notes = Shell.Notes.Append(note).ToArray();
+                Changed();
+            };
+        });
+    }
+
+    private async Task<bool> SendEmail()
+    {
+        await MessageDialog.ShowMessage("Unimplemented");
+        return true;
+    }
+
+    private async Task<bool> Save()
+    {
+        await MessageDialog.ShowMessage("Unimplemented");
+        return true;
+    }
+
+    protected override Task<TimeSpan> OnRefresh()
+    {
+        CanEditDescription = Shell.ManagerID == Repositories.Me.ID;
+        return Task.FromResult(TimeSpan.Zero);
+    }
+
+    public void Changed()
+    {
+        var changed = Shell.IsChanged();
+        SaveButton.IsVisible = changed;
+        ShareButton.IsVisible = !changed && SelectedTab == 0;
+        AddNoteButton.IsVisible = SelectedTab == 1;
+        ImageMenuButton.IsVisible = SelectedTab == 2;
+        NewFormButton.IsVisible = SelectedTab == 3;
+    }
+
+    [RelayCommand]
+    private async Task SelectJob()
+    {
+        var job = (await SelectionViewModel.ExecutePopup<JobShell>(model =>
+        {
+            model.Columns.BeginUpdate()
+                .Add(new AvaloniaDataGridTextColumn<JobShell>
+                {
+                    Column = x => x.JobNumber,
+                    Caption = "Number",
+                    Width = GridLength.Auto
+                })
+                .Add(new AvaloniaDataGridTextColumn<JobShell>
+                {
+                    Column = x => x.Name,
+                    Caption = "Name",
+                    Width = GridLength.Star
+                })
+                .EndUpdate();
+            model.AddFilters(Repositories.Jobs.AvailableFilters.Select(x => x.Name).NotNull());
+        }, args =>
+        {
+            Repositories.Jobs.SelectFilter(args.Filter);
+            return Repositories.Jobs.Refresh(args.Force);
+        }))?.FirstOrDefault();
+        if(job is not null)
+        {
+            Shell.JobID = job.ID;
+            Shell.JobNumber = job.JobNumber;
+            Shell.JobName = job.Name;
+            Changed();
+        }
+    }
+
+    [RelayCommand]
+    private async Task SelectType()
+    {
+        var type = (await SelectionViewModel.ExecutePopup<KanbanTypeShell>(model =>
+        {
+            model.Columns.BeginUpdate()
+                .Add(new AvaloniaDataGridTextColumn<KanbanTypeShell>
+                {
+                    Column = x => x.Description,
+                    Caption = "Type",
+                    Width = GridLength.Star
+                })
+                .EndUpdate();
+        }, args =>
+        {
+            return KanbanTypes.Refresh(args.Force);
+        }))?.FirstOrDefault();
+        if(type is not null)
+        {
+            Shell.TypeID = type.ID;
+            Shell.TypeName = type.Description;
+            Changed();
+        }
+    }
+
+    [RelayCommand]
+    private async Task SelectEmployee()
+    {
+        var employee = (await SelectionViewModel.ExecutePopup<EmployeeShell>(model =>
+        {
+            model.Columns.BeginUpdate()
+                .Add(new AvaloniaDataGridTextColumn<EmployeeShell>
+                {
+                    Column = x => x.Name,
+                    Caption = "Employee",
+                    Width = GridLength.Star
+                })
+                .EndUpdate();
+        }, args =>
+        {
+            return Employees.Refresh(args.Force);
+        }))?.FirstOrDefault();
+        if(employee is not null)
+        {
+            Shell.EmployeeID = employee.ID;
+            Shell.EmployeeName = employee.Name;
+            Changed();
+        }
+    }
+
+    [RelayCommand]
+    private async Task ChangeStatus()
+    {
+        var status = (await SelectionViewModel.ExecutePopup<KanbanStatus>(model =>
+        {
+            model.Columns.BeginUpdate()
+                .Add(new AvaloniaDataGridTextColumn<KanbanStatus>
+                {
+                    ColumnName = ".",
+                    Caption = "Status",
+                    Width = GridLength.Star
+                })
+                .EndUpdate();
+        }, args =>
+        {
+            return Enum.GetValues<KanbanStatus>();
+        }))?.FirstOrDefault();
+        if(status is not null)
+        {
+            Shell.Status = status.Value;
+            Changed();
+        }
+    }
 }

+ 1 - 1
PRS.Avalonia/PRS.Avalonia/Repositories/Employee/EmployeeModel.cs

@@ -8,7 +8,7 @@ namespace PRS.Avalonia;
 
 public class EmployeeModel : CoreRepository<EmployeeModel, EmployeeShell, Employee>
 {
-    public EmployeeModel(IModelHost host, Func<Filter<Employee>> filter) : base(host, filter)
+    public EmployeeModel(IModelHost host, Func<Filter<Employee>> filter, Func<string>? filename = null) : base(host, filter, filename)
     {
     }
 

+ 1 - 1
PRS.Avalonia/PRS.Avalonia/Repositories/KanbanType/KanbanTypeModel.cs

@@ -7,7 +7,7 @@ namespace PRS.Avalonia;
 
 public class KanbanTypeModel : CoreRepository<KanbanTypeModel, KanbanTypeShell, KanbanType>
 {
-    public KanbanTypeModel(IModelHost host, Func<Filter<KanbanType>> filter) : base(host, filter)
+    public KanbanTypeModel(IModelHost host, Func<Filter<KanbanType>> filter, Func<string>? filename = null) : base(host, filter, filename)
     {
     }
 }

+ 15 - 7
PRS.Avalonia/PRS.Avalonia/RepositoryLayer.cs

@@ -44,13 +44,7 @@ public partial class RepositoryLayer : ObservableObject
         MyQualifications = new EmployeeQualificationModel(Data,
             () => new Filter<EmployeeQualification>(x => x.Employee.UserLink.ID).IsEqualTo(ClientFactory.UserGuid));
 
-        MyTasks = new KanbanModel(Data,
-            () => new Filter<Kanban>(x => x.ID)
-                .InQuery(new Filter<KanbanSubscriber>(x => x.Employee.ID).IsEqualTo(Me.ID), x => x.Kanban.ID)
-                .And(
-                    new Filter<Kanban>(x => x.Status).IsNotEqualTo(KanbanStatus.Complete)
-                        .Or(x => x.Completed).IsGreaterThanOrEqualTo(DateTime.Today.AddDays(-7))),
-            () => DefaultCacheFileName<KanbanShell>());
+        MyTasks = TaskModel();
         
         Jobs = new JobModel(Data,
             () => JobFilter(),
@@ -98,6 +92,15 @@ public partial class RepositoryLayer : ObservableObject
     public EmployeeQualificationModel MyQualifications { get; private set; }
     
     public JobModel Jobs { get; private set; }
+
+    public KanbanModel TaskModel() =>
+        new KanbanModel(Data,
+            () => new Filter<Kanban>(x => x.ID)
+                .InQuery(new Filter<KanbanSubscriber>(x => x.Employee.ID).IsEqualTo(Me.ID), x => x.Kanban.ID)
+                .And(
+                    new Filter<Kanban>(x => x.Status).IsNotEqualTo(KanbanStatus.Complete)
+                        .Or(x => x.Completed).IsGreaterThanOrEqualTo(DateTime.Today.AddDays(-7))),
+            () => DefaultCacheFileName<KanbanShell>());
     
     public Filter<Job> JobFilter()
     {
@@ -117,6 +120,11 @@ public partial class RepositoryLayer : ObservableObject
                 .And(x => x.ID).InQuery(new Filter<DigitalFormLayout>(x => x.Active).IsEqualTo(true), x => x.Form.ID),
             () => DefaultCacheFileName<DigitalFormShell>());
 
+    public EmployeeModel Employees() =>
+        new EmployeeModel(Data,
+            () => LookupFactory.DefineFilter<Employee>(),
+            () => DefaultCacheFileName<EmployeeShell>());
+
     #region Current TimeSheet
 
     private readonly TimeSheetModel _currentTimeSheets;