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.flsa;
017    
018    import java.math.BigDecimal;
019    import java.util.ArrayList;
020    import java.util.HashMap;
021    import java.util.List;
022    import java.util.Map;
023    
024    import org.apache.commons.lang.StringUtils;
025    import org.joda.time.DateTime;
026    import org.joda.time.DateTimeZone;
027    import org.joda.time.Interval;
028    import org.joda.time.LocalDateTime;
029    import org.kuali.hr.lm.leaveblock.LeaveBlock;
030    import org.kuali.hr.time.timeblock.TimeBlock;
031    import org.kuali.hr.time.timeblock.TimeHourDetail;
032    import org.kuali.hr.time.util.TKUtils;
033    import org.kuali.hr.time.util.TkConstants;
034    
035    public class FlsaDay {
036    
037            private Map<String,List<TimeBlock>> earnCodeToTimeBlocks = new HashMap<String,List<TimeBlock>>();
038            private List<TimeBlock> appliedTimeBlocks = new ArrayList<TimeBlock>();
039            
040            private Map<String,List<LeaveBlock>> earnCodeToLeaveBlocks = new HashMap<String,List<LeaveBlock>>();
041            private List<LeaveBlock> appliedLeaveBlocks = new ArrayList<LeaveBlock>();
042    
043            Interval flsaDateInterval;
044            LocalDateTime flsaDate;
045        DateTimeZone timeZone;
046    
047        /**
048         *
049         * @param flsaDate A LocalDateTime because we want to be conscious of the
050         * relative nature of this flsa/window
051         * @param timeBlocks
052         * @param timeZone The timezone we are constructing, relative.
053         */
054            public FlsaDay(LocalDateTime flsaDate, List<TimeBlock> timeBlocks, List<LeaveBlock> leaveBlocks, DateTimeZone timeZone) {
055                    this.flsaDate = flsaDate;
056            this.timeZone = timeZone;
057                    flsaDateInterval = new Interval(flsaDate.toDateTime(timeZone), flsaDate.toDateTime(timeZone).plusHours(24));
058                    this.setTimeBlocks(timeBlocks);
059                    this.setLeaveBlocks(leaveBlocks);
060            }
061    
062            /**
063             * Handles the breaking apart of existing time blocks around FLSA boundaries.
064             *
065             * This method will compare the FLSA interval against the timeblock interval
066             * to determine how many hours overlap.  It will then examine the time hour
067             * details
068             *
069             * @param timeBlocks a sorted list of time blocks.
070             */
071            public void setTimeBlocks(List<TimeBlock> timeBlocks) {
072                    for (TimeBlock block : timeBlocks) {
073                applyBlock(block, this.appliedTimeBlocks);
074                            //if (!applyBlock(block, this.appliedTimeBlocks)) {
075                            //      break;
076                //}
077            }
078            }
079            
080            /**
081             * Handles the breaking apart of existing leave blocks around FLSA boundaries.
082             *
083             * This method will compare the FLSA interval against the leaveblock interval
084             * to determine how many hours overlap.  It will then examine the leave hour
085             * details
086             *
087             * @param leaveBlocks a sorted list of leave blocks.
088             */
089            public void setLeaveBlocks(List<LeaveBlock> leaveBlocks) {
090                    for (LeaveBlock block : leaveBlocks) {
091                applyBlock(block, this.appliedLeaveBlocks);
092                            /*if (!applyBlock(block, this.appliedLeaveBlocks)) {
093                                    break;
094                }*/
095            }
096            }
097    
098            /**
099             * This method will compute the mappings present for this object:
100             *
101             * earnCodeToTimeBlocks
102             * earnCodeToHours
103             *
104             */
105            public void remapTimeHourDetails() {
106                    List<TimeBlock> reApplied = new ArrayList<TimeBlock>(appliedTimeBlocks.size());
107                    earnCodeToTimeBlocks.clear();
108                    for (TimeBlock block : appliedTimeBlocks) {
109                            applyBlock(block, reApplied);
110                    }
111            }
112            
113            /**
114             * This method will compute the mappings present for this object:
115             *
116             * earnCodeToTimeBlocks
117             * earnCodeToHours
118             *
119             */
120            public void remapLeaveHourDetails() {
121                    List<LeaveBlock> reApplied = new ArrayList<LeaveBlock>(appliedLeaveBlocks.size());
122                    earnCodeToLeaveBlocks.clear();
123                    for (LeaveBlock block : appliedLeaveBlocks) {
124                            applyBlock(block, reApplied);
125                    }
126            }
127    
128            /**
129         * This method determines if the provided TimeBlock is applicable to this
130         * FLSA day, and if so will add it to the applyList. It could be the case
131         * that a TimeBlock is on the boundary of the FLSA day so that only a
132         * partial amount of the hours for that TimeBlock will count towards this
133         * day.
134         *
135         * |---------+------------------+---------|
136         * | Day 1   | Day 1/2 Boundary | Day 2   |
137         * |---------+------------------+---------|
138         * | Block 1 |             | Block 2      |
139         * |---------+------------------+---------|
140         *
141         * The not so obvious ascii diagram above is intended to illustrate the case
142         * where on day one you have 1 fully overlapping time block (block1) and one
143         * partially overlapping time block (block2). Block 2 belongs to both FLSA
144         * Day 1 and Day 2.
145         *
146             * @param block A time block that we want to check and apply to this day.
147             * @param applyList A list of time blocks we want to add applicable time blocks to.
148             *
149             * @return True if the block is applicable, false otherwise.  The return
150             * value can be used as a quick exit for the setTimeBlocks() method.
151             *
152             * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare.
153         *
154         * Danger may still lurk in day-boundary overlapping time blocks that have multiple Time Hour Detail entries.
155             */
156            private boolean applyBlock(TimeBlock block, List<TimeBlock> applyList) {
157                    DateTime beginDateTime = new DateTime(block.getBeginTimestamp(), this.timeZone);
158                    DateTime endDateTime = new DateTime(block.getEndTimestamp(), this.timeZone);
159    
160                    if (beginDateTime.isAfter(flsaDateInterval.getEnd())) {
161                            return false;
162            }
163    
164                    Interval timeBlockInterval = null;
165                    //Requested to have zero hour time blocks be able to be added to the GUI
166                    boolean zeroHoursTimeBlock = false;
167                    if(endDateTime.getMillis() > beginDateTime.getMillis()){
168                            timeBlockInterval = new Interval(beginDateTime,endDateTime);
169                    }
170                    
171                    if(flsaDateInterval.contains(beginDateTime)){
172                            zeroHoursTimeBlock = true;
173                    }
174    
175                    Interval overlapInterval = flsaDateInterval.overlap(timeBlockInterval);
176                    long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis();
177                    BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap);
178                    if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){
179                            if(block.getHours().compareTo(BigDecimal.ZERO) > 0){
180                                    overlapHours = block.getHours();
181                            }
182                    }
183    
184            // Local lookup for this time-block to ensure we are not over applicable hours.
185            // You will notice below we are earn codes globally per day, and also locally per timeblock.
186            // The local per-time block mapping is used only to verify that we have not gone over allocated overlap time
187            // for the individual time block.
188            Map<String,BigDecimal> localEarnCodeToHours = new HashMap<String,BigDecimal>();
189    
190                    if (zeroHoursTimeBlock || overlapHours.compareTo(BigDecimal.ZERO) > 0 || (flsaDateInterval.contains(beginDateTime) && StringUtils.equals(block.getEarnCodeType(),TkConstants.EARN_CODE_AMOUNT)))  {
191    
192                List<TimeHourDetail> details = block.getTimeHourDetails();
193                for (TimeHourDetail thd : details) {
194                    BigDecimal localEcHours = localEarnCodeToHours.containsKey(thd.getEarnCode()) ? localEarnCodeToHours.get(thd.getEarnCode()) : BigDecimal.ZERO;
195                    //NOTE adding this in the last few hours before release.. remove if side effects are noticed
196                    if (overlapHours.compareTo(localEcHours) >= 0 || thd.getAmount().compareTo(BigDecimal.ZERO) == 0) {
197                        localEcHours = localEcHours.add(thd.getHours(), TkConstants.MATH_CONTEXT);
198                        localEarnCodeToHours.put(thd.getEarnCode(), localEcHours);
199                    }
200                }
201    
202                            List<TimeBlock> blocks = earnCodeToTimeBlocks.get(block.getEarnCode());
203                            if (blocks == null) {
204                                    blocks = new ArrayList<TimeBlock>();
205                                    earnCodeToTimeBlocks.put(block.getEarnCode(), blocks);
206                            }
207                            blocks.add(block);
208                            applyList.add(block);
209                    }
210    
211                    return true;
212            }
213            
214            /**
215         * This method determines if the provided LeaveBlock is applicable to this
216         * FLSA day, and if so will add it to the applyList. It could be the case
217         * that a LeaveBlock is on the boundary of the FLSA day so that only a
218         * partial amount of the hours for that LeaveBlock will count towards this
219         * day.
220         *
221         * |---------+------------------+---------|
222         * | Day 1   | Day 1/2 Boundary | Day 2   |
223         * |---------+------------------+---------|
224         * | Block 1 |             | Block 2      |
225         * |---------+------------------+---------|
226         *
227         * The not so obvious ascii diagram above is intended to illustrate the case
228         * where on day one you have 1 fully overlapping leave block (block1) and one
229         * partially overlapping leave block (block2). Block 2 belongs to both FLSA
230         * Day 1 and Day 2.
231         *
232             * @param block A leave block that we want to check and apply to this day.
233             * @param applyList A list of leave blocks we want to add applicable leave blocks to.
234             *
235             * @return True if the block is applicable, false otherwise.  The return
236             * value can be used as a quick exit for the setLeaveBlocks() method.
237             *
238             * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare.
239             */
240            private boolean applyBlock(LeaveBlock block, List<LeaveBlock> applyList) {
241                    DateTime beginDateTime = new DateTime(block.getLeaveDate(), this.timeZone);
242                    DateTime endDateTime = new DateTime(block.getLeaveDate(), this.timeZone);
243    
244                    if (beginDateTime.isAfter(flsaDateInterval.getEnd()))
245                            return false;
246    
247                    Interval leaveBlockInterval = null;
248                    if(endDateTime.getMillis() > beginDateTime.getMillis()){
249                            leaveBlockInterval = new Interval(beginDateTime,endDateTime);
250                    }
251    
252                    Interval overlapInterval = flsaDateInterval.overlap(leaveBlockInterval);
253                    long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis();
254                    BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap);
255                    if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){
256                            if(block.getLeaveAmount().negate().compareTo(BigDecimal.ZERO) > 0){
257                                    overlapHours = block.getLeaveAmount().negate();
258                            }
259                    }
260                    
261                    if (overlapHours.compareTo(BigDecimal.ZERO) > 0)  {
262                            List<LeaveBlock> blocks = earnCodeToLeaveBlocks.get(block.getEarnCode());
263                            if (blocks == null) {
264                                    blocks = new ArrayList<LeaveBlock>();
265                                    earnCodeToLeaveBlocks.put(block.getEarnCode(), blocks);
266                            }
267                            blocks.add(block);
268                            applyList.add(block);
269                    }
270    
271                    return true;
272            }
273    
274            public Map<String, List<TimeBlock>> getEarnCodeToTimeBlocks() {
275                    return earnCodeToTimeBlocks;
276            }
277    
278            public void setEarnCodeToTimeBlocks(Map<String, List<TimeBlock>> earnCodeToTimeBlocks) {
279                    this.earnCodeToTimeBlocks = earnCodeToTimeBlocks;
280            }
281    
282            public List<TimeBlock> getAppliedTimeBlocks() {
283                    return appliedTimeBlocks;
284            }
285    
286            public void setAppliedTimeBlocks(List<TimeBlock> appliedTimeBlocks) {
287                    this.appliedTimeBlocks = appliedTimeBlocks;
288            }
289    
290            public Map<String, List<LeaveBlock>> getEarnCodeToLeaveBlocks() {
291                    return earnCodeToLeaveBlocks;
292            }
293    
294            public void setEarnCodeToLeaveBlocks(Map<String, List<LeaveBlock>> earnCodeToLeaveBlocks) {
295                    this.earnCodeToLeaveBlocks = earnCodeToLeaveBlocks;
296            }
297    
298            public List<LeaveBlock> getAppliedLeaveBlocks() {
299                    return appliedLeaveBlocks;
300            }
301    
302            public void setAppliedLeaveBlocks(List<LeaveBlock> appliedLeaveBlocks) {
303                    this.appliedLeaveBlocks = appliedLeaveBlocks;
304            }
305    
306    }