using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media.Imaging; using AvalonDock.Layout; using com.sun.org.apache.bcel.@internal.generic; using Comal.Classes; using InABox.Clients; using InABox.Core; using InABox.DynamicGrid; using InABox.Wpf; 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 : INotifyPropertyChanged { public Guid EmployeeID { get; set; } public string Name { get; set; } public BitmapImage Image { get; set; } public int NumTasks { get => Tasks.Count(); } public double NumHours { get => Tasks.Sum(x => x.EstimatedTime.TotalHours); } public IEnumerable Tasks => Model.Categories .SelectMany(x => x.EmployeeCategoryDictionary.GetValueOrDefault(EmployeeID)?.Tasks ?? Enumerable.Empty()); 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; Name = name; Image = image; Model = model; } public void UpdateTasks() { OnPropertyChanged(nameof(Tasks)); } } public class TasksByUserEmployeeCategory { public Guid EmployeeID { get; set; } public KanbanStatus Status { get; set; } public SuspendableObservableCollection Tasks { get; set; } = new(); public TasksByUserEmployeeCategory(Guid employeeID, KanbanStatus status) { EmployeeID = employeeID; Status = status; } } public class TasksByUserCategory : INotifyPropertyChanged { private bool _collapsed; public bool Collapsed { get => _collapsed; set { _collapsed = value; OnPropertyChanged(); } } public KanbanStatus Status { get; set; } public IEnumerable EmployeeCategories => EmployeeCategoryDictionary.Values; public Dictionary EmployeeCategoryDictionary { get; set; } = new(); public TasksByUserCategory(KanbanStatus status, bool collapsed) { Status = status; 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 SuspendableObservableCollection SectionHeaders { get; set; } = new(); public SuspendableObservableCollection 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; } = new(); private ILookup TeamEmployees; private Dictionary Employees; private bool bPopulating; private KanbanViewMode _mode; public KanbanViewMode Mode { get => _mode; set { _mode = value; OnPropertyChanged(); } } public TasksByUserControl() { InitializeComponent(); } #region INotifyPropertyChanged 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)); } #endregion #region Setup private void LoadEmployees() { var empfilter = LookupFactory.DefineFilter(); var results = Client.QueryMultiple( new KeyedQueryDef( LookupFactory.DefineFilter(), new Columns(x => x.ID) .Add(x => x.Name) .Add(x => x.Thumbnail.ID), new SortOrder(x => x.Name)), new KeyedQueryDef( LookupFactory.DefineFilter(), new Columns(x => x.ID) .Add(x => x.Name), new SortOrder(x => x.Name)), new KeyedQueryDef( LookupFactory.DefineFilter(), new Columns(x => x.EmployeeLink.ID) .Add(x => x.TeamLink.ID))); TeamEmployees = results.Get().ToLookup(x => x.TeamLink.ID, x => x.EmployeeLink.ID); Employees = results.GetObjects().ToDictionary( x => x.ID, x => new EmployeeModel(x.ID, x.Name, x.Thumbnail.ID, null)); var teams = results.GetObjects().ToDictionary(x => x.ID, x => x.Name); SelectedTeams.ItemsSource = teams; foreach (var team in Host.KanbanSettings.UserSettings.SelectedTeams) { SelectedTeams.SelectedItems.Add(teams.FirstOrDefault(x => x.Key == team)); } } private void PopulateEmployees() { bPopulating = true; try { var availableemployees = new List(); foreach (var team in SelectedTeams.SelectedItems.Select(v => (KeyValuePair)v)) availableemployees.AddRange(TeamEmployees[team.Key].Where(x => !availableemployees.Contains(x))); SelectedEmployees.ItemsSource = Employees.Where(x => availableemployees.Contains(x.Key)); SelectedEmployees.SelectedItems.Clear(); foreach (var employee in Host.KanbanSettings.UserSettings.SelectedEmployees.Where(availableemployees.Contains)) SelectedEmployees.SelectedItems.Add(Employees.FirstOrDefault(x => Equals(x.Key, employee))); } catch (Exception e) { } bPopulating = false; } private void SetupToolbar() { IncludeCompleted.Visibility = Security.IsAllowed() ? Visibility.Visible : Visibility.Collapsed; IncludeCompleted.IsChecked = IncludeCompleted.Visibility == Visibility.Visible ? Host.KanbanSettings.UserSettings.IncludeCompleted : true; IncludeObserved.IsChecked = Host.KanbanSettings.UserSettings.IncludeObserved; IncludeManaged.IsChecked = Host.KanbanSettings.UserSettings.IncludeManaged; ViewType.SelectedIndex = Host.KanbanSettings.UserSettings.CompactView ? 1 : 0; } private void PopulateKanbanTypes() { TaskType.Items.Add(""); foreach (var kanbanType in Host.KanbanTypes) { TaskType.Items.Add(kanbanType); } } public void Setup() { SetupToolbar(); SplitPanel.AnchorWidth = Host.KanbanSettings.UserSettings.AnchorWidth; TeamsRow.Height = new GridLength(Host.KanbanSettings.UserSettings.TeamsHeight); LoadEmployees(); PopulateEmployees(); Mode = Host.KanbanSettings.UserSettings.CompactView ? KanbanViewMode.Compact : KanbanViewMode.Full; PopulateKanbanTypes(); FilterButton.SetSettings(Host.KanbanSettings.Filters, false); } #endregion #region Filters private void TaskPanelFilterButton_OnFilterRefresh() { Refresh(); } private void FilterButton_OnFiltersSelected(DynamicGridSelectedFilterSettings filters) { Host.KanbanSettings.Filters = filters; Host.SaveSettings(); } 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) { if (!IsReady) return; FilterKanbans(); } private void Search_KeyUp(object sender, KeyEventArgs e) { FilterKanbans(); } #endregion #region Refresh private Filter GetKanbanSubscriberFilter(Filter? additional = null) { var filter = new Filter(c => c.Kanban.Closed).IsEqualTo(DateTime.MinValue) .And(x => x.Kanban.Locked).IsEqualTo(false); var privateFilter = new Filter(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.Master != null) filter = filter.And(c => c.Kanban.JobLink.ID).IsEqualTo(Host.Master.ID); if (!Host.KanbanSettings.UserSettings.IncludeCompleted) filter = filter.And(new Filter(x => x.Kanban.Completed).IsEqualTo(DateTime.MinValue)); var emps = Employees.Where(x => Host.KanbanSettings.UserSettings.SelectedEmployees.Contains(x.Key)); 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(x => x.Manager).IsEqualTo(true).Or(x => x.Assignee).IsEqualTo(true)); else filter = filter.And(x => x.Assignee).IsEqualTo(true); } if (additional is not null) { return additional.And(filter); } else { return filter; } } private void ReloadColumns() { Model.SectionHeaders.SupressNotification = true; Model.SectionHeaders.Clear(); var emps = Host.KanbanSettings.UserSettings.SelectedEmployees.Where(x => Employees.ContainsKey(x)).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.Status); Model.Categories.Clear(); foreach (var status in _statusOrder) { if (status == KanbanStatus.Complete && !Host.KanbanSettings.UserSettings.IncludeCompleted) { continue; } var newCategory = new TasksByUserCategory(status, oldCategories.GetValueOrDefault(status)?.Collapsed ?? false); foreach (var employeeID in emps) { if (Employees.TryGetValue(employeeID, out var employee)) { var cat = new TasksByUserEmployeeCategory(employeeID, status); 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; } private static readonly KanbanStatus[] _statusOrder = new[] { KanbanStatus.Open, KanbanStatus.InProgress, KanbanStatus.Waiting, KanbanStatus.Complete }; private Columns GetKanbanColumns() { return new Columns( x => x.ID, x => x.DueDate, x => x.Completed, x => x.Description, x => x.Summary, x => x.Status, 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 LoadSubscribers(Filter? filter = null) { filter = GetKanbanSubscriberFilter(filter); var kanbanFilter = new Filter(x => x.ID).InQuery(filter, x => x.Kanban.ID); var buttonFilter = FilterButton.GetFilter(); if(buttonFilter is not null) { kanbanFilter.And(buttonFilter); } var results = Client.QueryMultiple( new KeyedQueryDef( filter, new Columns(x => x.ID, x => x.Employee.ID, x => x.Kanban.ID), new SortOrder(x => x.Kanban.DueDate) { Direction = SortDirection.Ascending }), new KeyedQueryDef( kanbanFilter, GetKanbanColumns())); var kanbans = results.GetObjects().ToDictionary(x => x.ID); return results.GetObjects().Select(x => { if (kanbans.TryGetValue(x.Kanban.ID, out var kanban)) { x.Kanban.Synchronise(kanban); return x; } else { return null; } }).NotNull(); } public void Refresh() { using (new WaitCursor()) { var models = CreateModels(LoadSubscribers()).ToList(); ReloadColumns(); AllTasks = models.OrderBy(x => x.DueDate).ToList(); FilterKanbans(); } } /// /// Take the full list of kanbans loaded from the database, and filter based on the search UI elements, filtering into the columns. /// private void FilterKanbans() { IEnumerable filtered = AllTasks; if (TaskType.SelectedItem is KanbanType kanbanType) { filtered = filtered.Where(x => x.Type.ID == kanbanType.ID); } if (!string.IsNullOrWhiteSpace(Search.Text)) { var searches = Search.Text.Split(); filtered = filtered.Where(x => x.Search(searches)); } var categoryMap = Model.Categories.ToDictionary(x => x.Status, x => x.EmployeeCategoryDictionary); foreach(var category in Model.Categories) { foreach(var empCat in category.EmployeeCategories) { empCat.Tasks.Clear(); } } SelectedTasks.Clear(); foreach (var task in filtered) { if(categoryMap.TryGetValue(task.Status, out var categoryDict)) { if (categoryDict.TryGetValue(task.EmployeeCategory, out var employeeCategory)) { employeeCategory.Tasks.Add(task); if (task.Checked) { SelectedTasks.Add(task); } } } } } private IEnumerable CreateModels(IEnumerable subscribers) { foreach(var subscriber in subscribers) { var kanban = subscriber.Kanban; var model = new TaskModel { Title = kanban.Title, ID = kanban.ID, Description = kanban.Summary ?? "", EmployeeCategory = subscriber.Employee.ID, Status = kanban.Status, 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( kanban.DueDate, kanban.Completed) : subscriber.Employee.ID == kanban.ManagerLink.ID ? Color.Silver : Color.Plum; if (kanban.Locked) { colour = colour.MixColors(0.5F, Color.White); } model.Color = System.Windows.Media.Color.FromArgb(colour.A, colour.R, colour.G, colour.B); var notes = new List> { new() }; var kanbanNotes = kanban.Notes; if (kanbanNotes != null) { foreach (var line in kanbanNotes) { if (line == "===================================") { notes.Add(new()); } else { notes.Last().Add(line); } } } model.Notes = string.Join("\n===================================\n", notes.Reverse>().Select(x => string.Join('\n', x))); SetTaskModelAssignedTo(model, kanban, subscriber.Employee.ID); yield return model; } } 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; var managerString = kanban.ManagerLink.ID == Guid.Empty || kanban.ManagerLink.ID == subscriberID ? "" : " by " + kanban.ManagerLink.Name; model.AssignedTo = !string.IsNullOrEmpty(employeeString) || !managerString.IsNullOrWhiteSpace() ? $"Assigned{employeeString}{managerString}" : ""; } #endregion #region Kanban private readonly List SelectedTasks = new(); private List AllTasks { get; set; } = new(); private void DoEdit(TaskModel task) { var result = Host.EditReferences(new[] { task }); if (result) { Refresh(); } } private void EditTask_Executed(object sender, ExecutedRoutedEventArgs e) { if (e.Parameter is not TaskModel model) return; DoEdit(model); } private void OpenTaskMenu_Executed(object sender, ExecutedRoutedEventArgs e) { if (e.Parameter is not KanbanResources.OpenTaskMenuCommandArgs args) return; Host.PopulateMenu(this, args.Model, args.Menu); } private void SelectTask_Executed(object sender, ExecutedRoutedEventArgs e) { if (e.Parameter is not TaskModel model) return; if (!SelectedTasks.Remove(model)) { SelectedTasks.Add(model); } } private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e) { 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.Status != category.Status || 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.Status.Equals(category.Status) || x.EmployeeID != category.EmployeeID) && x.IsAssignee) .ToList(); if (!models.Any()) { return; } var changingCategory = models.Any(x => !x.Status.Equals(category.Status)); var completing = changingCategory && category.Status == KanbanStatus.Complete; var completed = DateTime.Now; if (completing) { if (!MessageWindow.ShowYesNo($"Are you sure you want to complete the selected tasks?", "Confirm Completion")) return; } var kanbans = Host.LoadKanbans(models, new Columns(x => x.ID, x => x.EmployeeLink.ID, x => x.Private, x => x.Number, x => x.Status)); 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.Status = category.Status; 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(x => x.Kanban.ID).InList(kanbanIDs)))); AllTasks.Sort((x, y) => x.DueDate.CompareTo(y.DueDate)); FilterKanbans(); } } #endregion #region ITaskControl public ITaskHost Host { get; set; } public KanbanViewType KanbanViewType => KanbanViewType.User; public bool IsReady { get; set; } public string SectionName => "Tasks By User"; public DataModel DataModel(Selection selection) { var ids = SelectedModels().Select(x => x.ID).ToArray(); return new AutoDataModel(new Filter(x => x.ID).InList(ids)); } public IEnumerable SelectedModels(TaskModel? sender = null) { if (sender is null) { return SelectedTasks; } else { var result = SelectedTasks.ToList(); if (!result.Contains(sender)) { result.Add(sender); } return result; } } #endregion #region Settings private void SaveSettings() { Host.KanbanSettings.UserSettings.AnchorWidth = SplitPanel.AnchorWidth; Host.KanbanSettings.UserSettings.TeamsHeight = SelectedTeams.ActualHeight; var teams = SelectedTeams.SelectedItems.Select(x => ((KeyValuePair)x).Key); Host.KanbanSettings.UserSettings.SelectedTeams = teams.ToArray(); var emps = SelectedEmployees.SelectedItems.Select(x => ((KeyValuePair)x).Key); emps = emps.Where(e => TeamEmployees.Any(t => t.Contains(e))); Host.KanbanSettings.UserSettings.SelectedEmployees = emps.ToArray(); Host.KanbanSettings.UserSettings.IncludeCompleted = IncludeCompleted.IsChecked == true; Host.KanbanSettings.UserSettings.IncludeObserved = IncludeObserved.IsChecked == true; Host.KanbanSettings.UserSettings.IncludeManaged = IncludeManaged.IsChecked == true; Host.SaveSettings(); } private void SplitPanel_OnChanged(object sender, DynamicSplitPanelSettings e) { if (!IsReady || Equals(Host.KanbanSettings.UserSettings.AnchorWidth, e.AnchorWidth)) return; SaveSettings(); } private void SelectedTeams_ItemChecked(object sender, ItemCheckedEventArgs e) { if (!IsReady) return; PopulateEmployees(); SaveSettings(); Refresh(); } private void SelectedTeams_SizeChanged(object sender, SizeChangedEventArgs e) { if (!IsReady || Equals(Host.KanbanSettings.UserSettings.TeamsHeight, SelectedTeams.ActualHeight)) return; SaveSettings(); } private void SelectedEmployees_ItemChecked(object sender, ItemCheckedEventArgs e) { if (!IsReady || bPopulating || sender != SelectedEmployees) return; SaveSettings(); Refresh(); } private void ViewType_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (!IsReady) return; Mode = ViewType.SelectedIndex switch { 0 => KanbanViewMode.Full, 1 => KanbanViewMode.Compact, _ => KanbanViewMode.Full }; if (IsReady) { Host.KanbanSettings.UserSettings.CompactView = Mode == KanbanViewMode.Compact; Host.SaveSettings(); } } #endregion 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(x => x.ID).InQuery(GetKanbanSubscriberFilter(), x => x.Kanban.ID), new Columns(form.Fields), LookupFactory.DefineSort() ); var employee = "Tasks for " + string.Join(',', Employees.Where(x => Host.KanbanSettings.UserSettings.SelectedEmployees.Contains(x.Key)).Select(x => x.Value.Name)); ExcelExporter.DoExport( export, string.Format( "{0} ({1:dd-MMM-yy})", employee, DateTime.Today ) ); } 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; } }