Explorar el Código

Added Meeting Templates and improved meeting screen somewhat

Kenric Nugteren hace 2 semanas
padre
commit
d105405858

+ 0 - 19
prs.classes/Entities/Meeting/MeetingTemplateAttendee.cs

@@ -1,19 +0,0 @@
-using System;
-using InABox.Core;
-
-namespace Comal.Classes
-{
-    public class MeetingTemplateAttendee: Entity, IRemotable, IPersistent, IManyToMany<MeetingTemplate,Employee>, ILicense<MeetingLicence>
-    {
-        [NullEditor]
-        [EntityRelationship(DeleteAction.Cascade)]
-        public MeetingTemplateLink Meeting { get; set; }
-        
-        [EditorSequence(1)]
-        [EntityRelationship(DeleteAction.Cascade)]
-        public EmployeeLink Employee { get; set; }
-        
-        [EditorSequence(2)]
-        public bool IsAdmin { get; set; } = true;
-    }
-}

+ 0 - 19
prs.classes/Entities/Meeting/MeetingTemplateItem.cs

@@ -1,19 +0,0 @@
-using InABox.Core;
-
-namespace Comal.Classes
-{
-    public class MeetingTemplateItem : Entity, IRemotable, IPersistent, IOneToMany<MeetingTemplate>, IMeetingItem, ILicense<MeetingLicence>
-    {
-        [NullEditor]
-        [EntityRelationship(DeleteAction.Cascade)]
-        public MeetingTemplateLink Template { get; set; }
-        
-        [TextBoxEditor]
-        [EditorSequence(1)]
-        public string Title { get; set; }
-        
-        [MemoEditor]
-        [EditorSequence(2)]
-        public string Notes { get; set; }
-    }
-}

+ 0 - 0
prs.classes/Entities/Meeting/MeetingTemplate.cs → prs.classes/Entities/Meeting/Template/MeetingTemplate.cs


+ 32 - 0
prs.classes/Entities/Meeting/Template/MeetingTemplateAttendee.cs

@@ -0,0 +1,32 @@
+using System;
+using InABox.Core;
+
+namespace Comal.Classes
+{
+    [EntitySecurity(
+        CanView = typeof(CanView<MeetingTemplate>),
+        CanEdit = typeof(CanEdit<MeetingTemplate>),
+        CanDelete = typeof(CanEdit<MeetingTemplate>))]
+    public class MeetingTemplateAttendee: Entity, IRemotable, IPersistent, IManyToMany<MeetingTemplate,Employee>, ILicense<MeetingLicence>
+    {
+        [NullEditor]
+        [EntityRelationship(DeleteAction.Cascade)]
+        public MeetingTemplateLink Template { get; set; }
+        
+        [EditorSequence(1)]
+        [EntityRelationship(DeleteAction.Cascade)]
+        public EmployeeLink Employee { get; set; }
+        
+        [EditorSequence(2)]
+        public bool IsAdmin { get; set; } = true;
+
+        static MeetingTemplateAttendee()
+        {
+            DefaultColumns.Add<MeetingTemplateAttendee>(x => x.Template.Title);
+            DefaultColumns.Add<MeetingTemplateAttendee>(x => x.Template.Description);
+            DefaultColumns.Add<MeetingTemplateAttendee>(x => x.Employee.Code);
+            DefaultColumns.Add<MeetingTemplateAttendee>(x => x.Employee.Name);
+            DefaultColumns.Add<MeetingTemplateAttendee>(x => x.IsAdmin);
+        }
+    }
+}

+ 41 - 0
prs.classes/Entities/Meeting/Template/MeetingTemplateItem.cs

@@ -0,0 +1,41 @@
+using InABox.Core;
+using System;
+
+namespace Comal.Classes
+{
+    [EntitySecurity(
+        CanView = typeof(CanView<MeetingTemplate>),
+        CanEdit = typeof(CanEdit<MeetingTemplate>),
+        CanDelete = typeof(CanEdit<MeetingTemplate>))]
+    public class MeetingTemplateItem : Entity, IRemotable, IPersistent, IOneToMany<MeetingTemplate>, IMeetingItem, ILicense<MeetingLicence>
+    {
+        [NullEditor]
+        [EntityRelationship(DeleteAction.Cascade)]
+        public MeetingTemplateLink Template { get; set; }
+        
+        [NullEditor]
+        [EntityRelationship(DeleteAction.Cascade)]
+        public MeetingTemplateItemLink Parent { get; set; }
+        
+        [TextBoxEditor]
+        [EditorSequence(1)]
+        public string Title { get; set; }
+        
+        [MemoEditor]
+        [EditorSequence(2)]
+        public string Notes { get; set; }
+
+        static MeetingTemplateItem()
+        {
+            DefaultColumns.Add<MeetingTemplateItem>(x => x.Template.Title);
+            DefaultColumns.Add<MeetingTemplateItem>(x => x.Template.Description);
+            DefaultColumns.Add<MeetingTemplateItem>(x => x.Title);
+            DefaultColumns.Add<MeetingTemplateItem>(x => x.Notes);
+        }
+    }
+
+    public class MeetingTemplateItemLink : EntityLink<MeetingTemplateItem>
+    {
+        public override Guid ID { get; set; }
+    }
+}

+ 0 - 0
prs.classes/Entities/Meeting/MeetingTemplateLink.cs → prs.classes/Entities/Meeting/Template/MeetingTemplateLink.cs


+ 18 - 23
prs.desktop/Panels/Meeting/MeetingAssignmentGrid.cs

@@ -17,6 +17,7 @@ namespace PRSDesktop
         public MeetingAssignmentGrid()
         {
         }
+
         protected override void DoReconfigure(DynamicGridOptions options)
         {
             base.DoReconfigure(options);
@@ -37,8 +38,8 @@ namespace PRSDesktop
         	Filters<Assignment> criteria, Columns<Assignment> columns, ref SortOrder<Assignment>? sort,
         	CancellationToken token, Action<CoreTable?, Exception?> action)
         {
-            var meetingfilter = (Model == null) || (Model.ID == Guid.Empty)
-                ? new Filter<Assignment>().None()
+            var meetingfilter = (Model is null) || (Model.ID == Guid.Empty)
+                ? Filter.None<Assignment>()
                 : Filter<Assignment>.Where(x => x.Meeting.Link.ID).IsEqualTo(Model.ID);
             criteria.Add(meetingfilter);
             base.Reload(criteria, columns, ref sort, token, action);
@@ -46,15 +47,13 @@ namespace PRSDesktop
 
         protected override bool CanCreateItems()
         {
-            return Model == null
-                ? false
-                : base.CanCreateItems();
+            return Model != null && base.CanCreateItems();
         }
 
         public override Assignment CreateItem()
         {
             var result = base.CreateItem();
-            result.Meeting.Link.ID = Model.ID;
+            result.Meeting.Link.ID = Model!.ID; // We know Model is not null because of CanCreateItems()
             return result;
         }
 
@@ -62,32 +61,28 @@ namespace PRSDesktop
         {
             var employeeids = Data.ExtractValues<Assignment, Guid>(x => x.Employee.ID).Distinct().ToArray();
 
-            var existingfilter = employeeids.Any() ? Filter<Employee>.Where(x => x.ID).NotInList(employeeids) : null;
-            var defaultfilter = LookupFactory.DefineFilter<Employee>();
-            var filter = existingfilter != null
-                ? existingfilter.And(defaultfilter)
-                : defaultfilter;
-            MultiSelectDialog<Employee> dlg = new MultiSelectDialog<Employee>(filter, LookupFactory.DefineColumns<Employee>(), true);
-            if (dlg.ShowDialog() == true)
+            var filter = Filter<Employee>.And(
+                LookupFactory.DefineFilter<Employee>(),
+                employeeids.Length != 0 ? Filter<Employee>.Where(x => x.ID).NotInList(employeeids) : null);
+            if (MultiSelectDialog.SelectItems(out var items, filter, LookupFactory.DefineColumns<Employee>()))
             {
-                List<Assignment> updates = new List<Assignment>();
-                foreach (var item in dlg.Items())
+                var updates = new List<Assignment>();
+                foreach (var item in items)
                 {
-                    Assignment assignment = new Assignment();
-                    assignment.Employee.ID = item.ID;
-                    assignment.Employee.Synchronise(item);
-                    assignment.Meeting.Link.ID = Model.ID;
+                    var assignment = new Assignment();
+                    assignment.Employee.CopyFrom(item);
+                    assignment.Meeting.Link.ID = Model!.ID; // We know Model is not null because of CanCreateItems()
                     assignment.Date = Model.Date;
                     assignment.Booked.Start = Model.Start;
                     assignment.Booked.Finish = Model.Finish;
                     assignment.Activity.ID = Model.ActivityID;
-                    assignment.Title = Model.Title;
-                    assignment.Description = Model.Description;
+                    assignment.Title = Model.Title ?? "";
+                    assignment.Description = Model.Description ?? "";
                     updates.Add(assignment);
                 }
 
-                if (updates.Any())
-                    new Client<Assignment>().Save(updates, "Added by Meeting Editor");
+                if (updates.Count != 0)
+                    Client.Save(updates, "Added by Meeting Editor");
                 Refresh(false, true);
             }
         }

+ 129 - 12
prs.desktop/Panels/Meeting/MeetingGrid.cs

@@ -1,25 +1,142 @@
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Windows;
+using System.Windows.Controls;
 using Comal.Classes;
+using InABox.Clients;
 using InABox.Core;
 using InABox.DynamicGrid;
+using InABox.WPF;
 
-namespace PRSDesktop
+namespace PRSDesktop;
+
+public class MeetingGrid : DynamicDataGrid<Meeting>
 {
-    
-    public class MeetingGrid : DynamicDataGrid<Meeting>
+    public List<MeetingTemplate> Templates { get; set; } = new();
+
+    public MeetingGrid()
+    {
+        Templates = Client.Query(
+            Filter.All<MeetingTemplate>(),
+            RequiredTemplateColumns())
+            .ToList<MeetingTemplate>();
+    }
+
+    public static Columns<MeetingTemplate> RequiredTemplateColumns()
+    {
+        return Columns.None<MeetingTemplate>()
+            .Add(x => x.Title)
+            .Add(x => x.Description)
+            .Add(x => x.Activity.ID);
+    }
+
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        base.DoReconfigure(options);
+        options.Clear();
+        options.AddRows = true;
+        options.DeleteRows = true;
+        options.EditRows = true;
+        options.FilterRows = true;
+        options.SelectColumns = true;
+    }
+
+    protected override void DoAdd(bool openEditorOnDirectEdit = false)
     {
-        protected override void DoReconfigure(DynamicGridOptions options)
+        if(Templates.Count > 0)
         {
-            base.DoReconfigure(options);
-            options.Clear();
-            options.AddRows = true;
-            options.DeleteRows = true;
-            options.EditRows = true;
-            options.FilterRows = true;
-            options.SelectColumns = true;
-        }
+            var menu = new ContextMenu();
+            menu.AddItem("New Meeting", null, () =>
+            {
+                base.DoAdd(openEditorOnDirectEdit);
+            });
+            menu.AddSeparator();
+            foreach(var template in Templates)
+            {
+                menu.AddItem(template.Title, null, () =>
+                {
+                    var meeting = CreateItem();
+                    meeting.Title = template.Title;
+                    meeting.Description = template.Description;
+                    meeting.Activity.CopyFrom(template.Activity);
 
+                    if (EditItems([meeting]))
+                    {
+                        var data = new MultiQuery()
+                            .Add(
+                                Filter<MeetingTemplateItem>.Where(x => x.Template.ID).IsEqualTo(template.ID),
+                                Columns.None<MeetingTemplateItem>()
+                                    .Add(x => x.ID)
+                                    .Add(x => x.Parent.ID)
+                                    .Add(x => x.Title)
+                                    .Add(x => x.Notes))
+                            .Add(
+                                Filter<MeetingTemplateAttendee>.Where(x => x.Template.ID).IsEqualTo(template.ID),
+                                Columns.None<MeetingTemplateAttendee>()
+                                    .Add(x => x.Employee.ID)
+                                    .Add(x => x.IsAdmin))
+                            .Query();
+
+                        var meetingItems = new List<MeetingItem>();
+
+                        var dictionary = new Dictionary<MeetingItem, MeetingTemplateItem>();
+                        var reverseDictionary = new Dictionary<MeetingTemplateItem, MeetingItem>();
+                        var templateDictionary = new Dictionary<Guid, MeetingTemplateItem>();
+                        foreach(var templateItem in data.GetObjects<MeetingTemplateItem>())
+                        {
+                            var meetingItem = new MeetingItem();
+                            meetingItem.Meeting.CopyFrom(meeting);
+                            meetingItem.Title = templateItem.Title;
+                            meetingItem.Notes = templateItem.Notes;
+                            meetingItems.Add(meetingItem);
+
+                            dictionary.Add(meetingItem, templateItem);
+                            reverseDictionary.Add(templateItem, meetingItem);
+                            templateDictionary.Add(templateItem.ID, templateItem);
+                        }
+
+                        Client.Save(meetingItems, "Created Meeting Items from Template");
+
+                        foreach(var meetingItem in meetingItems)
+                        {
+                            var templateItem = dictionary[meetingItem];
+                            if(templateItem.Parent.ID != Guid.Empty
+                                && templateDictionary.TryGetValue(templateItem.Parent.ID, out var parent))
+                            {
+                                meetingItem.Parent.CopyFrom(reverseDictionary[parent]);
+                            }
+                        }
+
+                        Client.Save(meetingItems, "Linked Meeting Item Parents");
+
+                        var assignments = new List<Assignment>();
+                        foreach(var attendee in data.GetObjects<MeetingTemplateAttendee>())
+                        {
+                            var assignment = new Assignment();
+                            assignment.Employee.CopyFrom(attendee.Employee);
+                            assignment.Meeting.Link.CopyFrom(meeting);
+                            assignment.Meeting.IsAdmin = attendee.IsAdmin;
+                            assignment.Date = meeting.Date;
+                            assignment.Booked.Start = meeting.Time.Start;
+                            assignment.Booked.Finish = meeting.Time.Finish;
+                            assignment.Activity.CopyFrom(meeting.Activity);
+                            assignment.Title = meeting.Title;
+                            assignment.Description = meeting.Description;
+                            assignments.Add(assignment);
+                        }
+                        Client.Save(assignments, "Created Meeting Assignments from Template");
+
+                        AddNewRow(meeting);
+                    }
+                });
+            }
+
+            menu.IsOpen = true;
+        }
+        else
+        {
+            base.DoAdd(openEditorOnDirectEdit);
+        }
     }
 }

+ 5 - 2
prs.desktop/Panels/Meeting/MeetingItemDocumentGrid.cs

@@ -26,6 +26,7 @@ namespace PRSDesktop
             base.DoReconfigure(options);
             options.Clear();
             options.AddRows = true;
+            options.EditRows = true;
             options.DeleteRows = true;
             options.RecordCount = true;
         }
@@ -63,13 +64,15 @@ namespace PRSDesktop
             base.Reload(criteria, columns, ref sort, token, action);
         }
 
-        private BitmapImage DocumentImage(CoreRow arg)
+        private BitmapImage DocumentImage(CoreRow? arg)
         {
             return PRSDesktop.Resources.view.AsBitmapImage();
         }
 
-        private bool ViewDocument(CoreRow row)
+        private bool ViewDocument(CoreRow? row)
         {
+            if (row is null) return false;
+
             var doc = row.ToObject<MeetingItemDocument>();
             var viewer = new DocumentEditor(doc);
             viewer.SaveAllowed = Security.IsAllowed<CanSaveFactoryFloorDrawings>();

+ 12 - 0
prs.desktop/Panels/Meeting/MeetingPanel.xaml.cs

@@ -68,6 +68,18 @@ public partial class MeetingPanel : UserControl, IPanel<Meeting>
     
     public void CreateToolbarButtons(IPanelHost host)
     {
+        host.CreateSetupActionIfCanView<MeetingTemplate>("Meeting Templates", PRSDesktop.Resources.employees,
+            action =>
+            {
+                var grid = DynamicGridUtils.CreateDynamicGrid<MeetingTemplate>(typeof(DynamicDataGrid<>));
+                foreach(var column in MeetingGrid.RequiredTemplateColumns())
+                {
+                    grid.HiddenColumns.Add(column);
+                }
+                var list = new MasterList(typeof(MeetingTemplate), dynamicGrid: grid);
+                list.ShowDialog();
+                MeetingGrid.Templates = grid.Data.ToList<MeetingTemplate>();
+            });
     }
 
     public Dictionary<string, object[]> Selected()

+ 16 - 1
prs.desktop/Panels/Meeting/MeetingTreeView.cs

@@ -41,7 +41,22 @@ public class MeetingItemTree : DynamicDataGrid<MeetingItem>
         return columns;
     }
 
-    public MeetingModel? Model { get; set; }
+    private MeetingModel? _model;
+    public MeetingModel? Model
+    {
+        get => _model;
+        set
+        {
+            _model = value;
+            Reconfigure();
+        }
+    }
+
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        base.DoReconfigure(options);
+        options.ReadOnly = Model is null || Model.ID == Guid.Empty;
+    }
     
     private void MeetingItemTree_OnContextMenuOpening(CoreTreeNode<Guid>? node, ContextMenu menu)
     {

+ 42 - 0
prs.desktop/Panels/Meeting/Template/MeetingTemplateAttendeeGrid.cs

@@ -0,0 +1,42 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.WPF;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Media.Imaging;
+
+namespace PRSDesktop.Panels.Meeting.Template;
+
+public class MeetingTemplateAttendeeGrid : DynamicManyToManyGrid<MeetingTemplateAttendee, MeetingTemplate>
+{
+    private readonly BitmapImage tick = PRSDesktop.Resources.tick.AsBitmapImage();
+
+    protected override void Init()
+    {
+        base.Init();
+
+        ActionColumns.Add(new DynamicTickColumn<MeetingTemplateAttendee, bool>(x => x.IsAdmin, tick, tick, null, Tick_Click));
+    }
+
+    private bool Tick_Click(CoreRow? row)
+    {
+        if (row is null) return false;
+        var item = LoadItem(row);
+        item.IsAdmin = !item.IsAdmin;
+        SaveItem(item);
+        DoChanged();
+        UpdateRow(row, item);
+        return false;
+    }
+
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var cols = base.GenerateColumns();
+        cols.RemoveAll(x => x.ColumnName.EqualsProperty<MeetingTemplateAttendee>(x => x.IsAdmin));
+        return cols;
+    }
+}

+ 163 - 0
prs.desktop/Panels/Meeting/Template/MeetingTemplateItemGrid.cs

@@ -0,0 +1,163 @@
+using Comal.Classes;
+using InABox.Clients;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.WPF;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+
+namespace PRSDesktop.Panels.Meeting.Template;
+
+internal class MeetingTemplateItemGrid : DynamicOneToManyGrid<MeetingTemplate, MeetingTemplateItem>
+{
+    private Dictionary<MeetingTemplateItem, Guid> _temporaryIDs = new();
+    private Dictionary<MeetingTemplateItem, Guid> _temporaryParentIDs = new();
+
+    public MeetingTemplateItemGrid()
+    {
+        HiddenColumns.Add(x => x.Parent.ID);
+    }
+
+    private Guid GetItemID(MeetingTemplateItem item)
+    {
+        if(item.ID == Guid.Empty)
+        {
+            return _temporaryIDs[item];
+        }
+        else
+        {
+            return item.ID;
+        }
+    }
+    private Guid GetItemID(CoreRow row) => GetItemID(LoadItem(row));
+
+    private Guid GetParentID(MeetingTemplateItem item)
+    {
+        if(item.Parent.ID == Guid.Empty)
+        {
+            return _temporaryParentIDs.GetValueOrDefault(item);
+        }
+        else
+        {
+            return item.Parent.ID;
+        }
+    }
+    private Guid GetParentID(CoreRow row) => GetParentID(LoadItem(row));
+
+    public override void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
+    {
+        _temporaryIDs.Clear();
+        _temporaryParentIDs.Clear();
+        base.Load(item, PageDataHandler);
+    }
+
+    public override void AfterSave(object item)
+    {
+        // Get rid of cascade deletes
+        var currentLookup = Items.ToDictionary(GetItemID);
+        while (true)
+        {
+            var newItems = new List<MeetingTemplateItem>(Items.Count);
+            foreach(var templateItem in Items)
+            {
+                var parent = GetParentID(templateItem);
+                if (parent != Guid.Empty
+                    && !currentLookup.ContainsKey(parent))
+                {
+                    currentLookup.Remove(GetItemID(templateItem));
+                }
+                else
+                {
+                    newItems.Add(templateItem);
+                }
+            }
+            if(newItems.Count < Items.Count)
+            {
+                Items.Clear();
+                Items.AddRange(newItems);
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        base.AfterSave(item);
+
+        var lookup = Items.ToDictionary(x => _temporaryIDs.TryGetValue(x, out var id) ? id : x.ID);
+
+        // Now, all our items have got IDs, so we now link them up and (unfortunately) resave them.
+        foreach(var (templateItem, parentID) in _temporaryParentIDs)
+        {
+            templateItem.Parent.ID = lookup.GetValueOrDefault(parentID)?.ID ?? Guid.Empty;
+        }
+
+        Client.Save(Items.Where(x => x.IsChanged()), "Linked items to parents");
+    }
+
+    protected override IDynamicGridUIComponent<MeetingTemplateItem> CreateUIComponent()
+    {
+        var component = new DynamicGridTreeUIComponent<MeetingTemplateItem, Guid>(
+            GetItemID,
+            GetParentID,
+            Guid.Empty,
+            (item, id) =>
+            {
+                if (_temporaryParentIDs.ContainsKey(item))
+                {
+                    _temporaryParentIDs[item] = id;
+                }
+                else
+                {
+                    item.Parent.ID = id;
+                }
+            })
+        {
+            Parent = this,
+            ShowHeader = false,
+            MaxRowHeight = 30,
+            ShowNumbers = true
+        };
+
+        component.OnContextMenuOpening += MeetingTemplateItemGrid_OnContextMenuOpening;
+
+        return component;
+    }
+
+    protected override DynamicGridColumns LoadColumns()
+    {
+        var columns = new DynamicGridColumns();
+        columns.Add<MeetingTemplateItem>(x => x.Title, 0, "Title", "", Alignment.MiddleLeft);
+        return columns;
+    }
+    
+    private void MeetingTemplateItemGrid_OnContextMenuOpening(CoreTreeNode<Guid>? node, ContextMenu menu)
+    {
+        if(node is not null && node.ID != CoreUtils.FullGuid && Options.AddRows)
+        {
+            menu.AddItem("Add Child Item", null, node, (n) =>
+            {
+                var item = CreateItem();
+                _temporaryParentIDs[item] = n.ID;
+                if (EditItems(new[] { item }))
+                {
+                    DoChanged();
+                    Refresh(false, true);
+                }
+            });
+        }
+    }
+
+    public override MeetingTemplateItem CreateItem()
+    {
+        var item = base.CreateItem();
+        item.Title = "New Agenda Item";
+        _temporaryIDs[item] = Guid.NewGuid();
+        return item;
+    }
+}