using System.Collections.Generic; using System.Linq; using Comal.Classes; using InABox.Core; using System; using ExCSS; namespace Comal.Stores { internal class AssignmentStore : BaseStore { private void CheckActivityForms(Assignment assignment) { if (!assignment.ActivityLink.HasOriginalValue(x => x.ID)) { return; } List assignmentForms; if(assignment.ID != Guid.Empty) { assignmentForms = Provider.Query( Filter.Where(x => x.Parent.ID).IsEqualTo(assignment.ID), Columns.None().Add(x => x.ID, x => x.FormData) ).Rows.Select(x => x.ToObject()).ToList(); } else { assignmentForms = new(); } var toDelete = assignmentForms.Where(x => string.IsNullOrWhiteSpace(x.FormData)); Provider.Delete(toDelete, UserID); var activityForms = Provider.Query( Filter.Where(x => x.Activity.ID).IsEqualTo(assignment.ActivityLink.ID) .And(x => x.Form.AppliesTo).IsEqualTo(nameof(Assignment))); var newForms = new List(); foreach (var row in activityForms.Rows) { var formID = row.Get(x => x.Form.ID); if (!(assignmentForms.Any(x => x.Form.ID == formID) && !toDelete.Any(x => x.Form.ID == formID))) { var assignmentForm = new AssignmentForm(); assignmentForm.Form.ID = formID; assignmentForm.Parent.ID = assignment.ID; newForms.Add(assignmentForm); } } Provider.Save(newForms); } protected override void AfterSave(Assignment entity) { base.AfterSave(entity); CheckActivityForms(entity); CheckAssignmentCosts(entity); } private void CheckAssignmentCosts(Assignment entity) { if(!entity.Actual.IsChanged() && !entity.Booked.IsChanged() && !entity.HasOriginalValue(x => x.EmployeeLink.ID) && !entity.HasOriginalValue(x => x.Date)) { // No change we care about. return; } var changedEmployee = entity.TryGetOriginalValue(x => x.EmployeeLink.ID, out var employeeID); if (!changedEmployee) { employeeID = entity.EmployeeLink.ID; } var changedDate = entity.TryGetOriginalValue(x => x.Date, out var date); if (!changedDate) { date = entity.Date; } if(changedEmployee || changedDate) { CheckAssignmentCosts(date, employeeID); } CheckAssignmentCosts(entity.Date, entity.EmployeeLink.ID); } private void CheckAssignmentCosts(DateTime date, Guid employeeID) { if (employeeID == Guid.Empty) return; var assignments = Provider.Query( Filter.Where(x => x.Date).IsEqualTo(date) .And(x => x.EmployeeLink.ID).IsEqualTo(employeeID), Columns.Required() .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(); var employee = Provider.Query( Filter.Where(x => x.ID).IsEqualTo(employeeID), Columns.None() .Add(x => x.RosterStart) .Add(x => x.HourlyRate)) .ToObjects().FirstOrDefault(); if (employee is null) return; var rosterItems = Provider.Query( Filter.Where(x => x.Employee.ID).IsEqualTo(employeeID), Columns.None() .Add(x => x.Overtime.ID), new SortOrder(x => x.Day)) .ToArray(); var overtimeID = RosterUtils.GetRoster(rosterItems, employee.RosterStart, date)?.Overtime.ID ?? Guid.Empty; if(overtimeID == Guid.Empty) return; var overtime = Provider.Query( Filter.Where(x => x.Overtime.ID).IsEqualTo(overtimeID), Columns.None() .Add(x => x.Interval) .Add(x => x.IntervalType) .Add(x => x.Multiplier) .Add(x => x.IsPaid), new SortOrder(x => x.Sequence)) .ToArray(); // We need to sort the assignments, because both the chopping algorithm and the overtime algorithm requires it. assignments.SortBy(x => x.EffectiveStartTime()); // Do assignment choppage. var lastEnd = TimeSpan.Zero; var durations = assignments.ToArray(assignment => { var start = assignment.EffectiveStartTime(); var finish = assignment.EffectiveFinishTime(); // We know this check is enough to check overlap, since the assignments are in increasing order of their start time. if (lastEnd <= start) { // This assignment does not overlap any previous assignments. lastEnd = assignment.EffectiveFinishTime(); return finish - start; } else if(finish >= lastEnd) { // This assignment ends after the assignment that we are overlapping. var duration = finish - lastEnd; lastEnd = finish; return duration; } else { // This assignment is entirely contained within the assignment being overlapped. return TimeSpan.Zero; } }); var totalHours = new double[assignments.Length]; OvertimeUtils.EvaluateOvertime(Enumerable.Range(0, assignments.Length), overtime, i => durations[i], (i, interval, duration) => { if(interval is not null) { var multiplier = interval.IsPaid ? interval.Multiplier : 0; } if (interval?.IsPaid != false) { totalHours[i] += duration.TotalHours * (interval?.Multiplier ?? 1); } }); foreach(var (assignment, hours) in assignments.Zip(totalHours)) { assignment.Cost = hours * employee.HourlyRate; } Provider.Save(assignments.Where(x => x.IsChanged())); } } }