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.sql.Time; 019 import java.util.ArrayList; 020 import java.util.Collections; 021 import java.util.Comparator; 022 import java.util.List; 023 import java.util.ListIterator; 024 025 import org.apache.commons.collections.CollectionUtils; 026 import org.joda.time.DateTime; 027 import org.joda.time.DateTimeZone; 028 import org.joda.time.Interval; 029 import org.joda.time.LocalDateTime; 030 import org.joda.time.LocalTime; 031 import org.kuali.hr.time.calendar.Calendar; 032 import org.kuali.hr.time.calendar.CalendarEntries; 033 import org.kuali.hr.time.flsa.FlsaDay; 034 import org.kuali.hr.time.flsa.FlsaWeek; 035 import org.kuali.hr.time.service.base.TkServiceLocator; 036 import org.kuali.hr.time.timeblock.TimeBlock; 037 import org.kuali.hr.time.workflow.TimesheetDocumentHeader; 038 039 public class TkTimeBlockAggregate { 040 private List<List<TimeBlock>> dayTimeBlockList = new ArrayList<List<TimeBlock>>(); 041 private CalendarEntries payCalendarEntry; 042 private Calendar payCalendar; 043 044 /** 045 * Defaults to using SYSTEM time zone. 046 * 047 * @param timeBlocks 048 * @param payCalendarEntry 049 */ 050 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, CalendarEntries payCalendarEntry){ 051 this(timeBlocks, payCalendarEntry, TkServiceLocator.getCalendarService().getCalendar(payCalendarEntry.getHrCalendarId())); 052 } 053 054 /** 055 * Defaults to using SYSTEM time zone. 056 * 057 * @param timeBlocks 058 * @param payCalendarEntry 059 * @param payCalendar 060 */ 061 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, CalendarEntries payCalendarEntry, Calendar payCalendar) { 062 this(timeBlocks, payCalendarEntry, payCalendar, false); 063 } 064 065 /** 066 * Provides the option to refer to the time zone adjusted time for the current 067 * user. 068 * @param timeBlocks 069 * @param payCalendarEntry 070 * @param payCalendar 071 * @param useUserTimeZone 072 */ 073 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, CalendarEntries payCalendarEntry, Calendar payCalendar, boolean useUserTimeZone) { 074 this.payCalendarEntry = payCalendarEntry; 075 this.payCalendar = payCalendar; 076 077 List<Interval> dayIntervals = null; 078 if (useUserTimeZone) { 079 dayIntervals = TKUtils.getDaySpanForCalendarEntry(payCalendarEntry); 080 } else { 081 dayIntervals = TKUtils.getDaySpanForCalendarEntry(payCalendarEntry, TKUtils.getSystemDateTimeZone()); 082 } 083 for(Interval dayInt : dayIntervals){ 084 List<TimeBlock> dayTimeBlocks = new ArrayList<TimeBlock>(); 085 for(TimeBlock timeBlock : timeBlocks){ 086 087 // Assumption: Timezones can only be switched at pay period end boundaries. 088 // If the above assumption becomes false, the logic below will need to 089 // accommodate virtual chopping of time blocks to have them fit nicely 090 // in the "days" that are displayed to users. 091 092 DateTime beginTime = useUserTimeZone ? timeBlock.getBeginTimeDisplay() : new DateTime(timeBlock.getBeginTimestamp(), TKUtils.getSystemDateTimeZone()); 093 DateTime endTime = useUserTimeZone ? timeBlock.getEndTimeDisplay() : new DateTime(timeBlock.getEndTimestamp(), TKUtils.getSystemDateTimeZone()); 094 if(dayInt.contains(beginTime)){ 095 if(dayInt.contains(endTime) || endTime.compareTo(dayInt.getEnd()) == 0){ 096 // determine if the time block needs to be pushed forward / backward 097 if(beginTime.getHourOfDay() < dayInt.getStart().getHourOfDay()) { 098 timeBlock.setPushBackward(true); 099 } 100 101 dayTimeBlocks.add(timeBlock); 102 } 103 } 104 } 105 dayTimeBlockList.add(dayTimeBlocks); 106 } 107 } 108 109 public TkTimeBlockAggregate(List<TimeBlock> timeBlocks, CalendarEntries payCalendarEntry, Calendar payCalendar, boolean useUserTimeZone, List<Interval> dayIntervals) { 110 this.payCalendarEntry = payCalendarEntry; 111 this.payCalendar = payCalendar; 112 113 for(Interval dayInt : dayIntervals){ 114 List<TimeBlock> dayTimeBlocks = new ArrayList<TimeBlock>(); 115 for(TimeBlock timeBlock : timeBlocks){ 116 117 // Assumption: Timezones can only be switched at pay period end boundaries. 118 // If the above assumption becomes false, the logic below will need to 119 // accommodate virtual chopping of time blocks to have them fit nicely 120 // in the "days" that are displayed to users. 121 122 DateTime beginTime = useUserTimeZone ? timeBlock.getBeginTimeDisplay() : new DateTime(timeBlock.getBeginTimestamp(), TKUtils.getSystemDateTimeZone()); 123 DateTime endTime = useUserTimeZone ? timeBlock.getEndTimeDisplay() : new DateTime(timeBlock.getEndTimestamp(), TKUtils.getSystemDateTimeZone()); 124 if(dayInt.contains(beginTime)){ 125 if(dayInt.contains(endTime) || endTime.compareTo(dayInt.getEnd()) == 0){ 126 // determine if the time block needs to be pushed forward / backward 127 if(beginTime.getHourOfDay() < dayInt.getStart().getHourOfDay()) { 128 timeBlock.setPushBackward(true); 129 } 130 131 dayTimeBlocks.add(timeBlock); 132 } 133 } 134 } 135 dayTimeBlockList.add(dayTimeBlocks); 136 } 137 138 } 139 140 141 public List<TimeBlock> getFlattenedTimeBlockList(){ 142 List<TimeBlock> lstTimeBlocks = new ArrayList<TimeBlock>(); 143 for(List<TimeBlock> timeBlocks : dayTimeBlockList){ 144 lstTimeBlocks.addAll(timeBlocks); 145 } 146 147 Collections.sort(lstTimeBlocks, new Comparator<TimeBlock>() { // Sort the Time Blocks 148 public int compare(TimeBlock tb1, TimeBlock tb2) { 149 if (tb1 != null && tb2 != null) 150 return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 151 return 0; 152 } 153 }); 154 155 return lstTimeBlocks; 156 } 157 158 /** 159 * Provides a way to access all of the time blocks for a given week. 160 * 161 * Outer list is 0 indexed list representing days in a week. 162 * Inner List are all of the time blocks for that day. 163 * 164 * Ex. 165 * 166 * List<List<TimeBlock>> week0 = getWeekTimeBlocks(0); 167 * List<TimeBlock> day0 = week0.get(0); 168 * 169 * @param week 170 * @return 171 */ 172 public List<List<TimeBlock>> getWeekTimeBlocks(int week){ 173 int startIndex = week*7; 174 int endIndex = (week*7)+7; 175 endIndex = endIndex > dayTimeBlockList.size() ? dayTimeBlockList.size() : endIndex; 176 177 // Need to sort each day by clock time. 178 List<List<TimeBlock>> wList = dayTimeBlockList.subList(startIndex, endIndex); 179 for (List<TimeBlock> dList : wList) { 180 Collections.sort(dList, new Comparator<TimeBlock>() { // Sort the Time Blocks 181 public int compare(TimeBlock tb1, TimeBlock tb2) { 182 if (tb1 != null && tb2 != null) 183 return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 184 return 0; 185 } 186 }); 187 } 188 189 return wList; 190 } 191 192 /** 193 * When consuming these weeks, you must be aware that you could be on a 194 * virtual day, ie noon to noon schedule and have your FLSA time start 195 * before the virtual day start, 196 * but still have a full 7 days for your week. 197 * 198 * @param zone The TimeZone to use when constructing this relative sorting. 199 */ 200 public List<FlsaWeek> getFlsaWeeks(DateTimeZone zone){ 201 int flsaDayConstant = payCalendar.getFlsaBeginDayConstant(); 202 Time flsaBeginTime = payCalendar.getFlsaBeginTime(); 203 204 // We can use these to build our interval, we have to make sure we 205 // place them on the proper day when we construct it. 206 LocalTime flsaBeginLocalTime = LocalTime.fromDateFields(flsaBeginTime); 207 208 // Defines both the start date and the start virtual time. 209 // We will add 1 day to this to move over all days. 210 // 211 // FLSA time is set. This is an FLSA start date. 212 LocalDateTime startLDT = payCalendarEntry.getBeginLocalDateTime(); 213 // DateTime startDate = new DateTime(payCalendarEntry.getBeginPeriodDateTime()); 214 // startDate = startDate.toLocalDate().toDateTime(flsaBeginLocalTime,TKUtils.getSystemDateTimeZone()); 215 216 List<FlsaWeek> flsaWeeks = new ArrayList<FlsaWeek>(); 217 List<TimeBlock> flatSortedBlockList = getFlattenedTimeBlockList(); 218 FlsaWeek currentWeek = new FlsaWeek(flsaDayConstant, flsaBeginLocalTime, LocalTime.fromDateFields(payCalendarEntry.getBeginPeriodDateTime())); 219 FlsaDay flsaDay = new FlsaDay(startLDT, flatSortedBlockList, zone); 220 currentWeek.addFlsaDay(flsaDay); 221 flsaWeeks.add(currentWeek); 222 223 for (int i = 1; i < dayTimeBlockList.size(); i++) { 224 LocalDateTime currentDate = startLDT.plusDays(i); 225 flsaDay = new FlsaDay(currentDate, flatSortedBlockList, zone); 226 227 if (currentDate.getDayOfWeek() == flsaDayConstant) { 228 currentWeek = new FlsaWeek(flsaDayConstant, flsaBeginLocalTime, flsaBeginLocalTime); 229 flsaWeeks.add(currentWeek); 230 } 231 232 currentWeek.addFlsaDay(flsaDay); 233 } 234 235 return flsaWeeks; 236 } 237 238 public List<List<FlsaWeek>> getFlsaWeeks(DateTimeZone zone, String principalId) { 239 List<List<FlsaWeek>> flsaWeeks = new ArrayList<List<FlsaWeek>>(); 240 241 List<FlsaWeek> currentWeeks = getFlsaWeeks(zone); 242 243 for (ListIterator<FlsaWeek> weekIterator = currentWeeks.listIterator(); weekIterator.hasNext(); ) { 244 List<FlsaWeek> flsaWeek = new ArrayList<FlsaWeek>(); 245 246 int index = weekIterator.nextIndex(); 247 FlsaWeek currentWeek = weekIterator.next(); 248 249 if (index == 0 && !currentWeek.isFirstWeekFull()) { 250 CalendarEntries previousCalendarEntry = TkServiceLocator.getCalendarEntriesService().getPreviousCalendarEntriesByCalendarId(payCalendar.getHrCalendarId(), payCalendarEntry); 251 if (previousCalendarEntry != null) { 252 TimesheetDocumentHeader timesheetDocumentHeader = TkServiceLocator.getTimesheetDocumentHeaderService().getDocumentHeader(principalId, previousCalendarEntry.getBeginPeriodDateTime(), previousCalendarEntry.getEndPeriodDateTime()); 253 if (timesheetDocumentHeader != null) { 254 List<TimeBlock> timeBlocks = TkServiceLocator.getTimeBlockService().getTimeBlocks(timesheetDocumentHeader.getDocumentId()); 255 if (CollectionUtils.isNotEmpty(timeBlocks)) { 256 TkTimeBlockAggregate previousAggregate = new TkTimeBlockAggregate(timeBlocks, previousCalendarEntry, payCalendar, true); 257 List<FlsaWeek> previousWeek = previousAggregate.getFlsaWeeks(zone); 258 if (CollectionUtils.isNotEmpty(previousWeek)) { 259 flsaWeek.add(previousWeek.get(previousWeek.size() - 1)); 260 } 261 } 262 } 263 } 264 } 265 266 flsaWeek.add(currentWeek); 267 268 if (index == currentWeeks.size() - 1 && !currentWeek.isLastWeekFull()) { 269 CalendarEntries nextCalendarEntry = TkServiceLocator.getCalendarEntriesService().getNextCalendarEntriesByCalendarId(payCalendar.getHrCalendarId(), payCalendarEntry); 270 if (nextCalendarEntry != null) { 271 TimesheetDocumentHeader timesheetDocumentHeader = TkServiceLocator.getTimesheetDocumentHeaderService().getDocumentHeader(principalId, nextCalendarEntry.getBeginPeriodDateTime(), nextCalendarEntry.getEndPeriodDateTime()); 272 if (timesheetDocumentHeader != null) { 273 List<TimeBlock> timeBlocks = TkServiceLocator.getTimeBlockService().getTimeBlocks(timesheetDocumentHeader.getDocumentId()); 274 if (CollectionUtils.isNotEmpty(timeBlocks)) { 275 TkTimeBlockAggregate nextAggregate = new TkTimeBlockAggregate(timeBlocks, nextCalendarEntry, payCalendar, true); 276 List<FlsaWeek> nextWeek = nextAggregate.getFlsaWeeks(zone); 277 if (CollectionUtils.isNotEmpty(nextWeek)) { 278 flsaWeek.add(nextWeek.get(0)); 279 } 280 } 281 } 282 } 283 } 284 285 flsaWeeks.add(flsaWeek); 286 } 287 288 return flsaWeeks; 289 } 290 291 /** 292 * @return the total number of weeks that this object represents. 293 */ 294 public int numberOfAggregatedWeeks() { 295 int weeks = 0; 296 297 if (this.dayTimeBlockList.size() > 0) { 298 weeks = this.dayTimeBlockList.size() / 7; 299 if (this.dayTimeBlockList.size() % 7 > 0) 300 weeks++; 301 } 302 303 return weeks; 304 } 305 306 public List<List<TimeBlock>> getDayTimeBlockList() { 307 return dayTimeBlockList; 308 } 309 310 public CalendarEntries getPayCalendarEntry() { 311 return payCalendarEntry; 312 } 313 314 public void setPayCalendarEntry(CalendarEntries payCalendarEntry) { 315 this.payCalendarEntry = payCalendarEntry; 316 } 317 318 public Calendar getPayCalendar() { 319 return payCalendar; 320 } 321 322 public void setPayCalendar(Calendar payCalendar) { 323 this.payCalendar = payCalendar; 324 } 325 326 }