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.timesummary.service;
017    
018    import java.math.BigDecimal;
019    import java.sql.Date;
020    import java.util.ArrayList;
021    import java.util.Collections;
022    import java.util.Comparator;
023    import java.util.GregorianCalendar;
024    import java.util.HashMap;
025    import java.util.HashSet;
026    import java.util.List;
027    import java.util.Map;
028    import java.util.Set;
029    import java.util.TreeMap;
030    
031    import org.apache.commons.collections.CollectionUtils;
032    import org.apache.commons.lang.StringUtils;
033    import org.apache.commons.lang.time.DateUtils;
034    import org.apache.log4j.Logger;
035    import org.joda.time.DateTimeFieldType;
036    import org.joda.time.Interval;
037    import org.joda.time.LocalDateTime;
038    import org.kuali.hr.job.Job;
039    import org.kuali.hr.lm.LMConstants;
040    import org.kuali.hr.lm.accrual.AccrualCategoryRule;
041    import org.kuali.hr.lm.leaveSummary.LeaveSummary;
042    import org.kuali.hr.lm.leaveSummary.LeaveSummaryRow;
043    import org.kuali.hr.lm.leaveblock.LeaveBlock;
044    import org.kuali.hr.lm.util.LeaveBlockAggregate;
045    import org.kuali.hr.time.assignment.Assignment;
046    import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
047    import org.kuali.hr.time.calendar.Calendar;
048    import org.kuali.hr.time.calendar.CalendarEntries;
049    import org.kuali.hr.time.earncode.EarnCode;
050    import org.kuali.hr.time.earncodegroup.EarnCodeGroup;
051    import org.kuali.hr.time.flsa.FlsaDay;
052    import org.kuali.hr.time.flsa.FlsaWeek;
053    import org.kuali.hr.time.service.base.TkServiceLocator;
054    import org.kuali.hr.time.timeblock.TimeBlock;
055    import org.kuali.hr.time.timeblock.TimeHourDetail;
056    import org.kuali.hr.time.timesheet.TimesheetDocument;
057    import org.kuali.hr.time.timesummary.AssignmentColumn;
058    import org.kuali.hr.time.timesummary.AssignmentRow;
059    import org.kuali.hr.time.timesummary.EarnCodeSection;
060    import org.kuali.hr.time.timesummary.EarnGroupSection;
061    import org.kuali.hr.time.timesummary.TimeSummary;
062    import org.kuali.hr.time.util.TKUtils;
063    import org.kuali.hr.time.util.TkConstants;
064    import org.kuali.hr.time.util.TkTimeBlockAggregate;
065    import org.kuali.hr.time.workarea.WorkArea;
066    
067    public class TimeSummaryServiceImpl implements TimeSummaryService {
068            private static final String OTHER_EARN_GROUP = "Other";
069            private static final Logger LOG = Logger.getLogger(TimeSummaryServiceImpl.class);
070            
071        @Override
072            public TimeSummary getTimeSummary(TimesheetDocument timesheetDocument) {
073                    TimeSummary timeSummary = new TimeSummary();
074    
075                    if(timesheetDocument == null || timesheetDocument.getTimeBlocks() == null) {
076                            return timeSummary;
077                    }
078    
079            List<Boolean> dayArrangements = new ArrayList<Boolean>();
080    
081                    timeSummary.setSummaryHeader(getHeaderForSummary(timesheetDocument.getCalendarEntry(), dayArrangements));
082                    TkTimeBlockAggregate tkTimeBlockAggregate = new TkTimeBlockAggregate(timesheetDocument.getTimeBlocks(), timesheetDocument.getCalendarEntry(), TkServiceLocator.getCalendarService().getCalendar(timesheetDocument.getCalendarEntry().getHrCalendarId()), true);
083    
084            List<Assignment> timeAssignments = timesheetDocument.getAssignments();
085            List<String> tAssignmentKeys = new ArrayList<String>();
086            Set<String> regularEarnCodes = new HashSet<String>();
087            for(Assignment assign : timeAssignments) {
088                tAssignmentKeys.add(assign.getAssignmentKey());
089                regularEarnCodes.add(assign.getJob().getPayTypeObj().getRegEarnCode());
090            }
091            
092            Date endDate = timesheetDocument.getCalendarEntry().getEndPeriodDate();
093            LocalDateTime tempLocalDate = timesheetDocument.getCalendarEntry().getEndLocalDateTime();
094            // 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
095            if(tempLocalDate.getHourOfDay() == 0 && tempLocalDate.getMinuteOfHour() == 0 && tempLocalDate.getMillisOfSecond() == 0) {
096                    endDate = new java.sql.Date(tempLocalDate.minusDays(1).toDate().getTime());
097            }
098            List<LeaveBlock> leaveBlocks =  TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(timesheetDocument.getPrincipalId(),
099                    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    }