using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Media; using System.Windows.Threading; using Comal.Classes; using InABox.Clients; using InABox.Configuration; using InABox.Core; using InABox.DynamicGrid; using InABox.Wpf; using InABox.WPF; using PRSDesktop.Grids; using Selection = InABox.Core.Selection; using SelectionChangedEventArgs = System.Windows.Controls.SelectionChangedEventArgs; namespace PRSDesktop { public partial class Calendar { private enum Suppress { Selector, // Prevent the Selector from Being Changed Calendar, // Prevent the Calendar from Being Reconfigured Events, // Prevent the Selectors from Responding to Events Refresh, // Stop the Data from Being refreshed Settings // Dont allow settings to be updated } private EventSuppressor? suppressor = null; public void DisableUpdate() { suppressor ??= new EventSuppressor(Suppress.Refresh, Suppress.Settings); } public void EnableUpdate() { if (suppressor != null) { suppressor.Dispose(); suppressor = null; } } #region Converters public static readonly IValueConverter DayOfWeekConverter = new FuncConverter(x => x.DayOfWeek.ToString()); #endregion #region Dependency Properties #region HeaderVisibility Dependency Property public static readonly DependencyProperty HeaderVisibilityProperty = DependencyProperty.Register( nameof(HeaderVisibility), typeof(Visibility), typeof(Calendar), new UIPropertyMetadata(Visibility.Collapsed) ); public Visibility HeaderVisibility { get => (Visibility)GetValue(HeaderVisibilityProperty); set => SetValue(HeaderVisibilityProperty,value); } #endregion #region SettingsVisible Dependency Property public static readonly DependencyProperty SettingsVisibleProperty = DependencyProperty.Register( nameof(SettingsVisible), typeof(CalendarSettingsVisibility), typeof(Calendar), new UIPropertyMetadata(CalendarSettingsVisibility.Disabled, SettingsVisible_Changed) ); private static void SettingsVisible_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; var value = (CalendarSettingsVisibility)e.NewValue; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.SettingsVisible = value; calendar.DoSaveSettings(); } calendar._splitPanel.View = value == CalendarSettingsVisibility.Visible ? DynamicSplitPanelView.Combined : DynamicSplitPanelView.Master; calendar._splitPanel.AllowableViews = value == CalendarSettingsVisibility.Disabled ? DynamicSplitPanelView.Master : DynamicSplitPanelView.Master | DynamicSplitPanelView.Combined; } public CalendarSettingsVisibility SettingsVisible { get => (CalendarSettingsVisibility)GetValue(SettingsVisibleProperty); set => SetValue(SettingsVisibleProperty, value); } #endregion #region CalendarView Dependency Property public static readonly DependencyProperty CalendarViewProperty = DependencyProperty.Register( nameof(CalendarView), typeof(CalendarViewType), typeof(Calendar), new UIPropertyMetadata(CalendarViewType.Day, CalendarView_Changed) ); public CalendarViewType CalendarView { get => (CalendarViewType)GetValue(CalendarViewProperty); set => SetValue(CalendarViewProperty, value); } private static void CalendarView_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.CalendarView = calendar.CalendarView; calendar.DoSaveSettings(); } calendar.Refresh(); } #endregion #region EmployeeSelector Dependency Property public static readonly DependencyProperty EmployeeSelectionProperty = DependencyProperty.Register( nameof(EmployeeSelection), typeof(EmployeeSelectorData), typeof(Calendar), new UIPropertyMetadata(new EmployeeSelectorData(), EmployeeSelection_Changed) ); public EmployeeSelectorData EmployeeSelection { get => (EmployeeSelectorData)GetValue(EmployeeSelectionProperty); set => SetValue(EmployeeSelectionProperty, value); } private static void EmployeeSelection_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.EmployeeSelection = calendar.EmployeeSelection; calendar.DoSaveSettings(); } calendar.EmployeeSelector.Selection = calendar.EmployeeSelection; calendar._employees = calendar.EmployeeSelector.GetEmployeeData((row, rosters) => row.ToObject()); calendar.ReloadColumns(); calendar.Refresh(); } private void EmployeeSelector_OnSelectionChanged(object sender, EmployeeSelectorSelectionChangedArgs args) { if (EventSuppressor.IsSet(Suppress.Events)) return; EmployeeSelection = args.Selection; } #endregion #region EmployeeSettings Dependency Property public static readonly DependencyProperty EmployeeSettingsProperty = DependencyProperty.Register( nameof(EmployeeSettings), typeof(EmployeeSelectorSettings), typeof(Calendar), new UIPropertyMetadata(new EmployeeSelectorSettings(), EmployeeSettings_Changed) ); private static void EmployeeSettings_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.EmployeeSelector = calendar.EmployeeSettings; calendar.DoSaveSettings(); } calendar.EmployeeSelector.Settings = calendar.EmployeeSettings; } public EmployeeSelectorSettings EmployeeSettings { get => (EmployeeSelectorSettings)GetValue(EmployeeSettingsProperty); set => SetValue(EmployeeSettingsProperty, value); } private void EmployeeSelector_OnSettingsChanged(object sender, EmployeeSelectorSettingsChangedArgs args) { if (EventSuppressor.IsSet(Suppress.Events)) return; EmployeeSettings = args.Settings; } #endregion #region TimeInterval DependencyProperty public static readonly DependencyProperty TimeIntervalProperty = DependencyProperty.Register( "TimeInterval", typeof(CalendarTimeInterval), typeof(Calendar), new PropertyMetadata(CalendarTimeInterval.FifteenMinutes, TimeInterval_Changed) ); public CalendarTimeInterval TimeInterval { get => (CalendarTimeInterval)GetValue(TimeIntervalProperty); set => SetValue(TimeIntervalProperty, value); } private static void TimeInterval_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.TimeInterval = calendar.TimeInterval; calendar.DoSaveSettings(); } calendar.IntervalSelector.SelectedIndex = (int)calendar.TimeInterval; calendar.CalendarControl.RowInterval = TimeIntervalToTimeSpan(calendar.TimeInterval); } private void IntervalSelector_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; TimeInterval = (CalendarTimeInterval)IntervalSelector.SelectedIndex; } private static TimeSpan TimeIntervalToTimeSpan(CalendarTimeInterval interval) { return interval switch { CalendarTimeInterval.FiveMinutes => new TimeSpan(0, 5, 0), CalendarTimeInterval.SixMinutes => new TimeSpan(0, 6, 0), CalendarTimeInterval.TenMinutes => new TimeSpan(0, 10, 0), CalendarTimeInterval.FifteenMinutes => new TimeSpan(0, 15, 0), CalendarTimeInterval.TwentyMinutes => new TimeSpan(0, 20, 0), CalendarTimeInterval.ThirtyMinutes => new TimeSpan(0, 30, 0), CalendarTimeInterval.SixtyMinutes or _ => new TimeSpan(1, 0, 0) }; } #endregion #region SelectedDate Dependency Property public static readonly DependencyProperty SelectedDateProperty = DependencyProperty.Register( nameof(SelectedDate), typeof(DateTime), typeof(Calendar), new UIPropertyMetadata(DateTime.Today, SelectedDate_Changed) ); public DateTime SelectedDate { get => (DateTime)GetValue(SelectedDateProperty); set => SetValue(SelectedDateProperty, value); } private static void SelectedDate_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.Date = calendar.SelectedDate; calendar.DoSaveSettings(); } calendar.Refresh(); } public DateTime StartDate => (Properties?.CalendarView ?? CalendarViewType.Day) == CalendarViewType.Day ? SelectedDate : SelectedDate.StartOfWeek(DayOfWeek.Monday); public DateTime EndDate => Properties.CalendarView == CalendarViewType.Day ? StartDate.AddDays(1) : Properties.CalendarView == CalendarViewType.WorkWeek ? StartDate.AddDays(5) : StartDate.AddDays(7); #endregion #region StartHour Dependency Properties public static readonly DependencyProperty StartHourProperty = DependencyProperty.Register( nameof(StartHour), typeof(int), typeof(Calendar), new UIPropertyMetadata(6, StartHour_Changed, StartHour_Coerce) ); private static object StartHour_Coerce(DependencyObject d, object baseValue) { if (d is not Calendar calendar || baseValue is not int value) return baseValue; return Math.Min(calendar.EndHour - 1, Math.Max(0, value)); } public int StartHour { get => (int)GetValue(StartHourProperty); set => SetValue(StartHourProperty, value); } private static void StartHour_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.StartHour = calendar.StartHour; calendar.DoSaveSettings(); } calendar.StartTimeSelector.Text = FormatHour(calendar.StartHour); calendar.CalendarControl.StartHour = TimeSpan.FromHours(calendar.StartHour); } private void StartTimeSelector_Down_Click(object sender, RoutedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; StartHour -= 1; } private void StartTimeSelector_Up_Click(object sender, RoutedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; StartHour += 1; } #endregion #region End Hour Property public static readonly DependencyProperty EndHourProperty = DependencyProperty.Register( nameof(EndHour), typeof(int), typeof(Calendar), new UIPropertyMetadata(18, EndHour_Changed, EndHour_Coerce) ); public int EndHour { get => (int)GetValue(EndHourProperty); set => SetValue(EndHourProperty, value); } private static object EndHour_Coerce(DependencyObject d, object baseValue) { if (d is not Calendar calendar || baseValue is not int value) return baseValue; return Math.Max(calendar.StartHour + 1, Math.Min(24, value)); } private static void EndHour_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.EndHour = calendar.EndHour; calendar.DoSaveSettings(); } calendar.FinishTimeSelector.Text = FormatHour(calendar.EndHour); calendar.CalendarControl.EndHour = TimeSpan.FromHours(calendar.EndHour); } private void FinishTimeSelector_Down_Click(object sender, RoutedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; EndHour -= 1; } private void FinishTimeSelector_Up_Click(object sender, RoutedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; EndHour += 1; } private static string FormatHour(int hour) { return hour <= 0 || hour >= 24 ? "Midnight" : hour < 12 ? string.Format("{0}:00 AM", hour) : hour > 12 ? string.Format("{0}:00 PM", hour) : "12:00 NN"; } #endregion #region AssignmentType Dependency Property public static readonly DependencyProperty AssignmentTypeProperty = DependencyProperty.Register( nameof(AssignmentType), typeof(CalendarAssignmentType), typeof(Calendar), new UIPropertyMetadata(CalendarAssignmentType.Booked, AssignmentType_Changed) ); public CalendarAssignmentType AssignmentType { get => (CalendarAssignmentType)GetValue(AssignmentTypeProperty); set => SetValue(AssignmentTypeProperty, value); } private static void AssignmentType_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.AssignmentType = calendar.AssignmentType; calendar.DoSaveSettings(); } calendar.AssignmentTypeSelector.SelectedIndex = (int)calendar.AssignmentType; calendar.Refresh(); } private void AssignmentTypeSelector_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; using (new EventSuppressor(Suppress.Selector)) { AssignmentType = (CalendarAssignmentType)AssignmentTypeSelector.SelectedIndex; } } #endregion #region BackgroundType Dependency Property public static readonly DependencyProperty BackgroundTypeProperty = DependencyProperty.Register( nameof(BackgroundType), typeof(CalendarBackgroundType), typeof(Calendar), new UIPropertyMetadata(CalendarBackgroundType.Roster, BackgroundType_Changed) ); public CalendarBackgroundType BackgroundType { get => (CalendarBackgroundType)GetValue(BackgroundTypeProperty); set => SetValue(BackgroundTypeProperty, value); } private static void BackgroundType_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; if (!EventSuppressor.IsSet(Suppress.Settings)) { calendar.Properties.BackgroundType = calendar.BackgroundType; calendar.DoSaveSettings(); } calendar.BackgroundTypeSelector.SelectedIndex = (int)calendar.BackgroundType; calendar.Refresh(); } private void BackgroundTypeSelector_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; using (new EventSuppressor(Suppress.Selector)) BackgroundType = (CalendarBackgroundType)BackgroundTypeSelector.SelectedIndex; } #endregion #region Zoom Dependency Properties public static readonly DependencyProperty ZoomProperty = DependencyProperty.Register( nameof(Zoom), typeof(double), typeof(Calendar), new UIPropertyMetadata(100.0, Zoom_Changed, Zoom_Coerce) ); private static object Zoom_Coerce(DependencyObject d, object baseValue) { if (baseValue is not double zoom) return baseValue; return Math.Max(zoom, 100.0); } public double Zoom { get => (double)GetValue(ZoomProperty); set => SetValue(ZoomProperty, value); } private static void Zoom_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Calendar calendar) return; calendar.CalendarControl.Zoom = calendar.Zoom / 100; calendar.ZoomSelector.Text = $"{calendar.Zoom:F0}%"; } private void ZoomSelector_Down_Click(object sender, RoutedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; ZoomOut(); } private void ZoomSelector_Up_Click(object sender, RoutedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; ZoomIn(); } public void ZoomIn() => Zoom *= 1.125; public void ZoomOut() => Zoom /= 1.125; public void ResetZoom() => Zoom = 100; #endregion #endregion #region Event Handlers public event LoadSettings? LoadSettings; public event SaveSettings? SaveSettings; public CalendarConfigurationEvent? ConfigurationChanged; public event CalendarDataMenuEvent? CustomiseContextMenu; public event CalendarDataEvent? SelectionChanged; public event CalendarDataEvent? ItemCreated; public event CalendarDataEvent? ItemChanged; public event CalendarHandledEvent? ItemEditing; #endregion public void SelectEmployee(Guid employeeid) => EmployeeSelector.SelectEmployee(employeeid); // Populated as requiew when EmployeeSelector.SelectionChanged is triggered private Employee[] _employees = []; // Populated once at startup private StandardLeave[] _standardleaves = []; private LeaveRequest[] _leaverequests = []; // Populated on each Refresh private TimeSheet[] _timesheets = []; // Populated on each Refresh private List _assignments = []; private ObservableCollection _appointments = new(); private bool bColumnsLoaded; private AssignmentGrid? ag; private LeaveRequestGrid? lg; private StandardLeaveGrid? slg; private DynamicDataGrid? mg; public bool IsReady { get; set; } public CalendarSettings Properties { get; set; } = new(); public Calendar() { using (EventSuppressor.All()) { InitializeComponent(); _splitPanel.DataContext = this; StartHour = 0; EndHour = 24; } CalendarControl.GetFillBlockStart = GetFillBlockStart; CalendarControl.GetFillBlockEnd = GetFillBlockEnd; CalendarControl.RowInterval = TimeIntervalToTimeSpan(TimeInterval); CalendarViewSelector.ItemsSource = new List> { new(CalendarViewType.Day, "Day"), new(CalendarViewType.WorkWeek, "Work Week"), new(CalendarViewType.Week, "Week"), }; CalendarViewSelector.DisplayMemberPath = "Value"; CalendarViewSelector.SelectedValuePath = "Key"; } public virtual void Setup() { using (new EventSuppressor(Suppress.Settings, Suppress.Refresh, Suppress.Events)) { Properties = LoadSettings?.Invoke(this) ?? new CalendarSettings(); SettingsVisible = Properties.SettingsVisible; SelectedDate = Properties.AlwaysStartOnToday ? DateTime.Today : Properties.Date; StartHour = Properties.StartHour; EndHour = Properties.EndHour; TimeInterval = Properties.TimeInterval; AssignmentType = Properties.AssignmentType; BackgroundType = Properties.BackgroundType; CalendarView = Properties.CalendarView; Zoom = Properties.Zoom; EmployeeSelector.Setup(); EmployeeSettings = Properties.EmployeeSelector; EmployeeSelection = Properties.EmployeeSelection; AlwaysTodayBox.IsChecked = Properties.AlwaysStartOnToday; ReloadColumns(); } } public virtual void Shutdown(CancelEventArgs? cancel) { } private void DoSaveSettings() { SaveSettings?.Invoke(this, Properties); } private bool bFirst = true; public virtual void Refresh() { RefreshData(StartDate, EndDate); } private bool _refreshing = false; private void RefreshData(DateTime startDate, DateTime endDate) { if (EventSuppressor.IsSet(Suppress.Refresh)) return; if (!_refreshing) { _refreshing = true; Dispatcher.BeginInvoke(() => DoRefreshData(startDate, endDate)); } } private void DoRefreshData(DateTime startDate, DateTime endDate) { _refreshing = false; using (new WaitCursor()) { if (!bColumnsLoaded) ReloadColumns(); var query = new MultiQuery(); var empids = _employees.ToArray(x => x.ID); if (BackgroundType != CalendarBackgroundType.Roster) { query.Add( Filter.Where(x => x.EmployeeLink.ID).InList(empids) .And(x => x.Date).IsGreaterThanOrEqualTo(startDate) .And(x => x.Date).IsLessThanOrEqualTo(endDate) .And(x=>x.LeaveRequestLink.ID).IsEqualTo(Guid.Empty) .And(x=>x.StandardLeaveLink.ID).IsEqualTo(Guid.Empty), TimeSheetModel.Columns ); } query.Add( Filter.Where(x => x.EmployeeLink.ID).InList(empids) .And(x => x.Date).IsGreaterThanOrEqualTo(startDate) .And(x => x.Date).IsLessThanOrEqualTo(endDate), AssignmentModel.Columns, new SortOrder(x => x.EmployeeLink.ID).ThenBy(x => x.Date).ThenBy(x => x.Booked.Duration, SortDirection.Descending) ); query.Add( Filter .Where(x => x.Status).IsNotEqualTo(LeaveRequestStatus.Rejected) .And(x => x.EmployeeLink.ID).InList(empids) .And(x => x.From).IsLessThanOrEqualTo(endDate) .And(x => x.To).IsGreaterThanOrEqualTo(startDate), Columns.None() .Add(x => x.ID) .Add(x => x.EmployeeLink.ID) .Add(x => x.From) .Add(x => x.FromTime) .Add(x => x.To) .Add(x => x.ToTime) .Add(x => x.LeaveType.Description) .Add(x => x.LeaveType.Color) .Add(x => x.Status) .Add(x => x.Notes)); query.Add( Filter .Where(x => x.From).IsLessThanOrEqualTo(endDate) .And(x => x.To).IsGreaterThanOrEqualTo(startDate), Columns.None() .Add(x => x.ID) .Add(c => c.ID) .Add(c => c.LeaveType.Description) .Add(c => c.Name) .Add(c => c.LeaveType.Color) .Add(c => c.From) .Add(c => c.FromTime) .Add(c => c.To) .Add(c => c.ToTime)); query.Query(); _timesheets = (BackgroundType == CalendarBackgroundType.Roster) ? [] : query.Get().ToArray(); _leaverequests = query.Get().ToArray(); _assignments = query.Get().ToList(); _standardleaves = query.Get().ToArray(); LoadBackground(); _appointments.Clear(); LoadStandardLeaves(_appointments); LoadLeaveRequests(_appointments); LoadAssignments(_appointments); try { CalendarControl.Dates = CoreUtils.Range(startDate, endDate, x => x.AddDays(1)); CalendarControl.ItemsSource = _appointments; } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } bFirst = false; } } public EmployeeRosterItem? GetRoster(Guid employeeid, DateTime date) { var employee = _employees.FirstOrDefault(x => x.ID == employeeid); if (employee is null) return null; var roster = RosterUtils.GetRoster(EmployeeSelector.GetRoster(employeeid), employee.RosterStart, date); return roster; } public bool GetActiveWindow(Guid employeeid, DateTime date, ref TimeSpan start, ref TimeSpan finish) { var result = false; foreach (var assignment in _assignments.Where(a => (a.EmployeeLink.ID == employeeid) && (a.Date) == date)) { result = true; var curstart = AssignmentType switch { CalendarAssignmentType.Booked => assignment.Booked.Start, CalendarAssignmentType.Actual => assignment.Actual.Start, _ => Assignment.EffectiveTime(assignment.Actual.Start, assignment.Booked.Start) }; var curfinish = AssignmentType switch { CalendarAssignmentType.Booked => assignment.Booked.Finish, CalendarAssignmentType.Actual => assignment.Actual.Finish, _ => Assignment.EffectiveTime( assignment.Actual.Finish, Assignment.EffectiveTime(assignment.Actual.Start, assignment.Booked.Start) .Add(assignment.Booked.Duration) ) }; start = start > curstart ? curstart : start; finish = finish < curfinish ? curfinish : finish; } if ((BackgroundType == CalendarBackgroundType.Roster) || ((BackgroundType == CalendarBackgroundType.Automatic) && (date >= DateTime.Today))) { var employee = _employees.FirstOrDefault(x => x.ID == employeeid); if (employee != null) { var roster = RosterUtils.GetRoster(EmployeeSelector.GetRoster(employeeid), employee.RosterStart, date); if (roster != null) { var blocks = roster.GetBlocks(date, TimeSpan.MinValue, TimeSpan.MaxValue); foreach (var block in blocks) { start = start > block.Start ? block.Start : start; finish = finish < block.Finish ? block.Finish : finish; } } } } else { foreach (var timesheet in _timesheets.Where(t => (t.EmployeeLink.ID == employeeid) && (t.Date == date))) { result = true; var curstart = !timesheet.Approved.IsEmpty() ? timesheet.ApprovedStart : timesheet.Start; var curfinish = !timesheet.Approved.IsEmpty() ? timesheet.ApprovedFinish : timesheet.Finish; start = start > curstart ? curstart : start; finish = finish < curfinish ? curfinish : finish; } } return result; } public Assignment[] GetAssignments(Guid employeeid, DateTime date) { return _assignments.Where(a => (a.EmployeeLink.ID == employeeid) && (a.Date == date)).ToArray(); } private void LoadBackground() { var regions = new ObservableCollection(); foreach (var employee in _employees) { for (var date = StartDate; date < EndDate; date = date.AddDays(1)) { if ((BackgroundType == CalendarBackgroundType.Roster) || ((BackgroundType == CalendarBackgroundType.Automatic) && (date >= DateTime.Today))) { var roster = RosterUtils.GetRoster(EmployeeSelector.GetRoster(employee.ID), employee.RosterStart, date); if (roster != null) { var blocks = roster.GetBlocks(date, TimeSpan.FromSeconds(0), TimeSpan.FromDays(1)); foreach (var block in blocks) { regions.Add(new CalendarRegion { Date = date, Column = employee, Start = block.Start, End = block.Finish, Background = Colors.Yellow.ToBrush(0.3) }); } } } else { foreach (var timesheet in _timesheets.Where(t => (t.EmployeeLink.ID == employee.ID) && (t.Date == date))) { var start = !timesheet.Approved.IsEmpty() ? timesheet.ApprovedStart : timesheet.Start; var finish = !timesheet.Approved.IsEmpty() ? timesheet.ApprovedFinish : timesheet.Finish; if(finish == default) { finish = TimeSpan.FromHours(24); } regions.Add(new CalendarRegion { Date = date, Column = employee, Start = start, End = finish, Background = (!timesheet.Approved.IsEmpty() ? Colors.LightGreen : Colors.LightSalmon).ToBrush(0.4), }); } } } } CalendarControl.Regions = regions; } private void LoadStandardLeaves(IList appointments) { for (var date = StartDate; date < EndDate; date = date.AddDays(1)) { var leaves = _standardleaves.Where(x => (x.From <= date) && (x.To.Add(x.ToTime) > date) ).ToArray(); foreach (var leave in leaves) { foreach (var employee in _employees) { var start = (date.Date == leave.From.Date) ? leave.FromTime : TimeSpan.FromSeconds(0); var finish = (date.Date == leave.To.Date) ? leave.ToTime : TimeSpan.FromDays(1).Subtract(TimeSpan.FromSeconds(1)); var roster = RosterUtils.GetRoster(EmployeeSelector.GetRoster(employee.ID), employee.RosterStart, date); if (roster != null) { var blocks = roster.GetBlocks(date, start, finish); foreach (var block in blocks) { var appt = new StandardLeaveAppointment(leave, employee, block); appointments.Add(appt); } } } } } } private void LoadLeaveRequests(IList appointments) { for (var date = StartDate; date < EndDate; date = date.AddDays(1)) { var ids = _employees.ToArray(x => x.ID); var leaves = _leaverequests.Where(x => (x.From <= date) && (x.To.Add(x.ToTime) > date) && ids.Contains(x.EmployeeLink.ID) ).ToArray(); foreach (var leave in leaves) { var employee = _employees.FirstOrDefault(x => x.ID == leave.EmployeeLink.ID); if (employee is null) return; var roster = RosterUtils.GetRoster(EmployeeSelector.GetRoster(employee.ID), employee.RosterStart, date); if (roster != null) { var start = (date.Date == leave.From.Date) ? leave.FromTime : TimeSpan.FromSeconds(0); var finish = (date.Date == leave.To.Date) ? leave.ToTime : TimeSpan.FromDays(1).Subtract(TimeSpan.FromSeconds(1)); var blocks = roster.GetBlocks(date, start, finish); foreach(var block in blocks) { appointments.Add(new LeaveRequestAppointment(leave, employee, block)); } } } } } private HashSet _savingAssignments = new(); private void LoadAssignment(Assignment assignment, IList appointments) { var employee = _employees.FirstOrDefault(x => x.ID == assignment.EmployeeLink.ID); if (employee is null) return; var model = new AssignmentAppointment(assignment, employee, AssignmentType); model.OnUpdate += () => { if (_savingAssignments.Add(assignment)) { Dispatcher.BeginInvoke(() => { _savingAssignments.Remove(assignment); Client.SaveAsync(assignment, "Edited by user").LogIfFail(); }); } }; model.EmployeeChanged += (o, e) => { model.Employee = _employees.FirstOrDefault(x => x.ID == assignment.EmployeeLink.ID); }; appointments.Add(model); } private void LoadAssignments(IList appointments) { foreach (var assignment in _assignments) LoadAssignment(assignment, appointments); } public DataModel DataModel(Selection selection) { var ids = _assignments.Select(x => x.ID).ToArray(); return new AutoDataModel(Filter.Where(x => x.ID).InList(ids)); } private static T CheckGrid([NotNull] ref T? grid) where T : new() { grid ??= new T(); return grid; } private void ReloadColumns() { CalendarControl.Columns = _employees.OrderBy(x => x.Name); bColumnsLoaded = true; } #region Block private (TimeSpan Start, TimeSpan End)? GetFillBlock(DateTime date, object? column, TimeSpan time) { if (column is not Employee employee) return null; if ((BackgroundType == CalendarBackgroundType.Roster) || ((BackgroundType == CalendarBackgroundType.Automatic) && (date >= DateTime.Today))) { var roster = RosterUtils.GetRoster(EmployeeSelector.GetRoster(employee.ID), employee.RosterStart, date); if (roster is null) return null; var blocks = roster.GetBlocks(date, TimeSpan.Zero, TimeSpan.FromDays(1)); var block = blocks.FirstOrDefault(x => x.Start <= time && time <= x.Finish); if (block is null) return null; return (block.Start, block.Finish); } else { foreach (var timesheet in _timesheets.Where(t => (t.EmployeeLink.ID == employee.ID) && (t.Date == date))) { var start = !timesheet.Approved.IsEmpty() ? timesheet.ApprovedStart : timesheet.Start; var finish = !timesheet.Approved.IsEmpty() ? timesheet.ApprovedFinish : timesheet.Finish; if(finish == default) { finish = TimeSpan.FromHours(24); } if(start <= time && time <= finish) { return (start, finish); } } return null; } } private TimeSpan? GetFillBlockStart(DateTime date, object? column, TimeSpan time) { var block = GetFillBlock(date, column, time); return block?.Start; } private TimeSpan? GetFillBlockEnd(DateTime date, object? column, TimeSpan time) { var block = GetFillBlock(date, column, time); return block?.End; } private void Calendar_BlockHeld(object sender, CalendarBlockEventArgs e) { OpenContextMenu(sender, e, false); } public enum ContextMenuItems { Create, Fill } public CalendarTimeSlot FillSlot(CalendarBlockEventArgs e, CalendarTimeSlot slot) { if(CalendarControl.GetEmptySpace(e.Point, out var start, out var end)) { if(start is null || end is null) { var block = GetFillBlock(e.Date, e.Column, CalendarControl.GetTime(e.Point)); return new(slot.EmployeeID, e.Date, start ?? block?.Start ?? e.Start, end ?? block?.End ?? e.End); } else { return new(slot.EmployeeID, e.Date, start.Value, end.Value); } } else { return slot; } } private void Calendar_BlockRightClicked(object sender, CalendarBlockEventArgs e) { OpenContextMenu(sender, e, true); } private void OpenContextMenu(object sender, CalendarBlockEventArgs e, bool allowFill) { object? value; if(e.Value is AssignmentAppointment appointment) { if (appointment.Model.Meeting.Link.ID != Guid.Empty) { e.Menu.AddItem("Edit Meeting", null, appointment.Model, EditMeeting); } else { e.Menu.AddItem("Edit Assignment", null, appointment.Model, EditAssignment); e.Menu.AddSeparator(); e.Menu.AddItem("Copy Assignment", null, appointment.Model, CopyAssignment); e.Menu.AddSeparator(); e.Menu.AddItem("Fill Available Time", null, () => { if(CalendarControl.GetEmptySpace(e.Point, out var start, out var end, time: e.Start, exclude: [appointment])) { var time = AssignmentType switch { CalendarAssignmentType.Automatic or CalendarAssignmentType.Actual => appointment.Model.Actual, CalendarAssignmentType.Booked => appointment.Model.Booked, _ => throw new InvalidEnumException(AssignmentType) }; if(start is null || end is null) { var block = GetFillBlock(e.Date, e.Column, e.Start); time.Start = start ?? block?.Start ?? time.Start; time.Finish = end ?? block?.End ?? time.Finish; } else { time.Start = start.Value; time.Finish = end.Value; } Client.Save(appointment.Model, "Adjusted to fill time"); } }); } e.Menu.AddSeparatorIfNeeded(); CreateDigitalFormsMenu(e.Menu, appointment); e.Menu.AddSeparatorIfNeeded(); if (appointment.Model.Meeting.Link.ID != Guid.Empty) { e.Menu.AddItem("Delete Meeting", null, appointment.Model, DeleteMeeting); } else { e.Menu.AddItem("Delete Assignment", null, appointment.Model, DeleteAssignment); } value = appointment.Model; } else if(e.Value is LeaveRequestAppointment leaveAppointment) { if (Security.CanView()) { e.Menu.AddItem( Security.CanEdit() ? "Edit Leave" : "View Leave", null, leaveAppointment.Model, EditLeave); } value = leaveAppointment.Model; } else if(e.Value is StandardLeaveAppointment standardLeaveAppointment) { if (Security.CanView()) { e.Menu.AddItem( Security.CanEdit() ? "Edit Standard Leave" : "View Standard Leave", null, standardLeaveAppointment.Model, EditStandardLeave); } value = standardLeaveAppointment.Model; } else if (e.Value is null) { if (e.Column is not Employee employee) return; var slot = new CalendarTimeSlot(employee.ID, e.Date, e.Start, e.End); var createmenu = e.Menu.AddItem("Create...", null, null) .WithName($"Menu_{nameof(ContextMenuItems.Create)}"); createmenu.AddItem("New Assignment", null, slot, slot => CreateAssignment(slot)); // createmenu.AddItem("New Meeting", null, slot, CreateMeeting); if (allowFill) { var fillMenu = e.Menu.AddItem("Fill...", null, null) .WithName($"Menu_{nameof(ContextMenuItems.Fill)}"); fillMenu.AddItem("New Assignment", null, () => CreateAssignment(FillSlot(e, slot))); // fillMenu.AddItem("New Meeting", null, () => CreateMeeting(FillSlot(e, slot))); } if (_copiedmodel != null) { e.Menu.AddSeparator(); e.Menu.AddItem("Paste Assignment", null, slot, slot => PasteAssignment(slot, _copiedmodel)); } value = slot; } else { value = null; } e.Menu.AddSeparatorIfNeeded(); e.Menu.AddItem("Zoom In", null, ZoomIn); e.Menu.AddItem("Zoom Out", null, ZoomOut, enabled: Zoom > 100); e.Menu.AddItem("Reset Zoom", null, ResetZoom); CustomiseContextMenu?.Invoke(e.Menu, new CalendarDataMenuEventArgs(value, e)); } private static void CreateDigitalFormsMenu(ContextMenu menu, AssignmentAppointment appointment) { var digitalForms = menu.AddItem("Digital Forms", null, null); DynamicGridUtils.PopulateFormMenu( digitalForms, appointment.Model.ID, () => new Client().Load(Filter.Where(x => x.ID).IsEqualTo(appointment.Model.ID)).First(), false); } public void CreateMeeting(CalendarTimeSlot slot) { var meeting = new Meeting { Date = slot.Date }; meeting.Time.Start = slot.Start; meeting.Time.Finish = slot.End; ItemCreated?.Invoke(this, new CalendarDataEventArgs(meeting)); var args = new CalendarHandledEventArgs(meeting); ItemEditing?.Invoke(this, args); if (args.Status == CalendarHandledStatus.Cancel) return; if (args.Status == CalendarHandledStatus.Handled) { Refresh(); return; } CheckGrid(ref mg); var items = new[] { meeting }; bool bOK = mg.EditItems( items, (type) => { if (type == typeof(Assignment)) return LoadMeetingEmployees(slot.EmployeeID); else if (type == typeof(MeetingItem)) return LoadMeetingItems(); return null; }, true ); if (bOK) Refresh(); } private static CoreTable LoadMeetingEmployees(Guid employeeid) { var result = new CoreTable(); result.LoadColumns(typeof(Assignment)); var assignment = new Assignment(); LookupFactory.DoLookup(assignment, x => x.EmployeeLink, employeeid); result.LoadRows([assignment]); return result; } private static CoreTable LoadMeetingItems() { var result = new CoreTable(); result.LoadColumns(typeof(MeetingItem)); return result; } private void EditMeeting(Assignment model) { CheckGrid(ref mg); if (DynamicGridUtils.EditEntity(model.Meeting.Link.ID, out var meeting)) { ItemChanged?.Invoke(this, new CalendarDataEventArgs(meeting)); Refresh(); } } private void DeleteMeeting(Assignment model) { if (!MessageWindow.ShowYesNo("Are you sure you wish to delete this meeting?", "Confirm Delete")) return; var meeting = new Meeting { ID = model.Meeting.Link.ID }; Client.Delete(meeting, "Meeting Deleted from Scheduler"); ItemChanged?.Invoke(this, new CalendarDataEventArgs(meeting)); Refresh(); SelectionChanged?.Invoke(this, new CalendarDataEventArgs(null)); } public Assignment? CreateAssignment(CalendarTimeSlot slot) { var ass = new Assignment { Date = slot.Date }; ass.Booked.Start = slot.Start; ass.Booked.Finish = slot.End; if ((AssignmentType == CalendarAssignmentType.Actual) || ((AssignmentType == CalendarAssignmentType.Automatic) && (ass.Date <= DateTime.Today))) { ass.Actual.Start = ass.Booked.Start; ass.Actual.Finish = ass.Booked.Finish; } ass.EmployeeLink.ID = slot.EmployeeID; ItemCreated?.Invoke(this, new CalendarDataEventArgs(ass)); var args = new CalendarHandledEventArgs(ass); ItemEditing?.Invoke(this, args); if (args.Status == CalendarHandledStatus.Cancel) return null; if ((args.Status == CalendarHandledStatus.Handled) || CheckGrid(ref ag).EditItems([ass])) { _assignments.Add(ass); LoadAssignment(ass, _appointments); _copiedmodel = null; } return ass; } private void EditAssignment(Assignment model) { var grid = CheckGrid(ref ag); Client.EnsureColumns(model, grid.LoadEditorColumns()); if (DynamicGridUtils.EditEntity(model)) { ItemChanged?.Invoke(this, new CalendarDataEventArgs(model)); // Refresh(); } } private void DeleteAssignment(Assignment model) { if (!MessageWindow.ShowYesNo("Are you sure you wish to delete this assignment?", "Confirm Delete")) return; var ass = new Assignment { ID = model.ID }; Client.Delete(ass, "Assignment Deleted from Scheduler"); ItemChanged?.Invoke(this, new CalendarDataEventArgs(ass)); Refresh(); SelectionChanged?.Invoke(this, new CalendarDataEventArgs(null)); } private Assignment? _copiedmodel; private void CopyAssignment(Assignment model) { _copiedmodel = model; } private Assignment PasteAssignment(CalendarTimeSlot slot, Assignment copied) { var ass = copied.Clone(); ass.Date = slot.Date; ass.ID = Guid.Empty; ass.Number = 0; ass.CommitChanges(); ass.Booked.Start = slot.Start; ass.Booked.Finish = slot.End; if ((AssignmentType == CalendarAssignmentType.Actual) || ((AssignmentType == CalendarAssignmentType.Automatic) && (ass.Date <= DateTime.Today))) { ass.Actual.Start = ass.Booked.Start; ass.Actual.Finish = ass.Booked.Finish; } ass.EmployeeLink.ID = slot.EmployeeID; ass.EmployeeLink.Clear(); Client.Save(ass, ""); _assignments.Add(ass); LoadAssignment(ass, _appointments); _copiedmodel = null; return ass; } private void EditLeave(LeaveRequest model) { var grid = CheckGrid(ref lg); Client.EnsureColumns(model, grid.LoadEditorColumns()); if (DynamicGridUtils.EditEntity(model)) { ItemChanged?.Invoke(this, new CalendarDataEventArgs(model)); Refresh(); } } private void EditStandardLeave(StandardLeave model) { var grid = CheckGrid(ref slg); Client.EnsureColumns(model, grid.LoadEditorColumns()); if (DynamicGridUtils.EditEntity(model)) { ItemChanged?.Invoke(this, new CalendarDataEventArgs(model)); Refresh(); } } private void Calendar_BlockClicked(object sender, CalendarBlockEventArgs e) { if(e.Value is AssignmentAppointment appointment) { var args = new CalendarDataEventArgs(appointment.Model); SelectionChanged?.Invoke(this, args); } else { SelectionChanged?.Invoke(this, new CalendarDataEventArgs(null)); } } #endregion #region Layout private void _settingsButton_OnClick(object sender, RoutedEventArgs e) { _splitPanel.View = DynamicSplitPanelView.Master; } private void _splitPanel_OnOnChanged(object sender, DynamicSplitPanelSettings e) { if (EventSuppressor.IsSet(Suppress.Events)) return; if (e.View == DynamicSplitPanelView.Combined && SettingsVisible != CalendarSettingsVisibility.Visible) SettingsVisible = CalendarSettingsVisibility.Visible; else if (e.View == DynamicSplitPanelView.Master && SettingsVisible == CalendarSettingsVisibility.Visible) SettingsVisible = CalendarSettingsVisibility.Hidden; } #endregion private void TextBlock_SizeChanged(object sender, SizeChangedEventArgs e) { if (sender is not TextBlock block || block.Tag is not Employee employee) return; var display = employee.Name; var comps = employee.Name.Split(' '); if(block.ActualWidth < 75) { display = (comps[0].Length > 0 ? comps[0][..1] : "") + (comps.Length > 1 ? comps.Skip(1).First()[..1].ToUpper() : ""); } else if(block.ActualWidth < 150) { display = comps.First() + " " + (comps.Length > 1 ? comps.Skip(1).First()[..1].ToUpper() : ""); } block.Text = display; } #region Date Selection private void Left_Click(object sender, RoutedEventArgs e) { SelectedDate = SelectedDate.AddDays(CalendarView switch { CalendarViewType.Day => -1, CalendarViewType.WorkWeek or CalendarViewType.Week => -7, _ => throw new InvalidEnumException(CalendarView) }); } private void Right_Click(object sender, RoutedEventArgs e) { SelectedDate = SelectedDate.AddDays(CalendarView switch { CalendarViewType.Day => 1, CalendarViewType.WorkWeek or CalendarViewType.Week => 7, _ => throw new InvalidEnumException(CalendarView) }); } private void Today_Click(object sender, RoutedEventArgs e) { SelectedDate = DateTime.Today; } private void AlwaysTodayBox_Checked(object sender, RoutedEventArgs e) { if (EventSuppressor.IsSet(Suppress.Events)) return; Properties.AlwaysStartOnToday = AlwaysTodayBox.IsChecked == true; DoSaveSettings(); } #endregion } }