public Task <IEnumerable <CurrentTimeEntryDto> > GetMissingShiftTime(int patrolId, int?userId, DateTime from, DateTime to) { return(_connection.QueryAsync <User, TimeEntry, ScheduledShift, TimeEntryScheduledShiftAssignment, Shift, Group, CurrentTimeEntryDto>(@" select u.id ,u.firstname ,u.lastname ,0 id --has to be non null or dommel will null out the entire object ,min(te.clockin) clockin ,max(te.clockout) clockout ,ss.id ,ss.startsat ,ss.endsat ,ss.durationseconds ,0 id --has to be non null or dommel will null out the entire object ,sum(tessa.durationseconds) durationseconds ,s.id ,s.name ,g.id ,g.name from scheduledshifts ss inner join scheduledshiftassignments ssa on ssa.scheduledshiftid=ss.id inner join users u on u.id=ssa.assigneduserid left join timeentryscheduledshiftassignments tessa on tessa.scheduledshiftassignmentid=ssa.id left join timeentrys te on te.id=tessa.timeentryid left join shifts s on s.id=ss.shiftid left join groups g on g.id=ss.groupid where ss.startsat <= @to and ss.endsat >= @from and ss.patrolId = @patrolId and (@userId is null or u.id=@userId) group by u.id ,u.firstname ,u.lastname ,ss.id ,ss.startsat ,ss.endsat ,ss.durationseconds ,s.id ,s.name ,g.id ,g.name having sum(tessa.durationseconds) < ss.durationseconds or sum(tessa.durationseconds) is null order by ss.startsat desc " , (u, te, ss, tessa, s, g) => { var dto = new CurrentTimeEntryDto(); dto.User = u; dto.TimeEntry = te; dto.ScheduledShift = ss; dto.TimeEntryScheduledShiftAssignment = tessa; dto.Shift = s; dto.Group = g; return dto; }, new { patrolId, userId, from, to })); }
public async Task <CurrentTimeEntryDto> GetCurrent(int patrolId, int userId) { var result = new CurrentTimeEntryDto(); var activeEntries = await _timeEntryRepository.GetActiveTimeEntries(patrolId, userId); result.TimeEntry = activeEntries.OrderBy(x => x.ClockIn).FirstOrDefault(); if (result.TimeEntry != null) { var tessas = await _timeEntryRepository.GetScheduledShiftAssignmentsForTimeEntry(result.TimeEntry.Id); result.TimeEntryScheduledShiftAssignment = tessas.OrderBy(x => x.Id).FirstOrDefault(); if (result.TimeEntryScheduledShiftAssignment != null) { var scheduledShiftAssignment = await _shiftRepository.GetScheduledShiftAssignment(result.TimeEntryScheduledShiftAssignment.ScheduledShiftAssignmentId); result.ScheduledShift = await _shiftRepository.GetScheduledShift(scheduledShiftAssignment.ScheduledShiftId); 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); } } } else { //find the first shift in the next week var scheduledShifts = await _shiftRepository.GetScheduledShiftAssignments(patrolId, userId, _systemClock.UtcNow.UtcDateTime, _systemClock.UtcNow.UtcDateTime + new TimeSpan(7, 0, 0, 0)); var scheduledShiftDto = scheduledShifts.OrderBy(x => x.StartsAt).FirstOrDefault(); if (scheduledShiftDto != null) { result.ScheduledShift = await _shiftRepository.GetScheduledShift(scheduledShiftDto.ScheduledShiftId); 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); } } } return(result); }
public Task <IEnumerable <CurrentTimeEntryDto> > GetTimeEntries(int patrolId, int?userId, DateTime from, DateTime to) { return(_connection.QueryAsync <User, TimeEntry, ScheduledShift, TimeEntryScheduledShiftAssignment, Shift, Group, CurrentTimeEntryDto>(@" select u.id ,u.firstname ,u.lastname ,u.email ,te.id ,te.clockin ,te.clockout ,te.durationseconds ,ss.id ,ss.startsat ,ss.endsat ,ss.durationseconds ,tessa.id ,tessa.durationseconds ,s.id ,s.name ,g.id ,g.name from timeentrys te inner join users u on u.id=te.userid left join timeentryscheduledshiftassignments tessa on tessa.timeentryid=te.id left join scheduledshiftassignments ssa on ssa.id=tessa.scheduledshiftassignmentid left join scheduledshifts ss on ss.id=ssa.scheduledshiftid left join shifts s on s.id=ss.shiftid left join groups g on g.id=ss.groupid where te.patrolid=@patrolId and te.clockout >= @from and te.clockin <= @to and (@userId is null or te.userid=@userId) order by te.clockin desc " , (u, te, ss, tessa, s, g) => { var dto = new CurrentTimeEntryDto(); dto.User = u; dto.TimeEntry = te; dto.ScheduledShift = ss; dto.TimeEntryScheduledShiftAssignment = tessa; dto.Shift = s; dto.Group = g; return dto; }, new { patrolId, userId, from, to })); }
public Task <IEnumerable <CurrentTimeEntryDto> > GetMissingTimeEntries(int patrolId, DateTime now) { return(_connection.QueryAsync <User, TimeEntry, ScheduledShift, Shift, Group, CurrentTimeEntryDto>(@" select u.id ,u.firstname ,u.lastname ,u.email ,te.id ,te.clockin ,te.clockout ,te.durationseconds ,ss.id ,ss.startsat ,ss.endsat ,ss.durationseconds ,s.id ,s.name ,g.id ,g.name from scheduledshifts ss inner join scheduledshiftassignments ssa on ssa.scheduledshiftid=ss.id inner join users u on u.id=ssa.assigneduserid left join timeentrys te on te.userid=ssa.assigneduserid and te.patrolid=@patrolid and te.clockin<@now and te.clockout is null left join shifts s on s.id=ss.shiftid left join groups g on g.id=ss.groupid where ss.patrolid=@patrolId and ss.startsat <= @now and ss.endsat >= @now and te.id is null order by u.lastname,u.firstname " , (u, te, ss, s, g) => { var dto = new CurrentTimeEntryDto(); dto.User = u; dto.TimeEntry = te; dto.ScheduledShift = ss; dto.Shift = s; dto.Group = g; return dto; }, new { patrolId, now })); }
public async Task <CurrentTimeEntryDto> ClockIn(int patrolId, int userId, DateTime?now = null) { if (!now.HasValue) { now = _systemClock.UtcNow.UtcDateTime; } var existingEntries = await _timeEntryRepository.GetActiveTimeEntries(patrolId, userId); //if there are existing entries we don't need to do anything if (!existingEntries.Any()) { var result = new CurrentTimeEntryDto(); var entry = new TimeEntry() { ClockIn = now.Value, PatrolId = patrolId, UserId = userId }; await _timeEntryRepository.InsertTimeEntry(entry); result.TimeEntry = entry; //if the patrol has schedulingenabled, try to find a shift to associate var patrol = await _patrolRepository.GetPatrol(patrolId); if (patrol.EnableScheduling) { //find shifts for the user that occur in the next day //TODO: does this range need to be configurable by patrol? eg: how early can you allow clocking in? var shifts = await _shiftRepository.GetScheduledShiftAssignments(patrolId, userId, entry.ClockIn, entry.ClockIn + new TimeSpan(1, 0, 0, 0), ShiftStatus.Assigned); shifts = shifts.OrderBy(x => x.StartsAt); if (shifts.Any()) { //if there is more than one match, associate the one that starts the earliest var shift = shifts.First(); var scheduledShift = await _shiftRepository.GetScheduledShift(shift.ScheduledShiftId); var timeEntryScheduledShiftAssignment = new TimeEntryScheduledShiftAssignment() { ScheduledShiftAssignmentId = shift.Id, TimeEntryId = entry.Id }; await _timeEntryRepository.InsertTimeEntryScheduledShiftAssignment(timeEntryScheduledShiftAssignment); result.TimeEntryScheduledShiftAssignment = timeEntryScheduledShiftAssignment; result.ScheduledShift = scheduledShift; if (scheduledShift.ShiftId.HasValue) { result.Shift = await _shiftRepository.GetShift(scheduledShift.ShiftId.Value); } if (scheduledShift.GroupId.HasValue) { result.Group = await _groupRepository.GetGroup(scheduledShift.GroupId.Value); } } } return(result); } else { var result = new CurrentTimeEntryDto(); var entry = existingEntries.Where(x => x.ClockIn < now).OrderBy(x => x.ClockIn).FirstOrDefault(); var timeEntryScheduledShiftAssignments = await _timeEntryRepository.GetScheduledShiftAssignmentsForTimeEntry(entry.Id); if (timeEntryScheduledShiftAssignments.Any()) { var tessa = timeEntryScheduledShiftAssignments.FirstOrDefault(); result.TimeEntryScheduledShiftAssignment = tessa; var assignment = await _shiftRepository.GetScheduledShiftAssignment(tessa.ScheduledShiftAssignmentId); result.ScheduledShift = await _shiftRepository.GetScheduledShift(assignment.ScheduledShiftId); 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); } } return(result); } }
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); }