| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917 |
- using Comal.Classes;
- using InABox.Configuration;
- using InABox.Core;
- using InABox.Database;
- using InABox.Database.Stores;
- using InABox.Scripting;
- using org.omg.CosNaming.NamingContextPackage;
- using sun.util.resources.cldr.haw;
- using Syncfusion.UI.Xaml.Grid.Converters;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
- namespace PRSStores.AssignmentCosting
- {
- public class TimeSheetBlock
- {
- public TimeSpan Start { get; set; }
- public TimeSpan Finish { get; set; }
- public TimeSpan Duration => Finish - Start;
- public required bool IsPaid { get; set; }
- }
- public class DayTimeSheet
- {
- public IList<TimeSheetBlock> Blocks { get; private init; }
- public IEnumerable<TimeSheetBlock> PaidBlocks => Blocks.Where(x => x.IsPaid);
- public TimeSpan PaidDuration => PaidBlocks.Aggregate(TimeSpan.Zero, (a, b) => a + b.Duration);
- private DayTimeSheet(IList<TimeSheetBlock> blocks)
- {
- Blocks = blocks;
- }
- /// <summary>
- /// Create a new <see cref="DayTimeSheet"/> from a given list of <see cref="TimeSheet"/>s. The <paramref name="overtime"/> is
- /// used to create blocks for unpaid time on the timesheet.
- /// </summary>
- /// <exception cref="Exception">If there are no elements in <paramref name="sheets"/></exception>
- public static DayTimeSheet FromTimeSheets(IEnumerable<TimeSheet> sheets, IList<OvertimeInterval> overtime)
- {
- var sheetArray = sheets.Select(x =>
- {
- return (
- start: x.Approved != DateTime.MinValue ? x.ApprovedStart : x.Start,
- finish: x.Approved != DateTime.MinValue ? x.ApprovedFinish : x.Finish,
- sheet: x);
- }).ToArray();
- sheetArray.SortBy(x => x.start);
- if(sheetArray.Length == 0)
- {
- throw new Exception("No timesheets provided.");
- }
- // Merge all timesheets into a single list of time blocks; overlapping timesheets will be merged into one.
- var blocks = new List<TimeSheetBlock>();
- TimeSheetBlock? current = null;
- foreach(var (start, finish, sheet) in sheetArray)
- {
- if(current is null || start > current.Finish)
- {
- current = new TimeSheetBlock
- {
- Start = start,
- Finish = finish,
- // Doesn't matter what we put here, since this block will not appear in the final list.
- IsPaid = true
- };
- blocks.Add(current);
- }
- else
- {
- current.Finish = finish;
- }
- }
- // Now, apply overtime to take out chunks of time that are unpaid.
- var newBlocks = new List<TimeSheetBlock>();
- current = null;
- TimeSheetBlock? currentUnpaidBlock = null;
- // We will definitely have at least one block, based on the above algorithm, and since we ensured that sheetArray is non-empty.
- var currentTime = TimeSpan.Zero;
- var met = new HashSet<TimeSheetBlock>();
- OvertimeUtils.EvaluateOvertime(blocks, overtime, x => x.Duration, (block, interval, duration) =>
- {
- if (interval is null) return;
- if (met.Add(block))
- {
- currentTime = block.Start;
- }
- if (interval.IsPaid)
- {
- if(current is null || currentTime > current.Finish)
- {
- current = new TimeSheetBlock
- {
- Start = currentTime,
- Finish = currentTime + duration,
- IsPaid = true
- };
- newBlocks.Add(current);
- currentUnpaidBlock = null;
- }
- else
- {
- current.Finish = currentTime + duration;
- }
- }
- else
- {
- if(currentUnpaidBlock is null || currentTime > currentUnpaidBlock.Finish)
- {
- currentUnpaidBlock = new TimeSheetBlock
- {
- Start = currentTime,
- Finish = currentTime + duration,
- IsPaid = false
- };
- newBlocks.Add(currentUnpaidBlock);
- current = null;
- }
- else
- {
- currentUnpaidBlock.Finish = currentTime + duration;
- }
- }
- currentTime += duration;
- });
- return new(newBlocks);
- }
- }
- public class Block
- {
- public Assignment? Assignment { get; set; } // Set to null for lost time blocks that won't be costed anywhere.
- public TimeSpan Start { get; set; }
- public TimeSpan Finish { get; set; }
- public TimeSpan Duration
- {
- get => Finish - Start;
- set => Finish = Start + value;
- }
- public Block Copy()
- {
- return new Block
- {
- Assignment = Assignment,
- Start = Start,
- Finish = Finish,
- };
- }
- public static Block FromAssignment(Assignment assignment)
- {
- return new Block
- {
- Assignment = assignment,
- Start = assignment.EffectiveStartTime(),
- Finish = assignment.EffectiveFinishTime()
- };
- }
- public static Block Transient(TimeSpan start, TimeSpan finish)
- {
- return new Block
- {
- Assignment = null,
- Start = start,
- Finish = finish
- };
- }
- }
- public class BeforeProcessArgs(Assignment[] assignments, TimeSheet[] timesheets)
- {
- public Assignment[] Assignments { get; } = assignments;
- public TimeSheet[] TimeSheets { get; } = timesheets;
- }
- public class AdjustTimeBlocksArgs(DayTimeSheet timeSheet, List<Block> blocks)
- {
- public DayTimeSheet TimeSheet { get; } = timeSheet;
- public List<Block> Blocks { get; set; } = blocks;
- }
- public class ProcessTimeBlocksArgs(DayTimeSheet timeSheet, List<Block> blocks)
- {
- public DayTimeSheet TimeSheet { get; } = timeSheet;
- public List<Block> Blocks { get; set; } = blocks;
- }
- public class AfterOvertimeArgs(DayTimeSheet timeSheet, List<Block> blocks, OvertimeInterval[] overtime, Assignment[] assignments)
- {
- public DayTimeSheet TimeSheet { get; } = timeSheet;
- public List<Block> Blocks { get; } = blocks;
- public OvertimeInterval[] Overtime { get; } = overtime;
- public Assignment[] Assignments { get; } = assignments;
- }
- public interface IAssignmentCostingScript
- {
- void BeforeProcess(BeforeProcessArgs args);
- void AdjustTimeBlocks(AdjustTimeBlocksArgs args);
- void ProcessTimeBlocks(ProcessTimeBlocksArgs args);
- void AfterOvertime(AfterOvertimeArgs args);
- }
- public class AssignmentCostingUtils : ISettingsStoreEventHandler<AssignmentCostSettings>
- {
- static AssignmentCostSettings? _costSettings;
- public static AssignmentCostSettings GetSettings(IStore store)
- {
- _costSettings ??= new GlobalConfiguration<AssignmentCostSettings>("", new DbConfigurationProvider<GlobalSettings>(store.UserID))
- .Load(false);
- return _costSettings;
- }
- #region ISettingsStoreEventHandler
- public IStore Parent { get; set; } = null!; // Set by GlobalSettingsStore
- public void AfterDelete(AssignmentCostSettings entity)
- {
- _costSettings = null;
- ClearScript();
- }
- public void AfterSave(AssignmentCostSettings entity)
- {
- _costSettings = entity;
- ClearScript();
- }
- public void BeforeDelete(AssignmentCostSettings entity)
- {
- }
- public void BeforeSave(AssignmentCostSettings entity)
- {
- }
- #endregion
- #region Script
- public static string DefaultScript()
- {
- return
- @"using PRSStores.AssignmentCosting;
- using InABox.Core;
- using System.Collections.Generic;
- // 'Module' *must* implement " + nameof(IAssignmentCostingScript) + @"
- public class Module: " + nameof(IAssignmentCostingScript) + @"
- {
- public void " + nameof(IAssignmentCostingScript.BeforeProcess) + @"(" + nameof(BeforeProcessArgs) + @" args)
- {
- // Perform pre-processing
- }
- public void " + nameof(IAssignmentCostingScript.AdjustTimeBlocks) + @"(" + nameof(AdjustTimeBlocksArgs) + @" args)
- {
- // At this stage, adjust the list of blocks (given by args." + nameof(AdjustTimeBlocksArgs.Blocks) + @") before any algorithm has
- // been applied to them. The blocks are ordered by start time, and are represented by one per each assignment being processed. At this stage,
- // they may overlap.
- // args." + nameof(AdjustTimeBlocksArgs.Blocks) + @" will then be passed through algorithm given in the settings, which will fill
- // up all the time on the day's timesheet by extending blocks as needed to fill all unfilled time.
- }
- public void " + nameof(IAssignmentCostingScript.ProcessTimeBlocks) + @"(" + nameof(ProcessTimeBlocksArgs) + @" args)
- {
- // Called after the time filling algorithm has been applied, but before the overtime rules. args." + nameof(ProcessTimeBlocksArgs.Blocks) + @" will
- // now contain a list of blocks, filling continuous time from the start of the timesheet to its end. Process as desired.
- }
- public void " + nameof(IAssignmentCostingScript.AfterOvertime) + @"(" + nameof(AfterOvertimeArgs) + @" args)
- {
- // Once the overtime rules have been applied and the assignment cost calculated, post-process the assignments before saving to the database.
- }
- }";
- }
- private static Type? _scriptObjectType;
- private static bool _hasCheckedScript;
- private static void ClearScript()
- {
- _scriptObjectType = null;
- _hasCheckedScript = false;
- }
- private static Type? GetScriptObjectType(IStore store)
- {
- if (_hasCheckedScript)
- {
- return _scriptObjectType;
- }
- var settings = GetSettings(store);
- if (!string.IsNullOrWhiteSpace(settings.Script))
- {
- var document = new ScriptDocument(settings.Script);
- if (!document.Compile())
- {
- Logger.Send(LogType.Error, store.UserID, "Script failed to compile!");
- _scriptObjectType = null;
- }
- else
- {
- _scriptObjectType = document.GetClassType();
- }
- }
- else
- {
- _scriptObjectType = null;
- }
- _hasCheckedScript = true;
- return _scriptObjectType;
- }
- private static IAssignmentCostingScript? GetScriptObject(IStore store)
- {
- var type = GetScriptObjectType(store);
- if(type is not null)
- {
- var obj = Activator.CreateInstance(type) as IAssignmentCostingScript;
- if(obj is null)
- {
- Logger.Send(LogType.Error, store.UserID, $"Assignment costing script module does not implement {typeof(IAssignmentCostingScript).Name}");
- }
- return obj;
- }
- else
- {
- return null;
- }
- }
- private static void ScriptBeforeProcess(IAssignmentCostingScript? script, Assignment[] assignments, TimeSheet[] timesheets)
- {
- script?.BeforeProcess(new(assignments, timesheets));
- }
- private static List<Block> ScriptAdjustTimeBlocks(IAssignmentCostingScript? script, DayTimeSheet timeSheet, List<Block> blocks)
- {
- if(script is not null)
- {
- var args = new AdjustTimeBlocksArgs(timeSheet, blocks);
- script.AdjustTimeBlocks(args);
- return args.Blocks;
- }
- else
- {
- return blocks;
- }
- }
- private static List<Block> ScriptProcessTimeBlocks(IAssignmentCostingScript? script, DayTimeSheet timeSheet, List<Block> blocks)
- {
- if(script is not null)
- {
- var args = new ProcessTimeBlocksArgs(timeSheet, blocks);
- script.ProcessTimeBlocks(args);
- return args.Blocks;
- }
- else
- {
- return blocks;
- }
- }
- private static void ScriptAfterOvertime(IAssignmentCostingScript? script, DayTimeSheet timeSheet, List<Block> blocks, OvertimeInterval[] overtime, Assignment[] assignments)
- {
- script?.AfterOvertime(new(timeSheet, blocks, overtime, assignments));
- }
- #endregion
- private static List<Block> AdjustTimeBlocksBasic(List<Block> blocks)
- {
- // No change for basic mode.
- return blocks;
- }
- private static List<Block> AdjustTimeBlocksScale(DayTimeSheet timeSheet, List<Block> blocks)
- {
- // We scale the blocks until their total is equal to 'PaidDuration'
- var totalBlockDuration = blocks.Aggregate(TimeSpan.Zero, (a, b) => a + b.Duration);
- var adjustFactor = timeSheet.PaidDuration.TotalHours / totalBlockDuration.TotalHours;
- // Adjust block size
- foreach(var block in blocks)
- {
- block.Duration *= adjustFactor;
- }
- // Now we need to adjust the block positioning.
- var paidBlocks = timeSheet.PaidBlocks.ToList();
- var curPaidBlockIdx = 0;
- TimeSheetBlock? GetPaidBlock() => curPaidBlockIdx < paidBlocks.Count ? paidBlocks[curPaidBlockIdx] : null;
- var timeSheetStart = GetPaidBlock()?.Start ?? TimeSpan.Zero;
- var curTime = timeSheetStart;
- var curPaidBlockDuration = GetPaidBlock()?.Duration ?? TimeSpan.Zero;
- void AdvancePaidBlock()
- {
- ++curPaidBlockIdx;
- var paidBlock = GetPaidBlock();
- curPaidBlockDuration = paidBlock?.Duration ?? TimeSpan.Zero;
- if(paidBlock is not null)
- {
- curTime = paidBlock.Start;
- }
- }
- var newBlocks = new List<Block>();
- foreach (var block in blocks)
- {
- var duration = block.Duration;
- if(duration == TimeSpan.Zero)
- {
- block.Start = curTime;
- block.Finish = curTime + duration;
- newBlocks.Add(block);
- continue;
- }
- while (duration > TimeSpan.Zero)
- {
- var paidBlock = GetPaidBlock();
- if (paidBlock != null)
- {
- if (duration > curPaidBlockDuration)
- {
- // In this case, the block is more than the rest of
- // the current timesheet, so we use up all the remaining timesheet
- // time, and then move to the next timesheet.
- var newBlock = block.Copy();
- newBlock.Start = curTime;
- newBlock.Finish = curTime + curPaidBlockDuration;
- newBlocks.Add(newBlock);
- duration -= curPaidBlockDuration;
- AdvancePaidBlock();
- }
- else
- {
- // Otherwise, we use up the entire block, and decrease the interval by the duration remaining.
- block.Start = curTime;
- block.Finish = curTime + duration;
- newBlocks.Add(block);
- curPaidBlockDuration -= duration;
- curTime += duration;
- duration = TimeSpan.Zero;
- if(curPaidBlockDuration == TimeSpan.Zero)
- {
- AdvancePaidBlock();
- }
- }
- }
- else
- {
- // Do nothing; we've finished the timesheets, so now we just set all the remaining blocks to zero.
- block.Start = curTime;
- block.Duration = TimeSpan.Zero;
- newBlocks.Add(block);
- break;
- }
- }
- }
- return newBlocks;
- }
- /// <summary>
- /// Taking a list of <see cref="Block"/>, chop and extend them to match the bounds of the <paramref name="timeSheet"/>.
- /// </summary>
- /// <remarks>
- /// <paramref name="blocks"/> <b>must</b> be in continuous time, however they need not correspond at all to <paramref name="timeSheet"/> yet;
- /// that is what this function does.
- /// </remarks>
- private static List<Block> MaskBlocksToTimeSheet(DayTimeSheet timeSheet, List<Block> blocks)
- {
- var paidBlocks = timeSheet.PaidBlocks.ToList();
- if(paidBlocks.Count == 0)
- {
- // No time to be paid, so all assignments have effective duration of zero.
- foreach(var block in blocks)
- {
- block.Duration = TimeSpan.Zero;
- }
- return blocks;
- }
- // Fill in gaps in the time sheet with unpaid blocks, so we get a continuous time sheet.
- var timeSheetBlocks = new List<TimeSheetBlock>();
- foreach(var block in timeSheet.Blocks)
- {
- if(timeSheetBlocks.Count > 0)
- {
- var last = timeSheetBlocks[^1];
- if(last.Finish < block.Start)
- {
- timeSheetBlocks.Add(new TimeSheetBlock
- {
- IsPaid = false,
- Start = last.Finish,
- Finish = block.Start
- });
- }
- }
- timeSheetBlocks.Add(block);
- }
- // Now that the blocks have continuous time, we chop to match the timesheet paid blocks.
- // First, shove all the blocks at the start of the day up to match with the time sheet start.
- var timeSheetStart = timeSheetBlocks[0].Start;
- var anyAtStart = false;
- foreach(var block in blocks)
- {
- if(block.Start < timeSheetStart)
- {
- anyAtStart = true;
- block.Start = timeSheetStart;
- if(block.Finish < timeSheetStart)
- {
- block.Finish = timeSheetStart;
- }
- }
- else
- {
- // Once we've found the first block that doesn't begin before the timesheet begins, we can set its time to start
- // at the time sheet, unless we already have one at the start.
- if (!anyAtStart)
- {
- block.Start = timeSheetStart;
- anyAtStart = true;
- }
- break;
- }
- }
- var curBlockIdx = 0;
- TimeSheetBlock? GetTimeSheetBlock() => curBlockIdx < timeSheetBlocks.Count ? timeSheetBlocks[curBlockIdx] : null;
- var curBlockDuration = GetTimeSheetBlock()?.Duration ?? TimeSpan.Zero;
- var curTime = timeSheetStart;
- void AdvanceBlock()
- {
- ++curBlockIdx;
- var block = GetTimeSheetBlock();
- curBlockDuration = block?.Duration ?? TimeSpan.Zero;
- if(block is not null)
- {
- curTime = block.Start;
- }
- }
- var newBlocks = new List<Block>();
- foreach (var block in blocks)
- {
- var duration = block.Duration;
- if(duration == TimeSpan.Zero)
- {
- newBlocks.Add(block);
- continue;
- }
- while (duration > TimeSpan.Zero)
- {
- var paidBlock = GetTimeSheetBlock();
- if (paidBlock != null)
- {
- if (duration > curBlockDuration)
- {
- // In this case, the block is more than the rest of
- // the current timesheet, so we use up all the remaining timesheet
- // time, and then move to the next timesheet.
- var newBlock = block.Copy();
- newBlock.Start = curTime;
- newBlock.Finish = curTime + curBlockDuration;
- if (paidBlock.IsPaid)
- {
- newBlocks.Add(newBlock);
- }
- duration -= curBlockDuration;
- AdvanceBlock();
- }
- else
- {
- // Otherwise, we use up the entire block, and decrease the interval by the duration remaining.
- block.Start = curTime;
- block.Finish = curTime + duration;
- if (paidBlock.IsPaid)
- {
- newBlocks.Add(block);
- }
- curTime = block.Finish;
- curBlockDuration -= duration;
- duration = TimeSpan.Zero;
- if(curBlockDuration == TimeSpan.Zero)
- {
- AdvanceBlock();
- }
- }
- }
- else
- {
- // Do nothing; we've finished the timesheets, so now we just set all the remaining blocks to zero.
- block.Duration = TimeSpan.Zero;
- break;
- }
- }
- }
- // Extend the last block to the end of the current timesheet block, if we haven't reached the end already.
- var lastBlock = newBlocks[^1];
- if(GetTimeSheetBlock() is TimeSheetBlock nextTimeSheetBlock)
- {
- lastBlock.Finish = nextTimeSheetBlock.Finish;
- AdvanceBlock();
- }
- // Next, if there are still more timesheet blocks, fill them with 'lastBlock' as well.
- while(GetTimeSheetBlock() is TimeSheetBlock block)
- {
- var newBlock = lastBlock.Copy();
- newBlock.Start = block.Start;
- newBlock.Finish = block.Finish;
- AdvanceBlock();
- }
- return newBlocks;
- }
- private static List<Block> AdjustTimeBlocksExtend(DayTimeSheet timeSheet, List<Block> blocks)
- {
- if(blocks.Count == 0)
- {
- return blocks;
- }
- // First, chop and extend blocks to fill continuous time, from the beginning of the first block to the end of the last block.
- // Block chopping
- var lastEnd = TimeSpan.Zero;
- foreach(var block in blocks)
- {
- // We know this check is enough to check overlap, since the blocks are in increasing order of their start time.
- if (lastEnd <= block.Start)
- {
- // This block does not overlap any previous blocks. Do not update duration.
- lastEnd = block.Finish;
- }
- else if(block.Finish >= lastEnd)
- {
- // This block ends after the block that we are overlapping. Update start.
- block.Start = lastEnd;
- lastEnd = block.Finish;
- }
- else
- {
- // This assignment is entirely contained within the assignment being overlapped.
- block.Start = lastEnd;
- block.Finish = lastEnd;
- }
- }
- // Block extending
- for(int i = 0; i < blocks.Count - 1; ++i)
- {
- var block = blocks[i];
- var nextBlock = blocks[i + 1];
- if(block.Finish < nextBlock.Start)
- {
- block.Finish = nextBlock.Start;
- }
- }
- // Now that the blocks have continuous time, we chop to match the timesheet paid blocks.
- return MaskBlocksToTimeSheet(timeSheet, blocks);
- }
- private static List<Block> AdjustTimeBlocksFillTimeSheet(DayTimeSheet timeSheet, List<Block> blocks)
- {
- if(blocks.Count == 0)
- {
- return blocks;
- }
- // First, chop blocks to ensure they don't overlap
- var lastEnd = TimeSpan.Zero;
- foreach(var block in blocks)
- {
- // We know this check is enough to check overlap, since the blocks are in increasing order of their start time.
- if (lastEnd <= block.Start)
- {
- // This block does not overlap any previous blocks. Do not update duration.
- lastEnd = block.Finish;
- }
- else if(block.Finish >= lastEnd)
- {
- // This block ends after the block that we are overlapping. Update start.
- block.Start = lastEnd;
- lastEnd = block.Finish;
- }
- else
- {
- // This assignment is entirely contained within the assignment being overlapped.
- block.Start = lastEnd;
- block.Finish = lastEnd;
- }
- }
- // Fill up remaining gaps in time with transient, "lost time" blocks. For simplicity, so that we don't have to query
- // the timesheet yet, we just go from the start to the end of the day. 'MaskBlocksToTimeSheet' will take care of the rest.
- var start = TimeSpan.Zero;
- var finish = TimeSpan.FromDays(1).Subtract(TimeSpan.FromTicks(1));
- // Fill to start of day.
- if (blocks[0].Start > start)
- {
- blocks.Insert(0, Block.Transient(start, blocks[0].Start));
- }
- // Fill in gaps between blocks
- var i = 0;
- while(i < blocks.Count - 1)
- {
- var block = blocks[i];
- var nextBlock = blocks[i + 1];
- if(nextBlock.Start > block.Finish)
- {
- blocks.Insert(i + 1, Block.Transient(block.Finish, nextBlock.Start));
- i += 2;
- }
- else
- {
- i += 1;
- }
- }
- // Fill to end of day.
- if (blocks[^1].Finish < finish)
- {
- blocks.Add(Block.Transient(blocks[^1].Finish, finish));
- }
-
- return MaskBlocksToTimeSheet(timeSheet, blocks);
- }
- public static List<Block> AdjustTimeBlocks(AssignmentCostSettings settings, DayTimeSheet timeSheet, List<Block> blocks)
- {
- return settings.Algorithm switch
- {
- AssignmentCostFillAlgorithm.Basic => AdjustTimeBlocksBasic(blocks),
- AssignmentCostFillAlgorithm.Scale => AdjustTimeBlocksScale(timeSheet, blocks),
- AssignmentCostFillAlgorithm.Extend => AdjustTimeBlocksExtend(timeSheet, blocks),
- AssignmentCostFillAlgorithm.FillTimeSheet => AdjustTimeBlocksFillTimeSheet(timeSheet, blocks),
- _ => blocks,
- };
- }
-
- public static void CheckAssignmentCosts(IStore store, DateTime date, Guid employeeID)
- {
- var settings = GetSettings(store);
- if (employeeID == Guid.Empty) return;
- var assignmentTask = Task.Run(() =>
- store.Provider.Query(
- Filter<Assignment>.Where(x => x.Date).IsEqualTo(date)
- .And(x => x.EmployeeLink.ID).IsEqualTo(employeeID),
- Columns.Required<Assignment>()
- .Add(x => x.ID)
- .Add(x => x.Cost)
- .Add(x => x.Actual.Start)
- .Add(x => x.Actual.Finish)
- .Add(x => x.Actual.Duration)
- .Add(x => x.Booked.Start)
- .Add(x => x.Booked.Finish)
- .Add(x => x.Booked.Duration))
- .ToArray<Assignment>());
- var timesheetTask = Task.Run(() =>
- store.Provider.Query(
- Filter<TimeSheet>.Where(x => x.Date).IsEqualTo(date)
- .And(x => x.EmployeeLink.ID).IsEqualTo(employeeID),
- Columns.None<TimeSheet>()
- .Add(x => x.Approved)
- .Add(x => x.Start)
- .Add(x => x.Finish)
- .Add(x => x.ApprovedStart)
- .Add(x => x.ApprovedFinish))
- .ToArray<TimeSheet>());
- var employeeTask = Task.Run(() =>
- store.Provider.Query(
- Filter<Employee>.Where(x => x.ID).IsEqualTo(employeeID),
- Columns.None<Employee>()
- .Add(x => x.RosterStart)
- .Add(x => x.HourlyRate))
- .ToObjects<Employee>().FirstOrDefault());
- var rosterItemsTask = Task.Run(() =>
- store.Provider.Query(
- Filter<EmployeeRosterItem>.Where(x => x.Employee.ID).IsEqualTo(employeeID),
- Columns.None<EmployeeRosterItem>()
- .Add(x => x.Overtime.ID),
- new SortOrder<EmployeeRosterItem>(x => x.Day))
- .ToArray<EmployeeRosterItem>());
- var timesheets = timesheetTask.Result;
- var employee = employeeTask.Result;
- if (timesheets.Length == 0 || employee is null) return;
- var assignments = assignmentTask.Result;
- var rosterItems = rosterItemsTask.Result;
- var overtimeID = RosterUtils.GetRoster(rosterItems, employee.RosterStart, date)?.Overtime.ID ?? Guid.Empty;
- if(overtimeID == Guid.Empty) return;
- var overtime = store.Provider.Query(
- Filter<OvertimeInterval>.Where(x => x.Overtime.ID).IsEqualTo(overtimeID),
- Columns.None<OvertimeInterval>()
- .Add(x => x.Interval)
- .Add(x => x.IntervalType)
- .Add(x => x.Multiplier)
- .Add(x => x.IsPaid),
- new SortOrder<OvertimeInterval>(x => x.Sequence))
- .ToArray<OvertimeInterval>();
- var script = GetScriptObject(store);
- ScriptBeforeProcess(script, assignments, timesheets);
- // We need to sort the assignments, because both the chopping algorithm and the overtime algorithm requires it.
- assignments.SortBy(x => x.EffectiveStartTime());
- var blocks = new List<Block>();
- foreach(var assignment in assignments)
- {
- blocks.Add(Block.FromAssignment(assignment));
- }
- var timeSheet = DayTimeSheet.FromTimeSheets(timesheets, overtime);
- blocks = ScriptAdjustTimeBlocks(script, timeSheet, blocks);
- blocks = AdjustTimeBlocks(settings, timeSheet, blocks);
- blocks = ScriptProcessTimeBlocks(script, timeSheet, blocks);
- var totalAccountingHours = new Dictionary<Assignment, double>();
- var totalHours = new Dictionary<Assignment, double>();
- OvertimeUtils.EvaluateOvertime(
- blocks,
- // We've already taken into account the IsPaid flags, so here we remove the unpaid ones from the intervals.
- overtime.Where(x => x.IsPaid).ToArray(),
- x => x.Duration,
- (block, interval, duration) =>
- {
- if(block.Assignment is not null)
- {
- totalAccountingHours[block.Assignment] =
- totalAccountingHours.GetValueOrAdd(block.Assignment)
- + duration.TotalHours;
- totalHours[block.Assignment] =
- totalHours.GetValueOrAdd(block.Assignment)
- + duration.TotalHours * (interval?.Multiplier ?? 1);
- }
- });
- foreach(var assignment in assignments)
- {
- assignment.Cost = totalHours.GetValueOrDefault(assignment) * employee.HourlyRate;
- assignment.CostedHours = totalAccountingHours.GetValueOrDefault(assignment);
- }
- ScriptAfterOvertime(script, timeSheet, blocks, overtime, assignments);
- store.Provider.Save(assignments.Where(x => x.IsChanged()));
- }
- }
- }
|