Преглед изворни кода

Added Multi-Employee Leave Request Approval System

frankvandenbos пре 3 месеци
родитељ
комит
5e929da817

+ 4 - 0
prs.classes/Entities/Employee/Employee.cs

@@ -94,6 +94,10 @@ namespace Comal.Classes
         [EditorSequence("Payroll",7)]
         public double HourlyRate { get; set; }
         
+        [EditorSequence("Payroll",8)]
+        [Caption("Leave Approval")]
+        public LeaveRequestApprovalSetLink LeaveRequestApprovalSet { get; set; }
+        
         [EditorSequence("Org Chart", 1)]
         public EmployeePositionLink Position { get; set; }
 

+ 2 - 0
prs.classes/Entities/Employee/EmployeeLink.cs

@@ -56,5 +56,7 @@ namespace Comal.Classes
         public DateTime RosterStart { get; set; }
 
         public string Email { get; set; }
+        
+        public LeaveRequestApprovalSetLink LeaveRequestApprovalSet { get; set; }
     }
 }

+ 60 - 0
prs.classes/Entities/LeaveRequest/Approvals/LeaveRequestApproval.cs

@@ -0,0 +1,60 @@
+using System;
+using InABox.Core;
+
+namespace Comal.Classes
+{
+    
+    public enum LeaveRequestApprovalStatus
+    {
+        Approved,
+        NotYetApproved,
+        Rejected
+    }
+    
+    public class LeaveRequestApproval : Entity, ISequenceable, IRemotable, IPersistent, ILicense<HumanResourcesLicense>
+    {
+        private class EmployeeLookup : LookupDefinitionGenerator<Employee, LeaveRequestApproval>
+        {
+            public override Filter<Employee>? DefineFilter(LeaveRequestApproval[] items)
+            {
+                return new Filter<Employee>(x => x.ID).NotInList(items.ToArray(x => x.Employee.ID));
+            }
+
+            public override Columns<LeaveRequestApproval> DefineFilterColumns()
+            {
+                return base.DefineFilterColumns().Add(x => x.Employee.ID);
+            }
+        }
+        [EditorSequence(1)]
+        [EntityRelationship(DeleteAction.Cascade)]
+        [LookupDefinition(typeof(EmployeeLookup))]
+        public EmployeeLink Employee { get; set; }
+
+        [NullEditor]
+        [EntityRelationship(DeleteAction.Cascade)]
+        public LeaveRequestLink LeaveRequest { get; set; }
+
+        [EditorSequence(2)] 
+        public LeaveRequestApprovalStatus Status { get; set; } = LeaveRequestApprovalStatus.NotYetApproved;
+        
+        [EditorSequence(3)]
+        public string Notes { get; set; }
+
+        [NullEditor]
+        public bool IsCustom { get; set; }
+
+        [NullEditor]
+        public long Sequence { get; set; }
+
+        static LeaveRequestApproval()
+        {
+            DefaultColumns.Add<LeaveRequestApproval>(x => x.LeaveRequest.EmployeeLink.Code);
+            DefaultColumns.Add<LeaveRequestApproval>(x => x.LeaveRequest.EmployeeLink.Name);
+            DefaultColumns.Add<LeaveRequestApproval>(x => x.Employee.Code);
+            DefaultColumns.Add<LeaveRequestApproval>(x => x.Employee.Name);
+            DefaultColumns.Add<LeaveRequestApproval>(x => x.Status);
+            DefaultColumns.Add<LeaveRequestApproval>(x => x.Notes);
+        }
+    }
+
+}

+ 59 - 0
prs.classes/Entities/LeaveRequest/Approvals/LeaveRequestApprovalSet.cs

@@ -0,0 +1,59 @@
+using System;
+using AutoProperties;
+using InABox.Core;
+
+namespace Comal.Classes
+{
+    public class LeaveRequestApprovalSet : Entity, IRemotable, IPersistent, ILicense<HumanResourcesLicense>
+    {
+        [UniqueCodeEditor]
+        [EditorSequence(1)]
+        public string Code { get; set; }
+
+        [EditorSequence(2)]
+        public string Description { get; set; }
+
+        [EditorSequence(3)]
+        [ScriptEditor]
+        public string ApprovalSetUpdateScript { get; set; }
+
+        [InterceptIgnore]
+        [DoNotPersist]
+        [DoNotSerialize]
+        public static string UpdateApprovalsMethodName => "UpdateLeaveApprovals";
+
+        [Editable(Editable.Hidden)]
+        public bool IsDefault { get; set; }
+
+        public static string DefaultApprovalSetUpdateScript()
+        {
+            return @"
+using Comal.Classes;
+
+public class Module
+{
+    public void " + UpdateApprovalsMethodName + @"(LeaveRequest leave, List<LeaveRequestApproval> approvals)
+    {
+        // Update the 'approvals' list whenever the 'bill' is updated.
+    }
+}";
+        }
+
+        static LeaveRequestApprovalSet()
+        {
+            DefaultColumns.Add<LeaveRequestApprovalSet>(x => x.Code);
+            DefaultColumns.Add<LeaveRequestApprovalSet>(x => x.Description);
+        }
+    }
+
+    public class LeaveRequestApprovalSetLink : EntityLink<LeaveRequestApprovalSet>
+    {
+        [RequiredColumn]
+        [LookupEditor(typeof(LeaveRequestApprovalSet))]
+        public override Guid ID { get; set; }
+
+        public string Code { get; set; }
+
+        public string Description { get; set; }
+    }
+}

+ 24 - 0
prs.classes/Entities/LeaveRequest/Approvals/LeaveRequestApprovalSetEmployee.cs

@@ -0,0 +1,24 @@
+using InABox.Core;
+
+namespace Comal.Classes
+{
+    public class LeaveRequestApprovalSetEmployee : Entity, IRemotable, IPersistent, IManyToMany<LeaveRequestApprovalSet, Employee>, ISequenceable, ILicense<HumanResourcesLicense>
+    {
+        [EntityRelationship(DeleteAction.Cascade)]
+        public LeaveRequestApprovalSetLink ApprovalSet { get; set; }
+
+        [EntityRelationship(DeleteAction.Cascade)]
+        public EmployeeLink Employee { get; set; }
+
+        [NullEditor]
+        public long Sequence { get; set; }
+
+        static LeaveRequestApprovalSetEmployee()
+        {
+            DefaultColumns.Add<BillApprovalSetEmployee>(x => x.ApprovalSet.Code);
+            DefaultColumns.Add<BillApprovalSetEmployee>(x => x.ApprovalSet.Description);
+            DefaultColumns.Add<BillApprovalSetEmployee>(x => x.Employee.Code);
+            DefaultColumns.Add<BillApprovalSetEmployee>(x => x.Employee.Name);
+        }
+    }
+}

+ 39 - 4
prs.classes/Entities/LeaveRequest/LeaveRequest.cs

@@ -13,7 +13,7 @@ namespace Comal.Classes
         Rejected
     }
 
-
+    
     public class LeaveRequestOpenFormCount : CoreAggregate<LeaveRequest, LeaveRequestForm, Guid>
     {
         public override Expression<Func<LeaveRequestForm, Guid>> Aggregate => x => x.ID;
@@ -78,16 +78,44 @@ namespace Comal.Classes
         [MemoEditor]
         [EditorSequence(8)]
         public string Notes { get; set; }
-
-        [LoggableProperty]
+        
         [EditorSequence(9)]
+        [Caption("Approval Set")]
+        public LeaveRequestApprovalSetLink ApprovalSet { get; set; }
+        
+        private class OutstandingApprovalsFormula : ComplexFormulaGenerator<LeaveRequest, string>
+        {
+            public override IComplexFormulaNode<LeaveRequest, string> GetFormula() =>
+                Aggregate(AggregateCalculation.Concat,
+                        x => x.Property(x => x.Employee.Code),
+                        filter: new Filter<LeaveRequestApproval>(x => x.Status).IsEqualTo(LeaveRequestApprovalStatus.NotYetApproved))
+                    .WithLink(x => x.LeaveRequest.ID, x => x.ID);
+        }
+        [ComplexFormula(typeof(OutstandingApprovalsFormula))]
+        [Editable(Editable.Hidden)]
+        public string OutstandingApprovals { get; set; }
+        
+        private class ApprovalStatusFormula : ComplexFormulaGenerator<LeaveRequest, LeaveRequestApprovalStatus>
+        {
+            public override IComplexFormulaNode<LeaveRequest, LeaveRequestApprovalStatus> GetFormula() =>
+                Aggregate<LeaveRequestApproval>(
+                    AggregateCalculation.Maximum, 
+                    x=>x.Property(p=>p.Status)
+                ).WithLink(x=>x.LeaveRequest.ID,x=>x.ID);
+        }
+        [Editable(Editable.Hidden)]
+        [ComplexFormula(typeof(ApprovalStatusFormula))]
+        public LeaveRequestApprovalStatus ApprovalStatus { get; set; }
+        
+        [LoggableProperty]
+        [EditorSequence(10)]
         [RequiredColumn]
         [EnumLookupEditor(typeof(LeaveRequestStatus))]
         public LeaveRequestStatus Status { get; set; } = LeaveRequestStatus.InProgress;
 
         [Caption("Response")]
         [MemoEditor]
-        [EditorSequence(10)]
+        [EditorSequence(11)]
         public string StatusNotes { get; set; }
         
         [EditorSequence("Internal Notes", 1)]
@@ -166,5 +194,12 @@ namespace Comal.Classes
         {
             return string.Format("{0}: {1} {2:dd MMM yy} - {3:dd MMM yy} {4}", EmployeeLink.Name, LeaveType.Description, From, To, Status);
         }
+
+        static LeaveRequest()
+        {
+            LinkedProperties.Register<LeaveRequest,LeaveRequestApprovalSetLink,Guid>(x=>x.EmployeeLink.LeaveRequestApprovalSet, x=>x.ID, x=>x.ApprovalSet.ID);
+            LinkedProperties.Register<LeaveRequest,LeaveRequestApprovalSetLink,string>(x=>x.EmployeeLink.LeaveRequestApprovalSet, x=>x.Code, x=>x.ApprovalSet.Code);
+            LinkedProperties.Register<LeaveRequest,LeaveRequestApprovalSetLink,string>(x=>x.EmployeeLink.LeaveRequestApprovalSet, x=>x.Description, x=>x.ApprovalSet.Description);
+        }
     }
 }

+ 5 - 0
prs.classes/SecurityDescriptors/HumanResources_Descriptors.cs

@@ -22,6 +22,11 @@ namespace Comal.Classes
     {
     }
 
+    [Caption("Manage Leave Requests")]
+    public class ManageLeaveRequestApprovals : EnabledSecurityDescriptor<HumanResourcesLicense, LeaveRequest>
+    {
+    }
+    
     [Caption("Edit Org Chart Structure")]
     public class CanEditOrgChart : EnabledSecurityDescriptor<HumanResourcesLicense, Employee>
     {

+ 131 - 0
prs.desktop/Grids/LeaveRequestGrid.cs

@@ -1,13 +1,16 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Media.Imaging;
 using Comal.Classes;
+using InABox.Clients;
 using InABox.Core;
 using InABox.DynamicGrid;
 using InABox.WPF;
+using Syncfusion.Linq;
 
 namespace PRSDesktop
 {
@@ -22,9 +25,137 @@ namespace PRSDesktop
             
             HiddenColumns.Add(x=>x.OpenForms);
             HiddenColumns.Add(x=>x.FormCount);
+            HiddenColumns.Add(x=>x.ApprovalSet.ID);
+            HiddenColumns.Add(x=>x.OutstandingApprovals);
             ActionColumns.Add(new DynamicImageColumn(FormImage, FormAction) { Position = DynamicActionColumnPosition.End, ToolTip = FormToolTip});
+            ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
         }
+
+        private Guid[]? availableApprovalSets = null;
         
+        private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
+        {
+            if (row is null) return;
+
+            var menu = column.GetMenu();
+            
+            availableApprovalSets ??= Client.Query(
+                new Filter<LeaveRequestApprovalSetEmployee>(x=>x.Employee.ID).IsEqualTo(App.EmployeeID),
+                Columns.None<LeaveRequestApprovalSetEmployee>().Add(x=>x.ApprovalSet.ID)
+            ).Rows
+                .Select(r =>r.Get<LeaveRequestApprovalSetEmployee,Guid>(c=>c.ApprovalSet.ID))
+                .Distinct()
+                .ToArray();
+
+            if (availableApprovalSets.Contains(row.Get<LeaveRequest, Guid>(x => x.ApprovalSet.ID)))
+            {
+                var approvals = row.Get<LeaveRequest, string>(x => x.OutstandingApprovals).Split(',');
+                if (approvals.Contains(App.EmployeeCode))
+                {
+                    menu.AddItem("Approve", null, () => UpdateLeaveRequest(row,LeaveRequestApprovalStatus.Approved));
+                    menu.AddItem("Reject", null, () => UpdateLeaveRequest(row,LeaveRequestApprovalStatus.Rejected, true));
+                }
+                else
+                    menu.AddItem("Reset", null, () => UpdateLeaveRequest(row,LeaveRequestApprovalStatus.NotYetApproved));
+            }
+            
+            if (Security.IsAllowed<ManageLeaveRequestApprovals>())
+            {
+                if (menu.Items.Count > 0)
+                    menu.AddSeparator();
+                
+                var approvalSetItem = menu.AddItem("Approval Set", null, null);
+                approvalSetItem.AddItem("Loading...", null, null, enabled: false);
+                
+                menu.AddItem("Manage Approvals", null, row, ManageApprovals_Click);
+                
+                var id = row.Get<LeaveRequest, Guid>(x => x.ID);
+                var selectedApproval = row.Get<LeaveRequest, Guid>(x => x.ApprovalSet.ID);
+                Task.Run(() =>
+                {
+                    return Client.Query<LeaveRequestApprovalSet>(
+                            null,
+                            Columns.None<LeaveRequestApprovalSet>().Add(x => x.ID).Add(x => x.Code).Add(x => x.Description))
+                        .ToArray<LeaveRequestApprovalSet>();
+                }).ContinueWith(approvals =>
+                {
+                    approvalSetItem.Items.Clear();
+                    foreach(var approval in approvals.Result)
+                    {
+                        var item = approvalSetItem.AddItem($"{approval.Code}: {approval.Description}", null, (row, approval), ApprovalSet_Click);
+                        item.IsChecked = approval.ID == selectedApproval;
+                    }
+
+                    if (row.Get<LeaveRequest, Guid>(x => x.ApprovalSet.ID) != Guid.Empty)
+                    {
+                        if (approvalSetItem.Items.Count > 0)
+                            approvalSetItem.AddSeparator();
+                        approvalSetItem.AddItem("Clear Approval Set", null, row, ApprovalSetClear_Click);
+                    }
+                }, TaskScheduler.FromCurrentSynchronizationContext());
+            }
+        }
+
+        private void UpdateLeaveRequest(CoreRow row, LeaveRequestApprovalStatus status, bool requireNotes = false)
+        {
+            var note = "";
+            if (requireNotes)
+            {
+                if (!TextBoxDialog.Execute("Reason for Rejection", ref note, false))
+                    return;
+            }
+            
+            var requestid = row.Get<LeaveRequest, Guid>(x => x.ID);
+            var approval = Client.Query(
+                new Filter<LeaveRequestApproval>(x => x.LeaveRequest.ID).IsEqualTo(requestid)
+                    .And(x => x.Employee.ID).IsEqualTo(App.EmployeeID),
+                Columns.None<LeaveRequestApproval>().Add(x=>x.ID).Add(x=>x.Status)
+            ).Rows
+                .FirstOrDefault()?
+                .ToObject<LeaveRequestApproval>();
+            if ((approval != null) && (approval.Status != status))
+            {
+                approval.Status = status;
+                approval.Notes = note;
+                Client.Save(approval,$"Status Updated to {status}");
+                Refresh(false, true);
+            }
+        }
+
+        private void ApprovalSetClear_Click(CoreRow row)
+        {
+            var bill = row.ToObject<LeaveRequest>();
+            bill.ApprovalSet.ID = Guid.Empty;
+            SaveItem(bill);
+            Refresh(false, true);
+        }
+
+        private void ManageApprovals_Click(CoreRow row)
+        {
+            var request = row.ToObject<LeaveRequest>();
+
+            var grid = new LeaveRequestApprovalGrid(request);
+            var dlg = new DynamicContentDialog(grid, buttonsVisible: false)
+            {
+                Title = "Manage Approvals"
+            };
+            grid.Refresh(true, true);
+            dlg.ShowDialog();
+
+            request = LoadItem(row);
+            UpdateRow(row, request);
+
+            DoChanged();
+        }
+
+        private void ApprovalSet_Click((CoreRow row, LeaveRequestApprovalSet approvalSet) item)
+        {
+            var request = item.row.ToObject<LeaveRequest>();
+            if (request.ApprovalSet.ID == item.approvalSet.ID) return;
+            request.ApprovalSet.CopyFrom(item.approvalSet);
+            SaveItem(request);
+            Refresh(false, true);
+        }
         
         private readonly BitmapImage _openforms = PRSDesktop.Resources.warning.AsBitmapImage();
         private readonly BitmapImage _completedforms = PRSDesktop.Resources.contract.AsBitmapImage();

+ 1 - 0
prs.desktop/Panels/LeaveRequests/LeaveRequestPanel.xaml.cs

@@ -30,6 +30,7 @@ public partial class LeaveRequestPanel : UserControl, IPanel<LeaveRequest>
         HumanResourcesSetupActions.EmployeeActivities(host);
         host.CreateSetupSeparator();
         HumanResourcesSetupActions.EmployeeStandardLeave(host);
+        HumanResourcesSetupActions.LeaveRequestApprovals(host);
     }
 
     public string SectionName => "Leave Requests";

+ 178 - 0
prs.desktop/Panels/Suppliers/Bills/LeaveRequestApprovalGrid.cs

@@ -0,0 +1,178 @@
+using System;
+using System.Data;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using Comal.Classes;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.WPF;
+
+namespace PRSDesktop;
+
+public class LeaveRequestApprovalGrid : DynamicDataGrid<LeaveRequestApproval>, ISpecificGrid
+{
+    private LeaveRequest Request;
+
+    public LeaveRequestApprovalGrid(LeaveRequest request)
+    {
+        Request = request;
+    }
+
+    private Button ApproveButton = null!;
+    private Button RejectButton = null!;
+    private Button ClearButton = null!;
+
+    protected override void Init()
+    {
+        base.Init();
+
+        HiddenColumns.Add(x => x.Employee.ID);
+        HiddenColumns.Add(x => x.IsCustom);
+
+        foreach (var column in LookupFactory.DefineLookupFilterColumns<LeaveRequestApproval, Employee, EmployeeLink>(
+                     x => x.Employee))
+        {
+            HiddenColumns.Add(column);
+        }
+        
+        ApproveButton = AddButton("Approve", null, (b,r) => UpdateStatus(r,LeaveRequestApprovalStatus.Approved));
+        ApproveButton.Visibility = Visibility.Collapsed;
+        RejectButton = AddButton("Reject", null, (b,r) => UpdateStatus(r,LeaveRequestApprovalStatus.Rejected, true));
+        RejectButton.Visibility = Visibility.Collapsed;
+        ClearButton = AddButton("Clear", null, (b,r) => UpdateStatus(r,LeaveRequestApprovalStatus.NotYetApproved));
+        ClearButton.Visibility = Visibility.Collapsed;
+    }
+
+    public bool UpdateStatus(CoreRow[] rows, LeaveRequestApprovalStatus status, bool requireNotes = false)
+    {
+        var note = "";
+        if (requireNotes)
+        {
+            if (!TextBoxDialog.Execute("Reason for Rejection", ref note, false))
+                return false;
+        }
+        
+        var items = LoadItems(rows);
+        
+        foreach (var item in items)
+        {
+            item.Status = status;
+            item.Notes = note;
+        }
+
+        SaveItems(items);
+        DoChanged();
+        return true;
+    }
+    
+    protected override void SelectItems(CoreRow[]? rows)
+    {
+        base.SelectItems(rows);
+
+        if (rows is null || rows.Length == 0 || !Security.IsAllowed<ManageLeaveRequestApprovals>())
+        {
+            ApproveButton.Visibility = Visibility.Collapsed;
+            RejectButton.Visibility = Visibility.Collapsed;
+            ClearButton.Visibility = Visibility.Collapsed;
+        }
+        else
+        {
+            ApproveButton.Visibility = rows.Any(r => r.Get<LeaveRequestApproval, LeaveRequestApprovalStatus>(x => x.Status) != LeaveRequestApprovalStatus.Approved)
+                ? Visibility.Visible 
+                : Visibility.Collapsed;
+            RejectButton.Visibility = rows.Any(r => r.Get<LeaveRequestApproval, LeaveRequestApprovalStatus>(x => x.Status) != LeaveRequestApprovalStatus.Rejected)
+                ? Visibility.Visible 
+                : Visibility.Collapsed;
+            ClearButton.Visibility = rows.Any(r => r.Get<LeaveRequestApproval, LeaveRequestApprovalStatus>(x => x.Status) != LeaveRequestApprovalStatus.NotYetApproved)
+                ? Visibility.Visible 
+                : Visibility.Collapsed;
+        }
+    }
+
+    #region UI Component
+
+    private class UIComponent : DynamicGridGridUIComponent<LeaveRequestApproval>
+    {
+        protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
+        {
+            if (row.Get<LeaveRequestApproval, bool>(x => x.IsCustom))
+                return null;
+            return Colors.Gainsboro.ToBrush(0.5);
+        }
+    }
+
+    protected override IDynamicGridUIComponent<LeaveRequestApproval> CreateUIComponent()
+    {
+        return new UIComponent { Parent = this };
+    }
+
+    #endregion
+
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        base.DoReconfigure(options);
+        options.AddRows = Security.IsAllowed<ManageLeaveRequestApprovals>();
+        options.EditRows = Security.IsAllowed<ManageLeaveRequestApprovals>();
+        options.DeleteRows = Security.IsAllowed<ManageLeaveRequestApprovals>();
+    }
+
+    public override LeaveRequestApproval CreateItem()
+    {
+        var item = base.CreateItem();
+        item.LeaveRequest.CopyFrom(Request);
+        item.IsCustom = true;
+        return item;
+    }
+
+    protected override void Reload(Filters<LeaveRequestApproval> criteria, Columns<LeaveRequestApproval> columns,
+        ref SortOrder<LeaveRequestApproval>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        criteria.Add(new Filter<LeaveRequestApproval>(x => x.LeaveRequest.ID).IsEqualTo(Request.ID));
+        base.Reload(criteria, columns, ref sort, token, action);
+    }
+
+    protected override void DoEdit()
+    {
+        if (SelectedRows.Any(x => !x.Get<BillApproval, bool>(x => x.IsCustom)))
+            return;
+        base.DoEdit();
+    }
+
+    protected override void DoDelete()
+    {
+        if (SelectedRows.Any(x => !x.Get<BillApproval, bool>(x => x.IsCustom)))
+            return;
+        base.DoDelete();
+    }
+
+    protected override void DoAdd(bool openEditorOnDirectEdit = false)
+    {
+        var columns = LookupFactory.DefineLookupColumns<LeaveRequestApproval, Employee, EmployeeLink>(x => x.Employee);
+
+        var employeeColumn = new Column<LeaveRequestApproval>(x => x.Employee);
+        foreach (var column in DataColumns())
+        {
+            if (employeeColumn.IsParentOf(column.Property))
+            {
+                columns.Add(column.Property[(employeeColumn.Property.Length + 1)..]);
+            }
+        }
+
+        if (MultiSelectDialog.SelectItem(out var employee,
+                LookupFactory.DefineLookupFilter<LeaveRequestApproval, Employee, EmployeeLink>(x => x.Employee,
+                    Data.ToArray<LeaveRequestApproval>()),
+                columns,
+                "Select Leave Request Approval Employee:"))
+        {
+            CreateItems(() =>
+            {
+                var approval = CreateItem();
+                approval.Employee.CopyFrom(employee);
+                return CoreUtils.One(approval);
+            });
+        }
+    }
+}

+ 9 - 0
prs.desktop/Setups/HumanResourcesSetupActions.cs

@@ -94,6 +94,15 @@ public static class HumanResourcesSetupActions
             list.ShowDialog();
         });
     }
+    
+    public static void LeaveRequestApprovals(IPanelHost host)
+    {
+        host.CreateSetupActionIfCanView<StandardLeave>("Leave Request Approvals", PRSDesktop.Resources.leave, (action) =>
+        {
+            var list = new MasterList(typeof(LeaveRequestApprovalSet));
+            list.ShowDialog();
+        });
+    }
 
     public static void EmployeeStandardLeave(IPanelHost host)
     {

+ 65 - 0
prs.stores/LeaveRequestApprovalSetStore.cs

@@ -0,0 +1,65 @@
+using Comal.Classes;
+using Comal.Stores;
+using InABox.Core;
+using InABox.Database;
+using InABox.Scripting;
+
+namespace PRSStores;
+
+public class LeaveRequestApprovalSetStore : BaseStore<LeaveRequestApprovalSet>
+{
+    private static Dictionary<Guid, ScriptDocument?> _scripts = new();
+
+    protected override void BeforeSave(LeaveRequestApprovalSet entity)
+    {
+        base.BeforeSave(entity);
+
+        if (entity.HasOriginalValue(x => x.ApprovalSetUpdateScript))
+        {
+            UpdateApprovalSetScript(entity.ID, entity.ApprovalSetUpdateScript);
+        }
+    }
+
+    private static ScriptDocument? UpdateApprovalSetScript(Guid approvalSetID, string script)
+    {
+        ScriptDocument? document = null;
+        if (!script.IsNullOrWhiteSpace())
+        {
+            document = new ScriptDocument(script);
+            if (!document.Compile())
+            {
+                InABox.Core.Logger.Send(LogType.Error, "",
+                    $"Leave Request Approval Script failed to compile: {document.Result}");
+                document = null;
+            }
+        }
+
+        lock (_scripts)
+        {
+            _scripts[approvalSetID] = document;
+        }
+
+        return document;
+    }
+
+    public static ScriptDocument? GetUpdateApprovalsScript(Guid approvalSetID)
+    {
+        lock (_scripts)
+        {
+            if (_scripts.TryGetValue(approvalSetID, out var script)) return script;
+        }
+
+        var set = DbFactory.NewProvider(Logger.Main).Query<LeaveRequestApprovalSet>(
+                new Filter<LeaveRequestApprovalSet>(x => x.ID).IsEqualTo(approvalSetID),
+                Columns.None<LeaveRequestApprovalSet>().Add(x => x.ApprovalSetUpdateScript))
+            .ToObjects<LeaveRequestApprovalSet>().FirstOrDefault();
+        if (set is not null)
+        {
+            return UpdateApprovalSetScript(approvalSetID, set.ApprovalSetUpdateScript);
+        }
+        else
+        {
+            return null;
+        }
+    }
+}

+ 108 - 1
prs.stores/LeaveRequestStore.cs

@@ -3,9 +3,11 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using Comal.Classes;
+using Comal.Stores;
 using InABox.Core;
+using InABox.Scripting;
 
-namespace Comal.Stores
+namespace PRSStores
 {
     internal class LeaveRequestStore : BaseStore<LeaveRequest>
     {
@@ -13,6 +15,7 @@ namespace Comal.Stores
         protected override void OnSave(LeaveRequest entity, ref string auditnote)
         {
             base.OnSave(entity, ref auditnote);
+            
             if (entity.HasOriginalValue(c => c.Status))
                 NotifyEmployee(
                     entity,
@@ -34,5 +37,109 @@ namespace Comal.Stores
                 );
         }
 
+        protected override void AfterSave(LeaveRequest entity)
+        {
+            base.AfterSave(entity);
+            if (entity.HasOriginalValue(c => c.EmployeeLink.ID) || entity.HasOriginalValue(c=>c.ApprovalSet.ID))
+                UpdateLeaveRequestApprovals(entity);
+        }
+
+        private void ClearLeaveRequestApprovals(LeaveRequest request)
+        {
+            // We need to clear all automatic bill approvals.
+            var approvals = Provider.Query(
+                    new Filter<LeaveRequestApproval>(x => x.LeaveRequest.ID).IsEqualTo(request.ID)
+                        .And(x => x.IsCustom).IsEqualTo(false),
+                    Columns.Required<LeaveRequestApproval>().Add(x => x.ID))
+                .ToList<LeaveRequestApproval>();
+            Provider.Delete(approvals, UserID);
+        }
+        
+        private void UpdateLeaveRequestApprovals(LeaveRequest request)
+        {
+
+            if (request.ID == Guid.Empty) return;
+
+            if(request.ApprovalSet.ID == Guid.Empty)
+            {
+                ClearLeaveRequestApprovals(request);
+                return;
+            }
+
+            var setEmployees = Provider.Query(
+                new Filter<LeaveRequestApprovalSetEmployee>(x => x.ApprovalSet.ID).IsEqualTo(request.ApprovalSet.ID),
+                Columns.None<LeaveRequestApprovalSetEmployee>().Add(x => x.Employee.ID),
+                new SortOrder<LeaveRequestApprovalSetEmployee>(x => x.Sequence))
+                .ToArray<LeaveRequestApprovalSetEmployee>();
+            var approvals = Provider.Query(
+                new Filter<LeaveRequestApproval>(x => x.LeaveRequest.ID).IsEqualTo(request.ID),
+                Columns.Required<LeaveRequestApproval>().Add(x => x.Employee.ID).Add(x => x.IsCustom).Add(x => x.Status))
+                .ToList<LeaveRequestApproval>();
+
+            try
+            {
+                var customApprovals = new List<LeaveRequestApproval>();
+                foreach(var approval in approvals)
+                {
+                    if (approval.IsCustom)
+                    {
+                        customApprovals.Add(approval);
+                    }
+                    else
+                    {
+                        if(approval.Status != LeaveRequestApprovalStatus.NotYetApproved)
+                        {
+                            // These really are custom approvals for our purposes, since they've been modified.
+                            customApprovals.Add(approval);
+                    }
+                        }
+                }
+
+                // Build up the automatic list.
+                var autoApprovals = new List<LeaveRequestApproval>();
+                foreach(var employee in setEmployees)
+                {
+                    var approval = new LeaveRequestApproval();
+                    approval.Employee.CopyFrom(employee.Employee);
+                    approval.IsCustom = false;
+                    autoApprovals.Add(approval);
+                }
+                if(LeaveRequestApprovalSetStore.GetUpdateApprovalsScript(request.ApprovalSet.ID) is ScriptDocument script)
+                {
+                    script.Execute(methodname: LeaveRequestApprovalSet.UpdateApprovalsMethodName, parameters: [request, autoApprovals]);
+                }
+
+                // Now, we have to merge the custom and auto lists, only saving auto approvals if there are no custom ones for that employee.
+                var toAdd = new List<LeaveRequestApproval>();
+                var toRemove = new List<LeaveRequestApproval>();
+
+                var newList = customApprovals.ToList();
+                foreach(var approval in autoApprovals)
+                {
+                    newList.Add(approval);
+                    if (!approvals.Any(x => x.Employee.ID == approval.Employee.ID))
+                    {
+                        toAdd.Add(approval);
+                    }
+                }
+                foreach(var approval in approvals)
+                {
+                    if (!newList.Any(x => x.Employee.ID == approval.Employee.ID))
+                    {
+                        toRemove.Add(approval);
+                    }
+                }
+                foreach(var approval in toAdd)
+                {
+                    approval.LeaveRequest.ID = request.ID;
+                }
+                Provider.Save(toAdd);
+                Provider.Delete(toRemove, UserID);
+            }
+            catch(Exception e)
+            {
+                CoreUtils.LogException(UserID, e, "Error running leave request approval script");
+            }
+        }
     }
 }