AssignmentCostingUtils.cs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. using Comal.Classes;
  2. using InABox.Configuration;
  3. using InABox.Core;
  4. using InABox.Database;
  5. using InABox.Database.Stores;
  6. using InABox.Scripting;
  7. using org.omg.CosNaming.NamingContextPackage;
  8. using sun.util.resources.cldr.haw;
  9. using Syncfusion.UI.Xaml.Grid.Converters;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.Linq;
  13. using System.Text;
  14. using System.Threading.Tasks;
  15. namespace PRSStores.AssignmentCosting
  16. {
  17. public class TimeSheetBlock
  18. {
  19. public TimeSpan Start { get; set; }
  20. public TimeSpan Finish { get; set; }
  21. public TimeSpan Duration => Finish - Start;
  22. public required bool IsPaid { get; set; }
  23. }
  24. public class DayTimeSheet
  25. {
  26. public IList<TimeSheetBlock> Blocks { get; private init; }
  27. public IEnumerable<TimeSheetBlock> PaidBlocks => Blocks.Where(x => x.IsPaid);
  28. public TimeSpan PaidDuration => PaidBlocks.Aggregate(TimeSpan.Zero, (a, b) => a + b.Duration);
  29. private DayTimeSheet(IList<TimeSheetBlock> blocks)
  30. {
  31. Blocks = blocks;
  32. }
  33. /// <summary>
  34. /// Create a new <see cref="DayTimeSheet"/> from a given list of <see cref="TimeSheet"/>s. The <paramref name="overtime"/> is
  35. /// used to create blocks for unpaid time on the timesheet.
  36. /// </summary>
  37. /// <exception cref="Exception">If there are no elements in <paramref name="sheets"/></exception>
  38. public static DayTimeSheet FromTimeSheets(IEnumerable<TimeSheet> sheets, IList<OvertimeInterval> overtime)
  39. {
  40. var sheetArray = sheets.Select(x =>
  41. {
  42. return (
  43. start: x.Approved != DateTime.MinValue ? x.ApprovedStart : x.Start,
  44. finish: x.Approved != DateTime.MinValue ? x.ApprovedFinish : x.Finish,
  45. sheet: x);
  46. }).ToArray();
  47. sheetArray.SortBy(x => x.start);
  48. if(sheetArray.Length == 0)
  49. {
  50. throw new Exception("No timesheets provided.");
  51. }
  52. // Merge all timesheets into a single list of time blocks; overlapping timesheets will be merged into one.
  53. var blocks = new List<TimeSheetBlock>();
  54. TimeSheetBlock? current = null;
  55. foreach(var (start, finish, sheet) in sheetArray)
  56. {
  57. if(current is null || start > current.Finish)
  58. {
  59. current = new TimeSheetBlock
  60. {
  61. Start = start,
  62. Finish = finish,
  63. // Doesn't matter what we put here, since this block will not appear in the final list.
  64. IsPaid = true
  65. };
  66. blocks.Add(current);
  67. }
  68. else
  69. {
  70. current.Finish = finish;
  71. }
  72. }
  73. // Now, apply overtime to take out chunks of time that are unpaid.
  74. var newBlocks = new List<TimeSheetBlock>();
  75. current = null;
  76. TimeSheetBlock? currentUnpaidBlock = null;
  77. // We will definitely have at least one block, based on the above algorithm, and since we ensured that sheetArray is non-empty.
  78. var currentTime = TimeSpan.Zero;
  79. var met = new HashSet<TimeSheetBlock>();
  80. OvertimeUtils.EvaluateOvertime(blocks, overtime, x => x.Duration, (block, interval, duration) =>
  81. {
  82. if (interval is null) return;
  83. if (met.Add(block))
  84. {
  85. currentTime = block.Start;
  86. }
  87. if (interval.IsPaid)
  88. {
  89. if(current is null || currentTime > current.Finish)
  90. {
  91. current = new TimeSheetBlock
  92. {
  93. Start = currentTime,
  94. Finish = currentTime + duration,
  95. IsPaid = true
  96. };
  97. newBlocks.Add(current);
  98. currentUnpaidBlock = null;
  99. }
  100. else
  101. {
  102. current.Finish = currentTime + duration;
  103. }
  104. }
  105. else
  106. {
  107. if(currentUnpaidBlock is null || currentTime > currentUnpaidBlock.Finish)
  108. {
  109. currentUnpaidBlock = new TimeSheetBlock
  110. {
  111. Start = currentTime,
  112. Finish = currentTime + duration,
  113. IsPaid = false
  114. };
  115. newBlocks.Add(currentUnpaidBlock);
  116. current = null;
  117. }
  118. else
  119. {
  120. currentUnpaidBlock.Finish = currentTime + duration;
  121. }
  122. }
  123. currentTime += duration;
  124. });
  125. return new(newBlocks);
  126. }
  127. }
  128. public class Block
  129. {
  130. public Assignment? Assignment { get; set; } // Set to null for lost time blocks that won't be costed anywhere.
  131. public TimeSpan Start { get; set; }
  132. public TimeSpan Finish { get; set; }
  133. public TimeSpan Duration
  134. {
  135. get => Finish - Start;
  136. set => Finish = Start + value;
  137. }
  138. public Block Copy()
  139. {
  140. return new Block
  141. {
  142. Assignment = Assignment,
  143. Start = Start,
  144. Finish = Finish,
  145. };
  146. }
  147. public static Block FromAssignment(Assignment assignment)
  148. {
  149. return new Block
  150. {
  151. Assignment = assignment,
  152. Start = assignment.EffectiveStartTime(),
  153. Finish = assignment.EffectiveFinishTime()
  154. };
  155. }
  156. public static Block Transient(TimeSpan start, TimeSpan finish)
  157. {
  158. return new Block
  159. {
  160. Assignment = null,
  161. Start = start,
  162. Finish = finish
  163. };
  164. }
  165. }
  166. public class BeforeProcessArgs(Assignment[] assignments, TimeSheet[] timesheets)
  167. {
  168. public Assignment[] Assignments { get; } = assignments;
  169. public TimeSheet[] TimeSheets { get; } = timesheets;
  170. }
  171. public class AdjustTimeBlocksArgs(DayTimeSheet timeSheet, List<Block> blocks)
  172. {
  173. public DayTimeSheet TimeSheet { get; } = timeSheet;
  174. public List<Block> Blocks { get; set; } = blocks;
  175. }
  176. public class ProcessTimeBlocksArgs(DayTimeSheet timeSheet, List<Block> blocks)
  177. {
  178. public DayTimeSheet TimeSheet { get; } = timeSheet;
  179. public List<Block> Blocks { get; set; } = blocks;
  180. }
  181. public class AfterOvertimeArgs(DayTimeSheet timeSheet, List<Block> blocks, OvertimeInterval[] overtime, Assignment[] assignments)
  182. {
  183. public DayTimeSheet TimeSheet { get; } = timeSheet;
  184. public List<Block> Blocks { get; } = blocks;
  185. public OvertimeInterval[] Overtime { get; } = overtime;
  186. public Assignment[] Assignments { get; } = assignments;
  187. }
  188. public interface IAssignmentCostingScript
  189. {
  190. void BeforeProcess(BeforeProcessArgs args);
  191. void AdjustTimeBlocks(AdjustTimeBlocksArgs args);
  192. void ProcessTimeBlocks(ProcessTimeBlocksArgs args);
  193. void AfterOvertime(AfterOvertimeArgs args);
  194. }
  195. public class AssignmentCostingUtils : ISettingsStoreEventHandler<AssignmentCostSettings>
  196. {
  197. static AssignmentCostSettings? _costSettings;
  198. public static AssignmentCostSettings GetSettings(IStore store)
  199. {
  200. _costSettings ??= new GlobalConfiguration<AssignmentCostSettings>("", new DbConfigurationProvider<GlobalSettings>(store.UserID))
  201. .Load(false);
  202. return _costSettings;
  203. }
  204. #region ISettingsStoreEventHandler
  205. public IStore Parent { get; set; } = null!; // Set by GlobalSettingsStore
  206. public void AfterDelete(AssignmentCostSettings entity)
  207. {
  208. _costSettings = null;
  209. ClearScript();
  210. }
  211. public void AfterSave(AssignmentCostSettings entity)
  212. {
  213. _costSettings = entity;
  214. ClearScript();
  215. }
  216. public void BeforeDelete(AssignmentCostSettings entity)
  217. {
  218. }
  219. public void BeforeSave(AssignmentCostSettings entity)
  220. {
  221. }
  222. #endregion
  223. #region Script
  224. public static string DefaultScript()
  225. {
  226. return
  227. @"using PRSStores.AssignmentCosting;
  228. using InABox.Core;
  229. using System.Collections.Generic;
  230. // 'Module' *must* implement " + nameof(IAssignmentCostingScript) + @"
  231. public class Module: " + nameof(IAssignmentCostingScript) + @"
  232. {
  233. public void " + nameof(IAssignmentCostingScript.BeforeProcess) + @"(" + nameof(BeforeProcessArgs) + @" args)
  234. {
  235. // Perform pre-processing
  236. }
  237. public void " + nameof(IAssignmentCostingScript.AdjustTimeBlocks) + @"(" + nameof(AdjustTimeBlocksArgs) + @" args)
  238. {
  239. // At this stage, adjust the list of blocks (given by args." + nameof(AdjustTimeBlocksArgs.Blocks) + @") before any algorithm has
  240. // been applied to them. The blocks are ordered by start time, and are represented by one per each assignment being processed. At this stage,
  241. // they may overlap.
  242. // args." + nameof(AdjustTimeBlocksArgs.Blocks) + @" will then be passed through algorithm given in the settings, which will fill
  243. // up all the time on the day's timesheet by extending blocks as needed to fill all unfilled time.
  244. }
  245. public void " + nameof(IAssignmentCostingScript.ProcessTimeBlocks) + @"(" + nameof(ProcessTimeBlocksArgs) + @" args)
  246. {
  247. // Called after the time filling algorithm has been applied, but before the overtime rules. args." + nameof(ProcessTimeBlocksArgs.Blocks) + @" will
  248. // now contain a list of blocks, filling continuous time from the start of the timesheet to its end. Process as desired.
  249. }
  250. public void " + nameof(IAssignmentCostingScript.AfterOvertime) + @"(" + nameof(AfterOvertimeArgs) + @" args)
  251. {
  252. // Once the overtime rules have been applied and the assignment cost calculated, post-process the assignments before saving to the database.
  253. }
  254. }";
  255. }
  256. private static Type? _scriptObjectType;
  257. private static bool _hasCheckedScript;
  258. private static void ClearScript()
  259. {
  260. _scriptObjectType = null;
  261. _hasCheckedScript = false;
  262. }
  263. private static Type? GetScriptObjectType(IStore store)
  264. {
  265. if (_hasCheckedScript)
  266. {
  267. return _scriptObjectType;
  268. }
  269. var settings = GetSettings(store);
  270. if (!string.IsNullOrWhiteSpace(settings.Script))
  271. {
  272. var document = new ScriptDocument(settings.Script);
  273. if (!document.Compile())
  274. {
  275. Logger.Send(LogType.Error, store.UserID, "Script failed to compile!");
  276. _scriptObjectType = null;
  277. }
  278. else
  279. {
  280. _scriptObjectType = document.GetClassType();
  281. }
  282. }
  283. else
  284. {
  285. _scriptObjectType = null;
  286. }
  287. _hasCheckedScript = true;
  288. return _scriptObjectType;
  289. }
  290. private static IAssignmentCostingScript? GetScriptObject(IStore store)
  291. {
  292. var type = GetScriptObjectType(store);
  293. if(type is not null)
  294. {
  295. var obj = Activator.CreateInstance(type) as IAssignmentCostingScript;
  296. if(obj is null)
  297. {
  298. Logger.Send(LogType.Error, store.UserID, $"Assignment costing script module does not implement {typeof(IAssignmentCostingScript).Name}");
  299. }
  300. return obj;
  301. }
  302. else
  303. {
  304. return null;
  305. }
  306. }
  307. private static void ScriptBeforeProcess(IAssignmentCostingScript? script, Assignment[] assignments, TimeSheet[] timesheets)
  308. {
  309. script?.BeforeProcess(new(assignments, timesheets));
  310. }
  311. private static List<Block> ScriptAdjustTimeBlocks(IAssignmentCostingScript? script, DayTimeSheet timeSheet, List<Block> blocks)
  312. {
  313. if(script is not null)
  314. {
  315. var args = new AdjustTimeBlocksArgs(timeSheet, blocks);
  316. script.AdjustTimeBlocks(args);
  317. return args.Blocks;
  318. }
  319. else
  320. {
  321. return blocks;
  322. }
  323. }
  324. private static List<Block> ScriptProcessTimeBlocks(IAssignmentCostingScript? script, DayTimeSheet timeSheet, List<Block> blocks)
  325. {
  326. if(script is not null)
  327. {
  328. var args = new ProcessTimeBlocksArgs(timeSheet, blocks);
  329. script.ProcessTimeBlocks(args);
  330. return args.Blocks;
  331. }
  332. else
  333. {
  334. return blocks;
  335. }
  336. }
  337. private static void ScriptAfterOvertime(IAssignmentCostingScript? script, DayTimeSheet timeSheet, List<Block> blocks, OvertimeInterval[] overtime, Assignment[] assignments)
  338. {
  339. script?.AfterOvertime(new(timeSheet, blocks, overtime, assignments));
  340. }
  341. #endregion
  342. private static List<Block> AdjustTimeBlocksBasic(List<Block> blocks)
  343. {
  344. // No change for basic mode.
  345. return blocks;
  346. }
  347. private static List<Block> AdjustTimeBlocksScale(DayTimeSheet timeSheet, List<Block> blocks)
  348. {
  349. // We scale the blocks until their total is equal to 'PaidDuration'
  350. var totalBlockDuration = blocks.Aggregate(TimeSpan.Zero, (a, b) => a + b.Duration);
  351. var adjustFactor = timeSheet.PaidDuration.TotalHours / totalBlockDuration.TotalHours;
  352. // Adjust block size
  353. foreach(var block in blocks)
  354. {
  355. block.Duration *= adjustFactor;
  356. }
  357. // Now we need to adjust the block positioning.
  358. var paidBlocks = timeSheet.PaidBlocks.ToList();
  359. var curPaidBlockIdx = 0;
  360. TimeSheetBlock? GetPaidBlock() => curPaidBlockIdx < paidBlocks.Count ? paidBlocks[curPaidBlockIdx] : null;
  361. var timeSheetStart = GetPaidBlock()?.Start ?? TimeSpan.Zero;
  362. var curTime = timeSheetStart;
  363. var curPaidBlockDuration = GetPaidBlock()?.Duration ?? TimeSpan.Zero;
  364. void AdvancePaidBlock()
  365. {
  366. ++curPaidBlockIdx;
  367. var paidBlock = GetPaidBlock();
  368. curPaidBlockDuration = paidBlock?.Duration ?? TimeSpan.Zero;
  369. if(paidBlock is not null)
  370. {
  371. curTime = paidBlock.Start;
  372. }
  373. }
  374. var newBlocks = new List<Block>();
  375. foreach (var block in blocks)
  376. {
  377. var duration = block.Duration;
  378. if(duration == TimeSpan.Zero)
  379. {
  380. block.Start = curTime;
  381. block.Finish = curTime + duration;
  382. newBlocks.Add(block);
  383. continue;
  384. }
  385. while (duration > TimeSpan.Zero)
  386. {
  387. var paidBlock = GetPaidBlock();
  388. if (paidBlock != null)
  389. {
  390. if (duration > curPaidBlockDuration)
  391. {
  392. // In this case, the block is more than the rest of
  393. // the current timesheet, so we use up all the remaining timesheet
  394. // time, and then move to the next timesheet.
  395. var newBlock = block.Copy();
  396. newBlock.Start = curTime;
  397. newBlock.Finish = curTime + curPaidBlockDuration;
  398. newBlocks.Add(newBlock);
  399. duration -= curPaidBlockDuration;
  400. AdvancePaidBlock();
  401. }
  402. else
  403. {
  404. // Otherwise, we use up the entire block, and decrease the interval by the duration remaining.
  405. block.Start = curTime;
  406. block.Finish = curTime + duration;
  407. newBlocks.Add(block);
  408. curPaidBlockDuration -= duration;
  409. curTime += duration;
  410. duration = TimeSpan.Zero;
  411. if(curPaidBlockDuration == TimeSpan.Zero)
  412. {
  413. AdvancePaidBlock();
  414. }
  415. }
  416. }
  417. else
  418. {
  419. // Do nothing; we've finished the timesheets, so now we just set all the remaining blocks to zero.
  420. block.Start = curTime;
  421. block.Duration = TimeSpan.Zero;
  422. newBlocks.Add(block);
  423. break;
  424. }
  425. }
  426. }
  427. return newBlocks;
  428. }
  429. /// <summary>
  430. /// Taking a list of <see cref="Block"/>, chop and extend them to match the bounds of the <paramref name="timeSheet"/>.
  431. /// </summary>
  432. /// <remarks>
  433. /// <paramref name="blocks"/> <b>must</b> be in continuous time, however they need not correspond at all to <paramref name="timeSheet"/> yet;
  434. /// that is what this function does.
  435. /// </remarks>
  436. private static List<Block> MaskBlocksToTimeSheet(DayTimeSheet timeSheet, List<Block> blocks)
  437. {
  438. var paidBlocks = timeSheet.PaidBlocks.ToList();
  439. if(paidBlocks.Count == 0)
  440. {
  441. // No time to be paid, so all assignments have effective duration of zero.
  442. foreach(var block in blocks)
  443. {
  444. block.Duration = TimeSpan.Zero;
  445. }
  446. return blocks;
  447. }
  448. // Fill in gaps in the time sheet with unpaid blocks, so we get a continuous time sheet.
  449. var timeSheetBlocks = new List<TimeSheetBlock>();
  450. foreach(var block in timeSheet.Blocks)
  451. {
  452. if(timeSheetBlocks.Count > 0)
  453. {
  454. var last = timeSheetBlocks[^1];
  455. if(last.Finish < block.Start)
  456. {
  457. timeSheetBlocks.Add(new TimeSheetBlock
  458. {
  459. IsPaid = false,
  460. Start = last.Finish,
  461. Finish = block.Start
  462. });
  463. }
  464. }
  465. timeSheetBlocks.Add(block);
  466. }
  467. // Now that the blocks have continuous time, we chop to match the timesheet paid blocks.
  468. // First, shove all the blocks at the start of the day up to match with the time sheet start.
  469. var timeSheetStart = timeSheetBlocks[0].Start;
  470. var anyAtStart = false;
  471. foreach(var block in blocks)
  472. {
  473. if(block.Start < timeSheetStart)
  474. {
  475. anyAtStart = true;
  476. block.Start = timeSheetStart;
  477. if(block.Finish < timeSheetStart)
  478. {
  479. block.Finish = timeSheetStart;
  480. }
  481. }
  482. else
  483. {
  484. // Once we've found the first block that doesn't begin before the timesheet begins, we can set its time to start
  485. // at the time sheet, unless we already have one at the start.
  486. if (!anyAtStart)
  487. {
  488. block.Start = timeSheetStart;
  489. anyAtStart = true;
  490. }
  491. break;
  492. }
  493. }
  494. var curBlockIdx = 0;
  495. TimeSheetBlock? GetTimeSheetBlock() => curBlockIdx < timeSheetBlocks.Count ? timeSheetBlocks[curBlockIdx] : null;
  496. var curBlockDuration = GetTimeSheetBlock()?.Duration ?? TimeSpan.Zero;
  497. var curTime = timeSheetStart;
  498. void AdvanceBlock()
  499. {
  500. ++curBlockIdx;
  501. var block = GetTimeSheetBlock();
  502. curBlockDuration = block?.Duration ?? TimeSpan.Zero;
  503. if(block is not null)
  504. {
  505. curTime = block.Start;
  506. }
  507. }
  508. var newBlocks = new List<Block>();
  509. foreach (var block in blocks)
  510. {
  511. var duration = block.Duration;
  512. if(duration == TimeSpan.Zero)
  513. {
  514. newBlocks.Add(block);
  515. continue;
  516. }
  517. while (duration > TimeSpan.Zero)
  518. {
  519. var paidBlock = GetTimeSheetBlock();
  520. if (paidBlock != null)
  521. {
  522. if (duration > curBlockDuration)
  523. {
  524. // In this case, the block is more than the rest of
  525. // the current timesheet, so we use up all the remaining timesheet
  526. // time, and then move to the next timesheet.
  527. var newBlock = block.Copy();
  528. newBlock.Start = curTime;
  529. newBlock.Finish = curTime + curBlockDuration;
  530. if (paidBlock.IsPaid)
  531. {
  532. newBlocks.Add(newBlock);
  533. }
  534. duration -= curBlockDuration;
  535. AdvanceBlock();
  536. }
  537. else
  538. {
  539. // Otherwise, we use up the entire block, and decrease the interval by the duration remaining.
  540. block.Start = curTime;
  541. block.Finish = curTime + duration;
  542. if (paidBlock.IsPaid)
  543. {
  544. newBlocks.Add(block);
  545. }
  546. curTime = block.Finish;
  547. curBlockDuration -= duration;
  548. duration = TimeSpan.Zero;
  549. if(curBlockDuration == TimeSpan.Zero)
  550. {
  551. AdvanceBlock();
  552. }
  553. }
  554. }
  555. else
  556. {
  557. // Do nothing; we've finished the timesheets, so now we just set all the remaining blocks to zero.
  558. block.Duration = TimeSpan.Zero;
  559. break;
  560. }
  561. }
  562. }
  563. // Extend the last block to the end of the current timesheet block, if we haven't reached the end already.
  564. var lastBlock = newBlocks[^1];
  565. if(GetTimeSheetBlock() is TimeSheetBlock nextTimeSheetBlock)
  566. {
  567. lastBlock.Finish = nextTimeSheetBlock.Finish;
  568. AdvanceBlock();
  569. }
  570. // Next, if there are still more timesheet blocks, fill them with 'lastBlock' as well.
  571. while(GetTimeSheetBlock() is TimeSheetBlock block)
  572. {
  573. var newBlock = lastBlock.Copy();
  574. newBlock.Start = block.Start;
  575. newBlock.Finish = block.Finish;
  576. AdvanceBlock();
  577. }
  578. return newBlocks;
  579. }
  580. private static List<Block> AdjustTimeBlocksExtend(DayTimeSheet timeSheet, List<Block> blocks)
  581. {
  582. if(blocks.Count == 0)
  583. {
  584. return blocks;
  585. }
  586. // First, chop and extend blocks to fill continuous time, from the beginning of the first block to the end of the last block.
  587. // Block chopping
  588. var lastEnd = TimeSpan.Zero;
  589. foreach(var block in blocks)
  590. {
  591. // We know this check is enough to check overlap, since the blocks are in increasing order of their start time.
  592. if (lastEnd <= block.Start)
  593. {
  594. // This block does not overlap any previous blocks. Do not update duration.
  595. lastEnd = block.Finish;
  596. }
  597. else if(block.Finish >= lastEnd)
  598. {
  599. // This block ends after the block that we are overlapping. Update start.
  600. block.Start = lastEnd;
  601. lastEnd = block.Finish;
  602. }
  603. else
  604. {
  605. // This assignment is entirely contained within the assignment being overlapped.
  606. block.Start = lastEnd;
  607. block.Finish = lastEnd;
  608. }
  609. }
  610. // Block extending
  611. for(int i = 0; i < blocks.Count - 1; ++i)
  612. {
  613. var block = blocks[i];
  614. var nextBlock = blocks[i + 1];
  615. if(block.Finish < nextBlock.Start)
  616. {
  617. block.Finish = nextBlock.Start;
  618. }
  619. }
  620. // Now that the blocks have continuous time, we chop to match the timesheet paid blocks.
  621. return MaskBlocksToTimeSheet(timeSheet, blocks);
  622. }
  623. private static List<Block> AdjustTimeBlocksFillTimeSheet(DayTimeSheet timeSheet, List<Block> blocks)
  624. {
  625. if(blocks.Count == 0)
  626. {
  627. return blocks;
  628. }
  629. // First, chop blocks to ensure they don't overlap
  630. var lastEnd = TimeSpan.Zero;
  631. foreach(var block in blocks)
  632. {
  633. // We know this check is enough to check overlap, since the blocks are in increasing order of their start time.
  634. if (lastEnd <= block.Start)
  635. {
  636. // This block does not overlap any previous blocks. Do not update duration.
  637. lastEnd = block.Finish;
  638. }
  639. else if(block.Finish >= lastEnd)
  640. {
  641. // This block ends after the block that we are overlapping. Update start.
  642. block.Start = lastEnd;
  643. lastEnd = block.Finish;
  644. }
  645. else
  646. {
  647. // This assignment is entirely contained within the assignment being overlapped.
  648. block.Start = lastEnd;
  649. block.Finish = lastEnd;
  650. }
  651. }
  652. // Fill up remaining gaps in time with transient, "lost time" blocks. For simplicity, so that we don't have to query
  653. // the timesheet yet, we just go from the start to the end of the day. 'MaskBlocksToTimeSheet' will take care of the rest.
  654. var start = TimeSpan.Zero;
  655. var finish = TimeSpan.FromDays(1).Subtract(TimeSpan.FromTicks(1));
  656. // Fill to start of day.
  657. if (blocks[0].Start > start)
  658. {
  659. blocks.Insert(0, Block.Transient(start, blocks[0].Start));
  660. }
  661. // Fill in gaps between blocks
  662. var i = 0;
  663. while(i < blocks.Count - 1)
  664. {
  665. var block = blocks[i];
  666. var nextBlock = blocks[i + 1];
  667. if(nextBlock.Start > block.Finish)
  668. {
  669. blocks.Insert(i + 1, Block.Transient(block.Finish, nextBlock.Start));
  670. i += 2;
  671. }
  672. else
  673. {
  674. i += 1;
  675. }
  676. }
  677. // Fill to end of day.
  678. if (blocks[^1].Finish < finish)
  679. {
  680. blocks.Add(Block.Transient(blocks[^1].Finish, finish));
  681. }
  682. return MaskBlocksToTimeSheet(timeSheet, blocks);
  683. }
  684. public static List<Block> AdjustTimeBlocks(AssignmentCostSettings settings, DayTimeSheet timeSheet, List<Block> blocks)
  685. {
  686. return settings.Algorithm switch
  687. {
  688. AssignmentCostFillAlgorithm.Basic => AdjustTimeBlocksBasic(blocks),
  689. AssignmentCostFillAlgorithm.Scale => AdjustTimeBlocksScale(timeSheet, blocks),
  690. AssignmentCostFillAlgorithm.Extend => AdjustTimeBlocksExtend(timeSheet, blocks),
  691. AssignmentCostFillAlgorithm.FillTimeSheet => AdjustTimeBlocksFillTimeSheet(timeSheet, blocks),
  692. _ => blocks,
  693. };
  694. }
  695. public static void CheckAssignmentCosts(IStore store, DateTime date, Guid employeeID)
  696. {
  697. var settings = GetSettings(store);
  698. if (employeeID == Guid.Empty) return;
  699. var assignmentTask = Task.Run(() =>
  700. store.Provider.Query(
  701. Filter<Assignment>.Where(x => x.Date).IsEqualTo(date)
  702. .And(x => x.EmployeeLink.ID).IsEqualTo(employeeID),
  703. Columns.Required<Assignment>()
  704. .Add(x => x.ID)
  705. .Add(x => x.Cost)
  706. .Add(x => x.Actual.Start)
  707. .Add(x => x.Actual.Finish)
  708. .Add(x => x.Actual.Duration)
  709. .Add(x => x.Booked.Start)
  710. .Add(x => x.Booked.Finish)
  711. .Add(x => x.Booked.Duration))
  712. .ToArray<Assignment>());
  713. var timesheetTask = Task.Run(() =>
  714. store.Provider.Query(
  715. Filter<TimeSheet>.Where(x => x.Date).IsEqualTo(date)
  716. .And(x => x.EmployeeLink.ID).IsEqualTo(employeeID),
  717. Columns.None<TimeSheet>()
  718. .Add(x => x.Approved)
  719. .Add(x => x.Start)
  720. .Add(x => x.Finish)
  721. .Add(x => x.ApprovedStart)
  722. .Add(x => x.ApprovedFinish))
  723. .ToArray<TimeSheet>());
  724. var employeeTask = Task.Run(() =>
  725. store.Provider.Query(
  726. Filter<Employee>.Where(x => x.ID).IsEqualTo(employeeID),
  727. Columns.None<Employee>()
  728. .Add(x => x.RosterStart)
  729. .Add(x => x.HourlyRate))
  730. .ToObjects<Employee>().FirstOrDefault());
  731. var rosterItemsTask = Task.Run(() =>
  732. store.Provider.Query(
  733. Filter<EmployeeRosterItem>.Where(x => x.Employee.ID).IsEqualTo(employeeID),
  734. Columns.None<EmployeeRosterItem>()
  735. .Add(x => x.Overtime.ID),
  736. new SortOrder<EmployeeRosterItem>(x => x.Day))
  737. .ToArray<EmployeeRosterItem>());
  738. var timesheets = timesheetTask.Result;
  739. var employee = employeeTask.Result;
  740. if (timesheets.Length == 0 || employee is null) return;
  741. var assignments = assignmentTask.Result;
  742. var rosterItems = rosterItemsTask.Result;
  743. var overtimeID = RosterUtils.GetRoster(rosterItems, employee.RosterStart, date)?.Overtime.ID ?? Guid.Empty;
  744. if(overtimeID == Guid.Empty) return;
  745. var overtime = store.Provider.Query(
  746. Filter<OvertimeInterval>.Where(x => x.Overtime.ID).IsEqualTo(overtimeID),
  747. Columns.None<OvertimeInterval>()
  748. .Add(x => x.Interval)
  749. .Add(x => x.IntervalType)
  750. .Add(x => x.Multiplier)
  751. .Add(x => x.IsPaid),
  752. new SortOrder<OvertimeInterval>(x => x.Sequence))
  753. .ToArray<OvertimeInterval>();
  754. var script = GetScriptObject(store);
  755. ScriptBeforeProcess(script, assignments, timesheets);
  756. // We need to sort the assignments, because both the chopping algorithm and the overtime algorithm requires it.
  757. assignments.SortBy(x => x.EffectiveStartTime());
  758. var blocks = new List<Block>();
  759. foreach(var assignment in assignments)
  760. {
  761. blocks.Add(Block.FromAssignment(assignment));
  762. }
  763. var timeSheet = DayTimeSheet.FromTimeSheets(timesheets, overtime);
  764. blocks = ScriptAdjustTimeBlocks(script, timeSheet, blocks);
  765. blocks = AdjustTimeBlocks(settings, timeSheet, blocks);
  766. blocks = ScriptProcessTimeBlocks(script, timeSheet, blocks);
  767. var totalAccountingHours = new Dictionary<Assignment, double>();
  768. var totalHours = new Dictionary<Assignment, double>();
  769. OvertimeUtils.EvaluateOvertime(
  770. blocks,
  771. // We've already taken into account the IsPaid flags, so here we remove the unpaid ones from the intervals.
  772. overtime.Where(x => x.IsPaid).ToArray(),
  773. x => x.Duration,
  774. (block, interval, duration) =>
  775. {
  776. if(block.Assignment is not null)
  777. {
  778. totalAccountingHours[block.Assignment] =
  779. totalAccountingHours.GetValueOrAdd(block.Assignment)
  780. + duration.TotalHours;
  781. totalHours[block.Assignment] =
  782. totalHours.GetValueOrAdd(block.Assignment)
  783. + duration.TotalHours * (interval?.Multiplier ?? 1);
  784. }
  785. });
  786. foreach(var assignment in assignments)
  787. {
  788. assignment.Cost = totalHours.GetValueOrDefault(assignment) * employee.HourlyRate;
  789. assignment.CostedHours = totalAccountingHours.GetValueOrDefault(assignment);
  790. }
  791. ScriptAfterOvertime(script, timeSheet, blocks, overtime, assignments);
  792. store.Provider.Save(assignments.Where(x => x.IsChanged()));
  793. }
  794. }
  795. }