001 /**
002 * Copyright 2004-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.hr.time.shiftdiff.rule.service;
017
018 import org.apache.log4j.Logger;
019 import org.joda.time.*;
020 import org.kuali.hr.job.Job;
021 import org.kuali.hr.time.calendar.CalendarEntries;
022 import org.kuali.hr.time.principal.PrincipalHRAttributes;
023 import org.kuali.hr.time.service.base.TkServiceLocator;
024 import org.kuali.hr.time.shiftdiff.rule.ShiftDifferentialRule;
025 import org.kuali.hr.time.shiftdiff.rule.dao.ShiftDifferentialRuleDao;
026 import org.kuali.hr.time.timeblock.TimeBlock;
027 import org.kuali.hr.time.timeblock.TimeHourDetail;
028 import org.kuali.hr.time.timesheet.TimesheetDocument;
029 import org.kuali.hr.time.util.TKUtils;
030 import org.kuali.hr.time.util.TkConstants;
031 import org.kuali.hr.time.util.TkTimeBlockAggregate;
032 import org.kuali.hr.time.workflow.TimesheetDocumentHeader;
033
034 import java.math.BigDecimal;
035 import java.sql.Date;
036 import java.util.*;
037
038
039 public class ShiftDifferentialRuleServiceImpl implements ShiftDifferentialRuleService {
040
041 @SuppressWarnings("unused")
042 private static final Logger LOG = Logger.getLogger(ShiftDifferentialRuleServiceImpl.class);
043 /**
044 * The maximum allowable time between timeblocks before we consider them to
045 * be day-boundary single time blocks.
046 */
047 private ShiftDifferentialRuleDao shiftDifferentialRuleDao = null;
048
049 private Map<Long,List<ShiftDifferentialRule>> getJobNumberToShiftRuleMap(TimesheetDocument timesheetDocument) {
050 Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts = new HashMap<Long,List<ShiftDifferentialRule>>();
051 PrincipalHRAttributes principalCal = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(timesheetDocument.getPrincipalId(),timesheetDocument.getPayCalendarEntry().getEndPeriodDate());
052
053 for (Job job : timesheetDocument.getJobs()) {
054 List<ShiftDifferentialRule> shiftDifferentialRules = getShiftDifferentalRules(job.getLocation(),job.getHrSalGroup(),job.getPayGrade(),principalCal.getPayCalendar(),
055 TKUtils.getTimelessDate(timesheetDocument.getPayCalendarEntry().getBeginPeriodDateTime()));
056 if (shiftDifferentialRules.size() > 0)
057 jobNumberToShifts.put(job.getJobNumber(), shiftDifferentialRules);
058 }
059
060 return jobNumberToShifts;
061 }
062
063 private Map<Long,List<TimeBlock>> getPreviousPayPeriodLastDayJobToTimeBlockMap(TimesheetDocument timesheetDocument, Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts) {
064 Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay = null;
065
066 // Get the last day of the last week of the previous pay period.
067 // This is the only day that can have impact on the current day.
068 List<TimeBlock> prevBlocks = TkServiceLocator.getTimesheetService().getPrevDocumentTimeBlocks(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getPayBeginDate());
069 if (prevBlocks.size() > 0) {
070 TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getPayBeginDate());
071 if (prevTdh != null) {
072 CalendarEntries prevPayCalendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(timesheetDocument.getPrincipalId(), prevTdh.getPayEndDate(), null);
073 TkTimeBlockAggregate prevTimeAggregate = new TkTimeBlockAggregate(prevBlocks, prevPayCalendarEntry, prevPayCalendarEntry.getCalendarObj(), true);
074 List<List<TimeBlock>> dayBlocks = prevTimeAggregate.getDayTimeBlockList();
075 List<TimeBlock> previousPeriodLastDayBlocks = dayBlocks.get(dayBlocks.size() - 1);
076 // Set back to null if there is nothing in the list.
077 if (previousPeriodLastDayBlocks.size() > 0) {
078 jobNumberToTimeBlocksPreviousDay = new HashMap<Long, List<TimeBlock>>();
079
080 for (TimeBlock block : previousPeriodLastDayBlocks) {
081 // Job Number to TimeBlock for Last Day of Previous Time
082 // Period
083 Long jobNumber = block.getJobNumber();
084 if (jobNumberToShifts.containsKey(jobNumber)) {
085 // we have a useful timeblock.
086 List<TimeBlock> jblist = jobNumberToTimeBlocksPreviousDay.get(jobNumber);
087 if (jblist == null) {
088 jblist = new ArrayList<TimeBlock>();
089 jobNumberToTimeBlocksPreviousDay.put(jobNumber, jblist);
090 }
091 jblist.add(block);
092 }
093 }
094 }
095 }
096 }
097
098 return jobNumberToTimeBlocksPreviousDay;
099 }
100
101 private boolean timeBlockHasEarnCode(Set<String> earnCodes, TimeBlock block) {
102 boolean present = false;
103
104 if (block != null && earnCodes != null)
105 present = earnCodes.contains(block.getEarnCode());
106
107 return present;
108 }
109
110 /**
111 * Returns a BigDecimal representing the sum of all of the negative time
112 * hour detail types. In this case, only LUN is considered. This can be
113 * modified to add other considerations.
114 *
115 * @param block The Timeblock to inspect.
116 *
117 * @return A big decimal.
118 */
119 private BigDecimal negativeTimeHourDetailSum(TimeBlock block) {
120 BigDecimal sum = BigDecimal.ZERO;
121
122 if (block != null) {
123 List<TimeHourDetail> details = block.getTimeHourDetails();
124 for (TimeHourDetail detail : details) {
125 if (detail.getEarnCode().equals(TkConstants.LUNCH_EARN_CODE)) {
126 sum = sum.add(detail.getHours());
127 }
128 }
129 }
130
131 return sum;
132 }
133
134 @Override
135 public void processShiftDifferentialRules(TimesheetDocument timesheetDocument, TkTimeBlockAggregate aggregate) {
136 DateTimeZone zone = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
137 List<List<TimeBlock>> blockDays = aggregate.getDayTimeBlockList();
138 DateTime periodStartDateTime = timesheetDocument.getPayCalendarEntry().getBeginLocalDateTime().toDateTime(zone);
139 Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts = getJobNumberToShiftRuleMap(timesheetDocument);
140
141
142 // If there are no shift differential rules, we have an early exit.
143 if (jobNumberToShifts.isEmpty()) {
144 return;
145 }
146
147 // Get the last day of the previous pay period. We need this to determine
148 // if there are hours from the previous pay period that will effect the
149 // shift rule on the first day of the currently-being-processed pay period.
150 //
151 // Will be set to null if not applicable.
152 boolean previousPayPeriodPrevDay = true;
153 Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay =
154 getPreviousPayPeriodLastDayJobToTimeBlockMap(timesheetDocument, jobNumberToShifts);
155
156 // We are going to look at the time blocks grouped by Days.
157 //
158 // This is a very large outer loop.
159 for (int pos = 0; pos < blockDays.size(); pos++) {
160 List<TimeBlock> blocks = blockDays.get(pos); // Timeblocks for this day.
161 if (blocks.isEmpty())
162 continue; // No Time blocks, no worries.
163
164 DateTime currentDay = periodStartDateTime.plusDays(pos);
165 Interval virtualDay = new Interval(currentDay, currentDay.plusHours(24));
166
167 // Builds our JobNumber to TimeBlock for Current Day List.
168 //
169 // Shift Differential Rules are also grouped by Job number, this
170 // provides a quick way to do the lookup / reference.
171 // We don't need every time block, only the ones that will be
172 // applicable to the shift rules.
173 Map<Long, List<TimeBlock>> jobNumberToTimeBlocks = new HashMap<Long,List<TimeBlock>>();
174 for (TimeBlock block : blocks) {
175 Long jobNumber = block.getJobNumber();
176 if (jobNumberToShifts.containsKey(jobNumber)) {
177 List<TimeBlock> jblist = jobNumberToTimeBlocks.get(jobNumber);
178 if (jblist == null) {
179 jblist = new ArrayList<TimeBlock>();
180 jobNumberToTimeBlocks.put(jobNumber, jblist);
181 }
182 jblist.add(block);
183 }
184 }
185
186
187 // Large Outer Loop to look at applying the Shift Rules based on
188 // the current JobNumber.
189 //
190 // This loop will handle previous day boundary time as well as the
191 // current day.
192 //
193 // There is room for refactoring here!
194 for (Long jobNumber: jobNumberToShifts.keySet()) {
195 List<ShiftDifferentialRule> shiftDifferentialRules = jobNumberToShifts.get(jobNumber);
196 // Obtain and sort our previous and current time blocks.
197 List<TimeBlock> ruleTimeBlocksPrev = null;
198 List<TimeBlock> ruleTimeBlocksCurr = jobNumberToTimeBlocks.get(jobNumber);
199 if (ruleTimeBlocksCurr != null && ruleTimeBlocksCurr.size() > 0) {
200 if (jobNumberToTimeBlocksPreviousDay != null)
201 ruleTimeBlocksPrev = jobNumberToTimeBlocksPreviousDay.get(jobNumber);
202 if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0)
203 this.sortTimeBlocksInverse(ruleTimeBlocksPrev);
204 this.sortTimeBlocksNatural(ruleTimeBlocksCurr);
205 } else {
206 // Skip to next job, there is nothing for this job
207 // on this day, and because of this we don't care
208 // about the previous day either.
209 continue;
210 }
211
212 for (ShiftDifferentialRule rule : shiftDifferentialRules) {
213 Set<String> fromEarnGroup = TkServiceLocator.getEarnGroupService().getEarnCodeListForEarnGroup(rule.getFromEarnGroup(), TKUtils.getTimelessDate(timesheetDocument.getPayCalendarEntry().getBeginPeriodDateTime()));
214
215 // Because of the way java.sql.Time are stored, we need to first
216 // construct a LocalTime in the System Time Zone, then convert that
217 // time to the users time zone.
218 LocalTime ruleStart = new LocalTime(rule.getBeginTime(), TKUtils.getSystemDateTimeZone());
219 LocalTime ruleEnd = new LocalTime(rule.getEndTime(), TKUtils.getSystemDateTimeZone());
220 ruleStart = new LocalTime(ruleStart, zone);
221 ruleEnd = new LocalTime(ruleEnd, zone);
222
223
224 DateTime shiftEnd = ruleEnd.toDateTime(currentDay);
225 DateTime shiftStart = ruleStart.toDateTime(currentDay);
226
227 if (shiftEnd.isBefore(shiftStart) || shiftEnd.isEqual(shiftStart))
228 shiftEnd = shiftEnd.plusDays(1);
229 Interval shiftInterval = new Interval(shiftStart, shiftEnd);
230
231 // Set up buckets to handle previous days time accumulations
232 BigDecimal hoursBeforeVirtualDay = BigDecimal.ZERO;
233
234 // Check current day first block to see if start time gap from virtual day start is greater than max gap
235 // if so, we can skip the previous day checks.
236 TimeBlock firstBlockOfCurrentDay = null;
237 for (TimeBlock b : ruleTimeBlocksCurr) {
238 if (timeBlockHasEarnCode(fromEarnGroup, b)) {
239 firstBlockOfCurrentDay = b;
240 break;
241 }
242 }
243
244 // Previous Day :: We have prior block container of nonzero size, and the previous day is active.
245 Interval previousDayShiftInterval = new Interval(shiftStart.minusDays(1), shiftEnd.minusDays(1));
246
247 // Blank initialization pointer for picking which interval to pass to applyPremium()
248 Interval evalInterval = null;
249 if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0 && dayIsRuleActive(currentDay.minusDays(1), rule)) {
250 // Simple heuristic to see if we even need to worry about
251 // the Shift rule for this set of data.
252 if (shiftEnd.isAfter(virtualDay.getEnd())) {
253 // Compare first block of previous day with first block of current day for max gaptitude.
254 TimeBlock firstBlockOfPreviousDay = null;
255 for (TimeBlock b : ruleTimeBlocksPrev) {
256 if (timeBlockHasEarnCode(fromEarnGroup, b)) {
257 firstBlockOfPreviousDay = b;
258 break;
259 }
260 }
261 // Only if we actually have at least one block.
262 // Adding Assumption: We must have both a valid current and previous block. Max Gap can not be more than a virtual day.
263 // If this assumption does not hold, additional logic will be needed to iteratively go back in time to figure out which
264 // blocks are valid.
265 if ( (firstBlockOfPreviousDay != null) && (firstBlockOfCurrentDay != null)) {
266 Interval previousBlockInterval = new Interval(new DateTime(firstBlockOfPreviousDay.getEndTimestamp(), zone), new DateTime(firstBlockOfCurrentDay.getBeginTimestamp(), zone));
267 Duration blockGapDuration = previousBlockInterval.toDuration();
268 BigDecimal bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis());
269 // if maxGap is 0, ignore gaps and assign shift to time blocks within the hours
270 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) {
271 // If we are here, we know we have at least one valid time block to pull some hours forward from.
272
273
274 // These are inversely sorted.
275 for (int i=0; i<ruleTimeBlocksPrev.size(); i++) {
276 TimeBlock b = ruleTimeBlocksPrev.get(i);
277 if (timeBlockHasEarnCode(fromEarnGroup, b)) {
278 Interval blockInterval = new Interval(new DateTime(b.getBeginTimestamp(), zone), new DateTime(b.getEndTimestamp(), zone));
279
280 // Calculate Block Gap, the duration between clock outs and clock ins of adjacent time blocks.
281 if (previousBlockInterval != null) {
282 blockGapDuration = new Duration(new DateTime(b.getEndTimestamp(), zone), previousBlockInterval.getStart());
283 bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis());
284 }
285
286 // Check Gap, if good, sum hours, if maxGap is 0, ignore gaps
287 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) {
288 // Calculate Overlap and add it to hours before virtual day bucket.
289 if (blockInterval.overlaps(previousDayShiftInterval)) {
290 BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval.overlap(previousDayShiftInterval).toDurationMillis());
291 hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs);
292 }
293
294 } else {
295 // Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded.
296 break;
297 }
298
299 previousBlockInterval = blockInterval;
300
301 }
302 }
303 } else {
304 // DO NOTHING!
305 }
306 }
307 }
308 }
309
310 BigDecimal hoursToApply = BigDecimal.ZERO;
311 BigDecimal hoursToApplyPrevious = BigDecimal.ZERO;
312 // If the hours before virtual day are less than or equal to
313 // min hours, we have already applied the time, so we don't
314 // set hoursToApplyPrevious
315 if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) {
316 // we need to apply these hours.
317 hoursToApplyPrevious = hoursBeforeVirtualDay;
318 }
319
320
321 // Current Day
322
323 TimeBlock previous = null; // Previous Time Block
324 List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to.
325 List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them.
326 // Iterate over sorted list, checking time boundaries vs Shift Intervals.
327 long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay);
328
329 boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks.
330 if (!dayIsRuleActive(currentDay, rule)) {
331 if (dayIsRuleActive(currentDay.minusDays(1), rule)) {
332 previousDayOnly = true;
333 } else {
334 // Nothing to see here, move to next rule.
335 continue;
336 }
337
338 }
339
340 /*
341 * We will touch each time block and accumulate time blocks that are applicable to
342 * the current rule we are on.
343 */
344
345 // These blocks are only used for detail application
346 // We don't want to pass along the previous pay period,
347 // because we don't want to modify the time blocks on that
348 // period. If null is passed, time will be placed on the
349 // first block of the first period if the previous period
350 // block had influence.
351 List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev);
352
353 for (TimeBlock current : ruleTimeBlocksCurr) {
354 if (!timeBlockHasEarnCode(fromEarnGroup, current)) {
355 // TODO: WorkSchedule considerations somewhere in here?
356 continue;
357 }
358
359 Interval blockInterval = new Interval(new DateTime(current.getBeginTimestamp(), zone), new DateTime(current.getEndTimestamp(), zone));
360
361 // Check both Intervals, since the time blocks could still
362 // be applicable to the previous day. These two intervals should
363 // not have any overlap.
364 if (previousDayShiftInterval.overlaps(shiftInterval))
365 throw new RuntimeException("Interval of greater than 24 hours created in the rules processing.");
366
367 // This block of code handles cases where you have time
368 // that spills to multiple days and a shift rule that
369 // has a valid window on multiple consecutive days. Time
370 // must be applied with the correct shift interval.
371 Interval overlap = previousDayShiftInterval.overlap(blockInterval);
372 evalInterval = previousDayShiftInterval;
373 if (overlap == null) {
374 if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) {
375 // we have hours from previous day, and the shift
376 // window is going to move to current day.
377 // Need to apply this now, and move window forward
378 // for current time block.
379 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
380 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
381 accumulatedMillis = 0L; // reset accumulated hours..
382 hoursToApply = BigDecimal.ZERO;
383 hoursToApplyPrevious = BigDecimal.ZERO;
384 }
385
386 // Because of our position in the loop, when we are at this point,
387 // we know we've passed any previous day shift intervals, so we can
388 // determine if we should skip the current day based on the boolean
389 // we set earlier.
390 if (previousDayOnly) {
391 continue;
392 }
393
394 overlap = shiftInterval.overlap(blockInterval);
395 evalInterval = shiftInterval;
396 }
397
398 // Time bucketing and application as normal:
399 //
400 if (overlap != null) {
401 // There IS overlap.
402 if (previous != null) {
403 // only check max gap if max gap of rule is not 0
404 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0 && exceedsMaxGap(previous, current, rule.getMaxGap())) {
405 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
406 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
407 accumulatedMillis = 0L; // reset accumulated hours..
408 hoursToApply = BigDecimal.ZERO;
409 hoursToApplyPrevious = BigDecimal.ZERO;
410 } else {
411 long millis = overlap.toDurationMillis();
412 accumulatedMillis += millis;
413 hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
414 }
415 } else {
416 // Overlap shift at first time block.
417 long millis = overlap.toDurationMillis();
418 accumulatedMillis += millis;
419 hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
420 }
421 accumulatedBlocks.add(current);
422 accumulatedBlockIntervals.add(blockInterval);
423 previous = current; // current can still apply to next.
424 } else {
425 // No Overlap / Outside of Rule
426 if (previous != null) {
427 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
428 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
429 accumulatedMillis = 0L; // reset accumulated hours..
430 hoursToApply = BigDecimal.ZERO;
431 hoursToApplyPrevious = BigDecimal.ZERO;
432 }
433 }
434
435 }
436
437 // All time blocks are iterated over, check for remainders.
438 // Check containers for time, and apply if needed.
439 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
440 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
441 }
442 }
443 // Keep track of previous as we move day by day.
444 jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks;
445 previousPayPeriodPrevDay = false;
446 }
447
448 }
449
450 @Override
451 public List<ShiftDifferentialRule> getShiftDifferentialRules(String location, String hrSalGroup, String payGrade, Date fromEffdt, Date toEffdt, String active, String showHist) {
452 return shiftDifferentialRuleDao.getShiftDifferentialRules(location, hrSalGroup, payGrade, fromEffdt, toEffdt, active, showHist);
453 }
454
455 private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) {
456 List<TimeBlock> filtered;
457
458 if (blocks == null || blocks.size() == 0)
459 filtered = null;
460 else {
461 filtered = new ArrayList<TimeBlock>();
462 for (TimeBlock b : blocks) {
463 if (timeBlockHasEarnCode(fromEarnGroup, b))
464 filtered.add(b);
465 }
466 }
467
468 return filtered;
469 }
470
471
472 private void applyAccumulatedWrapper(BigDecimal accumHours, Interval evalInterval, List<Interval>accumulatedBlockIntervals, List<TimeBlock>accumulatedBlocks, List<TimeBlock> previousBlocks, BigDecimal hoursToApplyPrevious, BigDecimal hoursToApply, ShiftDifferentialRule rule) {
473 if (accumHours.compareTo(rule.getMinHours()) >= 0) {
474 this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode());
475 }
476 accumulatedBlocks.clear();
477 accumulatedBlockIntervals.clear();
478 }
479
480 private void sortTimeBlocksInverse(List<TimeBlock> blocks) {
481 Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
482 public int compare(TimeBlock tb1, TimeBlock tb2) {
483 if (tb1 != null && tb2 != null)
484 return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
485 return 0;
486 }
487 });
488 }
489
490 private void sortTimeBlocksNatural(List<TimeBlock> blocks) {
491 Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
492 public int compare(TimeBlock tb1, TimeBlock tb2) {
493 if (tb1 != null && tb2 != null)
494 return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
495 return 0;
496 }
497 });
498 }
499
500 /**
501 *
502 * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block.
503 * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation.
504 * @param blocks The blocks we are applying hours to.
505 * @param previousBlocks If present, this is the list of time blocks from a previous "day", on which the initial hours (from previous day) should be placed.
506 * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL)
507 * @param hours hours to apply
508 * @param earnCode what earn code to create time hour detail entry for.
509 */
510 void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode) {
511 for (int i=0; i<blocks.size(); i++) {
512 TimeBlock b = blocks.get(i);
513
514 // Only apply initial hours to the first timeblock.
515 if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) {
516 // ONLY if they're on the same document ID, do we apply to previous,
517 // otherwise we dump all on the current document.
518 if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) {
519 for (TimeBlock pb : previousBlocks) {
520 BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number
521 initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours!
522 if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply.
523 break;
524
525 // Adjust hours on the block by the lunch sub hours, so we're not over applying.
526 BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub));
527 addPremiumTimeHourDetail(pb, hoursToApply, earnCode);
528 initialHours = initialHours.subtract(hoursToApply, TkConstants.MATH_CONTEXT);
529 if (initialHours.compareTo(BigDecimal.ZERO) <= 0)
530 break;
531 }
532 } else {
533 addPremiumTimeHourDetail(b, initialHours, earnCode);
534 }
535 }
536
537 BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number
538 hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours!
539
540 if (hours.compareTo(BigDecimal.ZERO) > 0) {
541 Interval blockInterval = blockIntervals.get(i);
542 Interval overlapInterval = shift.overlap(blockInterval);
543 if (overlapInterval == null)
544 continue;
545
546 long overlap = overlapInterval.toDurationMillis();
547 BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule
548 // Adjust this time block's hoursMax (below) by lunchSub to
549 // make sure the time applied is the correct amount per block.
550 BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub));
551
552 addPremiumTimeHourDetail(b, hoursToApply, earnCode);
553 hours = hours.subtract(hoursToApply, TkConstants.MATH_CONTEXT);
554 }
555 }
556 }
557
558 void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) {
559 List<TimeHourDetail> details = block.getTimeHourDetails();
560 TimeHourDetail premium = new TimeHourDetail();
561 premium.setHours(hours);
562 premium.setEarnCode(earnCode);
563 premium.setTkTimeBlockId(block.getTkTimeBlockId());
564 details.add(premium);
565 }
566
567 /**
568 * Does the difference between the previous time blocks clock out time and the
569 * current time blocks clock in time exceed the max gap. max gap is in minutes
570 *
571 * @param previous
572 * @param current
573 * @param maxGap
574 * @return
575 */
576 boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) {
577 long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime();
578 BigDecimal gapMinutes = TKUtils.convertMillisToMinutes(difference);
579
580 return (gapMinutes.compareTo(maxGap) > 0);
581 }
582
583 public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) {
584 this.shiftDifferentialRuleDao = shiftDifferentialRuleDao;
585 }
586
587 @Override
588 public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) {
589 return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId);
590 }
591
592 @Override
593 public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, Date asOfDate) {
594 List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>();
595
596 // location, sal group, pay grade
597
598 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
599
600 // location, sal group, *
601 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate));
602
603 // location, *, pay grade
604 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate));
605
606 // location, *, *
607 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate));
608
609 // *, sal group, pay grade
610 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
611
612 // *, sal group, *
613 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate));
614
615 // *, *, pay grade
616 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate));
617
618 // *, *, *
619 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate));
620
621 if (sdrs == null)
622 sdrs = Collections.emptyList();
623
624 return sdrs;
625 }
626
627 private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) {
628 boolean active = false;
629
630 switch (currentDate.getDayOfWeek()) {
631 case DateTimeConstants.MONDAY:
632 active = sdr.isMonday();
633 break;
634 case DateTimeConstants.TUESDAY:
635 active = sdr.isTuesday();
636 break;
637 case DateTimeConstants.WEDNESDAY:
638 active = sdr.isWednesday();
639 break;
640 case DateTimeConstants.THURSDAY:
641 active = sdr.isThursday();
642 break;
643 case DateTimeConstants.FRIDAY:
644 active = sdr.isFriday();
645 break;
646 case DateTimeConstants.SATURDAY:
647 active = sdr.isSaturday();
648 break;
649 case DateTimeConstants.SUNDAY:
650 active = sdr.isSunday();
651 break;
652 }
653
654 return active;
655 }
656
657 @Override
658 public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) {
659 shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules);
660 }
661
662 @Override
663 public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) {
664 shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule);
665 }
666
667 }