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.util; 017 018 import java.math.BigDecimal; 019 import java.sql.Time; 020 import java.sql.Timestamp; 021 import java.util.ArrayList; 022 import java.util.Collections; 023 import java.util.Comparator; 024 import java.util.List; 025 import java.util.ListIterator; 026 027 import org.apache.commons.collections.CollectionUtils; 028 import org.joda.time.DateTime; 029 import org.joda.time.DateTimeZone; 030 import org.joda.time.Interval; 031 import org.joda.time.LocalDateTime; 032 import org.joda.time.LocalTime; 033 import org.kuali.hr.lm.leaveblock.LeaveBlock; 034 import org.kuali.hr.lm.util.LeaveBlockAggregate; 035 import org.kuali.hr.time.assignment.Assignment; 036 import org.kuali.hr.time.calendar.Calendar; 037 import org.kuali.hr.time.calendar.CalendarEntries; 038 import org.kuali.hr.time.flsa.FlsaDay; 039 import org.kuali.hr.time.flsa.FlsaWeek; 040 import org.kuali.hr.time.service.base.TkServiceLocator; 041 import org.kuali.hr.time.timeblock.TimeBlock; 042 import org.kuali.hr.time.timeblock.TimeHourDetail; 043 import org.kuali.hr.time.timesheet.TimesheetDocument; 044 import org.kuali.hr.time.workflow.TimesheetDocumentHeader; 045 046 public class TkTimeBlockAggregate { 047 private List<List<TimeBlock>> dayTimeBlockList = new ArrayList<List<TimeBlock>>(); 048 private List<List<LeaveBlock>> dayLeaveBlockList = new ArrayList<List<LeaveBlock>>(); 049 private CalendarEntries payCalendarEntry; 050 private Calendar payCalendar; 051 052 /** 053 * Defaults to using SYSTEM time zone. 054 * 055 * @param timeBlocks 056 * @param payCalendarEntry 057 */ 058 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, CalendarEntries payCalendarEntry){ 059 this(timeBlocks, payCalendarEntry, TkServiceLocator.getCalendarService().getCalendar(payCalendarEntry.getHrCalendarId())); 060 } 061 062 /** 063 * Defaults to using SYSTEM time zone. 064 * 065 * @param timeBlocks 066 * @param payCalendarEntry 067 * @param payCalendar 068 */ 069 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, CalendarEntries payCalendarEntry, Calendar payCalendar) { 070 this(timeBlocks, payCalendarEntry, payCalendar, false); 071 } 072 073 /** 074 * Provides the option to refer to the time zone adjusted time for the current 075 * user. 076 * @param timeBlocks 077 * @param payCalendarEntry 078 * @param payCalendar 079 * @param useUserTimeZone 080 */ 081 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, CalendarEntries payCalendarEntry, Calendar payCalendar, boolean useUserTimeZone) { 082 this(timeBlocks, payCalendarEntry, payCalendar, useUserTimeZone, 083 useUserTimeZone ? TKUtils.getDaySpanForCalendarEntry(payCalendarEntry) : TKUtils.getDaySpanForCalendarEntry(payCalendarEntry, TKUtils.getSystemDateTimeZone())); 084 } 085 086 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, CalendarEntries payCalendarEntry, Calendar payCalendar, boolean useUserTimeZone, List<Interval> dayIntervals) { 087 this.payCalendarEntry = payCalendarEntry; 088 this.payCalendar = payCalendar; 089 090 for(Interval dayInt : dayIntervals){ 091 List<TimeBlock> dayTimeBlocks = new ArrayList<TimeBlock>(); 092 for(TimeBlock timeBlock : timeBlocks){ 093 094 // Assumption: Timezones can only be switched at pay period end boundaries. 095 // If the above assumption becomes false, the logic below will need to 096 // accommodate virtual chopping of time blocks to have them fit nicely 097 // in the "days" that are displayed to users. 098 099 DateTime beginTime = useUserTimeZone ? timeBlock.getBeginTimeDisplay() : new DateTime(timeBlock.getBeginTimestamp(), TKUtils.getSystemDateTimeZone()); 100 DateTime endTime = useUserTimeZone ? timeBlock.getEndTimeDisplay() : new DateTime(timeBlock.getEndTimestamp(), TKUtils.getSystemDateTimeZone()); 101 if(dayInt.contains(beginTime)){ 102 if(dayInt.contains(endTime) || endTime.compareTo(dayInt.getEnd()) == 0){ 103 // determine if the time block needs to be pushed forward / backward 104 if(beginTime.getHourOfDay() < dayInt.getStart().getHourOfDay()) { 105 timeBlock.setPushBackward(true); 106 } 107 108 dayTimeBlocks.add(timeBlock); 109 } 110 } 111 } 112 dayTimeBlockList.add(dayTimeBlocks); 113 } 114 115 } 116 117 /** 118 * Defaults to using SYSTEM time zone. 119 * 120 * @param timeBlocks 121 * @param leaveBlocks 122 * @param payCalendarEntry 123 */ 124 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, List<LeaveBlock> leaveBlocks, CalendarEntries payCalendarEntry){ 125 this(timeBlocks, leaveBlocks, payCalendarEntry, TkServiceLocator.getCalendarService().getCalendar(payCalendarEntry.getHrCalendarId())); 126 } 127 128 /** 129 * Defaults to using SYSTEM time zone. 130 * 131 * @param timeBlocks 132 * @param leaveBlocks 133 * @param payCalendarEntry 134 * @param payCalendar 135 */ 136 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, List<LeaveBlock> leaveBlocks, CalendarEntries payCalendarEntry, Calendar payCalendar) { 137 this(timeBlocks, leaveBlocks, payCalendarEntry, payCalendar, false); 138 } 139 140 /** 141 * Provides the option to refer to the time zone adjusted time for the current 142 * user. 143 * @param timeBlocks 144 * @param payCalendarEntry 145 * @param payCalendar 146 * @param useUserTimeZone 147 */ 148 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, List<LeaveBlock> leaveBlocks, CalendarEntries payCalendarEntry, Calendar payCalendar, boolean useUserTimeZone) { 149 this(timeBlocks, leaveBlocks, payCalendarEntry, payCalendar, useUserTimeZone, 150 useUserTimeZone ? TKUtils.getDaySpanForCalendarEntry(payCalendarEntry) : TKUtils.getDaySpanForCalendarEntry(payCalendarEntry, TKUtils.getSystemDateTimeZone())); 151 } 152 153 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, List<LeaveBlock> leaveBlocks, CalendarEntries payCalendarEntry, Calendar payCalendar, boolean useUserTimeZone, List<Interval> dayIntervals) { 154 this.payCalendarEntry = payCalendarEntry; 155 this.payCalendar = payCalendar; 156 157 for(Interval dayInt : dayIntervals){ 158 List<TimeBlock> dayTimeBlocks = new ArrayList<TimeBlock>(); 159 for(TimeBlock timeBlock : timeBlocks){ 160 161 // Assumption: Timezones can only be switched at pay period end boundaries. 162 // If the above assumption becomes false, the logic below will need to 163 // accommodate virtual chopping of time blocks to have them fit nicely 164 // in the "days" that are displayed to users. 165 166 DateTime beginTime = useUserTimeZone ? timeBlock.getBeginTimeDisplay() : new DateTime(timeBlock.getBeginTimestamp(), TKUtils.getSystemDateTimeZone()); 167 DateTime endTime = useUserTimeZone ? timeBlock.getEndTimeDisplay() : new DateTime(timeBlock.getEndTimestamp(), TKUtils.getSystemDateTimeZone()); 168 if(dayInt.contains(beginTime)){ 169 if(dayInt.contains(endTime) || endTime.compareTo(dayInt.getEnd()) == 0){ 170 // determine if the time block needs to be pushed forward / backward 171 if(beginTime.getHourOfDay() < dayInt.getStart().getHourOfDay()) { 172 timeBlock.setPushBackward(true); 173 } 174 175 dayTimeBlocks.add(timeBlock); 176 } 177 } 178 } 179 dayTimeBlockList.add(dayTimeBlocks); 180 181 List<LeaveBlock> dayLeaveBlocks = new ArrayList<LeaveBlock>(); 182 for(LeaveBlock leaveBlock : leaveBlocks){ 183 184 // Assumption: Timezones can only be switched at pay period end boundaries. 185 // If the above assumption becomes false, the logic below will need to 186 // accommodate virtual chopping of leave blocks to have them fit nicely 187 // in the "days" that are displayed to users. 188 189 DateTimeZone dateTimeZone = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback(); 190 DateTime beginTime = new DateTime(leaveBlock.getLeaveDate(), useUserTimeZone ? dateTimeZone : TKUtils.getSystemDateTimeZone()); 191 DateTime endTime = new DateTime(leaveBlock.getLeaveDate(), useUserTimeZone ? dateTimeZone : TKUtils.getSystemDateTimeZone()); 192 if(dayInt.contains(beginTime)){ 193 if(dayInt.contains(endTime) || endTime.compareTo(dayInt.getEnd()) == 0){ 194 dayLeaveBlocks.add(leaveBlock); 195 } 196 } 197 } 198 dayLeaveBlockList.add(dayLeaveBlocks); 199 } 200 } 201 202 public List<TimeBlock> getFlattenedTimeBlockList(){ 203 List<TimeBlock> lstTimeBlocks = new ArrayList<TimeBlock>(); 204 for(List<TimeBlock> timeBlocks : dayTimeBlockList){ 205 lstTimeBlocks.addAll(timeBlocks); 206 } 207 208 Collections.sort(lstTimeBlocks, new Comparator<TimeBlock>() { // Sort the Time Blocks 209 public int compare(TimeBlock tb1, TimeBlock tb2) { 210 if (tb1 != null && tb2 != null) 211 return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 212 return 0; 213 } 214 }); 215 216 return lstTimeBlocks; 217 } 218 219 public List<LeaveBlock> getFlattenedLeaveBlockList(){ 220 List<LeaveBlock> lstLeaveBlocks = new ArrayList<LeaveBlock>(); 221 for(List<LeaveBlock> leaveBlocks : dayLeaveBlockList){ 222 lstLeaveBlocks.addAll(leaveBlocks); 223 } 224 225 Collections.sort(lstLeaveBlocks, new Comparator<LeaveBlock>() { // Sort the Leave Blocks 226 public int compare(LeaveBlock lb1, LeaveBlock lb2) { 227 if (lb1 != null && lb2 != null) 228 return lb1.getLeaveDate().compareTo(lb2.getLeaveDate()); 229 return 0; 230 } 231 }); 232 233 return lstLeaveBlocks; 234 } 235 236 /** 237 * Provides a way to access all of the time blocks for a given week. 238 * 239 * Outer list is 0 indexed list representing days in a week. 240 * Inner List are all of the time blocks for that day. 241 * 242 * Ex. 243 * 244 * List<List<TimeBlock>> week0 = getWeekTimeBlocks(0); 245 * List<TimeBlock> day0 = week0.get(0); 246 * 247 * @param week 248 * @return 249 */ 250 public List<List<TimeBlock>> getWeekTimeBlocks(int week){ 251 int startIndex = week*7; 252 int endIndex = (week*7)+7; 253 endIndex = endIndex > dayTimeBlockList.size() ? dayTimeBlockList.size() : endIndex; 254 255 // Need to sort each day by clock time. 256 List<List<TimeBlock>> wList = dayTimeBlockList.subList(startIndex, endIndex); 257 for (List<TimeBlock> dList : wList) { 258 Collections.sort(dList, new Comparator<TimeBlock>() { // Sort the Time Blocks 259 public int compare(TimeBlock tb1, TimeBlock tb2) { 260 if (tb1 != null && tb2 != null) 261 return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 262 return 0; 263 } 264 }); 265 } 266 267 return wList; 268 } 269 270 /** 271 * Provides a way to access all of the leave blocks for a given week. 272 * 273 * Outer list is 0 indexed list representing days in a week. 274 * Inner List are all of the leave blocks for that day. 275 * 276 * Ex. 277 * 278 * List<List<LeaveBlock>> week0 = getWeekLeaveBlocks(0); 279 * List<LeaveBlock> day0 = week0.get(0); 280 * 281 * @param week 282 * @return 283 */ 284 public List<List<LeaveBlock>> getWeekLeaveBlocks(int week){ 285 int startIndex = week*7; 286 int endIndex = (week*7)+7; 287 endIndex = endIndex > dayLeaveBlockList.size() ? dayLeaveBlockList.size() : endIndex; 288 289 // Need to sort each day by clock time. 290 List<List<LeaveBlock>> wList = dayLeaveBlockList.subList(startIndex, endIndex); 291 for (List<LeaveBlock> dList : wList) { 292 Collections.sort(dList, new Comparator<LeaveBlock>() { // Sort the Leave Blocks 293 public int compare(LeaveBlock lb1, LeaveBlock lb2) { 294 if (lb1 != null && lb2 != null) 295 return lb1.getLeaveDate().compareTo(lb2.getLeaveDate()); 296 return 0; 297 } 298 }); 299 } 300 301 return wList; 302 } 303 304 /** 305 * When consuming these weeks, you must be aware that you could be on a 306 * virtual day, ie noon to noon schedule and have your FLSA time start 307 * before the virtual day start, 308 * but still have a full 7 days for your week. 309 * 310 * @param zone The TimeZone to use when constructing this relative sorting. 311 */ 312 public List<FlsaWeek> getFlsaWeeks(DateTimeZone zone){ 313 int flsaDayConstant = payCalendar.getFlsaBeginDayConstant(); 314 Time flsaBeginTime = payCalendar.getFlsaBeginTime(); 315 316 // We can use these to build our interval, we have to make sure we 317 // place them on the proper day when we construct it. 318 LocalTime flsaBeginLocalTime = LocalTime.fromDateFields(flsaBeginTime); 319 320 // Defines both the start date and the start virtual time. 321 // We will add 1 day to this to move over all days. 322 // 323 // FLSA time is set. This is an FLSA start date. 324 LocalDateTime startLDT = payCalendarEntry.getBeginLocalDateTime(); 325 // DateTime startDate = new DateTime(payCalendarEntry.getBeginPeriodDateTime()); 326 // startDate = startDate.toLocalDate().toDateTime(flsaBeginLocalTime,TKUtils.getSystemDateTimeZone()); 327 328 List<FlsaWeek> flsaWeeks = new ArrayList<FlsaWeek>(); 329 List<TimeBlock> flattenedTimeBlocks = getFlattenedTimeBlockList(); 330 List<LeaveBlock> flattenedLeaveBlocks = getFlattenedLeaveBlockList(); 331 FlsaWeek currentWeek = new FlsaWeek(flsaDayConstant, flsaBeginLocalTime, LocalTime.fromDateFields(payCalendarEntry.getBeginPeriodDateTime())); 332 FlsaDay flsaDay = new FlsaDay(startLDT, flattenedTimeBlocks, flattenedLeaveBlocks, zone); 333 currentWeek.addFlsaDay(flsaDay); 334 flsaWeeks.add(currentWeek); 335 336 for (int i = 1; i < dayTimeBlockList.size(); i++) { 337 LocalDateTime currentDate = startLDT.plusDays(i); 338 flsaDay = new FlsaDay(currentDate, flattenedTimeBlocks, flattenedLeaveBlocks, zone); 339 340 if (currentDate.getDayOfWeek() == flsaDayConstant) { 341 currentWeek = new FlsaWeek(flsaDayConstant, flsaBeginLocalTime, flsaBeginLocalTime); 342 flsaWeeks.add(currentWeek); 343 } 344 345 currentWeek.addFlsaDay(flsaDay); 346 } 347 348 return flsaWeeks; 349 } 350 351 public List<List<FlsaWeek>> getFlsaWeeks(DateTimeZone zone, String principalId) { 352 List<List<FlsaWeek>> flsaWeeks = new ArrayList<List<FlsaWeek>>(); 353 354 List<FlsaWeek> currentWeeks = getFlsaWeeks(zone); 355 356 for (ListIterator<FlsaWeek> weekIterator = currentWeeks.listIterator(); weekIterator.hasNext(); ) { 357 List<FlsaWeek> flsaWeek = new ArrayList<FlsaWeek>(); 358 359 int index = weekIterator.nextIndex(); 360 FlsaWeek currentWeek = weekIterator.next(); 361 362 if (index == 0 && !currentWeek.isFirstWeekFull()) { 363 CalendarEntries previousCalendarEntry = TkServiceLocator.getCalendarEntriesService().getPreviousCalendarEntriesByCalendarId(payCalendar.getHrCalendarId(), payCalendarEntry); 364 if (previousCalendarEntry != null) { 365 TimesheetDocumentHeader timesheetDocumentHeader = TkServiceLocator.getTimesheetDocumentHeaderService().getDocumentHeader(principalId, previousCalendarEntry.getBeginPeriodDateTime(), previousCalendarEntry.getEndPeriodDateTime()); 366 if (timesheetDocumentHeader != null) { 367 TimesheetDocument timesheetDocument = TkServiceLocator.getTimesheetService().getTimesheetDocument(timesheetDocumentHeader.getDocumentId()); 368 List<String> assignmentKeys = new ArrayList<String>(); 369 for(Assignment assignment : timesheetDocument.getAssignments()) { 370 assignmentKeys.add(assignment.getAssignmentKey()); 371 } 372 373 List<TimeBlock> timeBlocks = TkServiceLocator.getTimeBlockService().getTimeBlocks(timesheetDocumentHeader.getDocumentId()); 374 List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(principalId, previousCalendarEntry.getBeginPeriodDateTime(), previousCalendarEntry.getEndPeriodDateTime(), assignmentKeys); 375 if (CollectionUtils.isNotEmpty(timeBlocks)) { 376 TkTimeBlockAggregate previousAggregate = new TkTimeBlockAggregate(timeBlocks, leaveBlocks, previousCalendarEntry, payCalendar, true); 377 List<FlsaWeek> previousWeek = previousAggregate.getFlsaWeeks(zone); 378 if (CollectionUtils.isNotEmpty(previousWeek)) { 379 flsaWeek.add(previousWeek.get(previousWeek.size() - 1)); 380 } 381 } 382 } 383 } 384 } 385 386 flsaWeek.add(currentWeek); 387 388 if (index == currentWeeks.size() - 1 && !currentWeek.isLastWeekFull()) { 389 CalendarEntries nextCalendarEntry = TkServiceLocator.getCalendarEntriesService().getNextCalendarEntriesByCalendarId(payCalendar.getHrCalendarId(), payCalendarEntry); 390 if (nextCalendarEntry != null) { 391 TimesheetDocumentHeader timesheetDocumentHeader = TkServiceLocator.getTimesheetDocumentHeaderService().getDocumentHeader(principalId, nextCalendarEntry.getBeginPeriodDateTime(), nextCalendarEntry.getEndPeriodDateTime()); 392 if (timesheetDocumentHeader != null) { 393 TimesheetDocument timesheetDocument = TkServiceLocator.getTimesheetService().getTimesheetDocument(timesheetDocumentHeader.getDocumentId()); 394 List<String> assignmentKeys = new ArrayList<String>(); 395 for(Assignment assignment : timesheetDocument.getAssignments()) { 396 assignmentKeys.add(assignment.getAssignmentKey()); 397 } 398 399 List<TimeBlock> timeBlocks = TkServiceLocator.getTimeBlockService().getTimeBlocks(timesheetDocumentHeader.getDocumentId()); 400 List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(principalId, nextCalendarEntry.getBeginPeriodDateTime(), nextCalendarEntry.getEndPeriodDateTime(), assignmentKeys); 401 if (CollectionUtils.isNotEmpty(timeBlocks)) { 402 TkTimeBlockAggregate nextAggregate = new TkTimeBlockAggregate(timeBlocks, leaveBlocks, nextCalendarEntry, payCalendar, true); 403 List<FlsaWeek> nextWeek = nextAggregate.getFlsaWeeks(zone); 404 if (CollectionUtils.isNotEmpty(nextWeek)) { 405 flsaWeek.add(nextWeek.get(0)); 406 } 407 } 408 } 409 } 410 } 411 412 flsaWeeks.add(flsaWeek); 413 } 414 415 return flsaWeeks; 416 } 417 418 /** 419 * @return the total number of weeks that this object represents. 420 */ 421 public int numberOfAggregatedWeeks() { 422 int weeks = 0; 423 424 if (this.dayTimeBlockList.size() > 0) { 425 weeks = this.dayTimeBlockList.size() / 7; 426 if (this.dayTimeBlockList.size() % 7 > 0) 427 weeks++; 428 } 429 430 return weeks; 431 } 432 433 public List<List<TimeBlock>> getDayTimeBlockList() { 434 return dayTimeBlockList; 435 } 436 437 public List<List<LeaveBlock>> getDayLeaveBlockList() { 438 return dayLeaveBlockList; 439 } 440 441 public CalendarEntries getPayCalendarEntry() { 442 return payCalendarEntry; 443 } 444 445 public void setPayCalendarEntry(CalendarEntries payCalendarEntry) { 446 this.payCalendarEntry = payCalendarEntry; 447 } 448 449 public Calendar getPayCalendar() { 450 return payCalendar; 451 } 452 453 public void setPayCalendar(Calendar payCalendar) { 454 this.payCalendar = payCalendar; 455 } 456 457 //Very much a hack to add valid leave blocks to a time block aggregate... 458 public static TkTimeBlockAggregate combineTimeAndLeaveAggregates(TkTimeBlockAggregate tbAggregate, LeaveBlockAggregate lbAggregate) { 459 if (tbAggregate != null 460 && lbAggregate != null 461 && tbAggregate.getDayTimeBlockList().size() == lbAggregate.getDayLeaveBlockList().size()) { 462 for (int i = 0; i < tbAggregate.getDayTimeBlockList().size(); i++) { 463 List<LeaveBlock> leaveBlocks = lbAggregate.getDayLeaveBlockList().get(i); 464 if (CollectionUtils.isNotEmpty(leaveBlocks)) { 465 for (LeaveBlock lb : leaveBlocks) { 466 //convert leave block to generic time block and add to list 467 //conveniently, we only really need the hours amount 468 TimeBlock timeBlock = new TimeBlock(); 469 timeBlock.setHours(lb.getLeaveAmount().negate()); 470 timeBlock.setBeginTimestamp(new Timestamp(lb.getLeaveDate().getTime())); 471 timeBlock.setEndTimestamp(new Timestamp(new DateTime(lb.getLeaveDate()).plusMinutes(timeBlock.getHours().intValue()).getMillis())); 472 timeBlock.setAssignmentKey(lb.getAssignmentKey()); 473 timeBlock.setEarnCode(lb.getEarnCode()); 474 timeBlock.setPrincipalId(lb.getPrincipalId()); 475 timeBlock.setWorkArea(lb.getWorkArea()); 476 TimeHourDetail timeHourDetail = new TimeHourDetail(); 477 timeHourDetail.setEarnCode(timeBlock.getEarnCode()); 478 timeHourDetail.setHours(timeBlock.getHours()); 479 timeHourDetail.setAmount(BigDecimal.ZERO); 480 timeBlock.addTimeHourDetail(timeHourDetail); 481 tbAggregate.getDayTimeBlockList().get(i).add(timeBlock); 482 } 483 } 484 485 } 486 } 487 return tbAggregate; 488 } 489 490 }