public async Task <CurrentTimeEntryDto> ClockOut(int timeEntryId, DateTime?now = null) { if (!now.HasValue) { now = _systemClock.UtcNow.UtcDateTime; } var result = new CurrentTimeEntryDto(); var entry = await _timeEntryRepository.GetTimeEntry(timeEntryId); result.TimeEntry = entry; //make sure it's not already been clocked out if (!entry.ClockOut.HasValue) { entry.ClockOut = now.Value; entry.DurationSeconds = (int)(entry.ClockOut.Value - entry.ClockIn).TotalSeconds; await _timeEntryRepository.UpdateTimeEntry(entry); //if scheduling is enabled, complete entry=>schedule relating/calculating //TODO: all of this work can be done async from the clock out action, ideally in some fire-and-forget event system. var patrol = await _patrolRepository.GetPatrol(entry.PatrolId); if (patrol.EnableScheduling) { //track all seconds that need to be allocated from the current time entry int unallocatedSeconds = entry.DurationSeconds.Value; //track what has been allocated thus far int allocatedSeconds = 0; var shifts = (await _shiftRepository.GetScheduledShiftAssignments(entry.PatrolId, entry.UserId, entry.ClockIn, entry.ClockOut)).ToList(); shifts = shifts.OrderBy(x => x.StartsAt).ToList(); var timeEntryScheduledShiftAssignments = new List <TimeEntryScheduledShiftAssignment>(); timeEntryScheduledShiftAssignments.AddRange(await _timeEntryRepository.GetScheduledShiftAssignmentsForTimeEntry(entry.Id)); //reset all existing allocations to 0 foreach (var existing in timeEntryScheduledShiftAssignments) { existing.DurationSeconds = 0; } for (int i = 0; i < shifts.Count && unallocatedSeconds > 0; i++) { var shift = shifts[i]; //time entries related to this shift var otherEntryAllocatedScheduledShiftAssignments = (await _timeEntryRepository.GetScheduledShiftAssignmentsForScheduledShiftAssignment(shift.Id)).ToList(); otherEntryAllocatedScheduledShiftAssignments = otherEntryAllocatedScheduledShiftAssignments.Where(x => x.TimeEntryId != entry.Id).ToList(); //get the schedule entry so we can see how much can be allocated var scheduledShift = await _shiftRepository.GetScheduledShift(shift.ScheduledShiftId); result.ScheduledShift = scheduledShift; //see if this schedule entry is already covered by previous time entries int previouslyAllocatedShiftSeconds = otherEntryAllocatedScheduledShiftAssignments.Sum(x => x.DurationSeconds); if (previouslyAllocatedShiftSeconds < scheduledShift.DurationSeconds) { //if not, allocate what we can to this shift //offsetting by allocatedSeconds and previouslyAllocatedShiftSeconds prevents overlapping shifts from both being allocated to the same time var allocateStart = entry.ClockIn + new TimeSpan(0, 0, allocatedSeconds) > scheduledShift.StartsAt + new TimeSpan(0, 0, previouslyAllocatedShiftSeconds) ? entry.ClockIn + new TimeSpan(0, 0, allocatedSeconds) : scheduledShift.StartsAt + new TimeSpan(0, 0, previouslyAllocatedShiftSeconds); var allocateEnd = entry.ClockOut.Value > scheduledShift.EndsAt ? scheduledShift.EndsAt : entry.ClockOut.Value; //TODO: we could check for existing time entries that overlap between allocateStart/End, but that really shouldn't happen //since a user can't have multiple ongoing time entries var maximumOverlappedAvailableAllocationSeconds = (int)(allocateEnd - allocateStart).TotalSeconds; var neededSeconds = scheduledShift.DurationSeconds - previouslyAllocatedShiftSeconds; //figure out how much to allocate based on overlap and what's remaining in the shift var secondsToAllocate = maximumOverlappedAvailableAllocationSeconds > neededSeconds ? neededSeconds : maximumOverlappedAvailableAllocationSeconds; //figure out how much to allocate based on what's remaining in the time entry secondsToAllocate = secondsToAllocate > unallocatedSeconds ? unallocatedSeconds : secondsToAllocate; //do the allocation //find a entryscheduledshiftassignment to alloate with, or make one var timeEntryScheduledShiftAssignment = timeEntryScheduledShiftAssignments.SingleOrDefault(x => x.ScheduledShiftAssignmentId == shift.Id); if (timeEntryScheduledShiftAssignment == null) { timeEntryScheduledShiftAssignment = new TimeEntryScheduledShiftAssignment() { ScheduledShiftAssignmentId = shift.Id, TimeEntryId = entry.Id, DurationSeconds = secondsToAllocate, }; await _timeEntryRepository.InsertTimeEntryScheduledShiftAssignment(timeEntryScheduledShiftAssignment); otherEntryAllocatedScheduledShiftAssignments.Add(timeEntryScheduledShiftAssignment); } else { timeEntryScheduledShiftAssignment.DurationSeconds = secondsToAllocate; await _timeEntryRepository.UpdateTimeEntryScheduledShiftAssignment(timeEntryScheduledShiftAssignment); } //adjust running totals unallocatedSeconds = unallocatedSeconds - secondsToAllocate; allocatedSeconds = allocatedSeconds + secondsToAllocate; result.TimeEntryScheduledShiftAssignment = timeEntryScheduledShiftAssignment; } else { //_logger.LogDebug("Previously allocated > current duration"); } if (result.ScheduledShift.ShiftId.HasValue) { result.Shift = await _shiftRepository.GetShift(result.ScheduledShift.ShiftId.Value); } if (result.ScheduledShift.GroupId.HasValue) { result.Group = await _groupRepository.GetGroup(result.ScheduledShift.GroupId.Value); } } //there's no scenario where this should happen, the only way it could occur is if a schedule change //occurred mid-shift, which we do not allow. But just in case, clean it up anyway foreach (var timeEntryScheduledShiftAssignment in timeEntryScheduledShiftAssignments) { if (timeEntryScheduledShiftAssignment.DurationSeconds == 0) { await _timeEntryRepository.DeleteTimeEntryScheduledShiftAssignment(timeEntryScheduledShiftAssignment); } } } } return(result); }