View Javadoc

1   /**
2    * Copyright 2004-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.hr.time.timesummary.service;
17  
18  import java.math.BigDecimal;
19  import java.sql.Date;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.Comparator;
23  import java.util.GregorianCalendar;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.TreeMap;
30  
31  import org.apache.commons.collections.CollectionUtils;
32  import org.apache.commons.lang.StringUtils;
33  import org.apache.commons.lang.time.DateUtils;
34  import org.apache.log4j.Logger;
35  import org.joda.time.DateTimeFieldType;
36  import org.joda.time.Interval;
37  import org.joda.time.LocalDateTime;
38  import org.kuali.hr.job.Job;
39  import org.kuali.hr.lm.LMConstants;
40  import org.kuali.hr.lm.accrual.AccrualCategoryRule;
41  import org.kuali.hr.lm.leaveSummary.LeaveSummary;
42  import org.kuali.hr.lm.leaveSummary.LeaveSummaryRow;
43  import org.kuali.hr.lm.leaveblock.LeaveBlock;
44  import org.kuali.hr.lm.util.LeaveBlockAggregate;
45  import org.kuali.hr.time.assignment.Assignment;
46  import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
47  import org.kuali.hr.time.calendar.Calendar;
48  import org.kuali.hr.time.calendar.CalendarEntries;
49  import org.kuali.hr.time.earncode.EarnCode;
50  import org.kuali.hr.time.earncodegroup.EarnCodeGroup;
51  import org.kuali.hr.time.flsa.FlsaDay;
52  import org.kuali.hr.time.flsa.FlsaWeek;
53  import org.kuali.hr.time.service.base.TkServiceLocator;
54  import org.kuali.hr.time.timeblock.TimeBlock;
55  import org.kuali.hr.time.timeblock.TimeHourDetail;
56  import org.kuali.hr.time.timesheet.TimesheetDocument;
57  import org.kuali.hr.time.timesummary.AssignmentColumn;
58  import org.kuali.hr.time.timesummary.AssignmentRow;
59  import org.kuali.hr.time.timesummary.EarnCodeSection;
60  import org.kuali.hr.time.timesummary.EarnGroupSection;
61  import org.kuali.hr.time.timesummary.TimeSummary;
62  import org.kuali.hr.time.util.TKUtils;
63  import org.kuali.hr.time.util.TkConstants;
64  import org.kuali.hr.time.util.TkTimeBlockAggregate;
65  import org.kuali.hr.time.workarea.WorkArea;
66  
67  public class TimeSummaryServiceImpl implements TimeSummaryService {
68  	private static final String OTHER_EARN_GROUP = "Other";
69  	private static final Logger LOG = Logger.getLogger(TimeSummaryServiceImpl.class);
70  	
71      @Override
72  	public TimeSummary getTimeSummary(TimesheetDocument timesheetDocument) {
73  		TimeSummary timeSummary = new TimeSummary();
74  
75  		if(timesheetDocument == null || timesheetDocument.getTimeBlocks() == null) {
76  			return timeSummary;
77  		}
78  
79          List<Boolean> dayArrangements = new ArrayList<Boolean>();
80  
81  		timeSummary.setSummaryHeader(getHeaderForSummary(timesheetDocument.getCalendarEntry(), dayArrangements));
82  		TkTimeBlockAggregate tkTimeBlockAggregate = new TkTimeBlockAggregate(timesheetDocument.getTimeBlocks(), timesheetDocument.getCalendarEntry(), TkServiceLocator.getCalendarService().getCalendar(timesheetDocument.getCalendarEntry().getHrCalendarId()), true);
83  
84          List<Assignment> timeAssignments = timesheetDocument.getAssignments();
85          List<String> tAssignmentKeys = new ArrayList<String>();
86          Set<String> regularEarnCodes = new HashSet<String>();
87          for(Assignment assign : timeAssignments) {
88              tAssignmentKeys.add(assign.getAssignmentKey());
89              regularEarnCodes.add(assign.getJob().getPayTypeObj().getRegEarnCode());
90          }
91          
92          Date endDate = timesheetDocument.getCalendarEntry().getEndPeriodDate();
93          LocalDateTime tempLocalDate = timesheetDocument.getCalendarEntry().getEndLocalDateTime();
94          // if the end date of the calendar entry is the beginning time of a day, use the previous day as the end date to retrieve blocks
95          if(tempLocalDate.getHourOfDay() == 0 && tempLocalDate.getMinuteOfHour() == 0 && tempLocalDate.getMillisOfSecond() == 0) {
96          	endDate = new java.sql.Date(tempLocalDate.minusDays(1).toDate().getTime());
97          }
98          List<LeaveBlock> leaveBlocks =  TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(timesheetDocument.getPrincipalId(),
99                  timesheetDocument.getCalendarEntry().getBeginPeriodDate(), endDate, tAssignmentKeys);
100         LeaveBlockAggregate leaveBlockAggregate = new LeaveBlockAggregate(leaveBlocks, timesheetDocument.getCalendarEntry());
101         tkTimeBlockAggregate = TkTimeBlockAggregate.combineTimeAndLeaveAggregates(tkTimeBlockAggregate, leaveBlockAggregate);
102 
103 		timeSummary.setWorkedHours(getWorkedHours(tkTimeBlockAggregate, regularEarnCodes));
104 
105         List<EarnGroupSection> earnGroupSections = getEarnGroupSections(tkTimeBlockAggregate, timeSummary.getSummaryHeader().size()+1, 
106         			dayArrangements, timesheetDocument.getAsOfDate(), timesheetDocument.getDocEndDate());
107         timeSummary.setSections(sortEarnGroupSections(earnGroupSections, regularEarnCodes));
108         
109         try {
110 			List<LeaveSummaryRow> maxedLeaveRows = getMaxedLeaveRows(timesheetDocument.getCalendarEntry(),timesheetDocument.getPrincipalId());
111 			timeSummary.setMaxedLeaveRows(maxedLeaveRows);
112 		} catch (Exception e) {
113 			// TODO Auto-generated catch block
114 			LOG.error("error retreiving maxed leave rows", e);
115 		}
116 
117 		return timeSummary;
118 	}
119 
120     private List<EarnGroupSection> sortEarnGroupSections(List<EarnGroupSection> sections, Set<String> regularEarnCodes) {
121         List<EarnGroupSection> sortedList = new ArrayList<EarnGroupSection>();
122         //first sort by alpha
123         Collections.sort(sections, new Comparator<EarnGroupSection>() {
124             @Override
125             public int compare(EarnGroupSection egs1, EarnGroupSection egs2) {
126                 if (egs1 == null ^ egs2 == null) {
127                     return egs1 == null ? -1 : 1;
128                 }
129                 if (egs1 == null && egs2 == null) {
130                     return 0;
131                 }
132                 //'other' earn group needs to be last.
133                 boolean isOther1 = StringUtils.equals(egs1.getEarnGroup(), OTHER_EARN_GROUP);
134                 boolean isOther2 = StringUtils.equals(egs2.getEarnGroup(), OTHER_EARN_GROUP);
135                 if (isOther1 ^ isOther2) {
136                     return isOther1 ? 1 : -1;
137                 }
138                 if (isOther1 && isOther2) {
139                     return 0;
140                 }
141                 return egs1.getEarnGroup().compareTo(egs2.getEarnGroup());
142             }
143         });
144 
145         List<EarnGroupSection> copy = new ArrayList<EarnGroupSection>(sections);
146         //loop through in reverse
147         for (EarnGroupSection egs : copy) {
148             if (!CollectionUtils.intersection(regularEarnCodes, egs.getEarnCodeToEarnCodeSectionMap().keySet()).isEmpty()) {
149                 sortedList.add(egs);
150                 sections.remove(egs);
151             }
152         }
153         sortedList.addAll(sections);
154         return sortedList;
155     }
156 	
157     private List<LeaveSummaryRow> getMaxedLeaveRows(
158 			CalendarEntries calendarEntry, String principalId) throws Exception {
159     	List<LeaveSummaryRow> maxedLeaveRows = new ArrayList<LeaveSummaryRow>();
160     	
161     	if (TkServiceLocator.getLeaveApprovalService().isActiveAssignmentFoundOnJobFlsaStatus(principalId, TkConstants.FLSA_STATUS_NON_EXEMPT, true)) {
162     		
163         	Map<String,Set<LeaveBlock>> eligibilities = TkServiceLocator.getAccrualCategoryMaxBalanceService().getMaxBalanceViolations(calendarEntry,principalId);
164         	Set<LeaveBlock> onDemandTransfers = eligibilities.get(LMConstants.MAX_BAL_ACTION_FREQ.ON_DEMAND);
165 
166         	Interval calendarEntryInterval = new Interval(calendarEntry.getBeginPeriodDate().getTime(),calendarEntry.getEndPeriodDate().getTime());
167         	
168         	//use the current date if on the current calendar? yes -> no warning given until accrual is reached. If accrual occurs on last day of period or last day of service interval
169         	//change, no warning given to the employee of balance limits being exceeded except on or after that day.
170 
171         	if(!onDemandTransfers.isEmpty()) {
172             	for(LeaveBlock lb : onDemandTransfers) {
173             		Date leaveDate = lb.getLeaveDate();
174                 	LeaveSummary summary = TkServiceLocator.getLeaveSummaryService().getLeaveSummaryAsOfDate(principalId, new java.sql.Date(DateUtils.addDays(leaveDate, 1).getTime()));
175                 	LeaveSummaryRow row = summary.getLeaveSummaryRowForAccrualCtgy(lb.getAccrualCategory());
176             		if(row != null) {
177             			//AccrualCategory accrualCategory = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(row.getAccrualCategoryId());
178                     	//AccrualCategoryRule currentRule = TkServiceLocator.getAccrualCategoryRuleService().getAccrualCategoryRuleForDate(accrualCategory, asOfDate, pha.getServiceDate());
179                     	if(calendarEntryInterval.contains(leaveDate.getTime())) {
180                     		//do not allow the on-demand max balance action if the rule the action occurs under is no longer in effect,
181                     		//or if the infraction did not occur within this interval. ( if it occurred during the previous interval, 
182                     		//the employee will have the option to take action in that interval up to & including the end date of that interval. )
183 	            			row.setInfractingLeaveBlockId(lb.getAccrualCategoryRuleId());
184 	            			AccrualCategoryRule aRule = TkServiceLocator.getAccrualCategoryRuleService().getAccrualCategoryRule(lb.getAccrualCategoryRuleId());
185 	            			
186 	            			if(StringUtils.equals(aRule.getActionAtMaxBalance(),LMConstants.ACTION_AT_MAX_BAL.TRANSFER))
187 	            				row.setTransferable(true);
188 	            			else if(StringUtils.equals(aRule.getActionAtMaxBalance(),LMConstants.ACTION_AT_MAX_BAL.PAYOUT))
189 	            				row.setPayoutable(true);
190 	            			
191 	            			boolean exists = false;
192 	            			for(LeaveSummaryRow maxedRow : maxedLeaveRows) {
193 	            				if(StringUtils.equals(maxedRow.getAccrualCategoryId(),row.getAccrualCategoryId()))
194 	            					exists = true;
195 	            			}
196 	            			if(!exists)
197             					maxedLeaveRows.add(row);
198                     	}
199             		}
200             	}
201         	}
202     	}
203 		return maxedLeaveRows;
204 	}
205 
206 	/**
207      * Aggregates timeblocks into the appropriate earngroup-> earncode -> assignment rows
208      * @param tkTimeBlockAggregate
209      * @param numEntries
210      * @param dayArrangements
211      * @param asOfDate
212      * @return
213      */
214 	public List<EarnGroupSection> getEarnGroupSections(TkTimeBlockAggregate tkTimeBlockAggregate, int numEntries, List<Boolean> dayArrangements, Date asOfDate , Date docEndDate){
215 		List<EarnGroupSection> earnGroupSections = new ArrayList<EarnGroupSection>();
216 //		List<FlsaWeek> flsaWeeks = tkTimeBlockAggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback());
217 		List<FlsaWeek> flsaWeeks = tkTimeBlockAggregate.getFlsaWeeks(TKUtils.getSystemDateTimeZone());
218 		Map<String, EarnCodeSection> earnCodeToEarnCodeSection = new TreeMap<String, EarnCodeSection>();
219 		Map<String, EarnGroupSection> earnGroupToEarnGroupSection = new HashMap<String, EarnGroupSection>();
220 		
221 		int dayCount = 0;
222 		
223 		//TODO remove this and correct the aggregate .. not sure what the down stream changes are
224 		//so leaving this for initial release
225 		List<FlsaWeek> trimmedFlsaWeeks = new ArrayList<FlsaWeek>();
226 		for(FlsaWeek flsaWeek : flsaWeeks){
227 			if(flsaWeek.getFlsaDays().size() > 0){
228 				trimmedFlsaWeeks.add(flsaWeek);
229 			}
230 		}
231 		
232 		//For every flsa week and day aggegate each time hour detail 
233 		// buckets it by earn code section first
234 		for(FlsaWeek flsaWeek : trimmedFlsaWeeks){
235 			int weekSize = 0;
236 			List<FlsaDay> flsaDays = flsaWeek.getFlsaDays();
237 			for(FlsaDay flsaDay : flsaDays){
238 				Map<String, List<TimeBlock>> earnCodeToTimeBlocks = flsaDay.getEarnCodeToTimeBlocks();
239 				
240 				for(List<TimeBlock> timeBlocks : earnCodeToTimeBlocks.values()){
241 					for(TimeBlock timeBlock : timeBlocks){
242 						for(TimeHourDetail thd : timeBlock.getTimeHourDetails()){
243 							if(StringUtils.equals(TkConstants.LUNCH_EARN_CODE, thd.getEarnCode())){
244 								continue;
245 							}
246 							EarnCodeSection earnCodeSection = earnCodeToEarnCodeSection.get(thd.getEarnCode());
247 							if(earnCodeSection == null){
248 								earnCodeSection = new EarnCodeSection();
249 								earnCodeSection.setEarnCode(thd.getEarnCode());
250 								EarnCode earnCodeObj = TkServiceLocator.getEarnCodeService().getEarnCode(thd.getEarnCode(), TKUtils.getTimelessDate(asOfDate));
251 								earnCodeSection.setDescription(earnCodeObj != null ? earnCodeObj.getDescription() : null);
252 								earnCodeSection.setIsAmountEarnCode(earnCodeObj != null ? TkConstants.EARN_CODE_AMOUNT.equalsIgnoreCase(earnCodeObj.getRecordMethod()) : false);
253 								for(int i = 0;i<(numEntries-1);i++){
254 									earnCodeSection.getTotals().add(BigDecimal.ZERO);
255 								}
256 								
257 								earnCodeToEarnCodeSection.put(thd.getEarnCode(), earnCodeSection);
258 							}
259 							String assignKey = timeBlock.getAssignmentKey();
260 							AssignmentRow assignRow = earnCodeSection.getAssignKeyToAssignmentRowMap().get(assignKey);
261 							if(assignRow == null){
262 								assignRow = new AssignmentRow();
263 								assignRow.setAssignmentKey(assignKey);
264 								AssignmentDescriptionKey assignmentKey = TkServiceLocator.getAssignmentService().getAssignmentDescriptionKey(assignKey);
265 								Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(timeBlock.getPrincipalId(), assignmentKey, TKUtils.getTimelessDate(asOfDate));
266 								// some assignment may not be effective at the beginning of the pay period, use the end date of the period to find it
267 								if(assignment == null) {
268 									assignment = TkServiceLocator.getAssignmentService().getAssignment(timeBlock.getPrincipalId(), assignmentKey, TKUtils.getTimelessDate(docEndDate));
269 								}
270 								//TODO push this up to the assignment fetch/fully populated instead of like this
271 								if(assignment != null){
272 									if(assignment.getJob() == null){
273 										Job aJob = TkServiceLocator.getJobService().getJob(assignment.getPrincipalId(),assignment.getJobNumber(),TKUtils.getTimelessDate(assignment.getEffectiveDate()));
274 										assignment.setJob(aJob);
275 									}
276 									if(assignment.getWorkAreaObj() == null){
277 										WorkArea aWorkArea = TkServiceLocator.getWorkAreaService().getWorkArea(assignment.getWorkArea(), TKUtils.getTimelessDate(assignment.getEffectiveDate()));
278 										assignment.setWorkAreaObj(aWorkArea);
279 									}
280 									assignRow.setDescr(assignment.getAssignmentDescription());
281 								}
282 								assignRow.setEarnCodeSection(earnCodeSection);
283 								for (int i = 0; i < numEntries - 1; i++) {
284 									assignRow.addAssignmentColumn(new AssignmentColumn());
285 								}
286 
287 								earnCodeSection.addAssignmentRow(assignRow);
288 							}
289 							assignRow.addToTotal(dayCount, thd.getHours());
290 							assignRow.addToAmount(dayCount, thd.getAmount());
291 						}
292 					}
293 				}
294 				dayCount++;
295 				weekSize++;
296 			}
297 			//end of flsa week accumulate weekly totals
298 			for(EarnCodeSection earnCodeSection : earnCodeToEarnCodeSection.values()){
299 				earnCodeSection.addWeeklyTotal(dayCount, weekSize);
300 			}			
301 			weekSize = 0;
302 
303 			dayCount++;
304 		}
305 		
306 		dayCount = 0;
307 		//now create all teh earn group sections and aggregate accordingly
308 		for(EarnCodeSection earnCodeSection : earnCodeToEarnCodeSection.values()){
309 			String earnCode = earnCodeSection.getEarnCode();
310 			EarnCodeGroup earnGroupObj = TkServiceLocator.getEarnCodeGroupService().getEarnCodeGroupSummaryForEarnCode(earnCode, TKUtils.getTimelessDate(asOfDate));
311 			String earnGroup = null;
312 			if(earnGroupObj == null){
313 				earnGroup = OTHER_EARN_GROUP;
314 			} else{
315 				earnGroup = earnGroupObj.getDescr();
316 			}
317 			
318 			EarnGroupSection earnGroupSection = earnGroupToEarnGroupSection.get(earnGroup);
319 			if(earnGroupSection == null){
320 				earnGroupSection = new EarnGroupSection();
321 				earnGroupSection.setEarnGroup(earnGroup);
322 				for(int i =0;i<(numEntries-1);i++){
323 					earnGroupSection.getTotals().add(BigDecimal.ZERO);
324 				}
325 				earnGroupToEarnGroupSection.put(earnGroup, earnGroupSection);
326 			}
327 			earnGroupSection.addEarnCodeSection(earnCodeSection, dayArrangements);
328 			
329 		}
330 		for(EarnGroupSection earnGroupSection : earnGroupToEarnGroupSection.values()){
331 			earnGroupSections.add(earnGroupSection);
332 		}
333 		return earnGroupSections;
334 	}
335 	
336 	/**
337 	 * Generate a list of string describing this pay calendar entry for the summary
338 	 * @param payCalEntry
339 	 * @return
340 	 */
341 	protected List<String> getSummaryHeader(CalendarEntries payCalEntry){
342 		List<String> summaryHeader = new ArrayList<String>();
343 		int dayCount = 0;
344 		java.util.Date beginDateTime = payCalEntry.getBeginPeriodDateTime();
345 		java.util.Date endDateTime = payCalEntry.getEndPeriodDateTime();
346 		boolean virtualDays = false;
347         LocalDateTime endDate = payCalEntry.getEndLocalDateTime();
348 
349         if (endDate.get(DateTimeFieldType.hourOfDay()) != 0 || endDate.get(DateTimeFieldType.minuteOfHour()) != 0 ||
350                 endDate.get(DateTimeFieldType.secondOfMinute()) != 0){
351             virtualDays = true;
352         }
353 		
354 		java.util.Date currDateTime = beginDateTime;
355 		java.util.Calendar cal = GregorianCalendar.getInstance();
356 		
357 		while(currDateTime.before(endDateTime)){
358 			LocalDateTime currDate = new LocalDateTime(currDateTime);
359 			summaryHeader.add(makeHeaderDiplayString(currDate, virtualDays));
360 			
361 			dayCount++;
362 			if((dayCount % 7) == 0){
363 				summaryHeader.add("Week "+ ((dayCount / 7)));
364 			}
365 			cal.setTime(currDateTime);
366 			cal.add(java.util.Calendar.HOUR, 24);
367 			currDateTime = cal.getTime();
368 		}
369 		
370 		summaryHeader.add("Period Total");
371 		return summaryHeader;
372 	}
373 
374     /**
375      * Provides the number of hours worked for the pay period indicated in the
376      * aggregate.
377      *
378      * @param aggregate The aggregate we are summing
379      *
380      * @return A list of BigDecimals containing the number of hours worked.
381      * This list will line up with the header.
382      */
383     private List<BigDecimal> getWorkedHours(TkTimeBlockAggregate aggregate, Set<String> regularEarnCodes) {
384         List<BigDecimal> hours = new ArrayList<BigDecimal>();
385         BigDecimal periodTotal = TkConstants.BIG_DECIMAL_SCALED_ZERO;
386         List<FlsaWeek> weekList = aggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback());
387 //        List<FlsaWeek> weekList = aggregate.getFlsaWeeks(DateTimeZone.forID(TKUtils.getSystemTimeZone()));
388         for (FlsaWeek week : weekList) {
389             BigDecimal weeklyTotal = TkConstants.BIG_DECIMAL_SCALED_ZERO;
390             for (FlsaDay day : week.getFlsaDays()) {
391                 BigDecimal totalForDay = TkConstants.BIG_DECIMAL_SCALED_ZERO;
392                 for (TimeBlock block : day.getAppliedTimeBlocks()) {
393                     EarnCode ec = TkServiceLocator.getEarnCodeService().getEarnCode(block.getEarnCode(), block.getEndDate());
394                     if (ec != null
395                            && (ec.getOvtEarnCode()
396                                || regularEarnCodes.contains(ec.getEarnCode()))) {
397                         totalForDay = totalForDay.add(block.getHours(), TkConstants.MATH_CONTEXT);
398                         weeklyTotal = weeklyTotal.add(block.getHours(), TkConstants.MATH_CONTEXT);
399                         periodTotal = periodTotal.add(block.getHours(), TkConstants.MATH_CONTEXT);
400                     }
401                 }
402                 hours.add(totalForDay);
403             }
404             hours.add(weeklyTotal);
405         }
406         hours.add(periodTotal);
407 
408         return hours;
409     }
410 
411 
412     /**
413      * Handles the generation of the display header for the time summary.
414      *
415      * @param cal The PayCalendarEntries object we are using to derive information.
416      * @param dayArrangements Container passed in to store the position of week / period aggregate sums
417      *
418      * @return An in-order string of days for this period that properly accounts
419      * for FLSA week boundaries in the pay period.
420      */
421     @Override
422     public List<String> getHeaderForSummary(CalendarEntries cal, List<Boolean> dayArrangements) {
423         List<String> header = new ArrayList<String>();
424         if (cal == null) {
425             return Collections.emptyList();
426         }
427         // Maps directly to joda time day of week constants.
428         int flsaBeginDay = this.getPayCalendarForEntry(cal).getFlsaBeginDayConstant();
429         boolean virtualDays = false;
430         LocalDateTime startDate = cal.getBeginLocalDateTime();
431         LocalDateTime endDate = cal.getEndLocalDateTime();
432 
433         // Increment end date if we are on a virtual day calendar, so that the
434         // for loop can account for having the proper amount of days on the
435         // summary calendar.
436         if (endDate.get(DateTimeFieldType.hourOfDay()) != 0 || endDate.get(DateTimeFieldType.minuteOfHour()) != 0 ||
437                 endDate.get(DateTimeFieldType.secondOfMinute()) != 0)
438         {
439             endDate = endDate.plusDays(1);
440             virtualDays = true;
441         }
442 
443         boolean afterFirstDay = false;
444         int week = 1;
445         for (LocalDateTime currentDate = startDate; currentDate.compareTo(endDate) < 0; currentDate = currentDate.plusDays(1)) {
446 
447             if (currentDate.getDayOfWeek() == flsaBeginDay && afterFirstDay) {
448                 header.add("Week " + week);
449                 dayArrangements.add(false);
450                 week++;
451             }
452 
453             header.add(makeHeaderDiplayString(currentDate, virtualDays));
454             dayArrangements.add(true);
455 
456 
457             afterFirstDay = true;
458         }
459 
460         // We may have a very small final "week" on this pay period. For now
461         // we will mark it as a week, and if someone doesn't like it, it can
462         // be removed.
463         if (!header.get(header.size()-1).startsWith("Week")) {
464             dayArrangements.add(false);
465             header.add("Week " + week);
466         }
467 
468 
469         header.add("Period Total");
470         dayArrangements.add(false);
471         return header;
472     }
473 
474     /**
475      * Helper function to generate display text for the summary header.
476      * @param currentDate The date we are generating for.
477      * @param virtualDays Whether or not virtual days apply.
478      * @return A string appropriate for UI display.
479      */
480     private String makeHeaderDiplayString(LocalDateTime currentDate, boolean virtualDays) {
481         StringBuilder display = new StringBuilder();
482         
483         display.append(currentDate.toString("E"));
484         if (virtualDays) {
485         	LocalDateTime nextDate = currentDate.plusDays(1);
486         	display.append(" - ");
487             display.append(nextDate.toString("E"));
488         }
489         
490         display.append("<br />");
491         
492         display.append(currentDate.toString(TkConstants.DT_ABBREV_DATE_FORMAT));
493         if (virtualDays) {
494             LocalDateTime nextDate = currentDate.plusDays(1);
495             display.append(" - ");
496             display.append(nextDate.toString(TkConstants.DT_ABBREV_DATE_FORMAT));
497         }
498 
499         return display.toString();
500     }
501 
502     /**
503      * @param calEntry Calendar entry we are using for lookup.
504      * @return The PayCalendar that owns the provided entry.
505      */
506     private Calendar getPayCalendarForEntry(CalendarEntries calEntry) {
507         Calendar cal = null;
508 
509         if (calEntry != null) {
510             cal = TkServiceLocator.getCalendarService().getCalendar(calEntry.getHrCalendarId());
511         }
512 
513         return cal;
514     }
515 
516 }