using System; using System.Collections.Generic; using System.Data; using System.Drawing; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using Comal.Classes; using InABox.Clients; using InABox.Configuration; using InABox.Core; using InABox.DynamicGrid; using InABox.WPF; using NPOI.SS.Formula.Functions; using PRSDesktop.WidgetGroups; using System.ComponentModel; using Syncfusion.UI.Xaml.Grid; using Syncfusion.Windows.Tools.Controls; using SelectionChangedEventArgs = System.Windows.Controls.SelectionChangedEventArgs; using PRS.Shared; using Columns = InABox.Core.Columns; using InABox.Wpf; using Syncfusion.Data.Extensions; using NPOI.OpenXmlFormats.Spreadsheet; namespace PRSDesktop; public enum JobPlannerDisplayMode { JobColumns, DateColumns } public class JobResourcePlannerProperties : IUserConfigurationSettings, IDashboardProperties { public JobSelectorSettings JobSettings { get; set; } public JobSelectorData JobSelection { get; set; } public Guid ActivityType { get; set; } public int MonthsToView { get; set; } public TeamSelectorSettings TeamSettings { get; set; } public TeamSelectorData TeamSelection { get; set; } public double SplitterPosition { get; set; } public double HoursPerDay { get; set; } public double EmployeeSplitterPosition { get; set; } public bool IncludeUnApprovedLeave { get; set; } public JobPlannerDisplayMode DisplayMode { get; set; } public JobResourcePlannerProperties() { JobSettings = new JobSelectorSettings(); JobSelection = new JobSelectorData(); TeamSettings = new TeamSelectorSettings(); TeamSelection = new TeamSelectorData(); MonthsToView = 1; SplitterPosition = 0F; EmployeeSplitterPosition = 0F; HoursPerDay = 8.5; ActivityType = Guid.Empty; IncludeUnApprovedLeave = false; DisplayMode = JobPlannerDisplayMode.DateColumns; } } public partial class JobResourcePlanner : UserControl { private enum Suppress { This } private ActivityModel[] _activities = new ActivityModel[] { }; private LeaveRequestModel[] _leaverequests = new LeaveRequestModel[] { }; private StandardLeaveModel[] _standardleaves = new StandardLeaveModel[] { }; private JobModel[] _jobs = new JobModel[] { }; private EmployeeResourceModel[] _emps = new EmployeeResourceModel[] { }; private List _assignments = new List(); private DateTime[] _dates = Array.Empty(); private double[] _totals = Array.Empty(); private double[] _available = Array.Empty(); public JobResourcePlannerProperties Properties { get; set; } public event LoadSettings? LoadSettings; public event SaveSettings? SaveSettings; private void DoLoadSettings() { Properties = LoadSettings?.Invoke(this) ?? new JobResourcePlannerProperties(); } private void DoSaveSettings() { SaveSettings?.Invoke(this, Properties); } public JobResourcePlanner() { using (new EventSuppressor(Suppress.This)) InitializeComponent(); } public void Setup() { using (new EventSuppressor(Suppress.This)) { DoLoadSettings(); JobSelector.Setup(); JobSelector.Settings = Properties.JobSettings; JobSelector.Selection = Properties.JobSelection; _jobs = JobSelector.GetJobData(r => new JobModel(r)); TeamSelector.Setup(); TeamSelector.Settings = Properties.TeamSettings; TeamSelector.Selection = Properties.TeamSelection; _emps = TeamSelector.GetEmployeeData((row, rosters) => new EmployeeResourceModel(row, rosters)); ViewWindow.ItemsSource = new Dictionary() { { 1, "1 Month" }, { 3, "3 Months" }, { 6, "6 Months" }, { 12, "12 months" } }; ViewWindow.SelectedValue = Properties.MonthsToView; if (Properties.SplitterPosition != 0F) TeamSelectorRow.Height = new GridLength(Properties.SplitterPosition, GridUnitType.Pixel); if (Properties.EmployeeSplitterPosition != 0F) AvailableEmployeesRow.Height = new GridLength(Properties.SplitterPosition, GridUnitType.Pixel); HoursSelector.Text = $"{Properties.HoursPerDay:F2}"; LeaveType.SelectedIndex = Properties.IncludeUnApprovedLeave ? 1 : 0; Orientation.SelectedIndex = Properties.DisplayMode == JobPlannerDisplayMode.JobColumns ? 1 : 0; AvailableEmployees.Refresh(true, false); AssignedEmployees.Refresh(true, false); MultiQuery query = new MultiQuery(); query.Add( LookupFactory.DefineFilter(), StandardLeaveModel.Columns ); query.Add( new Filter(x => x.Status).IsNotEqualTo(LeaveRequestStatus.Rejected), LeaveRequestModel.Columns ); query.Add( LookupFactory.DefineFilter(), Columns.None().Add(x => x.ID).Add(x => x.Code).Add(x => x.Description), new SortOrder(x => x.Code) ); query.Query(); _standardleaves = query.Get().Rows.Select(r=>new StandardLeaveModel(r)).ToArray(); _leaverequests = query.Get().Rows.Select(r => new LeaveRequestModel(r)).ToArray(); _activities = query.Get().Rows.Select(r => new ActivityModel(r)).ToArray(); ActivityType.ItemsSource = _activities; ActivityType.SelectedValue = Properties.ActivityType; } } public void Shutdown(CancelEventArgs? cancel) { } private bool GetStandardLeaveTimes(DateTime date, out TimeSpan start, out TimeSpan finish) { bool result = false; start = TimeSpan.Zero; finish = TimeSpan.FromDays(1); var requests = _standardleaves.Where(x => (x.From <= date) && (x.To.Add(x.ToTime) > date) ); if (requests.Any()) { result = true; start = TimeSpan.FromDays(1); finish = TimeSpan.Zero; foreach (var leave in requests) { var curstart = leave.From == date ? leave.FromTime : TimeSpan.Zero; start = start > curstart ? curstart : start; var curfinish = leave.To == date ? leave.ToTime : TimeSpan.FromDays(1); finish = finish < curfinish ? curfinish : finish; } } return result; } private void AdjustStandardLeave(DateTime date, ref TimeSpan time) { TimeSpan result = TimeSpan.Zero; var leaves = _standardleaves.Where(x => (x.From <= date) && (x.To.Add(x.ToTime) > date) ); foreach (var leave in leaves) { result += (leave.To == date ? leave.ToTime : TimeSpan.FromDays(1)) - (leave.From == date ? leave.FromTime : TimeSpan.Zero); } time = time >= result ? time - result : TimeSpan.Zero; } private bool GetLeaveRequestTimes(DateTime date, EmployeeResourceModel emp, out TimeSpan start, out TimeSpan finish) { bool result = false; start = TimeSpan.Zero; finish = TimeSpan.FromDays(1); var requests = _leaverequests.Where(x => (x.From <= date) && (x.To.Add(x.ToTime) > date) && (x.EmployeeID == emp.ID) && (Properties.IncludeUnApprovedLeave ? true : x.Status == LeaveRequestStatus.Approved) ); if (requests.Any()) { result = true; start = TimeSpan.FromDays(1); finish = TimeSpan.Zero; foreach (var leave in requests) { var curstart = leave.From == date ? leave.FromTime : TimeSpan.Zero; start = start > curstart ? curstart : start; var curfinish = leave.To == date ? leave.ToTime : TimeSpan.FromDays(1); finish = finish < curfinish ? curfinish : finish; } } return result; } private void AdjustLeaveRequests(DateTime date, EmployeeResourceModel emp, ref TimeSpan time) { TimeSpan result = TimeSpan.Zero; var requests = _leaverequests.Where(x => (x.From <= date) && (x.To.Add(x.ToTime) > date) && (x.EmployeeID == emp.ID) && (Properties.IncludeUnApprovedLeave ? true : x.Status == LeaveRequestStatus.Approved) ); foreach (var leave in requests) { result += (leave.To == date ? leave.ToTime : TimeSpan.FromDays(1)) - (leave.From == date ? leave.FromTime : TimeSpan.Zero); } time = time >= result ? time - result : TimeSpan.Zero; } public void Refresh() { using (new WaitCursor()) { var jobids = _jobs.Select(x => x.ID).ToArray(); var empids = _emps.Select(x => x.ID).ToArray(); var actids = _activities.Select(x => x.ID).ToArray(); DateTime fromDate = DateTime.Today; DateTime toDate = DateTime.Today.AddMonths(Properties.MonthsToView); MultiQuery query = new MultiQuery(); query.Add( new Filter(x=>x.EmployeeLink.ID).InList(empids) .And(x=>x.Date).IsGreaterThanOrEqualTo(fromDate) .And(x=>x.Date).IsLessThanOrEqualTo(toDate), AssignmentModel.Columns ); query.Query(); _assignments = query.Get().Rows.Select(r => new AssignmentModel(r)).ToList(); var data = new DataTable(); var dates = new List(); for (var curdate = fromDate; curdate <= toDate; curdate = curdate.AddDays(1)) { dates.Add(curdate); } _dates = dates.ToArray(); var dataValues = new object?[dates.Count, _jobs.Length]; var available = new double[dates.Count]; var totals = new double[dates.Count]; foreach (var (dateIdx, date) in dates.WithIndex()) { double avail = 0.0F; foreach (var emp in _emps) { var roster = RosterUtils.GetRoster(emp.Roster, emp.RosterStart, date); var hours = roster.GetBlocks(date, TimeSpan.MinValue, TimeSpan.MaxValue) .Aggregate(TimeSpan.Zero, (value, block) => value + block.Duration); AdjustStandardLeave(date, ref hours); AdjustLeaveRequests(date, emp, ref hours); avail += hours.TotalHours; } var total = avail; var values = new List { date }; var anyjobstoday = _assignments.Where(x => (x.Date.Date == date.Date)); avail -= anyjobstoday.Aggregate(0F, (value, model) => value + model.BookedDuration.TotalHours); foreach (var (jobIdx, job) in _jobs.WithIndex()) { var thisjobtoday = _assignments.Where(x => (x.Date.Date == date.Date) && (x.JobID == job.ID)); if (thisjobtoday.Any()) { var assigned = thisjobtoday.Aggregate(0F, (value, model) => value + model.BookedDuration.TotalHours); dataValues[dateIdx, jobIdx] = assigned / Properties.HoursPerDay; } else dataValues[dateIdx, jobIdx] = null; } available[dateIdx] = avail / Properties.HoursPerDay; totals[dateIdx] = total / Properties.HoursPerDay; } _totals = totals; _available = available; if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns) { dataGrid.HeaderRowHeight = 200; data.Columns.Add("Date", typeof(DateTime)); data.Columns.Add("Available", typeof(object)); foreach (var job in _jobs) data.Columns.Add(job.ID.ToString(), typeof(object)); foreach(var (dateIdx, date) in dates.WithIndex()) { var values = new List(_jobs.Length + 3) { date }; foreach (var (jobIdx, job) in _jobs.WithIndex()) { values.Add(dataValues[dateIdx, jobIdx]); } values.Insert(1, available[dateIdx]); data.Rows.Add(values.ToArray()); } } else if(Properties.DisplayMode == JobPlannerDisplayMode.DateColumns) { dataGrid.HeaderRowHeight = 30; data.Columns.Add("Job", typeof(object)); var availableRow = new List { "Available" }; foreach(var (dateIdx, date) in dates.WithIndex()) { data.Columns.Add(dateIdx.ToString(), typeof(object)); availableRow.Add(available[dateIdx]); } data.Rows.Add(availableRow.ToArray()); foreach (var (jobIdx, job) in _jobs.WithIndex()) { var values = new List(dates.Count + 1) { job }; foreach(var (dateIdx, date) in dates.WithIndex()) { values.Add(dataValues[dateIdx, jobIdx]); } data.Rows.Add(values.ToArray()); } } dataGrid.ItemsSource = data; } } private interface ICellWrapper { DataRowView Row { get; } string ColumnName { get; } } public class GridCellInfoWrapper : ICellWrapper { private GridCellInfo Cell; public DataRowView Row => (Cell.RowData as DataRowView)!; public string ColumnName => (Cell.Column.ValueBinding as Binding)!.Path.Path; public GridCellInfoWrapper(GridCellInfo cell) { Cell = cell; } } public class GridCellWrapper : ICellWrapper { private GridCell Cell; public DataRowView Row => (Cell.DataContext as DataRowView)!; public string ColumnName => (Cell.ColumnBase?.GridColumn.ValueBinding as Binding)?.Path.Path ?? ""; public GridCellWrapper(GridCell cell) { Cell = cell; } } private int? GetDateIndex(ICellWrapper cell) { if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns) { if (cell?.Row?.Row?.ItemArray?.First() is DateTime dt) return _dates.IndexOf(dt); return null; } else { if(int.TryParse(cell.ColumnName, out var idx)) { return idx; } else { return null; } } } private double? GetTotal(ICellWrapper cell) { var dateIndex = GetDateIndex(cell); return dateIndex.HasValue ? _totals[dateIndex.Value] : null; } private double? GetAvailable(ICellWrapper cell) { if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns) { if (cell?.Row?.Row?.Table?.Columns?.Contains("Available") == true && cell.Row["Available"] is double d) return d; return null; } else { var dateIndex = GetDateIndex(cell); return dateIndex.HasValue ? _available[dateIndex.Value] : null; } } private abstract class LeaveConverter : IMultiValueConverter { protected JobResourcePlanner Planner { get; set; } public LeaveConverter(JobResourcePlanner planner) { Planner = planner; } protected System.Windows.Media.Color GetColor(object[] value) { if ((value[0] != DBNull.Value) && (value[0] != DependencyProperty.UnsetValue) && (value[0] is not double d || d != 0.0)) return Colors.DarkSeaGreen; if (value[1] is GridCell cell) { var cellWrp = new GridCellWrapper(cell); var total = Planner.GetTotal(cellWrp); var available = Planner.GetAvailable(cellWrp); if (!total.HasValue || total == 0.0) return Colors.LightGray; if (!available.HasValue || available == 0.0F) return Colors.LightSalmon; return Colors.LightYellow; } return Colors.WhiteSmoke; } public abstract object Convert(object[] value, Type targetType, object parameter, CultureInfo culture); public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } private class LeaveBackgroundConverter : LeaveConverter { public LeaveBackgroundConverter(JobResourcePlanner planner): base(planner) { } public override object Convert(object[] value, Type targetType, object parameter, CultureInfo culture) { return new SolidColorBrush(base.GetColor(value)) { Opacity = 0.8 }; } } private class LeaveForegroundConverter : LeaveConverter { public LeaveForegroundConverter(JobResourcePlanner planner): base(planner) { } public override object Convert(object[] value, Type targetType, object parameter, CultureInfo culture) { return new SolidColorBrush(ImageUtils.GetForegroundColor(base.GetColor(value))); } } private void DataGrid_AutoGeneratingColumn(object sender, AutoGeneratingColumnArgs e) { MultiBinding CreateBinding(String path, TConverter converter) where TConverter : IMultiValueConverter { var binding = new MultiBinding(); binding.Bindings.Add(new Binding(path)); binding.Bindings.Add(new Binding() { RelativeSource = new RelativeSource(RelativeSourceMode.Self) }); binding.Converter = converter; return binding; } var value = (e.Column.ValueBinding as Binding)!; if (value.Path.Path.Equals("Date")) { e.Column.Width = 80; e.Column.HeaderStyle = Resources["DateHeaderStyle"] as Style; e.Column.AllowFocus = false; } else if (value.Path.Path.Equals("Job")) { e.Column.Width = 250; e.Column.HeaderStyle = Resources["DateHeaderStyle"] as Style; e.Column.AllowFocus = false; e.Column.TextAlignment = TextAlignment.Left; e.Column.DisplayBinding = new Binding { Path = new PropertyPath(e.Column.MappingName), Converter = new FuncConverter(x => x is JobModel job ? job.Name : x) }; e.Column.HeaderText = "Project Name"; } else if (value.Path.Path.Equals("Available")) { e.Column = new GridNumericColumn() { NumberDecimalDigits = 2, MappingName = e.Column.MappingName}; e.Column.Width = 50; e.Column.HeaderStyle = e.Column.HeaderStyle = Resources["RotatedHeaderStyle"] as Style; e.Column.AllowFocus = false; e.Column.HeaderText = "Available"; } else { e.Column = new GridNumericColumn() { NumberDecimalDigits = 2, MappingName = e.Column.MappingName}; if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns) { e.Column.Width = 40; e.Column.HeaderStyle = Resources["RotatedHeaderStyle"] as Style; e.Column.HeaderText = (Guid.TryParse(value.Path.Path, out var id) ? _jobs.FirstOrDefault(x => x.ID == id)?.Name ?? value.Path.Path : value.Path.Path); } else { e.Column.Width = 50; e.Column.HeaderStyle = Resources["ContentHeaderStyle"] as Style; if(int.TryParse(value.Path.Path, out var idx)) { e.Column.HeaderText = _dates[idx].ToString("dd/MM"); } } e.Column.DisplayBinding = new Binding { Path = new PropertyPath(e.Column.MappingName) }; e.Column.AllowFocus = true; var style = new Style(typeof(GridCell)); style.Setters.Add(new Setter(BackgroundProperty, CreateBinding(value.Path.Path, new(this)))); style.Setters.Add(new Setter(ForegroundProperty, CreateBinding(value.Path.Path, new(this)))); e.Column.CellStyle = style; e.Column.TextAlignment = TextAlignment.Center; e.Column.HorizontalHeaderContentAlignment = HorizontalAlignment.Center; } e.Column.ColumnSizer = GridLengthUnitType.None; e.Column.ShowHeaderToolTip = false; e.Column.ShowToolTip = false; } private void DataGrid_ContextMenuOpening(object sender, ContextMenuEventArgs e) { var emps = TeamSelector.GetEmployeeData((row, rosters) => new EmployeeResourceModel(row, rosters)); } private void DataGrid_OnSelectionChanging(object? sender, GridSelectionChangingEventArgs e) { var selected = dataGrid.SelectionController.SelectedCells; var added = e.AddedItems.OfType(); if (selected.Any() && added.Any()) { if (Properties.DisplayMode == JobPlannerDisplayMode.JobColumns) e.Cancel = added.Any(a => selected.All(s => s.Column != a.Column)); else e.Cancel = selected.Union(added).GroupBy(x => x.Column).Any(g => g.Count() > 1); } } private bool bResettingSelection = false; private void DataGrid_OnPreviewMouseDown(object sender, MouseButtonEventArgs e) { bResettingSelection = true; dataGrid.SelectionController.ClearSelections(false); } private void DataGrid_OnCurrentCellActivating(object? sender, CurrentCellActivatingEventArgs e) { if (bResettingSelection) { bResettingSelection = false; return; } // if (Properties.DisplayMode == JobPlannerDisplayMode.JobColumns) // { // var thiscol = dataGrid.Columns[e.CurrentRowColumnIndex.ColumnIndex]; // var selected = dataGrid.SelectionController.SelectedCells; // if (selected.Any(x => x.Column != thiscol)) // e.Cancel = true; // else // e.Cancel = false; // // } // else // { // var current = e.CurrentRowColumnIndex.RowIndex; // var original = dataGrid.SelectionController.SelectedCells; // // This is Dumb, but it seems the only way to find if multiple rows have been selected // // ie if there are two cells with the same column, we must have multiple rows // // Stupid SelectedCell -> RowIndex is alwasy showing up as -1! // if (original.GroupBy(x => x.Column).Any(g=>g.Count() > 1)) // e.Cancel = true; // else // e.Cancel = false; // } } private void DataGrid_OnPreviewMouseUp(object sender, MouseButtonEventArgs e) { } private void GetCellData(ICellWrapper cell, out (Guid jobID, DateTime date) result) { result = (Guid.Empty, DateTime.MinValue); if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns) { if (Guid.TryParse(cell.ColumnName, out var emp)) { result.jobID = emp; } result.date = (DateTime)cell.Row.Row.ItemArray.First()!; } else { if(int.TryParse(cell.ColumnName, out var idx)) { result.date = _dates[idx]; } else { result.date = DateTime.MinValue; } var jobModel = cell.Row.Row.ItemArray.First() as JobModel; result.jobID = jobModel?.ID ?? Guid.Empty; } } private ICellWrapper Wrap(GridCellInfo cell) { return new GridCellInfoWrapper(cell); } private ICellWrapper Wrap(GridCell cell) { return new GridCellWrapper(cell); } private bool ExtractSelection(out Guid[] jobs, out DateTime[] dates) { var from = DateTime.MaxValue; var to = DateTime.MinValue; var jobList = new List(); foreach (var cell in dataGrid.GetSelectedCells()) { GetCellData(Wrap(cell), out var result); if (result.jobID == Guid.Empty) continue; if (!jobList.Contains(result.jobID)) jobList.Add(result.jobID); if(result.date != DateTime.MinValue) { if (result.date < from) from = result.date; if (result.date > to) to = result.date; } } if(jobList.Count > 0 && to != DateTime.MinValue && from != DateTime.MaxValue) { jobs = jobList.ToArray(); var datesList = new List(); for(DateTime date = from; date <= to; date = date.AddDays(1)) { datesList.Add(date); } dates = datesList.ToArray(); return true; } else { jobs = []; dates = []; return false; } } private void DataGrid_OnMouseUp(object sender, MouseButtonEventArgs e) { if (ExtractSelection(out var jobIDs, out var dates)) { if(jobIDs.Length == 1) { LoadAssignedEmployees(jobIDs, dates); LoadAvailableEmployees(dates); } else { AvailableEmployees.Items = []; AssignedEmployees.Items = []; AvailableEmployees.Refresh(false, true); AssignedEmployees.Refresh(false, true); } } } private JobPlannerEmployee GetEmployee(Guid id, List list) { var result = list.FirstOrDefault(x => x.ID == id); if (result == null) { result = new JobPlannerEmployee() { ID = id, Name = _emps.FirstOrDefault(x=>x.ID == id)?.Name ?? id.ToString(), Time = TimeSpan.Zero }; list.Add(result); } return result; } private void LoadAvailableEmployees(DateTime[] dates) { List availableemployees = new List(); foreach (var emp in _emps) { foreach (var date in dates) { var roster = RosterUtils.GetRoster(emp.Roster, emp.RosterStart, date); var blocks = roster?.GetBlocks(date, TimeSpan.MinValue, TimeSpan.MaxValue) ?? new RosterBlock[] { }; var rostered = blocks.Aggregate(TimeSpan.Zero, (time, block) => time += block.Duration); AdjustStandardLeave(date, ref rostered); AdjustLeaveRequests(date, emp, ref rostered); var assignments = _assignments.Where(x => (x.Date == date) && (x.EmployeeID == emp.ID)); var assigned = assignments.Aggregate(TimeSpan.Zero, (time, assign) => time += assign.BookedDuration); if (rostered > assigned) GetEmployee(emp.ID, availableemployees).Time += rostered.Subtract(assigned); AvailableEmployees.Items = availableemployees.OrderBy(x=>x.Name).ToList(); AvailableEmployees.Refresh(false, true); } } } private void LoadAssignedEmployees(Guid[] jobIDs, DateTime[] dates) { List assignedemployees = new List(); foreach (var assignment in _assignments.Where(x => dates.Contains(x.Date) && jobIDs.Contains(x.JobID))) GetEmployee(assignment.EmployeeID, assignedemployees).Time += assignment.BookedDuration; AssignedEmployees.Items = assignedemployees.OrderBy(x=>x.Name).ToList(); AssignedEmployees.Refresh(false, true); } private void JobSelector_OnSettingsChanged(object sender, JobSelectorSettingsChangedArgs args) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.JobSettings = args.Settings; DoSaveSettings(); } private void JobSelector_OnSelectionChanged(object sender, JobSelectorSelectionChangedArgs args) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.JobSelection = args.Selection; _jobs = JobSelector.GetJobData(x => new JobModel(x)); DoSaveSettings(); Refresh(); } private void TeamSelector_OnSettingsChanged(object sender, TeamSelectorSettingsChangedArgs args) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.TeamSettings = args.Settings; DoSaveSettings(); } private void TeamSelector_OnSelectionChanged(object sender, TeamSelectorSelectionChangedArgs args) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.TeamSelection = args.Selection; _emps = TeamSelector.GetEmployeeData((row, rosters) => new EmployeeResourceModel(row, rosters)); DoSaveSettings(); Refresh(); } private void JobSelector_OnSizeChanged(object sender, SizeChangedEventArgs e) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.SplitterPosition = TeamSelectorRow.Height.Value; DoSaveSettings(); } private void ViewWindow_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.MonthsToView = (int)ViewWindow.SelectedValue; DoSaveSettings(); Refresh(); } private void HoursSelector_Down_Click(object sender, RoutedEventArgs e) { Properties.HoursPerDay = Math.Max(0.25F, Properties.HoursPerDay - 0.25); HoursSelector.Text = $"{Properties.HoursPerDay:F2}"; DoSaveSettings(); Refresh(); } private void HoursSelector_Up_Click(object sender, RoutedEventArgs e) { Properties.HoursPerDay = Math.Min(24F, Properties.HoursPerDay + 0.25); HoursSelector.Text = $"{Properties.HoursPerDay:F2}"; DoSaveSettings(); Refresh(); } private void ActivityType_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.ActivityType = (Guid)(ActivityType.SelectedValue ?? Guid.Empty); DoSaveSettings(); } private void AvailableEmployees_OnSizeChanged(object sender, SizeChangedEventArgs e) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.EmployeeSplitterPosition = AvailableEmployeesRow.Height.Value; DoSaveSettings(); } private void AvailableEmployees_OnOnAction(object sender, JobPlannerEmployee[] availables) { List edges = new List(); void CheckEdges(params TimeSpan[] times) { foreach (var time in times) { if (!edges.Contains(time)) edges.Add(time); } } bool IsRostered(RosterBlock[] blocks, TimeSpan start, TimeSpan finish) { foreach (var block in blocks) { if ((block.Start <= start) && (block.Finish >= finish)) return true; } return false; } bool IsStandardLeave(DateTime date, TimeSpan start, TimeSpan finish) { return _standardleaves.Any(x => (x.From.Add(x.FromTime) <= date.Add(start)) && (x.To.Add(x.ToTime) >= date.Add(finish)) ); } bool IsLeaveRequest(DateTime date, EmployeeResourceModel emp, TimeSpan start, TimeSpan finish) { return _leaverequests.Any(x => (x.EmployeeID == emp.ID) && (x.From.Add(x.FromTime) <= date.Add(start)) && (x.To.Add(x.ToTime) >= date.Add(finish)) ); } bool IsAssigned(AssignmentModel[] assignments, TimeSpan start, TimeSpan finish) { foreach (var assignment in assignments) { if ((assignment.BookedStart <= start) && (assignment.BookedFinish >= finish)) return true; } return false; } if (ExtractSelection(out var jobIDs, out var dates)) { if (dataGrid.ItemsSource is DataTable table) { List updates = new List(); foreach (var available in availables) { foreach (var date in dates) { var dateIdx = _dates.IndexOf(date); var emp = _emps.FirstOrDefault(x => x.ID == available.ID); if (emp != null) { var roster = RosterUtils.GetRoster(emp.Roster, emp.RosterStart, date); var blocks = roster?.GetBlocks(date, TimeSpan.MinValue, TimeSpan.MaxValue) ?? new RosterBlock[] { }; foreach (var block in blocks) CheckEdges(block.Start, block.Finish); if (GetStandardLeaveTimes(date, out TimeSpan stdleavestart, out TimeSpan stdleavefinish)) CheckEdges(stdleavestart, stdleavefinish); if (GetLeaveRequestTimes(date, emp, out TimeSpan leaverequeststart, out TimeSpan leaverequestfinish)) CheckEdges(leaverequeststart, leaverequestfinish); var assignments = _assignments.Where(x => (x.Date == date) && (x.EmployeeID == emp.ID)).ToArray(); foreach (var assignment in assignments) CheckEdges(assignment.BookedStart, assignment.BookedFinish); edges.Sort(); var adjustment = new double[jobIDs.Length]; for (int i = 0; i < edges.Count - 1; i++) { var start = edges[i]; var finish = edges[i + 1]; if (IsRostered(blocks, start, finish) && (!IsStandardLeave(date,start,finish)) && (!IsLeaveRequest(date,emp,start,finish)) && !IsAssigned(assignments, start, finish)) { foreach(var (idx, jobid) in jobIDs.WithIndex()) { Assignment assignment = new Assignment(); assignment.ActivityLink.ID = Properties.ActivityType; assignment.EmployeeLink.ID = emp.ID; assignment.Date = date; assignment.JobLink.ID = jobid; assignment.Booked.Start = start; assignment.Booked.Finish = finish; assignment.Booked.Duration = finish - start; updates.Add(assignment); adjustment[idx] += assignment.Booked.Duration.TotalHours; } } } if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns) { System.Data.DataRow row = table.Rows[dateIdx]; row.BeginEdit(); foreach(var (idx, jobid) in jobIDs.WithIndex()) { var adj = adjustment[idx]; _available[dateIdx] = Math.Max(0F, _available[dateIdx] - (adj / Properties.HoursPerDay)); row["Available"] = _available[dateIdx]; double jobvalue = (row[jobid.ToString()] == DBNull.Value) ? adj / Properties.HoursPerDay : (double)row[jobid.ToString()] + (adj / Properties.HoursPerDay); row[jobid.ToString()] = jobvalue <= 0F ? null : jobvalue; } row.EndEdit(); } else { var availableRow = table.Rows[0]; availableRow.BeginEdit(); foreach(var (idx, jobid) in jobIDs.WithIndex()) { var job = _jobs.WithIndex().First(x => x.Value.ID == jobid); var row = table.Rows[job.Key + 1]; var adj = adjustment[idx]; _available[dateIdx] = Math.Max(0F, _available[dateIdx] - (adj / Properties.HoursPerDay)); availableRow[dateIdx.ToString()] = _available[dateIdx]; row.BeginEdit(); double jobvalue = (row[dateIdx.ToString()] == DBNull.Value) ? adj / Properties.HoursPerDay : (double)row[dateIdx.ToString()] + (adj / Properties.HoursPerDay); row[dateIdx.ToString()] = jobvalue <= 0F ? null : jobvalue; row.EndEdit(); } availableRow.EndEdit(); foreach(var row in table.Rows.Cast()) { row.BeginEdit(); row[dateIdx.ToString()] = row[dateIdx.ToString()]; row.EndEdit(); } } } } var entry = AvailableEmployees.Items.FirstOrDefault(x => x.ID == available.ID); if (entry != null) { AvailableEmployees.Items.Remove(entry); GetEmployee(entry.ID, AssignedEmployees.Items).Time += entry.Time; } } if (updates.Any()) { using (new WaitCursor()) { new Client().Save(updates, "Assigned by Job Planner"); CoreTable temp = new CoreTable(); temp.LoadColumns(typeof(Assignment)); temp.LoadRows(updates); _assignments.AddRange(temp.Rows.Select(r => new AssignmentModel(r))); AssignedEmployees.Refresh(false, true); AvailableEmployees.Refresh(false, true); } } } } } private void AssignedEmployees_OnOnAction(object sender, JobPlannerEmployee[] employees) { if (ExtractSelection(out var jobIDs, out var dates)) { if (dataGrid.ItemsSource is DataTable table) { foreach (var date in dates) { var dateIdx = _dates.IndexOf(date); var emptimes = _assignments.Where(x => jobIDs.Contains(x.JobID) && (x.Date == date) && employees.Any(e => e.ID == x.EmployeeID) ).ToArray(); var emptime = emptimes.Aggregate(TimeSpan.Zero, (time, ass) => time += ass.BookedDuration); if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns) { System.Data.DataRow row = table.Rows[dateIdx]; row.BeginEdit(); foreach(var jobid in jobIDs) { _available[dateIdx] = _available[dateIdx] + (emptime.TotalHours / Properties.HoursPerDay); row["Available"] = _available[dateIdx]; double value = (row[jobid.ToString()] == DBNull.Value) ? 0.0F : (double)row[jobid.ToString()] - (emptime.TotalHours / Properties.HoursPerDay); row[jobid.ToString()] = value <= 0F ? null : value; } row.EndEdit(); } else { var availableRow = table.Rows[0]; availableRow.BeginEdit(); foreach(var jobid in jobIDs) { var jobIdx = _jobs.WithIndex().First(x => x.Value.ID == jobid).Key; _available[dateIdx] = _available[dateIdx] + (emptime.TotalHours/Properties.HoursPerDay); availableRow[dateIdx.ToString()] = _available[dateIdx]; var row = table.Rows[jobIdx + 1]; row.BeginEdit(); double value = (row[dateIdx.ToString()] == DBNull.Value) ? 0.0F : (double)row[dateIdx.ToString()] - (emptime.TotalHours / Properties.HoursPerDay); row[dateIdx.ToString()] = value <= 0F ? null : value; row.EndEdit(); } availableRow.EndEdit(); foreach(var row in table.Rows.Cast()) { row.BeginEdit(); row[dateIdx.ToString()] = row[dateIdx.ToString()]; row.EndEdit(); } } } } var assignments = _assignments.Where(x => jobIDs.Contains(x.JobID) && dates.Contains(x.Date) && employees.Any(e => e.ID == x.EmployeeID) ).ToArray(); if (assignments.Any()) { using (new WaitCursor()) { var deletes = assignments.Select(x => new Assignment() { ID = x.ID }).ToArray(); new Client().Delete(deletes, "Deleted from Job Planner"); var removes = AssignedEmployees.Items.Where(x => employees.Any(e => e.ID == x.ID)).ToArray(); foreach (var remove in removes) { GetEmployee(remove.ID, AvailableEmployees.Items).Time += remove.Time; AssignedEmployees.Items.Remove(remove); } foreach (var assignment in assignments) _assignments.Remove(assignment); AssignedEmployees.Refresh(false, true); AvailableEmployees.Refresh(false, true); } } } } private void LeaveType_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.IncludeUnApprovedLeave = LeaveType.SelectedIndex > 0; DoSaveSettings(); Refresh(); } private void Orientation_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (EventSuppressor.IsSet(Suppress.This)) return; Properties.DisplayMode = Orientation.SelectedIndex <= 0 ? JobPlannerDisplayMode.DateColumns : JobPlannerDisplayMode.JobColumns; DoSaveSettings(); Refresh(); } private void AvailableEmployees_OnAfterRefresh(object sender, AfterRefreshEventArgs args) { AvailableEmployees.Items = AvailableEmployees.Items.OrderBy(x => x.Name).ToList(); } private void AssignedEmployees_OnAfterRefresh(object sender, AfterRefreshEventArgs args) { AssignedEmployees.Items = AssignedEmployees.Items.OrderBy(x => x.Name).ToList(); } }