JobResourcePlanner.xaml.cs 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.Drawing;
  5. using System.Globalization;
  6. using System.Linq;
  7. using System.Windows;
  8. using System.Windows.Controls;
  9. using System.Windows.Data;
  10. using System.Windows.Input;
  11. using System.Windows.Media;
  12. using Comal.Classes;
  13. using InABox.Clients;
  14. using InABox.Configuration;
  15. using InABox.Core;
  16. using InABox.DynamicGrid;
  17. using InABox.WPF;
  18. using NPOI.SS.Formula.Functions;
  19. using PRSDesktop.WidgetGroups;
  20. using System.ComponentModel;
  21. using Syncfusion.UI.Xaml.Grid;
  22. using Syncfusion.Windows.Tools.Controls;
  23. using SelectionChangedEventArgs = System.Windows.Controls.SelectionChangedEventArgs;
  24. using PRS.Shared;
  25. using Columns = InABox.Core.Columns;
  26. using InABox.Wpf;
  27. using Syncfusion.Data.Extensions;
  28. using NPOI.OpenXmlFormats.Spreadsheet;
  29. namespace PRSDesktop;
  30. public enum JobPlannerDisplayMode
  31. {
  32. JobColumns,
  33. DateColumns
  34. }
  35. public class JobResourcePlannerProperties : IUserConfigurationSettings, IDashboardProperties
  36. {
  37. public JobSelectorSettings JobSettings { get; set; }
  38. public JobSelectorData JobSelection { get; set; }
  39. public Guid ActivityType { get; set; }
  40. public int MonthsToView { get; set; }
  41. public TeamSelectorSettings TeamSettings { get; set; }
  42. public TeamSelectorData TeamSelection { get; set; }
  43. public double SplitterPosition { get; set; }
  44. public double HoursPerDay { get; set; }
  45. public double EmployeeSplitterPosition { get; set; }
  46. public bool IncludeUnApprovedLeave { get; set; }
  47. public JobPlannerDisplayMode DisplayMode { get; set; }
  48. public JobResourcePlannerProperties()
  49. {
  50. JobSettings = new JobSelectorSettings();
  51. JobSelection = new JobSelectorData();
  52. TeamSettings = new TeamSelectorSettings();
  53. TeamSelection = new TeamSelectorData();
  54. MonthsToView = 1;
  55. SplitterPosition = 0F;
  56. EmployeeSplitterPosition = 0F;
  57. HoursPerDay = 8.5;
  58. ActivityType = Guid.Empty;
  59. IncludeUnApprovedLeave = false;
  60. DisplayMode = JobPlannerDisplayMode.DateColumns;
  61. }
  62. }
  63. public partial class JobResourcePlanner : UserControl
  64. {
  65. private enum Suppress
  66. {
  67. This
  68. }
  69. private ActivityModel[] _activities = new ActivityModel[] { };
  70. private LeaveRequestModel[] _leaverequests = new LeaveRequestModel[] { };
  71. private StandardLeaveModel[] _standardleaves = new StandardLeaveModel[] { };
  72. private JobModel[] _jobs = new JobModel[] { };
  73. private EmployeeResourceModel[] _emps = new EmployeeResourceModel[] { };
  74. private List<AssignmentModel> _assignments = new List<AssignmentModel>();
  75. private DateTime[] _dates = Array.Empty<DateTime>();
  76. private double[] _totals = Array.Empty<double>();
  77. private double[] _available = Array.Empty<double>();
  78. public JobResourcePlannerProperties Properties { get; set; }
  79. public event LoadSettings<JobResourcePlannerProperties>? LoadSettings;
  80. public event SaveSettings<JobResourcePlannerProperties>? SaveSettings;
  81. private void DoLoadSettings()
  82. {
  83. Properties = LoadSettings?.Invoke(this) ?? new JobResourcePlannerProperties();
  84. }
  85. private void DoSaveSettings()
  86. {
  87. SaveSettings?.Invoke(this, Properties);
  88. }
  89. public JobResourcePlanner()
  90. {
  91. using (new EventSuppressor(Suppress.This))
  92. InitializeComponent();
  93. }
  94. public void Setup()
  95. {
  96. using (new EventSuppressor(Suppress.This))
  97. {
  98. DoLoadSettings();
  99. JobSelector.Setup();
  100. JobSelector.Settings = Properties.JobSettings;
  101. JobSelector.Selection = Properties.JobSelection;
  102. _jobs = JobSelector.GetJobData<JobModel>(r => new JobModel(r));
  103. TeamSelector.Setup();
  104. TeamSelector.Settings = Properties.TeamSettings;
  105. TeamSelector.Selection = Properties.TeamSelection;
  106. _emps = TeamSelector.GetEmployeeData<EmployeeResourceModel>((row, rosters) => new EmployeeResourceModel(row, rosters));
  107. ViewWindow.ItemsSource = new Dictionary<int, String>()
  108. {
  109. { 1, "1 Month" },
  110. { 3, "3 Months" },
  111. { 6, "6 Months" },
  112. { 12, "12 months" }
  113. };
  114. ViewWindow.SelectedValue = Properties.MonthsToView;
  115. if (Properties.SplitterPosition != 0F)
  116. TeamSelectorRow.Height = new GridLength(Properties.SplitterPosition, GridUnitType.Pixel);
  117. if (Properties.EmployeeSplitterPosition != 0F)
  118. AvailableEmployeesRow.Height = new GridLength(Properties.SplitterPosition, GridUnitType.Pixel);
  119. HoursSelector.Text = $"{Properties.HoursPerDay:F2}";
  120. LeaveType.SelectedIndex = Properties.IncludeUnApprovedLeave ? 1 : 0;
  121. Orientation.SelectedIndex = Properties.DisplayMode == JobPlannerDisplayMode.JobColumns ? 1 : 0;
  122. AvailableEmployees.Refresh(true, false);
  123. AssignedEmployees.Refresh(true, false);
  124. MultiQuery query = new MultiQuery();
  125. query.Add<StandardLeave>(
  126. LookupFactory.DefineFilter<StandardLeave>(),
  127. StandardLeaveModel.Columns
  128. );
  129. query.Add<LeaveRequest>(
  130. new Filter<LeaveRequest>(x => x.Status).IsNotEqualTo(LeaveRequestStatus.Rejected),
  131. LeaveRequestModel.Columns
  132. );
  133. query.Add<Activity>(
  134. LookupFactory.DefineFilter<Activity>(),
  135. Columns.None<Activity>().Add(x => x.ID).Add(x => x.Code).Add(x => x.Description),
  136. new SortOrder<Activity>(x => x.Code)
  137. );
  138. query.Query();
  139. _standardleaves = query.Get<StandardLeave>().Rows.Select(r=>new StandardLeaveModel(r)).ToArray();
  140. _leaverequests = query.Get<LeaveRequest>().Rows.Select(r => new LeaveRequestModel(r)).ToArray();
  141. _activities = query.Get<Activity>().Rows.Select(r => new ActivityModel(r)).ToArray();
  142. ActivityType.ItemsSource = _activities;
  143. ActivityType.SelectedValue = Properties.ActivityType;
  144. }
  145. }
  146. public void Shutdown(CancelEventArgs? cancel)
  147. {
  148. }
  149. private bool GetStandardLeaveTimes(DateTime date, out TimeSpan start, out TimeSpan finish)
  150. {
  151. bool result = false;
  152. start = TimeSpan.Zero;
  153. finish = TimeSpan.FromDays(1);
  154. var requests = _standardleaves.Where(x =>
  155. (x.From <= date)
  156. && (x.To.Add(x.ToTime) > date)
  157. );
  158. if (requests.Any())
  159. {
  160. result = true;
  161. start = TimeSpan.FromDays(1);
  162. finish = TimeSpan.Zero;
  163. foreach (var leave in requests)
  164. {
  165. var curstart = leave.From == date ? leave.FromTime : TimeSpan.Zero;
  166. start = start > curstart ? curstart : start;
  167. var curfinish = leave.To == date ? leave.ToTime : TimeSpan.FromDays(1);
  168. finish = finish < curfinish ? curfinish : finish;
  169. }
  170. }
  171. return result;
  172. }
  173. private void AdjustStandardLeave(DateTime date, ref TimeSpan time)
  174. {
  175. TimeSpan result = TimeSpan.Zero;
  176. var leaves = _standardleaves.Where(x =>
  177. (x.From <= date)
  178. && (x.To.Add(x.ToTime) > date)
  179. );
  180. foreach (var leave in leaves)
  181. {
  182. result += (leave.To == date ? leave.ToTime : TimeSpan.FromDays(1)) -
  183. (leave.From == date ? leave.FromTime : TimeSpan.Zero);
  184. }
  185. time = time >= result ? time - result : TimeSpan.Zero;
  186. }
  187. private bool GetLeaveRequestTimes(DateTime date, EmployeeResourceModel emp, out TimeSpan start, out TimeSpan finish)
  188. {
  189. bool result = false;
  190. start = TimeSpan.Zero;
  191. finish = TimeSpan.FromDays(1);
  192. var requests = _leaverequests.Where(x =>
  193. (x.From <= date)
  194. && (x.To.Add(x.ToTime) > date)
  195. && (x.EmployeeID == emp.ID)
  196. && (Properties.IncludeUnApprovedLeave ? true : x.Status == LeaveRequestStatus.Approved)
  197. );
  198. if (requests.Any())
  199. {
  200. result = true;
  201. start = TimeSpan.FromDays(1);
  202. finish = TimeSpan.Zero;
  203. foreach (var leave in requests)
  204. {
  205. var curstart = leave.From == date ? leave.FromTime : TimeSpan.Zero;
  206. start = start > curstart ? curstart : start;
  207. var curfinish = leave.To == date ? leave.ToTime : TimeSpan.FromDays(1);
  208. finish = finish < curfinish ? curfinish : finish;
  209. }
  210. }
  211. return result;
  212. }
  213. private void AdjustLeaveRequests(DateTime date, EmployeeResourceModel emp, ref TimeSpan time)
  214. {
  215. TimeSpan result = TimeSpan.Zero;
  216. var requests = _leaverequests.Where(x =>
  217. (x.From <= date)
  218. && (x.To.Add(x.ToTime) > date)
  219. && (x.EmployeeID == emp.ID)
  220. && (Properties.IncludeUnApprovedLeave ? true : x.Status == LeaveRequestStatus.Approved)
  221. );
  222. foreach (var leave in requests)
  223. {
  224. result += (leave.To == date ? leave.ToTime : TimeSpan.FromDays(1)) -
  225. (leave.From == date ? leave.FromTime : TimeSpan.Zero);
  226. }
  227. time = time >= result ? time - result : TimeSpan.Zero;
  228. }
  229. public void Refresh()
  230. {
  231. using (new WaitCursor())
  232. {
  233. var jobids = _jobs.Select(x => x.ID).ToArray();
  234. var empids = _emps.Select(x => x.ID).ToArray();
  235. var actids = _activities.Select(x => x.ID).ToArray();
  236. DateTime fromDate = DateTime.Today;
  237. DateTime toDate = DateTime.Today.AddMonths(Properties.MonthsToView);
  238. MultiQuery query = new MultiQuery();
  239. query.Add<Assignment>(
  240. new Filter<Assignment>(x=>x.EmployeeLink.ID).InList(empids)
  241. .And(x=>x.Date).IsGreaterThanOrEqualTo(fromDate)
  242. .And(x=>x.Date).IsLessThanOrEqualTo(toDate),
  243. AssignmentModel.Columns
  244. );
  245. query.Query();
  246. _assignments = query.Get<Assignment>().Rows.Select(r => new AssignmentModel(r)).ToList();
  247. var data = new DataTable();
  248. var dates = new List<DateTime>();
  249. for (var curdate = fromDate; curdate <= toDate; curdate = curdate.AddDays(1))
  250. {
  251. dates.Add(curdate);
  252. }
  253. _dates = dates.ToArray();
  254. var dataValues = new object?[dates.Count, _jobs.Length];
  255. var available = new double[dates.Count];
  256. var totals = new double[dates.Count];
  257. foreach (var (dateIdx, date) in dates.WithIndex())
  258. {
  259. double avail = 0.0F;
  260. foreach (var emp in _emps)
  261. {
  262. var roster = RosterUtils.GetRoster(emp.Roster, emp.RosterStart, date);
  263. var hours = roster.GetBlocks(date, TimeSpan.MinValue, TimeSpan.MaxValue)
  264. .Aggregate<RosterBlock, TimeSpan>(TimeSpan.Zero, (value, block) => value + block.Duration);
  265. AdjustStandardLeave(date, ref hours);
  266. AdjustLeaveRequests(date, emp, ref hours);
  267. avail += hours.TotalHours;
  268. }
  269. var total = avail;
  270. var values = new List<object?> { date };
  271. var anyjobstoday = _assignments.Where(x => (x.Date.Date == date.Date));
  272. avail -= anyjobstoday.Aggregate<AssignmentModel, double>(0F, (value, model) => value + model.BookedDuration.TotalHours);
  273. foreach (var (jobIdx, job) in _jobs.WithIndex())
  274. {
  275. var thisjobtoday = _assignments.Where(x => (x.Date.Date == date.Date) && (x.JobID == job.ID));
  276. if (thisjobtoday.Any())
  277. {
  278. var assigned = thisjobtoday.Aggregate<AssignmentModel, double>(0F,
  279. (value, model) => value + model.BookedDuration.TotalHours);
  280. dataValues[dateIdx, jobIdx] = assigned / Properties.HoursPerDay;
  281. }
  282. else
  283. dataValues[dateIdx, jobIdx] = null;
  284. }
  285. available[dateIdx] = avail / Properties.HoursPerDay;
  286. totals[dateIdx] = total / Properties.HoursPerDay;
  287. }
  288. _totals = totals;
  289. _available = available;
  290. if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
  291. {
  292. dataGrid.HeaderRowHeight = 200;
  293. data.Columns.Add("Date", typeof(DateTime));
  294. data.Columns.Add("Available", typeof(object));
  295. foreach (var job in _jobs)
  296. data.Columns.Add(job.ID.ToString(), typeof(object));
  297. foreach(var (dateIdx, date) in dates.WithIndex())
  298. {
  299. var values = new List<object?>(_jobs.Length + 3) { date };
  300. foreach (var (jobIdx, job) in _jobs.WithIndex())
  301. {
  302. values.Add(dataValues[dateIdx, jobIdx]);
  303. }
  304. values.Insert(1, available[dateIdx]);
  305. data.Rows.Add(values.ToArray());
  306. }
  307. }
  308. else if(Properties.DisplayMode == JobPlannerDisplayMode.DateColumns)
  309. {
  310. dataGrid.HeaderRowHeight = 30;
  311. data.Columns.Add("Job", typeof(object));
  312. var availableRow = new List<object?> { "Available" };
  313. foreach(var (dateIdx, date) in dates.WithIndex())
  314. {
  315. data.Columns.Add(dateIdx.ToString(), typeof(object));
  316. availableRow.Add(available[dateIdx]);
  317. }
  318. data.Rows.Add(availableRow.ToArray());
  319. foreach (var (jobIdx, job) in _jobs.WithIndex())
  320. {
  321. var values = new List<object?>(dates.Count + 1) { job };
  322. foreach(var (dateIdx, date) in dates.WithIndex())
  323. {
  324. values.Add(dataValues[dateIdx, jobIdx]);
  325. }
  326. data.Rows.Add(values.ToArray());
  327. }
  328. }
  329. dataGrid.ItemsSource = data;
  330. }
  331. }
  332. private interface ICellWrapper
  333. {
  334. DataRowView Row { get; }
  335. string ColumnName { get; }
  336. }
  337. public class GridCellInfoWrapper : ICellWrapper
  338. {
  339. private GridCellInfo Cell;
  340. public DataRowView Row => (Cell.RowData as DataRowView)!;
  341. public string ColumnName => (Cell.Column.ValueBinding as Binding)!.Path.Path;
  342. public GridCellInfoWrapper(GridCellInfo cell)
  343. {
  344. Cell = cell;
  345. }
  346. }
  347. public class GridCellWrapper : ICellWrapper
  348. {
  349. private GridCell Cell;
  350. public DataRowView Row => (Cell.DataContext as DataRowView)!;
  351. public string ColumnName => (Cell.ColumnBase?.GridColumn.ValueBinding as Binding)?.Path.Path ?? "";
  352. public GridCellWrapper(GridCell cell)
  353. {
  354. Cell = cell;
  355. }
  356. }
  357. private int? GetDateIndex(ICellWrapper cell)
  358. {
  359. if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
  360. {
  361. if (cell?.Row?.Row?.ItemArray?.First() is DateTime dt)
  362. return _dates.IndexOf(dt);
  363. return null;
  364. }
  365. else
  366. {
  367. if(int.TryParse(cell.ColumnName, out var idx))
  368. {
  369. return idx;
  370. }
  371. else
  372. {
  373. return null;
  374. }
  375. }
  376. }
  377. private double? GetTotal(ICellWrapper cell)
  378. {
  379. var dateIndex = GetDateIndex(cell);
  380. return dateIndex.HasValue ? _totals[dateIndex.Value] : null;
  381. }
  382. private double? GetAvailable(ICellWrapper cell)
  383. {
  384. if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
  385. {
  386. if (cell?.Row?.Row?.Table?.Columns?.Contains("Available") == true && cell.Row["Available"] is double d)
  387. return d;
  388. return null;
  389. }
  390. else
  391. {
  392. var dateIndex = GetDateIndex(cell);
  393. return dateIndex.HasValue ? _available[dateIndex.Value] : null;
  394. }
  395. }
  396. private abstract class LeaveConverter : IMultiValueConverter
  397. {
  398. protected JobResourcePlanner Planner { get; set; }
  399. public LeaveConverter(JobResourcePlanner planner)
  400. {
  401. Planner = planner;
  402. }
  403. protected System.Windows.Media.Color GetColor(object[] value)
  404. {
  405. if ((value[0] != DBNull.Value) && (value[0] != DependencyProperty.UnsetValue) && (value[0] is not double d || d != 0.0))
  406. return Colors.DarkSeaGreen;
  407. if (value[1] is GridCell cell)
  408. {
  409. var cellWrp = new GridCellWrapper(cell);
  410. var total = Planner.GetTotal(cellWrp);
  411. var available = Planner.GetAvailable(cellWrp);
  412. if (!total.HasValue || total == 0.0)
  413. return Colors.LightGray;
  414. if (!available.HasValue || available == 0.0F)
  415. return Colors.LightSalmon;
  416. return Colors.LightYellow;
  417. }
  418. return Colors.WhiteSmoke;
  419. }
  420. public abstract object Convert(object[] value, Type targetType, object parameter, CultureInfo culture);
  421. public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
  422. {
  423. throw new NotImplementedException();
  424. }
  425. }
  426. private class LeaveBackgroundConverter : LeaveConverter
  427. {
  428. public LeaveBackgroundConverter(JobResourcePlanner planner): base(planner) { }
  429. public override object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
  430. {
  431. return new SolidColorBrush(base.GetColor(value)) { Opacity = 0.8 };
  432. }
  433. }
  434. private class LeaveForegroundConverter : LeaveConverter
  435. {
  436. public LeaveForegroundConverter(JobResourcePlanner planner): base(planner) { }
  437. public override object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
  438. {
  439. return new SolidColorBrush(ImageUtils.GetForegroundColor(base.GetColor(value)));
  440. }
  441. }
  442. private void DataGrid_AutoGeneratingColumn(object sender, AutoGeneratingColumnArgs e)
  443. {
  444. MultiBinding CreateBinding<TConverter>(String path, TConverter converter) where TConverter : IMultiValueConverter
  445. {
  446. var binding = new MultiBinding();
  447. binding.Bindings.Add(new Binding(path));
  448. binding.Bindings.Add(new Binding() { RelativeSource = new RelativeSource(RelativeSourceMode.Self) });
  449. binding.Converter = converter;
  450. return binding;
  451. }
  452. var value = (e.Column.ValueBinding as Binding)!;
  453. if (value.Path.Path.Equals("Date"))
  454. {
  455. e.Column.Width = 80;
  456. e.Column.HeaderStyle = Resources["DateHeaderStyle"] as Style;
  457. e.Column.AllowFocus = false;
  458. }
  459. else if (value.Path.Path.Equals("Job"))
  460. {
  461. e.Column.Width = 250;
  462. e.Column.HeaderStyle = Resources["DateHeaderStyle"] as Style;
  463. e.Column.AllowFocus = false;
  464. e.Column.TextAlignment = TextAlignment.Left;
  465. e.Column.DisplayBinding = new Binding
  466. {
  467. Path = new PropertyPath(e.Column.MappingName),
  468. Converter = new FuncConverter<object?, object?>(x => x is JobModel job ? job.Name : x)
  469. };
  470. e.Column.HeaderText = "Project Name";
  471. }
  472. else if (value.Path.Path.Equals("Available"))
  473. {
  474. e.Column = new GridNumericColumn() { NumberDecimalDigits = 2, MappingName = e.Column.MappingName};
  475. e.Column.Width = 50;
  476. e.Column.HeaderStyle = e.Column.HeaderStyle = Resources["RotatedHeaderStyle"] as Style;
  477. e.Column.AllowFocus = false;
  478. e.Column.HeaderText = "Available";
  479. }
  480. else
  481. {
  482. e.Column = new GridNumericColumn() { NumberDecimalDigits = 2, MappingName = e.Column.MappingName};
  483. if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
  484. {
  485. e.Column.Width = 40;
  486. e.Column.HeaderStyle = Resources["RotatedHeaderStyle"] as Style;
  487. e.Column.HeaderText = (Guid.TryParse(value.Path.Path, out var id)
  488. ? _jobs.FirstOrDefault(x => x.ID == id)?.Name ?? value.Path.Path
  489. : value.Path.Path);
  490. }
  491. else
  492. {
  493. e.Column.Width = 50;
  494. e.Column.HeaderStyle = Resources["ContentHeaderStyle"] as Style;
  495. if(int.TryParse(value.Path.Path, out var idx))
  496. {
  497. e.Column.HeaderText = _dates[idx].ToString("dd/MM");
  498. }
  499. }
  500. e.Column.DisplayBinding = new Binding { Path = new PropertyPath(e.Column.MappingName) };
  501. e.Column.AllowFocus = true;
  502. var style = new Style(typeof(GridCell));
  503. style.Setters.Add(new Setter(BackgroundProperty, CreateBinding<LeaveBackgroundConverter>(value.Path.Path, new(this))));
  504. style.Setters.Add(new Setter(ForegroundProperty, CreateBinding<LeaveForegroundConverter>(value.Path.Path, new(this))));
  505. e.Column.CellStyle = style;
  506. e.Column.TextAlignment = TextAlignment.Center;
  507. e.Column.HorizontalHeaderContentAlignment = HorizontalAlignment.Center;
  508. }
  509. e.Column.ColumnSizer = GridLengthUnitType.None;
  510. e.Column.ShowHeaderToolTip = false;
  511. e.Column.ShowToolTip = false;
  512. }
  513. private void DataGrid_ContextMenuOpening(object sender, ContextMenuEventArgs e)
  514. {
  515. var emps = TeamSelector.GetEmployeeData<EmployeeResourceModel>((row, rosters) => new EmployeeResourceModel(row, rosters));
  516. }
  517. private void DataGrid_OnSelectionChanging(object? sender, GridSelectionChangingEventArgs e)
  518. {
  519. var selected = dataGrid.SelectionController.SelectedCells;
  520. var added = e.AddedItems.OfType<GridCellInfo>();
  521. if (selected.Any() && added.Any())
  522. {
  523. if (Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
  524. e.Cancel = added.Any(a => selected.All(s => s.Column != a.Column));
  525. else
  526. e.Cancel = selected.Union(added).GroupBy(x => x.Column).Any(g => g.Count() > 1);
  527. }
  528. }
  529. private bool bResettingSelection = false;
  530. private void DataGrid_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
  531. {
  532. bResettingSelection = true;
  533. dataGrid.SelectionController.ClearSelections(false);
  534. }
  535. private void DataGrid_OnCurrentCellActivating(object? sender, CurrentCellActivatingEventArgs e)
  536. {
  537. if (bResettingSelection)
  538. {
  539. bResettingSelection = false;
  540. return;
  541. }
  542. // if (Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
  543. // {
  544. // var thiscol = dataGrid.Columns[e.CurrentRowColumnIndex.ColumnIndex];
  545. // var selected = dataGrid.SelectionController.SelectedCells;
  546. // if (selected.Any(x => x.Column != thiscol))
  547. // e.Cancel = true;
  548. // else
  549. // e.Cancel = false;
  550. //
  551. // }
  552. // else
  553. // {
  554. // var current = e.CurrentRowColumnIndex.RowIndex;
  555. // var original = dataGrid.SelectionController.SelectedCells;
  556. // // This is Dumb, but it seems the only way to find if multiple rows have been selected
  557. // // ie if there are two cells with the same column, we must have multiple rows
  558. // // Stupid SelectedCell -> RowIndex is alwasy showing up as -1!
  559. // if (original.GroupBy(x => x.Column).Any(g=>g.Count() > 1))
  560. // e.Cancel = true;
  561. // else
  562. // e.Cancel = false;
  563. // }
  564. }
  565. private void DataGrid_OnPreviewMouseUp(object sender, MouseButtonEventArgs e)
  566. {
  567. }
  568. private void GetCellData(ICellWrapper cell, out (Guid jobID, DateTime date) result)
  569. {
  570. result = (Guid.Empty, DateTime.MinValue);
  571. if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
  572. {
  573. if (Guid.TryParse(cell.ColumnName, out var emp))
  574. {
  575. result.jobID = emp;
  576. }
  577. result.date = (DateTime)cell.Row.Row.ItemArray.First()!;
  578. }
  579. else
  580. {
  581. if(int.TryParse(cell.ColumnName, out var idx))
  582. {
  583. result.date = _dates[idx];
  584. }
  585. else
  586. {
  587. result.date = DateTime.MinValue;
  588. }
  589. var jobModel = cell.Row.Row.ItemArray.First() as JobModel;
  590. result.jobID = jobModel?.ID ?? Guid.Empty;
  591. }
  592. }
  593. private ICellWrapper Wrap(GridCellInfo cell)
  594. {
  595. return new GridCellInfoWrapper(cell);
  596. }
  597. private ICellWrapper Wrap(GridCell cell)
  598. {
  599. return new GridCellWrapper(cell);
  600. }
  601. private bool ExtractSelection(out Guid[] jobs, out DateTime[] dates)
  602. {
  603. var from = DateTime.MaxValue;
  604. var to = DateTime.MinValue;
  605. var jobList = new List<Guid>();
  606. foreach (var cell in dataGrid.GetSelectedCells())
  607. {
  608. GetCellData(Wrap(cell), out var result);
  609. if (result.jobID == Guid.Empty) continue;
  610. if (!jobList.Contains(result.jobID))
  611. jobList.Add(result.jobID);
  612. if(result.date != DateTime.MinValue)
  613. {
  614. if (result.date < from)
  615. from = result.date;
  616. if (result.date > to)
  617. to = result.date;
  618. }
  619. }
  620. if(jobList.Count > 0 && to != DateTime.MinValue && from != DateTime.MaxValue)
  621. {
  622. jobs = jobList.ToArray();
  623. var datesList = new List<DateTime>();
  624. for(DateTime date = from; date <= to; date = date.AddDays(1))
  625. {
  626. datesList.Add(date);
  627. }
  628. dates = datesList.ToArray();
  629. return true;
  630. }
  631. else
  632. {
  633. jobs = [];
  634. dates = [];
  635. return false;
  636. }
  637. }
  638. private void DataGrid_OnMouseUp(object sender, MouseButtonEventArgs e)
  639. {
  640. if (ExtractSelection(out var jobIDs, out var dates))
  641. {
  642. if(jobIDs.Length == 1)
  643. {
  644. LoadAssignedEmployees(jobIDs, dates);
  645. LoadAvailableEmployees(dates);
  646. }
  647. else
  648. {
  649. AvailableEmployees.Items = [];
  650. AssignedEmployees.Items = [];
  651. AvailableEmployees.Refresh(false, true);
  652. AssignedEmployees.Refresh(false, true);
  653. }
  654. }
  655. }
  656. private JobPlannerEmployee GetEmployee(Guid id, List<JobPlannerEmployee> list)
  657. {
  658. var result = list.FirstOrDefault(x => x.ID == id);
  659. if (result == null)
  660. {
  661. result = new JobPlannerEmployee()
  662. {
  663. ID = id,
  664. Name = _emps.FirstOrDefault(x=>x.ID == id)?.Name ?? id.ToString(),
  665. Time = TimeSpan.Zero
  666. };
  667. list.Add(result);
  668. }
  669. return result;
  670. }
  671. private void LoadAvailableEmployees(DateTime[] dates)
  672. {
  673. List<JobPlannerEmployee> availableemployees = new List<JobPlannerEmployee>();
  674. foreach (var emp in _emps)
  675. {
  676. foreach (var date in dates)
  677. {
  678. var roster = RosterUtils.GetRoster(emp.Roster, emp.RosterStart, date);
  679. var blocks = roster?.GetBlocks(date, TimeSpan.MinValue, TimeSpan.MaxValue) ?? new RosterBlock[] { };
  680. var rostered = blocks.Aggregate(TimeSpan.Zero, (time, block) => time += block.Duration);
  681. AdjustStandardLeave(date, ref rostered);
  682. AdjustLeaveRequests(date, emp, ref rostered);
  683. var assignments = _assignments.Where(x => (x.Date == date) && (x.EmployeeID == emp.ID));
  684. var assigned = assignments.Aggregate(TimeSpan.Zero, (time, assign) => time += assign.BookedDuration);
  685. if (rostered > assigned)
  686. GetEmployee(emp.ID, availableemployees).Time += rostered.Subtract(assigned);
  687. AvailableEmployees.Items = availableemployees.OrderBy(x=>x.Name).ToList();
  688. AvailableEmployees.Refresh(false, true);
  689. }
  690. }
  691. }
  692. private void LoadAssignedEmployees(Guid[] jobIDs, DateTime[] dates)
  693. {
  694. List<JobPlannerEmployee> assignedemployees = new List<JobPlannerEmployee>();
  695. foreach (var assignment in _assignments.Where(x => dates.Contains(x.Date) && jobIDs.Contains(x.JobID)))
  696. GetEmployee(assignment.EmployeeID, assignedemployees).Time += assignment.BookedDuration;
  697. AssignedEmployees.Items = assignedemployees.OrderBy(x=>x.Name).ToList();
  698. AssignedEmployees.Refresh(false, true);
  699. }
  700. private void JobSelector_OnSettingsChanged(object sender, JobSelectorSettingsChangedArgs args)
  701. {
  702. if (EventSuppressor.IsSet(Suppress.This))
  703. return;
  704. Properties.JobSettings = args.Settings;
  705. DoSaveSettings();
  706. }
  707. private void JobSelector_OnSelectionChanged(object sender, JobSelectorSelectionChangedArgs args)
  708. {
  709. if (EventSuppressor.IsSet(Suppress.This))
  710. return;
  711. Properties.JobSelection = args.Selection;
  712. _jobs = JobSelector.GetJobData<JobModel>(x => new JobModel(x));
  713. DoSaveSettings();
  714. Refresh();
  715. }
  716. private void TeamSelector_OnSettingsChanged(object sender, TeamSelectorSettingsChangedArgs args)
  717. {
  718. if (EventSuppressor.IsSet(Suppress.This))
  719. return;
  720. Properties.TeamSettings = args.Settings;
  721. DoSaveSettings();
  722. }
  723. private void TeamSelector_OnSelectionChanged(object sender, TeamSelectorSelectionChangedArgs args)
  724. {
  725. if (EventSuppressor.IsSet(Suppress.This))
  726. return;
  727. Properties.TeamSelection = args.Selection;
  728. _emps = TeamSelector.GetEmployeeData<EmployeeResourceModel>((row, rosters) => new EmployeeResourceModel(row, rosters));
  729. DoSaveSettings();
  730. Refresh();
  731. }
  732. private void JobSelector_OnSizeChanged(object sender, SizeChangedEventArgs e)
  733. {
  734. if (EventSuppressor.IsSet(Suppress.This))
  735. return;
  736. Properties.SplitterPosition = TeamSelectorRow.Height.Value;
  737. DoSaveSettings();
  738. }
  739. private void ViewWindow_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  740. {
  741. if (EventSuppressor.IsSet(Suppress.This))
  742. return;
  743. Properties.MonthsToView = (int)ViewWindow.SelectedValue;
  744. DoSaveSettings();
  745. Refresh();
  746. }
  747. private void HoursSelector_Down_Click(object sender, RoutedEventArgs e)
  748. {
  749. Properties.HoursPerDay = Math.Max(0.25F, Properties.HoursPerDay - 0.25);
  750. HoursSelector.Text = $"{Properties.HoursPerDay:F2}";
  751. DoSaveSettings();
  752. Refresh();
  753. }
  754. private void HoursSelector_Up_Click(object sender, RoutedEventArgs e)
  755. {
  756. Properties.HoursPerDay = Math.Min(24F, Properties.HoursPerDay + 0.25);
  757. HoursSelector.Text = $"{Properties.HoursPerDay:F2}";
  758. DoSaveSettings();
  759. Refresh();
  760. }
  761. private void ActivityType_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  762. {
  763. if (EventSuppressor.IsSet(Suppress.This))
  764. return;
  765. Properties.ActivityType = (Guid)(ActivityType.SelectedValue ?? Guid.Empty);
  766. DoSaveSettings();
  767. }
  768. private void AvailableEmployees_OnSizeChanged(object sender, SizeChangedEventArgs e)
  769. {
  770. if (EventSuppressor.IsSet(Suppress.This))
  771. return;
  772. Properties.EmployeeSplitterPosition = AvailableEmployeesRow.Height.Value;
  773. DoSaveSettings();
  774. }
  775. private void AvailableEmployees_OnOnAction(object sender, JobPlannerEmployee[] availables)
  776. {
  777. List<TimeSpan> edges = new List<TimeSpan>();
  778. void CheckEdges(params TimeSpan[] times)
  779. {
  780. foreach (var time in times)
  781. {
  782. if (!edges.Contains(time))
  783. edges.Add(time);
  784. }
  785. }
  786. bool IsRostered(RosterBlock[] blocks, TimeSpan start, TimeSpan finish)
  787. {
  788. foreach (var block in blocks)
  789. {
  790. if ((block.Start <= start) && (block.Finish >= finish))
  791. return true;
  792. }
  793. return false;
  794. }
  795. bool IsStandardLeave(DateTime date, TimeSpan start, TimeSpan finish)
  796. {
  797. return _standardleaves.Any(x =>
  798. (x.From.Add(x.FromTime) <= date.Add(start))
  799. && (x.To.Add(x.ToTime) >= date.Add(finish))
  800. );
  801. }
  802. bool IsLeaveRequest(DateTime date, EmployeeResourceModel emp, TimeSpan start, TimeSpan finish)
  803. {
  804. return _leaverequests.Any(x =>
  805. (x.EmployeeID == emp.ID)
  806. && (x.From.Add(x.FromTime) <= date.Add(start))
  807. && (x.To.Add(x.ToTime) >= date.Add(finish))
  808. );
  809. }
  810. bool IsAssigned(AssignmentModel[] assignments, TimeSpan start, TimeSpan finish)
  811. {
  812. foreach (var assignment in assignments)
  813. {
  814. if ((assignment.BookedStart <= start) && (assignment.BookedFinish >= finish))
  815. return true;
  816. }
  817. return false;
  818. }
  819. if (ExtractSelection(out var jobIDs, out var dates))
  820. {
  821. if (dataGrid.ItemsSource is DataTable table)
  822. {
  823. List<Assignment> updates = new List<Assignment>();
  824. foreach (var available in availables)
  825. {
  826. foreach (var date in dates)
  827. {
  828. var dateIdx = _dates.IndexOf(date);
  829. var emp = _emps.FirstOrDefault(x => x.ID == available.ID);
  830. if (emp != null)
  831. {
  832. var roster = RosterUtils.GetRoster(emp.Roster, emp.RosterStart, date);
  833. var blocks = roster?.GetBlocks(date, TimeSpan.MinValue, TimeSpan.MaxValue) ?? new RosterBlock[] { };
  834. foreach (var block in blocks)
  835. CheckEdges(block.Start, block.Finish);
  836. if (GetStandardLeaveTimes(date, out TimeSpan stdleavestart, out TimeSpan stdleavefinish))
  837. CheckEdges(stdleavestart, stdleavefinish);
  838. if (GetLeaveRequestTimes(date, emp, out TimeSpan leaverequeststart, out TimeSpan leaverequestfinish))
  839. CheckEdges(leaverequeststart, leaverequestfinish);
  840. var assignments = _assignments.Where(x => (x.Date == date) && (x.EmployeeID == emp.ID)).ToArray();
  841. foreach (var assignment in assignments)
  842. CheckEdges(assignment.BookedStart, assignment.BookedFinish);
  843. edges.Sort();
  844. var adjustment = new double[jobIDs.Length];
  845. for (int i = 0; i < edges.Count - 1; i++)
  846. {
  847. var start = edges[i];
  848. var finish = edges[i + 1];
  849. if (IsRostered(blocks, start, finish)
  850. && (!IsStandardLeave(date,start,finish))
  851. && (!IsLeaveRequest(date,emp,start,finish))
  852. && !IsAssigned(assignments, start, finish))
  853. {
  854. foreach(var (idx, jobid) in jobIDs.WithIndex())
  855. {
  856. Assignment assignment = new Assignment();
  857. assignment.ActivityLink.ID = Properties.ActivityType;
  858. assignment.EmployeeLink.ID = emp.ID;
  859. assignment.Date = date;
  860. assignment.JobLink.ID = jobid;
  861. assignment.Booked.Start = start;
  862. assignment.Booked.Finish = finish;
  863. assignment.Booked.Duration = finish - start;
  864. updates.Add(assignment);
  865. adjustment[idx] += assignment.Booked.Duration.TotalHours;
  866. }
  867. }
  868. }
  869. if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
  870. {
  871. System.Data.DataRow row = table.Rows[dateIdx];
  872. row.BeginEdit();
  873. foreach(var (idx, jobid) in jobIDs.WithIndex())
  874. {
  875. var adj = adjustment[idx];
  876. _available[dateIdx] = Math.Max(0F, _available[dateIdx] - (adj / Properties.HoursPerDay));
  877. row["Available"] = _available[dateIdx];
  878. double jobvalue = (row[jobid.ToString()] == DBNull.Value)
  879. ? adj / Properties.HoursPerDay
  880. : (double)row[jobid.ToString()] + (adj / Properties.HoursPerDay);
  881. row[jobid.ToString()] = jobvalue <= 0F ? null : jobvalue;
  882. }
  883. row.EndEdit();
  884. }
  885. else
  886. {
  887. var availableRow = table.Rows[0];
  888. availableRow.BeginEdit();
  889. foreach(var (idx, jobid) in jobIDs.WithIndex())
  890. {
  891. var job = _jobs.WithIndex().First(x => x.Value.ID == jobid);
  892. var row = table.Rows[job.Key + 1];
  893. var adj = adjustment[idx];
  894. _available[dateIdx] = Math.Max(0F, _available[dateIdx] - (adj / Properties.HoursPerDay));
  895. availableRow[dateIdx.ToString()] = _available[dateIdx];
  896. row.BeginEdit();
  897. double jobvalue = (row[dateIdx.ToString()] == DBNull.Value)
  898. ? adj / Properties.HoursPerDay
  899. : (double)row[dateIdx.ToString()] + (adj / Properties.HoursPerDay);
  900. row[dateIdx.ToString()] = jobvalue <= 0F ? null : jobvalue;
  901. row.EndEdit();
  902. }
  903. availableRow.EndEdit();
  904. foreach(var row in table.Rows.Cast<System.Data.DataRow>())
  905. {
  906. row.BeginEdit();
  907. row[dateIdx.ToString()] = row[dateIdx.ToString()];
  908. row.EndEdit();
  909. }
  910. }
  911. }
  912. }
  913. var entry = AvailableEmployees.Items.FirstOrDefault(x => x.ID == available.ID);
  914. if (entry != null)
  915. {
  916. AvailableEmployees.Items.Remove(entry);
  917. GetEmployee(entry.ID, AssignedEmployees.Items).Time += entry.Time;
  918. }
  919. }
  920. if (updates.Any())
  921. {
  922. using (new WaitCursor())
  923. {
  924. new Client<Assignment>().Save(updates, "Assigned by Job Planner");
  925. CoreTable temp = new CoreTable();
  926. temp.LoadColumns(typeof(Assignment));
  927. temp.LoadRows(updates);
  928. _assignments.AddRange(temp.Rows.Select(r => new AssignmentModel(r)));
  929. AssignedEmployees.Refresh(false, true);
  930. AvailableEmployees.Refresh(false, true);
  931. }
  932. }
  933. }
  934. }
  935. }
  936. private void AssignedEmployees_OnOnAction(object sender, JobPlannerEmployee[] employees)
  937. {
  938. if (ExtractSelection(out var jobIDs, out var dates))
  939. {
  940. if (dataGrid.ItemsSource is DataTable table)
  941. {
  942. foreach (var date in dates)
  943. {
  944. var dateIdx = _dates.IndexOf(date);
  945. var emptimes = _assignments.Where(x =>
  946. jobIDs.Contains(x.JobID)
  947. && (x.Date == date)
  948. && employees.Any(e => e.ID == x.EmployeeID)
  949. ).ToArray();
  950. var emptime = emptimes.Aggregate(TimeSpan.Zero, (time, ass) => time += ass.BookedDuration);
  951. if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
  952. {
  953. System.Data.DataRow row = table.Rows[dateIdx];
  954. row.BeginEdit();
  955. foreach(var jobid in jobIDs)
  956. {
  957. _available[dateIdx] = _available[dateIdx] + (emptime.TotalHours / Properties.HoursPerDay);
  958. row["Available"] = _available[dateIdx];
  959. double value = (row[jobid.ToString()] == DBNull.Value)
  960. ? 0.0F
  961. : (double)row[jobid.ToString()] - (emptime.TotalHours / Properties.HoursPerDay);
  962. row[jobid.ToString()] = value <= 0F ? null : value;
  963. }
  964. row.EndEdit();
  965. }
  966. else
  967. {
  968. var availableRow = table.Rows[0];
  969. availableRow.BeginEdit();
  970. foreach(var jobid in jobIDs)
  971. {
  972. var jobIdx = _jobs.WithIndex().First(x => x.Value.ID == jobid).Key;
  973. _available[dateIdx] = _available[dateIdx] + (emptime.TotalHours/Properties.HoursPerDay);
  974. availableRow[dateIdx.ToString()] = _available[dateIdx];
  975. var row = table.Rows[jobIdx + 1];
  976. row.BeginEdit();
  977. double value = (row[dateIdx.ToString()] == DBNull.Value)
  978. ? 0.0F
  979. : (double)row[dateIdx.ToString()] - (emptime.TotalHours / Properties.HoursPerDay);
  980. row[dateIdx.ToString()] = value <= 0F ? null : value;
  981. row.EndEdit();
  982. }
  983. availableRow.EndEdit();
  984. foreach(var row in table.Rows.Cast<System.Data.DataRow>())
  985. {
  986. row.BeginEdit();
  987. row[dateIdx.ToString()] = row[dateIdx.ToString()];
  988. row.EndEdit();
  989. }
  990. }
  991. }
  992. }
  993. var assignments = _assignments.Where(x =>
  994. jobIDs.Contains(x.JobID)
  995. && dates.Contains(x.Date)
  996. && employees.Any(e => e.ID == x.EmployeeID)
  997. ).ToArray();
  998. if (assignments.Any())
  999. {
  1000. using (new WaitCursor())
  1001. {
  1002. var deletes = assignments.Select(x => new Assignment() { ID = x.ID }).ToArray();
  1003. new Client<Assignment>().Delete(deletes, "Deleted from Job Planner");
  1004. var removes = AssignedEmployees.Items.Where(x => employees.Any(e => e.ID == x.ID)).ToArray();
  1005. foreach (var remove in removes)
  1006. {
  1007. GetEmployee(remove.ID, AvailableEmployees.Items).Time += remove.Time;
  1008. AssignedEmployees.Items.Remove(remove);
  1009. }
  1010. foreach (var assignment in assignments)
  1011. _assignments.Remove(assignment);
  1012. AssignedEmployees.Refresh(false, true);
  1013. AvailableEmployees.Refresh(false, true);
  1014. }
  1015. }
  1016. }
  1017. }
  1018. private void LeaveType_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  1019. {
  1020. if (EventSuppressor.IsSet(Suppress.This))
  1021. return;
  1022. Properties.IncludeUnApprovedLeave = LeaveType.SelectedIndex > 0;
  1023. DoSaveSettings();
  1024. Refresh();
  1025. }
  1026. private void Orientation_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  1027. {
  1028. if (EventSuppressor.IsSet(Suppress.This))
  1029. return;
  1030. Properties.DisplayMode = Orientation.SelectedIndex <= 0
  1031. ? JobPlannerDisplayMode.DateColumns
  1032. : JobPlannerDisplayMode.JobColumns;
  1033. DoSaveSettings();
  1034. Refresh();
  1035. }
  1036. private void AvailableEmployees_OnAfterRefresh(object sender, AfterRefreshEventArgs args)
  1037. {
  1038. AvailableEmployees.Items = AvailableEmployees.Items.OrderBy(x => x.Name).ToList();
  1039. }
  1040. private void AssignedEmployees_OnAfterRefresh(object sender, AfterRefreshEventArgs args)
  1041. {
  1042. AssignedEmployees.Items = AssignedEmployees.Items.OrderBy(x => x.Name).ToList();
  1043. }
  1044. }