Selaa lähdekoodia

Overhauled TasksByUser screen and sped up task panel overall

Kenric Nugteren 1 vuosi sitten
vanhempi
commit
39112a2872

+ 12 - 1
prs.desktop/Panels/Tasks/KanbanResources.xaml

@@ -55,7 +55,18 @@
             </Border.Background>
             <Grid Margin="4">
                 <Grid.ColumnDefinitions>
-                    <ColumnDefinition x:Name="colImage" Width="Auto" />
+                    <ColumnDefinition x:Name="colImage">
+                        <ColumnDefinition.Style>
+                            <Style TargetType="ColumnDefinition">
+                                <Setter Property="Width" Value="Auto"/>
+                                <Style.Triggers>
+                                    <DataTrigger Binding="{Binding Path=Image}" Value="{x:Null}">
+                                        <Setter Property="Width" Value="0"/>
+                                    </DataTrigger>
+                                </Style.Triggers>
+                            </Style>
+                        </ColumnDefinition.Style>
+                    </ColumnDefinition>
                     <ColumnDefinition x:Name="colCheckbox" Width="Auto" />
                     <ColumnDefinition x:Name="colDescription" Width="*" />
                     <ColumnDefinition x:Name="colType" Width="Auto" />

+ 7 - 0
prs.desktop/Panels/Tasks/TaskModel.cs

@@ -33,11 +33,18 @@ public class TaskModel
     public int Number { get; set; }
     public bool Locked { get; set; }
     public TimeSpan EstimatedTime { get; set; }
+
+    public bool ShowEmployeeImage { get; set; }
+
     /// <summary>
     /// A string representation of the Notes field of Kanban.
     /// </summary>
     public string Notes { get; set; }
 
+    public bool IsAssignee => EmployeeID != Guid.Empty && EmployeeCategory == EmployeeID;
+
+    public bool IsManager => ManagerID != Guid.Empty && EmployeeCategory == ManagerID;
+
     public bool Search(string[] searches)
     {
         foreach (var search in searches)

+ 13 - 13
prs.desktop/Panels/Tasks/TaskPanel.xaml.cs

@@ -67,7 +67,7 @@ namespace PRSDesktop
 
                 new Client<Kanban>().Save(kanbans, $"Kanban Marked as Complete");
             });
-            control.Refresh(true);
+            control.Refresh();
         }
         private void AddChangeStatusButton(ITaskControl control, TaskModel[] models, MenuItem menu, string header, string status)
         {
@@ -94,7 +94,7 @@ namespace PRSDesktop
 
                 new Client<Kanban>().Save(kanbans, $"Kanban Marked as {status}");
             });
-            control.Refresh(true);
+            control.Refresh();
         }
 
         public bool CanChangeTasks(IEnumerable<TaskModel> models)
@@ -139,7 +139,7 @@ namespace PRSDesktop
             {
                 var tasks = (((MenuItem)e.Source).Tag as IEnumerable<TaskModel>)!;
                 if (EditReferences(tasks))
-                    control.Refresh(true);
+                    control.Refresh();
                 e.Handled = true;
             };
             edit.IsEnabled = referencetypes.Length == 1;
@@ -327,7 +327,7 @@ namespace PRSDesktop
                                 kanban.Title = kanban.Title + " (" + result.Number + ")";
                                 new Client<Kanban>().Save(kanban, "Converting Kanban to Setout");
                             });
-                            control.Refresh(true);
+                            control.Refresh();
                         }
                     });
                     menu.AddItem("Create Requisition from Task", null, models, tasks =>
@@ -385,7 +385,7 @@ namespace PRSDesktop
                                 new Client<Kanban>().Save(kanban, "Converted to Requisition", (_, __) => { });
                             });
                             MessageBox.Show(String.Format("Created Requisition {0}", result.Number));
-                            control.Refresh(true);
+                            control.Refresh();
                         }
                     });
                     menu.AddItem("Create Delivery from Task", null, models, tasks =>
@@ -398,7 +398,7 @@ namespace PRSDesktop
                             }
                         );
                         if (result != null)
-                            control.Refresh(true);
+                            control.Refresh();
                     });
                     menu.AddItem("Create Purchase Order from Task", null, models, tasks =>
                     {
@@ -410,7 +410,7 @@ namespace PRSDesktop
                             }
                         );
                         if (result != null)
-                            control.Refresh(true);
+                            control.Refresh();
                     });
                 }
             }
@@ -482,7 +482,7 @@ namespace PRSDesktop
                                 kanban.Closed = DateTime.Now;
                             new Client<Kanban>().Save(kanbans, "Kanban Marked as Closed");
                         });
-                        control.Refresh(true);
+                        control.Refresh();
                     });
                 }
 
@@ -504,7 +504,7 @@ namespace PRSDesktop
 
                             new Client<Kanban>().Save(kanbans, $"Kanban Task Type changed to {type}");
                         });
-                        control.Refresh(true);
+                        control.Refresh();
                     });
                 }
 
@@ -533,7 +533,7 @@ namespace PRSDesktop
 
                         new Client<Kanban>().Save(kanbans, $"Kanban Due Date changed to {selectedDate:dd MMM yyyy}");
                     });
-                    control.Refresh(true);
+                    control.Refresh();
 
                     menu.IsOpen = false;
                 };
@@ -649,7 +649,7 @@ namespace PRSDesktop
                             foreach (var kanban in kanbans)
                                 kanban.JobLink.ID = row.Get<Job, Guid>(x => x.ID);
                             new Client<Kanban>().Save(kanbans, "Updated Job Number");
-                            control.Refresh(false);
+                            control.Refresh();
                         }
                     });
                 }
@@ -676,7 +676,7 @@ namespace PRSDesktop
                 {
                     KanbanSettings.ViewType = panel.KanbanViewType;
                     new UserConfiguration<KanbanSettings>().Save(KanbanSettings);
-                    panel.Refresh(false);
+                    panel.Refresh();
                 }
             }
             finally
@@ -806,7 +806,7 @@ namespace PRSDesktop
             else
                 TasksPlannerTabItem.Visibility = Visibility.Visible;
             
-            GetCurrentPanel()?.Refresh(false);
+            GetCurrentPanel()?.Refresh();
         }
 
         public string SectionName => GetCurrentPanel().SectionName;

+ 13 - 11
prs.desktop/Panels/Tasks/TasksByStatusControl.xaml

@@ -112,17 +112,19 @@
                                 <RowDefinition Height="Auto"/>
                                 <RowDefinition Height="*"/>
                             </Grid.RowDefinitions>
-                            <StackPanel Orientation="Vertical"
-                                        Grid.Row="0">
-                                <TextBlock Text="{Binding Title,Mode=OneWay}" FontSize="16" FontWeight="DemiBold" HorizontalAlignment="Left"
-                                           Margin="10,0,10,0" />
-                                <TextBlock FontSize="12" HorizontalAlignment="Left" Margin="10,0,5,0">
-                                    <Run Text="{Binding NumTasks,Mode=OneWay}" />
-                                    <Run Text="Tasks /" />
-                                    <Run Text="{Binding NumHours,Mode=OneWay, StringFormat={}{0:F2}}"/>
-                                    <Run Text="Hours" />
-                                </TextBlock>
-                            </StackPanel>
+                            <Border BorderBrush="LightGray" BorderThickness="0,0,0,1" Margin="0,0,0,5">
+                                <StackPanel Orientation="Vertical"
+                                            Grid.Row="0">
+                                    <TextBlock Text="{Binding Title,Mode=OneWay}" FontSize="16" FontWeight="DemiBold" HorizontalAlignment="Left"
+                                               Margin="10,0,10,0" />
+                                    <TextBlock FontSize="12" HorizontalAlignment="Left" Margin="10,0,5,0">
+                                        <Run Text="{Binding NumTasks,Mode=OneWay}" />
+                                        <Run Text="Tasks /" />
+                                        <Run Text="{Binding NumHours,Mode=OneWay, StringFormat={}{0:F2}}"/>
+                                        <Run Text="Hours" />
+                                    </TextBlock>
+                                </StackPanel>
+                            </Border>
                             <ItemsControl Grid.Row="1" Margin="5,0"
                                           AllowDrop="True"
                                           DragOver="ItemsControl_DragOver"

+ 9 - 16
prs.desktop/Panels/Tasks/TasksByStatusControl.xaml.cs

@@ -405,7 +405,7 @@ public partial class TasksByStatusControl : UserControl, ITaskControl, INotifyPr
 
         SelectedTasks.Clear();
         if (IsReady)
-            Refresh(true);
+            Refresh();
     }
 
     #endregion
@@ -427,7 +427,7 @@ public partial class TasksByStatusControl : UserControl, ITaskControl, INotifyPr
                 kanban.ManagerLink.UserLink.ID = ClientFactory.UserGuid;
             });
         if (result != null)
-            Refresh(true);
+            Refresh();
     }
 
     private void DoEdit(TaskModel task)
@@ -435,7 +435,7 @@ public partial class TasksByStatusControl : UserControl, ITaskControl, INotifyPr
         var result = Host.EditReferences(new[] { task });
         if (result)
         {
-            Refresh(true);
+            Refresh();
         }
     }
 
@@ -541,7 +541,6 @@ public partial class TasksByStatusControl : UserControl, ITaskControl, INotifyPr
     {
         if (sender is not FrameworkElement element || element.Tag is not TasksByStatusColumn column) return;
 
-        e.Effects = DragDropEffects.None;
         if (e.Data.GetDataPresent(typeof(TaskModel)))
         {
             ChangeStatus(SelectedModels(e.Data.GetData(typeof(TaskModel)) as TaskModel), column.Category);
@@ -753,24 +752,18 @@ public partial class TasksByStatusControl : UserControl, ITaskControl, INotifyPr
             x => x.Locked);
     }
 
-    public void Refresh(bool resetselection)
+    public void Refresh()
     {
         using var cursor = new WaitCursor();
 
         IEnumerable<IKanban> kanbans;
         if (SelectedEmployee.ID != CoreUtils.FullGuid && SelectedEmployee.ID != Guid.Empty)
         {
-            var columns = new Columns<KanbanSubscriber>();
-            var kanbanColumn = new Column<KanbanSubscriber>(x => x.Kanban);
-            foreach(var column in GetKanbanColumns().ColumnNames())
-            {
-                columns.Add(new Column<KanbanSubscriber>($"{kanbanColumn.Property}.{column}"));
-            }
-            kanbans = new Client<KanbanSubscriber>().Query(
-                GetKanbanSubscriberFilter(),
-                columns,
-                new SortOrder<KanbanSubscriber>(x => x.Kanban.DueDate) { Direction = SortDirection.Ascending }
-            ).ToObjects<KanbanSubscriber>().Select(x => x.Kanban);
+            kanbans = Client.Query(
+                new Filter<Kanban>(x => x.ID).InQuery(GetKanbanSubscriberFilter(), x => x.Kanban.ID),
+                GetKanbanColumns().Cast<Kanban>(),
+                new SortOrder<Kanban>(x => x.DueDate) { Direction = SortDirection.Ascending }
+            ).ToObjects<Kanban>();
         }
         else
         {

+ 0 - 116
prs.desktop/Panels/Tasks/TasksByUserControl - Copy.xaml.cs

@@ -136,125 +136,9 @@ namespace PRSDesktop
         }
         
         public bool IsReady { get; set; }
-		
-
-        public void Refresh(bool resetselection)
-        {
-            var _swimlanes = new Dictionary<string, int>
-            {
-                { "Open", 0 },
-                { "In Progress", 1 },
-                { "Waiting", 2 },
-                { "Complete", 3 }
-            };
-
-            var filter = new Filter<KanbanSubscriber>(c => c.Kanban.Closed).IsEqualTo(DateTime.MinValue)
-                .And(x => x.Kanban.Locked).IsEqualTo(false);
-
-            var privateFilter = new Filter<KanbanSubscriber>(x => x.Kanban.Private).IsEqualTo(false);
-            if (App.EmployeeID != Guid.Empty)
-            {
-                privateFilter = privateFilter.Or(x => x.Employee.ID).IsEqualTo(App.EmployeeID);
-            }
-            filter.And(privateFilter);
-
-            if (Host.Job != null)
-            {
-                if (Host.Job.ID != Guid.Empty)
-                    filter = filter.And(c => c.Kanban.JobLink.ID).IsEqualTo(Host.Job.ID);
-                else
-                    filter = filter.And(c => c.Kanban.JobLink.ID).None();
-            }
-
-            if (!Host.KanbanSettings.UserSettings.IncludeCompleted)
-                filter = filter.And(new Filter<KanbanSubscriber>(x => x.Kanban.Completed).IsEqualTo(DateTime.MinValue));
-
-            var emps = _employees.Where(x => Host.KanbanSettings.UserSettings.SelectedEmployees.Contains(x.Key));
-            //if (Host.Settings.UserSettings.IncludeObserved)
-            //    filter = filter.And(x => x.Manager).IsEqualTo(false);
-            //else
-            //    filter = filter.And(x => x.Assignee).IsEqualTo(true);
-            filter = filter.And(c => c.Employee.ID).InList(emps.Select(x => x.Key).ToArray());
-            if (!Host.KanbanSettings.UserSettings.IncludeObserved)
-            {
-                if (Host.KanbanSettings.UserSettings.IncludeManaged)
-                    filter = filter.And(new Filter<KanbanSubscriber>(x => x.Manager).IsEqualTo(true).Or(x => x.Assignee).IsEqualTo(true));
-                else
-                    filter = filter.And(x => x.Assignee).IsEqualTo(true);
-            }
-
-            using (new WaitCursor())
-            {
-
-                UserTasksHeaderTimeConverter.Kanbans = models;
-
-                Kanban.Columns.Clear();
-                foreach (var employee in emps)
-                    Kanban.Columns.Add(new KanbanColumn
-                    {
-                        Categories = employee.Key.ToString(),
-                        Title = employee.Value,
-                        Width = Math.Max(150, Kanban.ActualWidth / emps.ToArray().Length - 1.0F)
-                    });
-                //var template = Resources["SimpleHeader"] as DataTemplate;
-                //var boundary = template.FindName("Boundary", null);
-
-                if (Kanban.Columns.Count > 0)
-                    Kanban.ColumnWidth = Math.Max(150, (Kanban.ActualWidth - 20F) / Kanban.Columns.Count - 1.0F);
-
-                _models = models.OrderBy(x => _swimlanes[x.Assignee]).ThenBy(x => x.DueDate).ToList();
-                FilterKanbans();
-                
-            }
-        }
-
-
-        private bool FilterKanban(TaskModel model, string searches, params Func<TaskModel, string>[] properties)
-        {
-            foreach (var search in searches.Split(' '))
-            foreach (var property in properties)
-                if (!property(model).Contains(search))
-                    return false;
-            return true;
-        }
-
-        private void FilterKanbans()
-        {
-            IEnumerable<TaskModel> Items = _models;
-            if (TaskType.SelectedItem is KanbanType kanbanType)
-            {
-                Items = Items.Where(x => x.Type.ID == kanbanType.ID);
-            }
-            if (!string.IsNullOrWhiteSpace(Search.Text))
-            {
-                var searches = Search.Text.Split();
-                Items = Items.Where(x => x.Search(searches));
-            }
-
-            if(object.Equals(Kanban.ItemsSource, Items))
-            {
-                // Triggers a refresh.
-                Kanban.ItemsSource = null;
-            }
-            Kanban.ItemsSource = Items;
-        }
-
-        private void ViewType_SelectionChanged(object sender, SelectionChangedEventArgs e)
-        {
-            if (Kanban != null)
-                Kanban.CardTemplate = ViewType.SelectedIndex > 0
-                    ? Resources["CompactKanban"] as DataTemplate
-                    : Resources["FullKanban"] as DataTemplate;
-            if (IsReady)
-            {
-                Host.KanbanSettings.StatusSettings.CompactView = ViewType.SelectedIndex > 0;
-                Host.SaveSettings();
-            }
-        }
 
         private void TaskType_SelectionChanged(object sender, SelectionChangedEventArgs e)
         {
-            FilterKanbans();
         }
 
         private void IncludeLocked_Checked(object sender, RoutedEventArgs e)

+ 148 - 71
prs.desktop/Panels/Tasks/TasksByUserControl.xaml

@@ -93,18 +93,17 @@
                            VerticalContentAlignment="Center" />
                 </Border>
 
-                <syncfusion:CheckListBox
-                    Grid.Row="4"
-                    x:Name="SelectedEmployees"
-                    DisplayMemberPath="Value"
-                    SelectedValuePath="Key"
-                    IsCheckOnFirstClick="True"
-                    SelectionChanged="EmployeesSelectionChanged"
-                    IsSelectAllEnabled="False"
-                    Background="White"
-                    BorderBrush="Gray"
-                    BorderThickness="0.75"
-                    Margin="0" />
+                <syncfusion:CheckListBox Grid.Row="4"
+                                         x:Name="SelectedEmployees"
+                                         DisplayMemberPath="Value.Name"
+                                         SelectedValuePath="Key"
+                                         IsCheckOnFirstClick="True"
+                                         ItemChecked="SelectedEmployees_ItemChecked"
+                                         IsSelectAllEnabled="False"
+                                         Background="White"
+                                         BorderBrush="Gray"
+                                         BorderThickness="0.75"
+                                         Margin="0" />
 
             </Grid>
 
@@ -113,17 +112,19 @@
         <dynamicgrid:DynamicSplitPanel.Detail>
             <Grid>
                 <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto" />
                     <RowDefinition Height="Auto" />
                     <RowDefinition Height="*" />
                 </Grid.RowDefinitions>
                 <Border
                     Background="WhiteSmoke"
-                    BorderThickness="0.75,0.75,0.75,0"
+                    BorderThickness="0.75,0.75,0.75,0.75"
                     BorderBrush="Gray"
                     CornerRadius="5,5,0,0"
                     Grid.Row="0"
                     Height="30"
-                    Padding="2">
+                    Padding="2"
+                    Margin="0,0,0,5">
                     <DockPanel>
                         <Label DockPanel.Dock="Left" Content="Search" Margin="5,0,0,0" />
                         <Button DockPanel.Dock="Right" x:Name="Export" Padding="10,0" Margin="5,0,0,0" Content="Export"
@@ -163,81 +164,157 @@
                     </ItemsControl.ItemsPanel>
                     <ItemsControl.ItemTemplate>
                         <DataTemplate DataType="local:TasksByUserEmployeeHeader">
-                            <Grid>
-                                <Grid.ColumnDefinitions>
-                                    <ColumnDefinition Width="Auto" />
-                                    <ColumnDefinition Width="*" />
-                                </Grid.ColumnDefinitions>
-                                <Grid.RowDefinitions>
-                                    <RowDefinition Height="*" />
-                                    <RowDefinition Height="*" />
-                                </Grid.RowDefinitions>
-                                <Border Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Width="40" Height="40" CornerRadius="20"
-                                        Margin="5" BorderBrush="Gray" BorderThickness="0.75" VerticalAlignment="Center"
-                                        HorizontalAlignment="Center">
-                                    <Border.Background>
-                                        <ImageBrush ImageSource="{Binding Image}"
-                                                    Stretch="UniformToFill"/>
-                                    </Border.Background>
-                                </Border>
-                                <TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding Name}" FontSize="16" FontWeight="DemiBold"
-                                           HorizontalAlignment="Left" Margin="10,0,10,0" />
-                                <TextBlock Grid.Column="1" Grid.Row="1" FontSize="12" HorizontalAlignment="Left" Margin="10,0,5,0">
-                                    <Run Text="{Binding NumTasks,Mode=OneWay}" />
-                                    <Run Text="Tasks /" />
-                                    <Run Text="{Binding NumHours,Mode=OneWay, StringFormat={}{0:F2}}" />
-                                    <Run Text="Hours" />
-                                </TextBlock>
-                            </Grid>
+                            <Border BorderBrush="LightGray" BorderThickness="0,0,0,1">
+                                <Grid>
+                                    <Grid.ColumnDefinitions>
+                                        <ColumnDefinition Width="Auto" />
+                                        <ColumnDefinition Width="*" />
+                                    </Grid.ColumnDefinitions>
+                                    <Grid.RowDefinitions>
+                                        <RowDefinition Height="*" />
+                                        <RowDefinition Height="*" />
+                                    </Grid.RowDefinitions>
+                                    <Border Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Width="40" Height="40" CornerRadius="20"
+                                            Margin="5" BorderBrush="Gray" BorderThickness="0.75" VerticalAlignment="Center"
+                                            HorizontalAlignment="Center">
+                                        <Border.Background>
+                                            <ImageBrush ImageSource="{Binding Image}"
+                                                        Stretch="UniformToFill"/>
+                                        </Border.Background>
+                                    </Border>
+                                    <TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding Name}" FontSize="16" FontWeight="DemiBold"
+                                               HorizontalAlignment="Left" Margin="10,0,10,0" />
+                                    <TextBlock Grid.Column="1" Grid.Row="1" FontSize="12" HorizontalAlignment="Left" Margin="10,0,5,0">
+                                        <Run Text="{Binding NumTasks,Mode=OneWay}" />
+                                        <Run Text="Tasks /" />
+                                        <Run Text="{Binding NumHours,Mode=OneWay, StringFormat={}{0:F2}}" />
+                                        <Run Text="Hours" />
+                                    </TextBlock>
+                                </Grid>
+                            </Border>
                         </DataTemplate>
                     </ItemsControl.ItemTemplate>
                 </ItemsControl>
-                <ScrollViewer>
+                <ScrollViewer Grid.Row="2">
                     <ItemsControl ItemsSource="{Binding ElementName=Control,Path=Model.Categories}">
                         <ItemsControl.ItemTemplate>
                             <DataTemplate DataType="local:TasksByUserCategory">
                                 <Grid>
                                     <Grid.RowDefinitions>
                                         <RowDefinition Height="Auto"/>
-                                        <RowDefinition Height="*"/>
+                                        <RowDefinition>
+                                            <RowDefinition.Style>
+                                                <Style TargetType="RowDefinition">
+                                                    <Setter Property="Height" Value="0"/>
+                                                    <Style.Triggers>
+                                                        <DataTrigger Binding="{Binding Collapsed}" Value="False">
+                                                            <Setter Property="Height" Value="*"/>
+                                                        </DataTrigger>
+                                                    </Style.Triggers>
+                                                </Style>
+                                            </RowDefinition.Style>
+                                        </RowDefinition>
                                     </Grid.RowDefinitions>
                                     <Grid.ColumnDefinitions>
                                         <ColumnDefinition Width="*"/>
                                         <ColumnDefinition Width="300"/>
                                         <ColumnDefinition Width="*"/>
                                     </Grid.ColumnDefinitions>
-                                    <Button Content="{Binding Category}"
+                                    <Rectangle Grid.Row="0" Grid.Column="0"
+                                               HorizontalAlignment="Stretch"
+                                               VerticalAlignment="Center"
+                                               Fill="LightGray"
+                                               Height="1"/>
+                                    <Rectangle Grid.Row="0" Grid.Column="2"
+                                               HorizontalAlignment="Stretch"
+                                               VerticalAlignment="Center"
+                                               Fill="LightGray"
+                                               Height="1"/>
+                                    <Button x:Name="FoldButton"
                                             Grid.Row="0"
-                                            Grid.Column="1"/>
-                                    <ItemsControl ItemsSource="{Binding EmployeeCategories}">
-                                        <ItemsControl.ItemsPanel>
-                                            <ItemsPanelTemplate>
-                                                <UniformGrid Rows="1"/>
-                                            </ItemsPanelTemplate>
-                                        </ItemsControl.ItemsPanel>
-                                        <ItemsControl.ItemTemplate>
-                                            <DataTemplate DataType="local:TasksByUserEmployeeCategory">
-                                                <ItemsControl Grid.Row="1" Margin="5,0"
+                                            Grid.Column="1"
+                                            Click="FoldButton_Click"
+                                            Tag="{Binding}"
+                                            Margin="5">
+                                        <DockPanel>
+                                            <Image Width="32" Height="32"
+                                                   RenderTransformOrigin="0.5,0.5" Source="pack://application:,,,/Resources/rightarrow.png"
+                                                   DockPanel.Dock="Left">
+                                                <Image.Style>
+                                                    <Style TargetType="Image">
+                                                        <Style.Triggers>
+                                                            <DataTrigger Binding="{Binding Collapsed}" Value="False">
+                                                                <Setter Property="RenderTransform">
+                                                                    <Setter.Value>
+                                                                        <RotateTransform Angle="90"/>
+                                                                    </Setter.Value>
+                                                                </Setter>
+                                                            </DataTrigger>
+                                                        </Style.Triggers>
+                                                    </Style>
+                                                </Image.Style>
+                                            </Image>
+                                            <Label Content="{Binding Category}"
+                                                   VerticalContentAlignment="Center"
+                                                   HorizontalContentAlignment="Left"
+                                                   DockPanel.Dock="Right"/>
+                                        </DockPanel>
+                                    </Button>
+                                    <Border Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3">
+                                        <ItemsControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
+                                                  ItemsSource="{Binding EmployeeCategories}">
+                                            <ItemsControl.ItemsPanel>
+                                                <ItemsPanelTemplate>
+                                                    <UniformGrid Rows="1"/>
+                                                </ItemsPanelTemplate>
+                                            </ItemsControl.ItemsPanel>
+                                            <ItemsControl.ItemTemplate>
+                                                <DataTemplate DataType="local:TasksByUserEmployeeCategory">
+                                                    <Border BorderBrush="LightGray"
+                                                            BorderThickness="0,0,1,0"
+                                                            Padding="5,0">
+                                                        <Grid Tag="{Binding}"
+                                                              Background="Transparent"
                                                               AllowDrop="True"
                                                               DragOver="ItemsControl_DragOver"
-                                                              Drop="ItemsControl_Drop"
-                                                              Tag="{Binding}"
-                                                              ItemsSource="{Binding Tasks,Mode=OneWay}">
-                                                    <ItemsControl.Style>
-                                                        <Style TargetType="{x:Type ItemsControl}" BasedOn="{StaticResource VirtualisedItemsControlStyle}">
-                                                            <Setter Property="ItemTemplate" Value="{StaticResource FullKanban}"/>
-                                                            <Style.Triggers>
-                                                                <DataTrigger Binding="{Binding Mode,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TasksByUserControl}}}"
-                                                         Value="{x:Static local:KanbanViewMode.Compact}">
-                                                                    <Setter Property="ItemTemplate" Value="{StaticResource CompactKanban}"/>
-                                                                </DataTrigger>
-                                                            </Style.Triggers>
-                                                        </Style>
-                                                    </ItemsControl.Style>
-                                                </ItemsControl>
-                                            </DataTemplate>
-                                        </ItemsControl.ItemTemplate>
-                                    </ItemsControl>
+                                                              Drop="ItemsControl_Drop">
+                                                            <Grid.RowDefinitions>
+                                                                <RowDefinition Height="Auto"/>
+                                                                <RowDefinition Height="*"/>
+                                                                <RowDefinition>
+                                                                    <RowDefinition.Style>
+                                                                        <Style TargetType="{x:Type RowDefinition}">
+                                                                            <Setter Property="Height" Value="75"/>
+                                                                            <Style.Triggers>
+                                                                                <DataTrigger Binding="{Binding Mode,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TasksByUserControl}}}"
+                                                                                             Value="{x:Static local:KanbanViewMode.Compact}">
+                                                                                    <Setter Property="Height" Value="30"/>
+                                                                                </DataTrigger>
+                                                                            </Style.Triggers>
+                                                                        </Style>
+                                                                    </RowDefinition.Style>
+                                                                </RowDefinition>
+                                                            </Grid.RowDefinitions>
+                                                            <ItemsControl Grid.Row="0"
+                                                                          ItemsSource="{Binding Tasks,Mode=OneWay}">
+                                                                <ItemsControl.Style>
+                                                                    <Style TargetType="{x:Type ItemsControl}">
+                                                                        <Setter Property="ItemTemplate" Value="{StaticResource FullKanban}"/>
+                                                                        <Style.Triggers>
+                                                                            <DataTrigger Binding="{Binding Mode,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TasksByUserControl}}}"
+                                                             Value="{x:Static local:KanbanViewMode.Compact}">
+                                                                                <Setter Property="ItemTemplate" Value="{StaticResource CompactKanban}"/>
+                                                                            </DataTrigger>
+                                                                        </Style.Triggers>
+                                                                    </Style>
+                                                                </ItemsControl.Style>
+                                                            </ItemsControl>
+                                                        </Grid>
+                                                    </Border>
+                                                </DataTemplate>
+                                            </ItemsControl.ItemTemplate>
+                                        </ItemsControl>
+                                    </Border>
                                 </Grid>
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>

+ 376 - 166
prs.desktop/Panels/Tasks/TasksByUserControl.xaml.cs

@@ -18,10 +18,11 @@ using InABox.DynamicGrid;
 using InABox.WPF;
 using Syncfusion.UI.Xaml.Kanban;
 using Syncfusion.Windows.Tools.Controls;
+using static com.sun.tools.javac.code.Symbol;
 
 namespace PRSDesktop;
 
-public class TasksByUserEmployeeHeader
+public class TasksByUserEmployeeHeader : INotifyPropertyChanged
 {
     public Guid EmployeeID { get; set; }
 
@@ -38,6 +39,21 @@ public class TasksByUserEmployeeHeader
 
     private TasksByUserModel Model;
 
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    // Create the OnPropertyChanged method to raise the event
+    // The calling member's name will be used as the parameter.
+    protected void OnPropertyChanged([CallerMemberName] string? name = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+
+        if (name.Equals(nameof(Tasks)))
+        {
+            OnPropertyChanged(nameof(NumTasks));
+            OnPropertyChanged(nameof(NumHours));
+        }
+    }
+
     public TasksByUserEmployeeHeader(Guid employeeID, string name, BitmapImage image, TasksByUserModel model)
     {
         EmployeeID = employeeID;
@@ -45,55 +61,84 @@ public class TasksByUserEmployeeHeader
         Image = image;
         Model = model;
     }
+
+    public void UpdateTasks()
+    {
+        OnPropertyChanged(nameof(Tasks));
+    }
 }
 
 public class TasksByUserEmployeeCategory
 {
     public Guid EmployeeID { get; set; }
 
-    public List<TaskModel> Tasks { get; set; } = new();
+    public string Category { get; set; }
+
+    public SuspendableObservableCollection<TaskModel> Tasks { get; set; } = new();
 
-    public TasksByUserEmployeeCategory(Guid employeeID)
+    public TasksByUserEmployeeCategory(Guid employeeID, string category)
     {
         EmployeeID = employeeID;
+        Category = category;
     }
 }
 
-public class TasksByUserCategory
+public class TasksByUserCategory : INotifyPropertyChanged
 {
+    private bool _collapsed;
+    public bool Collapsed
+    {
+        get => _collapsed;
+        set
+        {
+            _collapsed = value;
+            OnPropertyChanged();
+        }
+    }
+
     public string Category { get; set; }
 
     public IEnumerable<TasksByUserEmployeeCategory> EmployeeCategories => EmployeeCategoryDictionary.Values;
 
     public Dictionary<Guid, TasksByUserEmployeeCategory> EmployeeCategoryDictionary { get; set; } = new();
 
-    public TasksByUserCategory(string category)
+    public TasksByUserCategory(string category, bool collapsed)
     {
         Category = category;
+        Collapsed = collapsed;
+    }
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    // Create the OnPropertyChanged method to raise the event
+    // The calling member's name will be used as the parameter.
+    protected void OnPropertyChanged([CallerMemberName] string? name = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
     }
 }
 
 public class TasksByUserModel
 {
-    public List<TasksByUserEmployeeHeader> SectionHeaders { get; set; } = new();
+    public SuspendableObservableCollection<TasksByUserEmployeeHeader> SectionHeaders { get; set; } = new();
 
-    public List<TasksByUserCategory> Categories { get; set; } = new();
+    public SuspendableObservableCollection<TasksByUserCategory> Categories { get; set; } = new();
 }
 
 public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, ITaskControl
 {
     private static readonly BitmapImage anonymous = PRSDesktop.Resources.anonymous.AsBitmapImage();
 
-    public TasksByUserModel Model { get; set; }
+    public TasksByUserModel Model { get; set; } = new();
 
     private ILookup<Guid, Guid> TeamEmployees;
 
     private Dictionary<Guid, EmployeeModel> Employees;
 
-    private KanbanViewMode _mode;
-
     private bool bPopulating;
 
+    private KanbanViewMode _mode;
+
     public KanbanViewMode Mode
     {
         get => _mode;
@@ -154,7 +199,7 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
         SelectedTeams.ItemsSource = teams;
         foreach (var team in Host.KanbanSettings.UserSettings.SelectedTeams)
         {
-            SelectedTeams.SelectedItems.Add(teams.Where(x => x.Key == team));
+            SelectedTeams.SelectedItems.Add(teams.FirstOrDefault(x => x.Key == team));
         }
     }
 
@@ -169,7 +214,7 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
 
             SelectedEmployees.ItemsSource = Employees.Where(x => availableemployees.Contains(x.Key));
             SelectedEmployees.SelectedItems.Clear();
-            foreach (var employee in Host.KanbanSettings.UserSettings.SelectedEmployees.Where(x => availableemployees.Contains(x)))
+            foreach (var employee in Host.KanbanSettings.UserSettings.SelectedEmployees.Where(availableemployees.Contains))
                 SelectedEmployees.SelectedItems.Add(Employees.FirstOrDefault(x => Equals(x.Key, employee)));
         }
         catch (Exception e)
@@ -207,16 +252,54 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
         LoadEmployees();
         PopulateEmployees();
 
-        Mode = Host.KanbanSettings.StatusSettings.CompactView ? KanbanViewMode.Compact : KanbanViewMode.Full;
+        Mode = Host.KanbanSettings.UserSettings.CompactView ? KanbanViewMode.Compact : KanbanViewMode.Full;
 
         PopulateKanbanTypes();
     }
 
     #endregion
 
+    #region Filters 
+
+    private void IncludeCompleted_Checked(object sender, RoutedEventArgs e)
+    {
+        if (!IsReady)
+            return;
+        SaveSettings();
+        Refresh();
+    }
+
+    private void IncludeObserved_Checked(object sender, RoutedEventArgs e)
+    {
+        if (!IsReady)
+            return;
+        SaveSettings();
+        Refresh();
+    }
+
+    private void IncludeManaged_Checked(object sender, RoutedEventArgs e)
+    {
+        if (!IsReady)
+            return;
+        SaveSettings();
+        Refresh();
+    }
+
+    private void TaskType_SelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        FilterKanbans();
+    }
+
+    private void Search_KeyUp(object sender, KeyEventArgs e)
+    {
+        FilterKanbans();
+    }
+
+    #endregion
+
     #region Refresh
 
-    private Filter<KanbanSubscriber> GetKanbanSubscriberFilter()
+    private Filter<KanbanSubscriber> GetKanbanSubscriberFilter(Filter<KanbanSubscriber>? additional = null)
     {
         var filter = new Filter<KanbanSubscriber>(c => c.Kanban.Closed).IsEqualTo(DateTime.MinValue)
             .And(x => x.Kanban.Locked).IsEqualTo(false);
@@ -249,82 +332,138 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
             else
                 filter = filter.And(x => x.Assignee).IsEqualTo(true);
         }
-        return filter;
+        if (additional is not null)
+        {
+            return additional.And(filter);
+        }
+        else
+        {
+            return filter;
+        }
     }
 
     private void ReloadColumns()
     {
+        Model.SectionHeaders.SupressNotification = true;
         Model.SectionHeaders.Clear();
-        foreach (var employeeID in Host.KanbanSettings.UserSettings.SelectedEmployees)
+
+        var emps = Host.KanbanSettings.UserSettings.SelectedEmployees.OrderBy(x => Employees[x].Name).ToArray();
+
+        foreach (var employeeID in emps)
         {
             if (Employees.TryGetValue(employeeID, out var employee))
             {
                 Model.SectionHeaders.Add(new TasksByUserEmployeeHeader(employeeID, employee.Name, employee.Image ?? anonymous, Model));
             }
         }
+        Model.SectionHeaders.SupressNotification = false;
+        Model.Categories.SupressNotification = true;
+
+        var oldCategories = Model.Categories.ToDictionary(x => x.Category);
+
+        Model.Categories.Clear();
+
+        foreach (var category in _categoryOrder)
+        {
+            if (category.Equals(Kanban.COMPLETE) && !Host.KanbanSettings.UserSettings.IncludeCompleted)
+            {
+                continue;
+            }
+            var newCategory = new TasksByUserCategory(category, oldCategories.GetValueOrDefault(category)?.Collapsed ?? false);
+            foreach (var employeeID in emps)
+            {
+                if (Employees.TryGetValue(employeeID, out var employee))
+                {
+                    var cat = new TasksByUserEmployeeCategory(employeeID, category);
+                    newCategory.EmployeeCategoryDictionary[employeeID] = cat;
+                    var header = Model.SectionHeaders.First(x => x.EmployeeID == employeeID);
+                    cat.Tasks.CollectionChanged += (s, e) =>
+                    {
+                        header.UpdateTasks();
+                    };
+                }
+            }
+            Model.Categories.Add(newCategory);
+        }
+        Model.Categories.SupressNotification = false;
     }
 
-    public void Refresh()
-    {
-        var categoryOrder = new Dictionary<string, int>
+    private static readonly string[] _categoryOrder = new[]
             {
-                { Kanban.OPEN, 0 },
-                { Kanban.INPROGRESS, 1 },
-                { Kanban.WAITING, 2 },
-                { Kanban.COMPLETE, 3 }
+                Kanban.OPEN,
+                Kanban.INPROGRESS,
+                Kanban.WAITING,
+                Kanban.COMPLETE
             };
-        var filter = GetKanbanSubscriberFilter();
 
+    private Columns<Kanban> GetKanbanColumns()
+    {
+        return new Columns<Kanban>(
+            x => x.ID,
+            x => x.DueDate,
+            x => x.Completed,
+            x => x.Description,
+            x => x.Summary,
+            x => x.Category,
+            x => x.EmployeeLink.ID,
+            x => x.EmployeeLink.Name,
+            x => x.ManagerLink.ID,
+            x => x.ManagerLink.Name,
+            x => x.Notes,
+            x => x.Title,
+            x => x.JobLink.ID,
+            x => x.JobLink.JobNumber,
+            x => x.JobLink.Name,
+            x => x.Type.ID,
+            x => x.Type.Code,
+            x => x.Number,
+            x => x.Attachments,
+            x => x.Locked,
+            x => x.EstimatedTime);
+    }
+
+    private IEnumerable<KanbanSubscriber> LoadSubscribers(Filter<KanbanSubscriber>? filter = null)
+    {
+        filter = GetKanbanSubscriberFilter(filter);
+
+        var results = Client.QueryMultiple(
+            new KeyedQueryDef<KanbanSubscriber>(
+                filter,
+                new Columns<KanbanSubscriber>(x => x.ID, x => x.Employee.ID, x => x.Kanban.ID),
+                new SortOrder<KanbanSubscriber>(x => x.Kanban.DueDate) { Direction = SortDirection.Ascending }),
+            new KeyedQueryDef<Kanban>(
+                new Filter<Kanban>(x => x.ID).InQuery(filter, x => x.Kanban.ID),
+                GetKanbanColumns()));
+
+        var kanbans = results.GetObjects<Kanban>().ToDictionary(x => x.ID);
+        return results.GetObjects<KanbanSubscriber>().Select(x =>
+        {
+            if (kanbans.TryGetValue(x.Kanban.ID, out var kanban))
+            {
+                x.Kanban.Synchronise(kanban);
+            }
+            return x;
+        });
+    }
+
+    public void Refresh()
+    {
         using (new WaitCursor())
         {
-            var kanbans = new Client<KanbanSubscriber>().Query(
-                filter,
-                new Columns<KanbanSubscriber>
-                (
-                    x => x.Kanban.ID,
-                    x => x.Kanban.DueDate,
-                    x => x.Kanban.Completed,
-                    //x => x.Kanban.Description,
-                    x => x.Kanban.Summary,
-                    x => x.Kanban.Category,
-                    x => x.Kanban.EmployeeLink.ID,
-                    x => x.Kanban.EmployeeLink.Name,
-                    x => x.Kanban.ManagerLink.ID,
-                    x => x.Kanban.ManagerLink.Name,
-                    x => x.Kanban.Notes,
-                    x => x.Kanban.Title,
-                    x => x.Kanban.JobLink.ID,
-                    x => x.Kanban.JobLink.JobNumber,
-                    x => x.Kanban.JobLink.Name,
-                    x => x.Kanban.Type.ID,
-                    x => x.Kanban.Type.Code,
-                    x => x.Kanban.Number,
-                    x => x.Kanban.Attachments,
-                    x => x.Kanban.Locked,
-                    x => x.Employee.ID,
-                    x => x.Kanban.EstimatedTime
-                ),
-                new SortOrder<KanbanSubscriber>(x => x.Kanban.DueDate) { Direction = SortDirection.Ascending }
-            );
-            var models = CreateModels(kanbans.ToObjects<KanbanSubscriber>()).ToList();
+            var models = CreateModels(LoadSubscribers()).ToList();
 
             ReloadColumns();
 
-            AllTasks = models.OrderBy(x => categoryOrder[x.Category]).ThenBy(x => x.DueDate).ToList();
+            AllTasks = models.OrderBy(x => x.DueDate).ToList();
             FilterKanbans();
         }
     }
 
+    /// <summary>
+    /// Take the full list of kanbans loaded from the database, and filter based on the search UI elements, filtering into the columns.
+    /// </summary>
     private void FilterKanbans()
     {
-        var categoryOrder = new Dictionary<string, int>
-            {
-                { Kanban.OPEN, 0 },
-                { Kanban.INPROGRESS, 1 },
-                { Kanban.WAITING, 2 },
-                { Kanban.COMPLETE, 3 }
-            };
-
         IEnumerable<TaskModel> filtered = AllTasks;
         if (TaskType.SelectedItem is KanbanType kanbanType)
         {
@@ -336,30 +475,31 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
             filtered = filtered.Where(x => x.Search(searches));
         }
 
-        var categories = new Dictionary<string, TasksByUserCategory>();
-
-        foreach (var task in filtered)
+        var categoryMap = Model.Categories.ToDictionary(x => x.Category, x => x.EmployeeCategoryDictionary);
+        foreach(var category in Model.Categories)
         {
-            if(!categories.TryGetValue(task.Category, out var category))
+            foreach(var empCat in category.EmployeeCategories)
             {
-                category = new TasksByUserCategory(task.Category);
-                categories.Add(task.Category, category);
+                empCat.Tasks.Clear();
             }
+        }
 
-            if(!category.EmployeeCategoryDictionary.TryGetValue(task.EmployeeCategory, out var employeeCategory))
-            {
-                employeeCategory = new TasksByUserEmployeeCategory(task.EmployeeCategory);
-                category.EmployeeCategoryDictionary.Add(task.EmployeeCategory, employeeCategory);
-            }
+        SelectedTasks.Clear();
 
-            employeeCategory.Tasks.Add(task);
-            if (task.Checked)
+        foreach (var task in filtered)
+        {
+            if(categoryMap.TryGetValue(task.Category, out var categoryDict))
             {
-                SelectedTasks.Add(task);
+                if (categoryDict.TryGetValue(task.EmployeeCategory, out var employeeCategory))
+                {
+                    employeeCategory.Tasks.Add(task);
+                    if (task.Checked)
+                    {
+                        SelectedTasks.Add(task);
+                    }
+                }
             }
         }
-
-        Model.Categories.AddRange(categories.Values.OrderBy(x => categoryOrder[x.Category]));
     }
 
     private IEnumerable<TaskModel> CreateModels(IEnumerable<KanbanSubscriber> subscribers)
@@ -367,26 +507,31 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
         foreach(var subscriber in subscribers)
         {
             var kanban = subscriber.Kanban;
-
-            var empValid = Entity.IsEntityLinkValid<KanbanSubscriber, EmployeeLink>(x => x.Kanban.EmployeeLink, row);
-            var mgrValid = Entity.IsEntityLinkValid<KanbanSubscriber, EmployeeLink>(x => x.Kanban.ManagerLink, row);
-
-            var completed = row.Get<KanbanSubscriber, DateTime>(e => e.Kanban.Completed);
-            var locked = row.Get<KanbanSubscriber, bool>(e => e.Kanban.Locked);
-            var typeID = row.Get<KanbanSubscriber, Guid>(e => e.Kanban.Type.ID);
-            var typeCode = row.Get<KanbanSubscriber, string>(e => e.Kanban.Type.Code);
-            var job = row.Get<KanbanSubscriber, string>(x => x.Kanban.JobLink.JobNumber);
-
-            var model = new TaskModel();
-
-            model.Title = kanban.Title;
-            model.ID = kanban.ID;
-            model.Description = kanban.Summary ?? "";
-            model.EmployeeCategory = subscriber.Employee.ID;
-            model.Category = kanban.Category;
-
-            if (model.Category.IsNullOrWhiteSpace())
-                model.Category = "Open";
+            var model = new TaskModel
+            {
+                Title = kanban.Title,
+                ID = kanban.ID,
+                Description = kanban.Summary ?? "",
+                EmployeeCategory = subscriber.Employee.ID,
+                Category = kanban.Category.NotWhiteSpaceOr("Open"),
+                Attachments = kanban.Attachments > 0,
+                DueDate = kanban.DueDate,
+                CompletedDate = kanban.Completed,
+                Locked = kanban.Locked,
+                EstimatedTime = kanban.EstimatedTime,
+                EmployeeID = kanban.EmployeeLink.ID,
+                ManagerID = kanban.ManagerLink.ID,
+                JobID = kanban.JobLink.ID,
+                JobNumber = kanban.JobLink.JobNumber?.Trim() ?? "",
+                JobName = kanban.JobLink.Name,
+                Type = new KanbanType
+                {
+                    ID = kanban.Type.ID,
+                    Code = kanban.Type.Code
+                },
+                Number = kanban.Number,
+                Checked = SelectedTasks.Any(x => x.ID == kanban.ID)
+            };
 
             var colour = subscriber.Employee.ID == kanban.EmployeeLink.ID
                 ? TaskModel.KanbanColor(
@@ -401,12 +546,6 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
             }
             model.Color = System.Windows.Media.Color.FromArgb(colour.A, colour.R, colour.G, colour.B);
 
-            model.Attachments = kanban.Attachments > 0;
-            model.DueDate = kanban.DueDate;
-            model.CompletedDate = kanban.Completed;
-            model.Locked = kanban.Locked;
-            model.EstimatedTime = kanban.EstimatedTime;
-
             var notes = new List<List<string>> { new() };
             var kanbanNotes = kanban.Notes;
             if (kanbanNotes != null)
@@ -425,36 +564,27 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
             }
             model.Notes = string.Join("\n===================================\n", notes.Reverse<List<string>>().Select(x => string.Join('\n', x)));
 
-            model.EmployeeID = kanban.EmployeeLink.ID;
-            model.ManagerID = kanban.ManagerLink.ID;
-
-            var employeeString = kanban.EmployeeLink.ID == subscriber.Employee.ID
-                ? ""
-                : kanban.EmployeeLink.ID == Guid.Empty
-                    ? " to (Unallocated)"
-                    : " to " + kanban.EmployeeLink.Name;
+            SetTaskModelAssignedTo(model, kanban, subscriber.Employee.ID);
 
-            var managerString = kanban.ManagerLink.ID == Guid.Empty || kanban.ManagerLink.ID == subscriber.Employee.ID
-                ? ""
-                : " by " + kanban.ManagerLink.Name;
+            yield return model;
+        }
+    }
 
-            model.AssignedTo = !string.IsNullOrEmpty(employeeString) || !managerString.IsNullOrWhiteSpace()
-                ? $"Assigned{employeeString}{managerString}"
-                : "";
+    private static void SetTaskModelAssignedTo(TaskModel model, IKanban kanban, Guid subscriberID)
+    {
+        var employeeString = kanban.EmployeeLink.ID == subscriberID
+            ? ""
+            : kanban.EmployeeLink.ID == Guid.Empty
+                ? " to (Unallocated)"
+                : " to " + kanban.EmployeeLink.Name;
 
-            model.JobID = kanban.JobLink.ID;
-            model.JobNumber = kanban.JobLink.JobNumber?.Trim() ?? "";
-            model.JobName = kanban.JobLink.Name;
-            model.Checked = SelectedTasks.Any(x => x.ID == model.ID);
-            model.Type = new KanbanType
-            {
-                ID = typeID,
-                Code = typeCode
-            };
-            model.Number = kanban.Number;
+        var managerString = kanban.ManagerLink.ID == Guid.Empty || kanban.ManagerLink.ID == subscriberID
+            ? ""
+            : " by " + kanban.ManagerLink.Name;
 
-            yield return model;
-        }
+        model.AssignedTo = !string.IsNullOrEmpty(employeeString) || !managerString.IsNullOrWhiteSpace()
+            ? $"Assigned{employeeString}{managerString}"
+            : "";
     }
 
     #endregion
@@ -502,6 +632,85 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
         e.CanExecute = true;
     }
 
+
+    private void ItemsControl_DragOver(object sender, DragEventArgs e)
+    {
+        if (sender is not FrameworkElement element || element.Tag is not TasksByUserEmployeeCategory category) return;
+
+        e.Effects = DragDropEffects.None;
+        if (e.Data.GetDataPresent(typeof(TaskModel)))
+        {
+            var model = e.Data.GetData(typeof(TaskModel)) as TaskModel;
+            if (model is not null
+                && (model.Category != category.Category || model.EmployeeCategory != category.EmployeeID)
+                && !SelectedTasks.Any(x => x.Locked)
+                && SelectedTasks.Concat(CoreUtils.One(model)).Any(x => x.IsAssignee))
+            {
+                e.Effects = DragDropEffects.Move;
+            }
+        }
+    }
+
+    private void ItemsControl_Drop(object sender, DragEventArgs e)
+    {
+        if (sender is not FrameworkElement element || element.Tag is not TasksByUserEmployeeCategory category) return;
+
+        if (e.Data.GetDataPresent(typeof(TaskModel)))
+        {
+            var models = SelectedModels(e.Data.GetData(typeof(TaskModel)) as TaskModel)
+                .Where(x => (!x.Category.Equals(category.Category) || x.EmployeeID != category.EmployeeID) && x.IsAssignee)
+                .ToList();
+            if (!models.Any())
+            {
+                return;
+            }
+            var changingCategory = models.Any(x => !x.Category.Equals(category.Category));
+            var completing = changingCategory && category.Category.Equals(Kanban.COMPLETE);
+            var completed = DateTime.Now;
+
+            if (completing)
+            {
+                if (MessageBox.Show($"Are you sure you want to complete the selected tasks?", "Confirm Completion",
+                        MessageBoxButton.YesNo) != MessageBoxResult.Yes)
+                    return;
+            }
+
+            var kanbans = Host.LoadKanbans(models, new Columns<Kanban>(x => x.ID, x => x.EmployeeLink.ID, x => x.Private, x => x.Number));
+            var subscribers = new ClientKanbanSubscriberSet(kanbans.Select(x => x.ID));
+
+            foreach(var kanban in kanbans)
+            {
+                if (!kanban.Private)
+                {
+                    kanban.EmployeeLink.ID = category.EmployeeID;
+                    subscribers.EnsureAssignee(kanban.ID, kanban.EmployeeLink.ID);
+
+                    kanban.Category = category.Category;
+                    if (completing)
+                    {
+                        kanban.Completed = completed;
+                    }
+                }
+                else
+                {
+                    MessageBox.Show($"Cannot change assignee for task {kanban.Number} because it is private.");
+                    models.RemoveAll(x => x.ID == kanban.ID);
+                }
+            }
+
+            Client.Save(kanbans.Where(x => x.IsChanged()), $"Task Employee Updated");
+            subscribers.Save(true);
+
+            var kanbanIDs = models.Select(x => x.ID).ToArray();
+            AllTasks.RemoveAll(x => kanbanIDs.Contains(x.ID));
+
+            AllTasks.AddRange(CreateModels(LoadSubscribers(new Filter<KanbanSubscriber>(x => x.Kanban.ID).InList(kanbanIDs))));
+            AllTasks.Sort((x, y) => x.DueDate.CompareTo(y.DueDate));
+
+            FilterKanbans();
+        }
+    }
+
     #endregion
 
     #region ITaskControl
@@ -550,7 +759,7 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
         var teams = SelectedTeams.SelectedItems.Select(x => ((KeyValuePair<Guid, string>)x).Key);
         Host.KanbanSettings.UserSettings.SelectedTeams = teams.ToArray();
 
-        var emps = SelectedEmployees.SelectedItems.Select(x => ((KeyValuePair<Guid, string>)x).Key);
+        var emps = SelectedEmployees.SelectedItems.Select(x => ((KeyValuePair<Guid, EmployeeModel>)x).Key);
         emps = emps.Where(e => TeamEmployees.Any(t => t.Contains(e)));
         Host.KanbanSettings.UserSettings.SelectedEmployees = emps.ToArray();
 
@@ -584,58 +793,59 @@ public partial class TasksByUserControl : UserControl, INotifyPropertyChanged, I
         SaveSettings();
     }
 
-    private void EmployeesSelectionChanged(object sender, SelectionChangedEventArgs e)
+    private void SelectedEmployees_ItemChecked(object sender, ItemCheckedEventArgs e)
     {
-        if (!IsReady || bPopulating)
+        if (!IsReady || bPopulating || sender != SelectedEmployees)
             return;
         SaveSettings();
         Refresh();
     }
 
-    #endregion
-
-    private void Export_Click(object sender, RoutedEventArgs e)
-    {
-
-    }
-
-    private void IncludeCompleted_Checked(object sender, RoutedEventArgs e)
-    {
-
-    }
-
-    private void IncludeObserved_Checked(object sender, RoutedEventArgs e)
-    {
-
-    }
-
-    private void IncludeManaged_Checked(object sender, RoutedEventArgs e)
-    {
-
-    }
-
     private void ViewType_SelectionChanged(object sender, SelectionChangedEventArgs e)
     {
+        Mode = ViewType.SelectedIndex switch
+        {
+            0 => KanbanViewMode.Full,
+            1 => KanbanViewMode.Compact,
+            _ => KanbanViewMode.Full
+        };
 
+        if (IsReady)
+        {
+            Host.KanbanSettings.UserSettings.CompactView = Mode == KanbanViewMode.Compact;
+            Host.SaveSettings();
+        }
     }
 
-    private void TaskType_SelectionChanged(object sender, SelectionChangedEventArgs e)
-    {
-
-    }
+    #endregion
 
-    private void Search_KeyUp(object sender, KeyEventArgs e)
+    private void Export_Click(object sender, RoutedEventArgs e)
     {
+        var form = new DynamicExportForm(typeof(Kanban), GetKanbanColumns().ColumnNames());
+        if (form.ShowDialog() != true)
+            return;
+        var export = Client.Query(
+            new Filter<Kanban>(x => x.ID).InQuery(GetKanbanSubscriberFilter(), x => x.Kanban.ID),
+            new Columns<Kanban>(form.Fields),
+            LookupFactory.DefineSort<Kanban>()
+        );
 
-    }
-
-    private void ItemsControl_DragOver(object sender, DragEventArgs e)
-    {
+        var employee = "Tasks for " + string.Join(',', Employees.Where(x => Host.KanbanSettings.UserSettings.SelectedEmployees.Contains(x.Key)).Select(x => x.Value.Name));
 
+        ExcelExporter.DoExport<Kanban>(
+            export,
+            string.Format(
+                "{0} ({1:dd-MMM-yy})",
+                employee,
+                DateTime.Today
+            )
+        );
     }
 
-    private void ItemsControl_Drop(object sender, DragEventArgs e)
+    private void FoldButton_Click(object sender, RoutedEventArgs e)
     {
+        if (sender is not FrameworkElement element || element.Tag is not TasksByUserCategory category) return;
 
+        category.Collapsed = !category.Collapsed;
     }
 }