|
@@ -42,10 +42,17 @@ namespace PRS.Shared
|
|
|
public string PayID { get; set; } = "";
|
|
|
}
|
|
|
|
|
|
+ public enum TimesheetTimberlineActivityCalculation
|
|
|
+ {
|
|
|
+ TimesheetOnly,
|
|
|
+ TimesheetPriority,
|
|
|
+ AssignmentPriority
|
|
|
+ }
|
|
|
+
|
|
|
public class TimesheetTimberlineSettings : TimberlinePosterSettings<TimeSheet>
|
|
|
{
|
|
|
- [CheckBoxEditor(ToolTip = "Include in the export any assignments which exist outside of the relevant timesheet; if this is false, only assignments which are within the bounds of a timesheet are exported")]
|
|
|
- public bool IncludeExtraneousAssignments { get; set; }
|
|
|
+ [EnumLookupEditor(typeof(TimesheetTimberlineActivityCalculation), LookupWidth = 200)]
|
|
|
+ public TimesheetTimberlineActivityCalculation ActivityCalculation { get; set; }
|
|
|
|
|
|
protected override string DefaultScript()
|
|
|
{
|
|
@@ -76,6 +83,7 @@ public class Module
|
|
|
|
|
|
public event ITimberlinePoster<TimeSheet, TimesheetTimberlineSettings>.AddFragmentCallback? AddFragment;
|
|
|
|
|
|
+ private Dictionary<Guid, Activity> _activities = null!; // Initialised on DoProcess()
|
|
|
private Dictionary<Guid, OvertimeInterval[]> _overtimeIntervals = null!; // Initialised on DoProcess()
|
|
|
|
|
|
public bool BeforePost(IDataModel<TimeSheet> model)
|
|
@@ -146,19 +154,17 @@ public class Module
|
|
|
|
|
|
public TimeSheet TimeSheet { get; set; }
|
|
|
|
|
|
- public ActivityBlock(Assignment assignment, IList<TimeSheet> sheets)
|
|
|
+ public TimeSpan Duration => Finish - Start;
|
|
|
+
|
|
|
+ public ActivityBlock(Assignment assignment, TimeSheet sheet)
|
|
|
{
|
|
|
- Activity = assignment.ActivityLink.ID;
|
|
|
+ Activity = assignment.ActivityLink.ID != Guid.Empty
|
|
|
+ ? assignment.ActivityLink.ID
|
|
|
+ : sheet.ActivityLink.ID;
|
|
|
+
|
|
|
Start = assignment.EffectiveStartTime();
|
|
|
Finish = assignment.EffectiveFinishTime();
|
|
|
-
|
|
|
- TimeSheet = sheets.FirstOrDefault(ContainedInTimeSheet)
|
|
|
- ?? sheets.MinBy(sheet =>
|
|
|
- {
|
|
|
- // Grab the closest timesheet
|
|
|
- return Math.Min((Start - sheet.ApprovedFinish).TotalHours, (sheet.ApprovedStart - Finish).TotalHours);
|
|
|
- })
|
|
|
- ?? sheets.First();
|
|
|
+ TimeSheet = sheet;
|
|
|
}
|
|
|
|
|
|
public ActivityBlock(TimeSheet sheet)
|
|
@@ -169,32 +175,114 @@ public class Module
|
|
|
TimeSheet = sheet;
|
|
|
}
|
|
|
|
|
|
+ public ActivityBlock(TimeSheet sheet, TimeSpan start, TimeSpan finish)
|
|
|
+ {
|
|
|
+ Activity = sheet.ActivityLink.ID;
|
|
|
+ Start = start;
|
|
|
+ Finish = finish;
|
|
|
+ TimeSheet = sheet;
|
|
|
+ }
|
|
|
+
|
|
|
+ public ActivityBlock Chop(TimeSheet sheet)
|
|
|
+ {
|
|
|
+ if(Start < sheet.ApprovedStart)
|
|
|
+ {
|
|
|
+ Start = sheet.ApprovedStart;
|
|
|
+ }
|
|
|
+ if (Finish > sheet.ApprovedFinish)
|
|
|
+ {
|
|
|
+ Finish = sheet.ApprovedFinish;
|
|
|
+ }
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
public bool ContainedInTimeSheet(TimeSheet sheet) =>
|
|
|
Start < sheet.ApprovedFinish && Finish > sheet.ApprovedStart;
|
|
|
+
|
|
|
+ public bool IntersectsWith(ActivityBlock other)
|
|
|
+ {
|
|
|
+ return Start < other.Finish && Finish > other.Start;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- private List<ActivityBlock> GetActivityBlocks(IEnumerable<Assignment> assignments, IList<TimeSheet> sheets)
|
|
|
+ private IEnumerable<ActivityBlock> GetMaskedActivityBlocks(IEnumerable<Assignment> assignments, TimeSheet sheet)
|
|
|
{
|
|
|
- var activityBlocks = new List<ActivityBlock>();
|
|
|
- if (Settings.IncludeExtraneousAssignments)
|
|
|
+ if (sheet.ActivityLink.ID != Guid.Empty
|
|
|
+ && _activities.TryGetValue(sheet.ActivityLink.ID, out var activity)
|
|
|
+ && activity.IsLeave)
|
|
|
{
|
|
|
- activityBlocks.AddRange(assignments.Select(x => new ActivityBlock(x, sheets)));
|
|
|
+ yield return new ActivityBlock(sheet);
|
|
|
+ yield break;
|
|
|
}
|
|
|
- else
|
|
|
+
|
|
|
+ var blocks = assignments.Select(x => new ActivityBlock(x, sheet))
|
|
|
+ .Where(x => x.ContainedInTimeSheet(sheet)).Select(x => x.Chop(sheet))
|
|
|
+ .OrderBy(x => x.Start).ToList();
|
|
|
+
|
|
|
+ for(int i = 0; i < blocks.Count; ++i)
|
|
|
{
|
|
|
- activityBlocks.AddRange(assignments.Select(x => new ActivityBlock(x, sheets)).Where(block =>
|
|
|
+ var block = blocks[i];
|
|
|
+
|
|
|
+ var totalTime = block.Duration;
|
|
|
+ var maxFinish = block.Finish;
|
|
|
+
|
|
|
+ // Find all overlapping blocks; j represents the next non-overlapping block.
|
|
|
+ int j = i + 1;
|
|
|
+ for (; j < blocks.Count && block.IntersectsWith(blocks[j]); ++j)
|
|
|
{
|
|
|
- return sheets.Any(sheet => block.ContainedInTimeSheet(sheet));
|
|
|
- }));
|
|
|
+ totalTime += blocks[j].Duration;
|
|
|
+ if (blocks[j].Finish > maxFinish)
|
|
|
+ {
|
|
|
+ maxFinish = blocks[j].Finish;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ var netTime = maxFinish - block.Start;
|
|
|
+
|
|
|
+ var start = block.Start;
|
|
|
+ foreach(var newBlock in blocks.Skip(i).Take(j - i))
|
|
|
+ {
|
|
|
+ var frac = newBlock.Duration.TotalHours / totalTime.TotalHours;
|
|
|
+ var duration = netTime.Multiply(frac);
|
|
|
+
|
|
|
+ newBlock.Start = start;
|
|
|
+ newBlock.Finish = start + duration;
|
|
|
+ start = newBlock.Finish;
|
|
|
+ }
|
|
|
}
|
|
|
- if (activityBlocks.Count == 0)
|
|
|
+
|
|
|
+ var curTime = sheet.ApprovedStart;
|
|
|
+ foreach(var block in blocks)
|
|
|
{
|
|
|
- foreach (var sheet in sheets)
|
|
|
+ if (block.Start > curTime)
|
|
|
{
|
|
|
- activityBlocks.Add(new ActivityBlock(sheet));
|
|
|
+ yield return new ActivityBlock(sheet, curTime, block.Start);
|
|
|
}
|
|
|
+ yield return block;
|
|
|
+ curTime = block.Finish;
|
|
|
+ }
|
|
|
+ if(curTime < sheet.ApprovedFinish)
|
|
|
+ {
|
|
|
+ yield return new ActivityBlock(sheet, curTime, sheet.ApprovedFinish);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<ActivityBlock> GetActivityBlocks(IEnumerable<Assignment> assignments, IList<TimeSheet> sheets)
|
|
|
+ {
|
|
|
+ switch (Settings.ActivityCalculation)
|
|
|
+ {
|
|
|
+ case TimesheetTimberlineActivityCalculation.TimesheetOnly:
|
|
|
+ return sheets.Select(x => new ActivityBlock(x)).OrderBy(x => x.Start).ToList();
|
|
|
+ case TimesheetTimberlineActivityCalculation.TimesheetPriority:
|
|
|
+ var sheetLookup = sheets.ToLookup(x => x.ActivityLink.ID == Guid.Empty);
|
|
|
+ return sheetLookup[false].Select(x => new ActivityBlock(x))
|
|
|
+ .Concat(sheetLookup[true].SelectMany(x => GetMaskedActivityBlocks(assignments, x)))
|
|
|
+ .OrderBy(x => x.Start)
|
|
|
+ .ToList();
|
|
|
+ case TimesheetTimberlineActivityCalculation.AssignmentPriority:
|
|
|
+ return sheets.SelectMany(x => GetMaskedActivityBlocks(assignments, x)).OrderBy(x => x.Start).ToList();
|
|
|
+ default:
|
|
|
+ throw new Exception($"Invalide Activity calculation {Settings.ActivityCalculation}");
|
|
|
}
|
|
|
- return activityBlocks.OrderBy(x => x.Start).ToList();
|
|
|
}
|
|
|
|
|
|
private interface IBlock
|
|
@@ -325,7 +413,7 @@ public class Module
|
|
|
throw new Exception("No approved timesheets found");
|
|
|
}
|
|
|
|
|
|
- var activities = model.GetTable<Activity>().ToObjects<Activity>().ToDictionary(x => x.ID, x => x);
|
|
|
+ _activities = model.GetTable<Activity>().ToObjects<Activity>().ToDictionary(x => x.ID, x => x);
|
|
|
_overtimeIntervals = model.GetTable<OvertimeInterval>().ToObjects<OvertimeInterval>()
|
|
|
.GroupBy(x => x.Overtime.ID)
|
|
|
.ToDictionary(x => x.Key, x => x.OrderBy(x => x.Sequence).ToArray());
|
|
@@ -348,17 +436,18 @@ public class Module
|
|
|
|
|
|
var leave = new List<LeaveBlock>();
|
|
|
var workTime = new List<PaidWorkBlock>();
|
|
|
- var totalDuration = new TimeSpan();
|
|
|
foreach (var block in activityBlocks)
|
|
|
{
|
|
|
string payID;
|
|
|
bool isLeave;
|
|
|
- Guid activityID = block.Activity != Guid.Empty ? block.Activity : block.TimeSheet.ActivityLink.ID;
|
|
|
|
|
|
- if (activityID == Guid.Empty
|
|
|
- || !activities.TryGetValue(block.Activity, out var activity))
|
|
|
+ if (block.Activity == Guid.Empty
|
|
|
+ || !_activities.TryGetValue(block.Activity, out var activity))
|
|
|
{
|
|
|
- Logger.Send(LogType.Error, "", $"Error in Timesheet Timberline export: Activity {activityID} does not exist!");
|
|
|
+ if(block.Activity != Guid.Empty)
|
|
|
+ {
|
|
|
+ Logger.Send(LogType.Error, "", $"Error in Timesheet Timberline export: Activity {block.Activity} does not exist!");
|
|
|
+ }
|
|
|
|
|
|
payID = "";
|
|
|
isLeave = false;
|
|
@@ -369,7 +458,6 @@ public class Module
|
|
|
payID = activity.PayrollID;
|
|
|
}
|
|
|
|
|
|
- totalDuration = totalDuration.Add(block.Finish - block.Start);
|
|
|
if (isLeave)
|
|
|
{
|
|
|
leave.Add(new(payID, block.Finish - block.Start));
|
|
@@ -381,19 +469,12 @@ public class Module
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (totalDuration > TimeSpan.Zero && approvedDuration > TimeSpan.Zero)
|
|
|
+ if (approvedDuration > TimeSpan.Zero)
|
|
|
{
|
|
|
var employee = employees.GetValueOrDefault(key.Employee);
|
|
|
var employeeRosters = rosters.GetValueOrDefault(employee != null ? employee.ID : Guid.Empty);
|
|
|
var overtimeID = RosterUtils.GetRoster(employeeRosters, employee?.RosterStart, key.Date)?.Overtime.ID ?? Guid.Empty;
|
|
|
|
|
|
- var scaleFactor = approvedDuration.TotalHours / totalDuration.TotalHours;
|
|
|
-
|
|
|
- foreach(var block in (workTime as IEnumerable<IBlock>).Concat(leave))
|
|
|
- {
|
|
|
- block.Duration = TimeSpan.FromHours(block.Duration.TotalHours * scaleFactor);
|
|
|
- }
|
|
|
-
|
|
|
var workItems = EvaluateOvertime(workTime, overtimeID);
|
|
|
|
|
|
var blocks = (workItems as IEnumerable<IBlock>).Concat(leave);
|