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.detail.web;
017    
018    import java.math.BigDecimal;
019    import java.sql.Date;
020    import java.sql.Timestamp;
021    import java.text.SimpleDateFormat;
022    import java.util.ArrayList;
023    import java.util.Collection;
024    import java.util.Collections;
025    import java.util.HashMap;
026    import java.util.HashSet;
027    import java.util.List;
028    import java.util.Map;
029    import java.util.Map.Entry;
030    import java.util.Properties;
031    import java.util.Set;
032    
033    import javax.servlet.http.HttpServletRequest;
034    import javax.servlet.http.HttpServletResponse;
035    
036    import org.apache.commons.collections.MapUtils;
037    import org.apache.commons.lang.StringUtils;
038    import org.apache.commons.lang.SystemUtils;
039    import org.apache.struts.action.ActionForm;
040    import org.apache.struts.action.ActionForward;
041    import org.apache.struts.action.ActionMapping;
042    import org.joda.time.DateTime;
043    import org.joda.time.Interval;
044    import org.kuali.hr.lm.LMConstants;
045    import org.kuali.hr.lm.accrual.AccrualCategory;
046    import org.kuali.hr.lm.accrual.AccrualCategoryRule;
047    import org.kuali.hr.lm.balancetransfer.BalanceTransfer;
048    import org.kuali.hr.lm.balancetransfer.validation.BalanceTransferValidationUtils;
049    import org.kuali.hr.lm.leaveblock.LeaveBlock;
050    import org.kuali.hr.lm.leavecalendar.validation.LeaveCalendarValidationUtil;
051    import org.kuali.hr.lm.util.LeaveBlockAggregate;
052    import org.kuali.hr.time.assignment.Assignment;
053    import org.kuali.hr.time.calendar.Calendar;
054    import org.kuali.hr.time.calendar.CalendarEntries;
055    import org.kuali.hr.time.calendar.TkCalendar;
056    import org.kuali.hr.time.earncode.EarnCode;
057    import org.kuali.hr.time.principal.PrincipalHRAttributes;
058    import org.kuali.hr.time.roles.TkUserRoles;
059    import org.kuali.hr.time.roles.UserRoles;
060    import org.kuali.hr.time.service.base.TkServiceLocator;
061    import org.kuali.hr.time.timeblock.TimeBlock;
062    import org.kuali.hr.time.timeblock.TimeBlockHistory;
063    import org.kuali.hr.time.timesheet.TimesheetDocument;
064    import org.kuali.hr.time.timesheet.web.TimesheetAction;
065    import org.kuali.hr.time.timesheet.web.TimesheetActionForm;
066    import org.kuali.hr.time.timesummary.AssignmentColumn;
067    import org.kuali.hr.time.timesummary.AssignmentRow;
068    import org.kuali.hr.time.timesummary.EarnCodeSection;
069    import org.kuali.hr.time.timesummary.EarnGroupSection;
070    import org.kuali.hr.time.timesummary.TimeSummary;
071    import org.kuali.hr.time.util.TKContext;
072    import org.kuali.hr.time.util.TKUser;
073    import org.kuali.hr.time.util.TKUtils;
074    import org.kuali.hr.time.util.TkConstants;
075    import org.kuali.hr.time.util.TkTimeBlockAggregate;
076    import org.kuali.hr.time.workflow.TimesheetDocumentHeader;
077    import org.kuali.rice.kew.api.document.DocumentStatus;
078    import org.kuali.rice.kew.service.KEWServiceLocator;
079    import org.kuali.rice.kim.api.identity.principal.EntityNamePrincipalName;
080    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
081    import org.kuali.rice.krad.exception.AuthorizationException;
082    import org.kuali.rice.krad.util.GlobalVariables;
083    import org.kuali.rice.krad.util.ObjectUtils;
084    import org.kuali.rice.krad.util.UrlFactory;
085    
086    public class TimeDetailAction extends TimesheetAction {
087    
088        @Override
089        protected void checkTKAuthorization(ActionForm form, String methodToCall) throws AuthorizationException {
090            super.checkTKAuthorization(form, methodToCall); // Checks for read access first.
091            UserRoles roles = TkUserRoles.getUserRoles(GlobalVariables.getUserSession().getPrincipalId());
092            TimesheetDocument doc = TKContext.getCurrentTimesheetDocument();
093    
094            // Check for write access to Timeblock.
095            if (StringUtils.equals(methodToCall, "addTimeBlock") || StringUtils.equals(methodToCall, "deleteTimeBlock") || StringUtils.equals(methodToCall, "updateTimeBlock")) {
096                if (!roles.isDocumentWritable(doc)) {
097                    throw new AuthorizationException(roles.getPrincipalId(), "TimeDetailAction", "");
098                }
099            }
100        }
101    
102        @Override
103        public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
104            ActionForward forward = super.execute(mapping, form, request, response);
105            if (forward.getRedirect()) {
106                return forward;
107            }
108            if (TKContext.getCurrentTimesheetDocument() == null) {
109                return forward;
110            }
111            TimeDetailActionForm tdaf = (TimeDetailActionForm) form;
112            tdaf.setAssignmentDescriptions(TkServiceLocator.getAssignmentService().getAssignmentDescriptions(TKContext.getCurrentTimesheetDocument(), false));
113    
114            // Handle User preference / timezone information (pushed up from TkCalendar to avoid duplication)
115            // Set calendar
116            CalendarEntries payCalendarEntry = tdaf.getPayCalendarDates();
117            Calendar payCalendar = TkServiceLocator.getCalendarService().getCalendar(payCalendarEntry != null ? payCalendarEntry.getHrCalendarId() : null);
118            
119            //List<TimeBlock> timeBlocks = TkServiceLocator.getTimeBlockService().getTimeBlocks(Long.parseLong(tdaf.getTimesheetDocument().getDocumentHeader().getTimesheetDocumentId()));
120            List<TimeBlock> timeBlocks = TKContext.getCurrentTimesheetDocument().getTimeBlocks();
121            // get leave blocks
122            List<Assignment> timeAssignments = TKContext.getCurrentTimesheetDocument().getAssignments();
123            List<String> tAssignmentKeys = new ArrayList<String>();
124            for(Assignment assign : timeAssignments) {
125                    tAssignmentKeys.add(assign.getAssignmentKey());
126            }
127            List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(TKContext.getCurrentTimesheetDocument().getPrincipalId(), 
128                                                    payCalendarEntry.getBeginPeriodDate(), payCalendarEntry.getEndPeriodDate(), tAssignmentKeys);
129            List<LeaveBlock> balanceTransferLeaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksWithType(TKContext.getCurrentTimesheetDocument().getPrincipalId(),
130                             payCalendarEntry.getBeginPeriodDate(), payCalendarEntry.getEndPeriodDate(), LMConstants.LEAVE_BLOCK_TYPE.BALANCE_TRANSFER);
131            
132    //        List<String> warnings = tdaf.getWarnings();
133    /*        List<String> infoMessages = tdaf.getInfoMessages();
134            List<String> warningMessages = tdaf.getWarningMessages();
135            List<String> actionMessages = tdaf.getActionMessages();*/
136            
137            Map<String, Set<String>> allMessages = LeaveCalendarValidationUtil.getWarningMessagesForLeaveBlocks(balanceTransferLeaveBlocks);
138            Map<String, Set<String>> transactionalMessages = LeaveCalendarValidationUtil.validatePendingTransactions(TKContext.getTargetPrincipalId(),
139                            payCalendarEntry.getBeginPeriodDate(), payCalendarEntry.getEndPeriodDate());
140            
141            List<String> warnings = new ArrayList<String>();
142            //placing the following "validation" further down in this method will overwrite messages added prior to this call.
143            //allMessages.putAll(LeaveCalendarValidationUtil.validatePendingTransactions(viewPrincipal, payCalendarEntry.getBeginPeriodDate(), payCalendarEntry.getEndPeriodDate()));
144            
145            // add warning messages based on max carry over balances for each accrual category for non-exempt leave users
146            String viewPrincipal = TKContext.getTargetPrincipalId();
147            List<BalanceTransfer> losses = new ArrayList<BalanceTransfer>();
148            if (TkServiceLocator.getLeaveApprovalService().isActiveAssignmentFoundOnJobFlsaStatus(viewPrincipal, TkConstants.FLSA_STATUS_NON_EXEMPT, true)) {
149                PrincipalHRAttributes principalCalendar = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(viewPrincipal, payCalendarEntry.getEndPeriodDate());
150    
151                    Interval calendarInterval = new Interval(payCalendarEntry.getBeginPeriodDate().getTime(), payCalendarEntry.getEndPeriodDate().getTime());
152                    Map<String,Set<LeaveBlock>> maxBalInfractions = new HashMap<String,Set<LeaveBlock>>();
153                    
154                if(ObjectUtils.isNotNull(principalCalendar)) {
155                    maxBalInfractions = TkServiceLocator.getAccrualCategoryMaxBalanceService().getMaxBalanceViolations(payCalendarEntry, viewPrincipal);
156                    
157                    for(Entry<String,Set<LeaveBlock>> entry : maxBalInfractions.entrySet()) {
158                            for(LeaveBlock lb : entry.getValue()) {
159                                            if(calendarInterval.contains(lb.getLeaveDate().getTime())) {
160                                            AccrualCategory accrualCat = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(lb.getAccrualCategory(), lb.getLeaveDate());
161                                                    AccrualCategoryRule aRule = TkServiceLocator.getAccrualCategoryRuleService().getAccrualCategoryRule(lb.getAccrualCategoryRuleId());
162                                                    if(StringUtils.equals(aRule.getActionAtMaxBalance(),LMConstants.ACTION_AT_MAX_BAL.LOSE)) {
163                                                            DateTime aDate = null;
164                                                            if(StringUtils.equals(aRule.getMaxBalanceActionFrequency(), LMConstants.MAX_BAL_ACTION_FREQ.YEAR_END)) {
165                                                                    aDate = TkServiceLocator.getLeavePlanService().getRolloverDayOfLeavePlan(principalCalendar.getLeavePlan(), lb.getLeaveDate());
166                                                            }
167                                                            else {
168                                                                    Calendar cal = TkServiceLocator.getCalendarService().getCalendarByPrincipalIdAndDate(viewPrincipal, lb.getLeaveDate(), true);
169                                                                    CalendarEntries leaveEntry = TkServiceLocator.getCalendarEntriesService().getCurrentCalendarEntriesByCalendarId(cal.getHrCalendarId(), lb.getLeaveDate());
170                                                                    aDate = new DateTime(leaveEntry.getEndPeriodDate());
171                                                            }
172                                                            aDate = aDate.minusDays(1);
173                                                            if(calendarInterval.contains(aDate.getMillis()) && aDate.toDate().compareTo(payCalendarEntry.getEndPeriodDate()) <= 0) {
174                                                                    //may want to calculate summary for all rows, displayable or not, and determine displayability via tags.
175                                                                    AccrualCategory accrualCategory = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(aRule.getLmAccrualCategoryId());
176                                                                    BigDecimal accruedBalance = TkServiceLocator.getAccrualCategoryService().getAccruedBalanceForPrincipal(viewPrincipal, accrualCategory, lb.getLeaveDate());
177                                                                    
178                                                                    BalanceTransfer loseTransfer = TkServiceLocator.getBalanceTransferService().initializeTransfer(viewPrincipal, lb.getAccrualCategoryRuleId(), accruedBalance, lb.getLeaveDate());
179                                                                    boolean valid = BalanceTransferValidationUtils.validateTransfer(loseTransfer);
180                                                                    if(valid)
181                                                                            losses.add(loseTransfer);
182                                                            }
183                                                    }
184            
185            
186                                            // accrual categories within the leave plan that are hidden from the leave summary WILL appear.
187                                                    String message = "You have exceeded the maximum balance limit for '" + accrualCat.getAccrualCategory() + "' as of " + lb.getLeaveDate() + ". "+
188                                                    "Depending upon the accrual category rules, leave over this limit may be forfeited.";
189                                //  leave blocks are sorted in getMaxBalanceViolations() method, so we just take the one with the earliest leave date for an accrual category.
190                                if(!StringUtils.contains(allMessages.get("warningMessages").toString(),"You have exceeded the maximum balance limit for '"+accrualCat.getAccrualCategory())) {
191                                        allMessages.get("warningMessages").add(message);
192                                                    }
193                                            }
194                            }
195                    }
196                    }
197                tdaf.setForfeitures(losses);
198                
199                    if (principalCalendar != null) {
200                            Calendar calendar = TkServiceLocator.getCalendarService().getCalendarByPrincipalIdAndDate(viewPrincipal, tdaf.getEndPeriodDateTime(), true);
201                                            
202                                    if (calendar != null) {
203                                            List<CalendarEntries> leaveCalendarEntries = TkServiceLocator.getCalendarEntriesService().getCalendarEntriesEndingBetweenBeginAndEndDate(calendar.getHrCalendarId(), tdaf.getBeginPeriodDateTime(), tdaf.getEndPeriodDateTime());
204                                            
205                                            List<AccrualCategory> accrualCategories = TkServiceLocator.getAccrualCategoryService().getActiveLeaveAccrualCategoriesForLeavePlan(principalCalendar.getLeavePlan(), new java.sql.Date(tdaf.getEndPeriodDateTime().getTime()));
206                                            for (AccrualCategory accrualCategory : accrualCategories) {
207                                                    if (TkServiceLocator.getAccrualCategoryMaxCarryOverService().exceedsAccrualCategoryMaxCarryOver(accrualCategory.getAccrualCategory(), viewPrincipal, leaveCalendarEntries, tdaf.getEndPeriodDateTime())) {
208                                                            String message = "Your pending leave balance is greater than the annual max carry over for accrual category '" + accrualCategory.getAccrualCategory() + "' and upon approval, the excess balance will be lost.";
209                                                            if (!warnings.contains(message)) {
210                                                                    warnings.add(message);
211                                                            }
212                                                    }
213                                            }
214                                    }
215                            }
216            }
217    /*              warnings.addAll(allMessages.get("infoMessages"));
218                    warnings.addAll(allMessages.get("actionMessages"));
219                    warnings.addAll(allMessages.get("warningMessages"));*/
220            allMessages.get("warningMessages").addAll(warnings);
221            
222            List<String> infoMessages = tdaf.getInfoMessages();
223            infoMessages.addAll(allMessages.get("infoMessages"));
224            infoMessages.addAll(transactionalMessages.get("infoMessages"));
225    
226            List<String> warningMessages = tdaf.getWarningMessages();
227            warningMessages.addAll(allMessages.get("warningMessages"));
228            warningMessages.addAll(transactionalMessages.get("warningMessages"));
229    
230            List<String> actionMessages = tdaf.getActionMessages();
231            actionMessages.addAll(allMessages.get("actionMessages"));
232            actionMessages.addAll(transactionalMessages.get("actionMessages"));
233    
234            tdaf.setInfoMessages(infoMessages);
235            tdaf.setWarningMessages(warningMessages);
236            tdaf.setActionMessages(actionMessages);
237    
238            this.assignStypeClassMapForTimeSummary(tdaf,timeBlocks, leaveBlocks);
239            
240            List<Interval> intervals = TKUtils.getFullWeekDaySpanForCalendarEntry(payCalendarEntry);
241            LeaveBlockAggregate lbAggregate = new LeaveBlockAggregate(leaveBlocks, payCalendarEntry, intervals);
242            TkTimeBlockAggregate tbAggregate = new TkTimeBlockAggregate(timeBlocks, payCalendarEntry, payCalendar, true,intervals);
243            // use both time aggregate and leave aggregate to populate the calendar
244            TkCalendar cal = TkCalendar.getCalendar(tbAggregate, lbAggregate);
245            cal.assignAssignmentStyle(tdaf.getAssignStyleClassMap());
246            tdaf.setTkCalendar(cal);
247         
248            this.populateCalendarAndPayPeriodLists(request, tdaf);
249    
250            tdaf.setTimeBlockString(ActionFormUtils.getTimeBlocksJson(tbAggregate.getFlattenedTimeBlockList()));
251            tdaf.setLeaveBlockString(ActionFormUtils.getLeaveBlocksJson(lbAggregate.getFlattenedLeaveBlockList()));
252    
253            tdaf.setOvertimeEarnCodes(TkServiceLocator.getEarnCodeService().getOvertimeEarnCodesStrs(TKContext.getCurrentTimesheetDocument().getAsOfDate()));
254    
255            if (StringUtils.equals(TKContext.getCurrentTimesheetDocument().getPrincipalId(), GlobalVariables.getUserSession().getPrincipalId())) {
256                    tdaf.setWorkingOnItsOwn("true");
257            }
258            
259            tdaf.setDocEditable("false");
260            if (TKUser.isSystemAdmin()) {
261                tdaf.setDocEditable("true");
262            } else {
263                    String documentStatus = TKContext.getCurrentTimesheetDocument().getDocumentHeader().getDocumentStatus();
264                boolean docFinal = DocumentStatus.FINAL.getCode().equals(documentStatus)
265                        || DocumentStatus.CANCELED.getCode().equals(documentStatus)
266                        || DocumentStatus.DISAPPROVED.getCode().equals(documentStatus);
267                if (!docFinal) {
268                    if(StringUtils.equals(TKContext.getCurrentTimesheetDocument().getPrincipalId(), GlobalVariables.getUserSession().getPrincipalId())
269                                    || TKUser.isSystemAdmin()
270                                    || TKUser.isLocationAdmin()
271                                    || TKUser.isDepartmentAdmin()
272                                    || TKUser.isReviewer()
273                                    || TKUser.isApprover()) {
274                        tdaf.setDocEditable("true");
275                    }
276                    
277                        //if the timesheet has been approved by at least one of the approvers, the employee should not be able to edit it
278                        if (StringUtils.equals(TKContext.getCurrentTimesheetDocument().getPrincipalId(), GlobalVariables.getUserSession().getPrincipalId())
279                                    && TKContext.getCurrentTimesheetDocument().getDocumentHeader().getDocumentStatus().equals(TkConstants.ROUTE_STATUS.ENROUTE)) {
280                                    Collection actions = KEWServiceLocator.getActionTakenService().findByDocIdAndAction(TKContext.getCurrentTimesheetDocument().getDocumentHeader().getDocumentId(), TkConstants.DOCUMENT_ACTIONS.APPROVE);
281                                    if(!actions.isEmpty()) {
282                                            tdaf.setDocEditable("false");  
283                                    }
284                            }
285                }
286            }
287    
288            return forward;
289        }
290    
291        // use lists of time blocks and leave blocks to build the style class map and assign css class to associated summary rows
292            private void assignStypeClassMapForTimeSummary(TimeDetailActionForm tdaf, List<TimeBlock> timeBlocks, List<LeaveBlock> leaveBlocks) throws Exception {
293                    TimeSummary ts = TkServiceLocator.getTimeSummaryService().getTimeSummary(TKContext.getCurrentTimesheetDocument());
294            tdaf.setAssignStyleClassMap(ActionFormUtils.buildAssignmentStyleClassMap(timeBlocks, leaveBlocks));
295            Map<String, String> aMap = tdaf.getAssignStyleClassMap();
296            // set css classes for each assignment row
297            for (EarnGroupSection earnGroupSection : ts.getSections()) {
298                for (EarnCodeSection section : earnGroupSection.getEarnCodeSections()) {
299                    for (AssignmentRow assignRow : section.getAssignmentsRows()) {
300                            String assignmentCssStyle = MapUtils.getString(aMap, assignRow.getAssignmentKey());
301                            assignRow.setCssClass(assignmentCssStyle);
302                            for (AssignmentColumn assignmentColumn : assignRow.getAssignmentColumns()) {
303                                    assignmentColumn.setCssClass(assignmentCssStyle);
304                            }
305                    }
306                }
307    
308            }
309            tdaf.setTimeSummary(ts);
310          //  ActionFormUtils.validateHourLimit(tdaf);
311            ActionFormUtils.addWarningTextFromEarnGroup(tdaf);
312            ActionFormUtils.addUnapprovedIPWarningFromClockLog(tdaf);
313            }
314    
315            private void populateCalendarAndPayPeriodLists(HttpServletRequest request, TimeDetailActionForm tdaf) {
316                    String viewPrincipal = TKContext.getTargetPrincipalId();
317                    List<TimesheetDocumentHeader> documentHeaders = (List<TimesheetDocumentHeader>) TkServiceLocator.getTimesheetDocumentHeaderService().getDocumentHeadersForPrincipalId(TKContext.getTargetPrincipalId());
318            SimpleDateFormat sdf = new SimpleDateFormat("yyyy");
319            if(tdaf.getCalendarYears().isEmpty()) {
320                    // get calendar year drop down list contents
321                    Set<String> yearSet = new HashSet<String>();
322                    
323                    for(TimesheetDocumentHeader tdh : documentHeaders) {
324                            yearSet.add(sdf.format(tdh.getBeginDate()));
325                    }
326                    List<String> yearList = new ArrayList<String>(yearSet);
327                    Collections.sort(yearList);
328                    Collections.reverse(yearList);  // newest on top
329                    tdaf.setCalendarYears(yearList);
330            }
331            // if selected calendar year is passed in
332            if(request.getParameter("selectedCY")!= null) {
333                    tdaf.setSelectedCalendarYear(request.getParameter("selectedCY").toString());
334            }
335            // if there is no selected calendr year, use the year of current pay calendar entry
336            if(StringUtils.isEmpty(tdaf.getSelectedCalendarYear())) {
337                    tdaf.setSelectedCalendarYear(sdf.format(tdaf.getPayCalendarDates().getBeginPeriodDate()));
338            }
339            if(tdaf.getPayPeriodsMap().isEmpty()) {
340                    List<CalendarEntries> payPeriodList = new ArrayList<CalendarEntries>();
341                    for(TimesheetDocumentHeader tdh : documentHeaders) {
342                            if(sdf.format(tdh.getBeginDate()).equals(tdaf.getSelectedCalendarYear())) {
343                        CalendarEntries pe = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(tdh.getPrincipalId(), tdh.getEndDate(), TkConstants.PAY_CALENDAR_TYPE);
344                        //CalendarEntries pe = TkServiceLocator.getCalendarEntriesService().getCalendarEntriesByBeginAndEndDate(tdh.getBeginDate(), tdh.getEndDate());
345                                    payPeriodList.add(pe);
346                            }
347                    }
348                    tdaf.setPayPeriodsMap(ActionFormUtils.getPayPeriodsMap(payPeriodList, viewPrincipal));
349            }
350            if(request.getParameter("selectedPP")!= null) {
351                    tdaf.setSelectedPayPeriod(request.getParameter("selectedPP").toString());
352            }
353            if(StringUtils.isEmpty(tdaf.getSelectedPayPeriod())) {
354                    tdaf.setSelectedPayPeriod(tdaf.getPayCalendarDates().getHrCalendarEntriesId());
355            }
356            }
357    
358    
359        /**
360         * This method involves creating an object-copy of every TimeBlock on the
361         * time sheet for overtime re-calculation.
362         *
363         * @throws Exception
364         */
365        public ActionForward deleteTimeBlock(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
366            TimeDetailActionForm tdaf = (TimeDetailActionForm) form;
367            
368                    String principalId = TKContext.getPrincipalId();
369                    String targetPrincipalId = TKContext.getTargetPrincipalId();
370                    String documentId = tdaf.getDocumentId();
371                    
372            //Grab timeblock to be deleted from form
373            List<TimeBlock> timeBlocks = tdaf.getTimesheetDocument().getTimeBlocks();
374            TimeBlock deletedTimeBlock = null;
375            for (TimeBlock tb : timeBlocks) {
376                if (tb.getTkTimeBlockId().compareTo(tdaf.getTkTimeBlockId()) == 0) {
377                    deletedTimeBlock = tb;
378                    break;
379                }
380            }
381            if (deletedTimeBlock == null) {
382                return mapping.findForward("basic");
383            }
384            //Remove from the list of timeblocks
385            List<TimeBlock> referenceTimeBlocks = new ArrayList<TimeBlock>(tdaf.getTimesheetDocument().getTimeBlocks().size());
386            for (TimeBlock b : tdaf.getTimesheetDocument().getTimeBlocks()) {
387                referenceTimeBlocks.add(b.copy());
388            }
389    
390            // simple pointer, for clarity
391            List<TimeBlock> newTimeBlocks = tdaf.getTimesheetDocument().getTimeBlocks();
392            newTimeBlocks.remove(deletedTimeBlock);
393    
394            //Delete timeblock
395            TkServiceLocator.getTimeBlockService().deleteTimeBlock(deletedTimeBlock);
396            // Add a row to the history table
397            TimeBlockHistory tbh = new TimeBlockHistory(deletedTimeBlock);
398            tbh.setActionHistory(TkConstants.ACTIONS.DELETE_TIME_BLOCK);
399            TkServiceLocator.getTimeBlockHistoryService().saveTimeBlockHistory(tbh);
400            
401            List<Assignment> assignments = tdaf.getTimesheetDocument().getAssignments();
402            List<String> assignmentKeys = new ArrayList<String>();
403            for (Assignment assignment : assignments) {
404                    assignmentKeys.add(assignment.getAssignmentKey());
405            }
406            List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(principalId, tdaf.getTimesheetDocument().getAsOfDate(), tdaf.getTimesheetDocument().getDocEndDate(), assignmentKeys);
407    
408            //reset time block
409            TkServiceLocator.getTimesheetService().resetTimeBlock(newTimeBlocks, tdaf.getTimesheetDocument().getAsOfDate());
410            TkServiceLocator.getTkRuleControllerService().applyRules(TkConstants.ACTIONS.ADD_TIME_BLOCK, newTimeBlocks, leaveBlocks, tdaf.getPayCalendarDates(), tdaf.getTimesheetDocument(), TKContext.getPrincipalId());
411            TkServiceLocator.getTimeBlockService().saveTimeBlocks(referenceTimeBlocks, newTimeBlocks, TKContext.getPrincipalId());
412            
413            generateTimesheetChangedNotification(principalId, targetPrincipalId, documentId);
414    
415            return mapping.findForward("basic");
416        }
417    
418        /**
419         * This method involves creating an object-copy of every TimeBlock on the
420         * time sheet for overtime re-calculation.
421         * Based on the form's timeBlockId or leaveBlockId, existing Time/Leave blocks will be deleted and new ones created
422         *
423         * @throws Exception
424         */
425        public ActionForward addTimeBlock(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
426            TimeDetailActionForm tdaf = (TimeDetailActionForm) form;
427            
428                    String principalId = TKContext.getPrincipalId();
429                    String targetPrincipalId = TKContext.getTargetPrincipalId();
430                    String documentId = tdaf.getDocumentId();
431            
432            if(StringUtils.isNotEmpty(tdaf.getTkTimeBlockId())) {
433                    // the user is changing an existing time block, so need to delete this time block
434                    this.removeOldTimeBlock(tdaf);
435            } else if(StringUtils.isNotEmpty(tdaf.getLmLeaveBlockId())) {
436                    // the user is changing an existing leave block, so need to delete this leave block
437                    this.removeOldLeaveBlock(tdaf.getLmLeaveBlockId());
438            }
439            if(StringUtils.isNotEmpty(tdaf.getSelectedEarnCode())) {
440                    EarnCode ec = TkServiceLocator.getEarnCodeService().getEarnCode(tdaf.getSelectedEarnCode(), tdaf.getTimesheetDocument().getAsOfDate());
441                    if(ec != null && (ec.getLeavePlan() != null || ec.getEligibleForAccrual().equals("N"))) {       // leave blocks changes
442                            this.changeLeaveBlocks(tdaf);
443                    } else {        // time blocks changes
444                            this.changeTimeBlocks(tdaf);
445                    }
446            }
447            
448           // ActionFormUtils.validateHourLimit(tdaf);
449            ActionFormUtils.addWarningTextFromEarnGroup(tdaf);
450            
451            generateTimesheetChangedNotification(principalId, targetPrincipalId, documentId);
452    
453            return mapping.findForward("basic");
454        }
455        
456        private void removeOldTimeBlock(TimeDetailActionForm tdaf) {
457              if (tdaf.getTkTimeBlockId() != null) {
458                  TimeBlock tb = TkServiceLocator.getTimeBlockService().getTimeBlock(tdaf.getTkTimeBlockId());
459                  if (tb != null) {
460                      TimeBlockHistory tbh = new TimeBlockHistory(tb);
461                      TkServiceLocator.getTimeBlockService().deleteTimeBlock(tb);
462            
463                      // mark the original timeblock as deleted in the history table
464                              tbh.setActionHistory(TkConstants.ACTIONS.DELETE_TIME_BLOCK);
465                              TkServiceLocator.getTimeBlockHistoryService().saveTimeBlockHistory(tbh);
466            
467                              // delete the timeblock from the memory
468                      tdaf.getTimesheetDocument().getTimeBlocks().remove(tb);
469                  }
470              }
471        }
472        
473        private void removeOldLeaveBlock(String lbId) {
474              if (lbId != null) {
475                  LeaveBlock lb = TkServiceLocator.getLeaveBlockService().getLeaveBlock(lbId);
476                  if (lb != null) {
477                      TkServiceLocator.getLeaveBlockService().deleteLeaveBlock(lbId, TKContext.getPrincipalId());
478                  }
479              }
480        }
481        
482        // add/update leave blocks 
483            private void changeLeaveBlocks(TimeDetailActionForm tdaf) {
484                    DateTime beginDate = null;
485                    DateTime endDate = null;
486                    
487                    if(tdaf.getStartTime() != null && tdaf.getEndTime() != null) {
488                            beginDate = new DateTime(TKUtils.convertDateStringToTimestamp(tdaf.getStartDate(), tdaf.getStartTime()));
489                            endDate = new DateTime(TKUtils.convertDateStringToTimestamp(tdaf.getEndDate(), tdaf.getEndTime()));
490                    } else {
491                            beginDate = new DateTime(TKUtils.convertDateStringToTimestamp(tdaf.getStartDate()));
492                            endDate = new DateTime(TKUtils.convertDateStringToTimestamp(tdaf.getEndDate()));
493                    }
494                    
495                    String selectedEarnCode = tdaf.getSelectedEarnCode();
496                    BigDecimal leaveAmount = tdaf.getLeaveAmount();
497                    
498                    String desc = "";       // there's no description field in time calendar pop window
499                    String spanningWeeks = tdaf.getSpanningWeeks();
500                    Assignment currentAssignment = TkServiceLocator.getAssignmentService().getAssignment(tdaf.getTimesheetDocument(), tdaf.getSelectedAssignment());
501                    TkServiceLocator.getLeaveBlockService().addLeaveBlocks(beginDate, endDate, tdaf.getPayCalendarDates(), selectedEarnCode, leaveAmount, desc, currentAssignment, 
502                                    spanningWeeks, LMConstants.LEAVE_BLOCK_TYPE.TIME_CALENDAR, TKContext.getTargetPrincipalId());
503                    
504            List<Assignment> assignments = tdaf.getTimesheetDocument().getAssignments();
505            List<String> assignmentKeys = new ArrayList<String>();
506            for (Assignment assignment : assignments) {
507                    assignmentKeys.add(assignment.getAssignmentKey());
508            }
509            List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(TKContext.getTargetPrincipalId(), tdaf.getTimesheetDocument().getAsOfDate(), tdaf.getTimesheetDocument().getDocEndDate(), assignmentKeys);
510    
511                    // A bad hack to apply rules to all timeblocks on timesheet
512                    List<TimeBlock> newTimeBlocks = tdaf.getTimesheetDocument().getTimeBlocks();
513            TkServiceLocator.getTkRuleControllerService().applyRules(TkConstants.ACTIONS.ADD_TIME_BLOCK, newTimeBlocks, leaveBlocks, tdaf.getPayCalendarDates(), tdaf.getTimesheetDocument(), TKContext.getPrincipalId());
514            TkServiceLocator.getTimeBlockService().saveTimeBlocks(newTimeBlocks, newTimeBlocks, TKContext.getPrincipalId());
515            }
516            
517        // add/update time blocks
518            private void changeTimeBlocks(TimeDetailActionForm tdaf) {
519                    Timestamp overtimeBeginTimestamp = null;
520            Timestamp overtimeEndTimestamp = null;
521            boolean isClockLogCreated = false;
522            
523            // This is for updating a timeblock or changing
524            // If tkTimeBlockId is not null and the new timeblock is valid, delete the existing timeblock and a new one will be created after submitting the form.
525            if (tdaf.getTkTimeBlockId() != null) {
526                TimeBlock tb = TkServiceLocator.getTimeBlockService().getTimeBlock(tdaf.getTkTimeBlockId());
527                if (tb != null) {
528                        isClockLogCreated = tb.getClockLogCreated();
529                        if (StringUtils.isNotEmpty(tdaf.getOvertimePref())) {
530                        //TODO:  This doesn't do anything!!! these variables are never used.  Should they be?
531                            overtimeBeginTimestamp = tb.getBeginTimestamp();
532                            overtimeEndTimestamp = tb.getEndTimestamp();
533                        }
534                }
535                // old time block is deleted from addTimeBlock method
536                // this.removeOldTimeBlock(tdaf);
537            }
538    
539            Assignment currentAssignment = TkServiceLocator.getAssignmentService().getAssignment(tdaf.getTimesheetDocument(), tdaf.getSelectedAssignment());
540    
541    
542            // Surgery point - Need to construct a Date/Time with Appropriate Timezone.
543            Timestamp startTime = TKUtils.convertDateStringToTimestamp(tdaf.getStartDate(), tdaf.getStartTime());
544            Timestamp endTime = TKUtils.convertDateStringToTimestamp(tdaf.getEndDate(), tdaf.getEndTime());
545    
546            // We need a  cloned reference set so we know whether or not to
547            // persist any potential changes without making hundreds of DB calls.
548            List<TimeBlock> referenceTimeBlocks = new ArrayList<TimeBlock>(tdaf.getTimesheetDocument().getTimeBlocks().size());
549            for (TimeBlock tb : tdaf.getTimesheetDocument().getTimeBlocks()) {
550                referenceTimeBlocks.add(tb.copy());
551            }
552    
553            // This is just a reference, for code clarity, the above list is actually
554            // separate at the object level.
555            List<TimeBlock> newTimeBlocks = tdaf.getTimesheetDocument().getTimeBlocks();
556            DateTime startTemp = new DateTime(startTime);
557            DateTime endTemp = new DateTime(endTime);
558            // KPME-1446 add spanningweeks to the calls below 
559            if (StringUtils.equals(tdaf.getAcrossDays(), "y")
560                    && !(endTemp.getDayOfYear() - startTemp.getDayOfYear() <= 1
561                    && endTemp.getHourOfDay() == 0)) {
562                List<TimeBlock> timeBlocksToAdd = TkServiceLocator.getTimeBlockService().buildTimeBlocksSpanDates(currentAssignment,
563                        tdaf.getSelectedEarnCode(), tdaf.getTimesheetDocument(), startTime,
564                        endTime, tdaf.getHours(), tdaf.getAmount(), isClockLogCreated, Boolean.parseBoolean(tdaf.getLunchDeleted()), tdaf.getSpanningWeeks(), TKContext.getPrincipalId());
565                for (TimeBlock tb : timeBlocksToAdd) {
566                    if (!newTimeBlocks.contains(tb)) {
567                        newTimeBlocks.add(tb);
568                    }
569                }
570            } else {
571                List<TimeBlock> timeBlocksToAdd = TkServiceLocator.getTimeBlockService().buildTimeBlocks(currentAssignment,
572                        tdaf.getSelectedEarnCode(), tdaf.getTimesheetDocument(), startTime,
573                        endTime, tdaf.getHours(), tdaf.getAmount(), isClockLogCreated, Boolean.parseBoolean(tdaf.getLunchDeleted()), TKContext.getPrincipalId());
574                for (TimeBlock tb : timeBlocksToAdd) {
575                    if (!newTimeBlocks.contains(tb)) {
576                        newTimeBlocks.add(tb);
577                    }
578                }
579            }
580    
581            //reset time block
582            TkServiceLocator.getTimesheetService().resetTimeBlock(newTimeBlocks, tdaf.getTimesheetDocument().getAsOfDate());
583    
584            // apply overtime pref
585            // I changed start and end times comparison below. it used to be overtimeBeginTimestamp and overtimeEndTimestamp but
586            // for some reason, they're always null because, we have removed the time block before getting here. KPME-2162
587            for (TimeBlock tb : newTimeBlocks) {
588                if (tb.getBeginTimestamp().equals(startTime) && tb.getEndTimestamp().equals(endTime) && StringUtils.isNotEmpty(tdaf.getOvertimePref())) {
589                    tb.setOvertimePref(tdaf.getOvertimePref());
590                }
591    
592            }
593            
594            List<Assignment> assignments = tdaf.getTimesheetDocument().getAssignments();
595            List<String> assignmentKeys = new ArrayList<String>();
596            for (Assignment assignment : assignments) {
597                    assignmentKeys.add(assignment.getAssignmentKey());
598            }
599            List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(TKContext.getTargetPrincipalId(), tdaf.getTimesheetDocument().getAsOfDate(), tdaf.getTimesheetDocument().getDocEndDate(), assignmentKeys);
600    
601            TkServiceLocator.getTkRuleControllerService().applyRules(TkConstants.ACTIONS.ADD_TIME_BLOCK, newTimeBlocks, leaveBlocks, tdaf.getPayCalendarDates(), tdaf.getTimesheetDocument(), TKContext.getPrincipalId());
602            TkServiceLocator.getTimeBlockService().saveTimeBlocks(referenceTimeBlocks, newTimeBlocks, TKContext.getPrincipalId());
603            }
604    
605        public ActionForward updateTimeBlock(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
606    
607            TimeDetailActionForm tdaf = (TimeDetailActionForm) form;
608            Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(tdaf.getTimesheetDocument(), tdaf.getSelectedAssignment());
609    
610            //Grab timeblock to be updated from form
611            List<TimeBlock> timeBlocks = tdaf.getTimesheetDocument().getTimeBlocks();
612            TimeBlock updatedTimeBlock = null;
613            for (TimeBlock tb : timeBlocks) {
614                if (tb.getTkTimeBlockId().compareTo(tdaf.getTkTimeBlockId()) == 0) {
615                    updatedTimeBlock = tb;
616                    tb.setJobNumber(assignment.getJobNumber());
617                    tb.setWorkArea(assignment.getWorkArea());
618                    tb.setTask(assignment.getTask());
619                    break;
620                }
621            }
622    
623            Set<String> earnCodes = new HashSet<String>();
624            if (updatedTimeBlock != null) {
625                List<EarnCode> validEarnCodes = TkServiceLocator.getEarnCodeService().getEarnCodesForTime(assignment, updatedTimeBlock.getBeginDate(), true);
626                for (EarnCode e : validEarnCodes) {
627                    earnCodes.add(e.getEarnCode());
628                }
629            }
630    
631            if (updatedTimeBlock != null
632                    && earnCodes.contains(updatedTimeBlock.getEarnCode())) {
633                TkServiceLocator.getTimeBlockService().updateTimeBlock(updatedTimeBlock);
634    
635                TimeBlockHistory tbh = new TimeBlockHistory(updatedTimeBlock);
636                tbh.setActionHistory(TkConstants.ACTIONS.UPDATE_TIME_BLOCK);
637                TkServiceLocator.getTimeBlockHistoryService().saveTimeBlockHistory(tbh);
638            }
639            tdaf.setMethodToCall("addTimeBlock");
640            return mapping.findForward("basic");
641        }
642    
643    
644        public ActionForward actualTimeInquiry(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
645            return mapping.findForward("ati");
646        }
647    
648        public ActionForward deleteLunchDeduction(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
649    
650            TimeDetailActionForm tdaf = (TimeDetailActionForm) form;
651            String timeHourDetailId = tdaf.getTkTimeHourDetailId();
652            TkServiceLocator.getTimeBlockService().deleteLunchDeduction(timeHourDetailId);
653    
654            List<TimeBlock> newTimeBlocks = tdaf.getTimesheetDocument().getTimeBlocks();
655            
656            List<Assignment> assignments = tdaf.getTimesheetDocument().getAssignments();
657            List<String> assignmentKeys = new ArrayList<String>();
658            for (Assignment assignment : assignments) {
659                    assignmentKeys.add(assignment.getAssignmentKey());
660            }
661            List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(TKContext.getTargetPrincipalId(), tdaf.getTimesheetDocument().getAsOfDate(), tdaf.getTimesheetDocument().getDocEndDate(), assignmentKeys);
662    
663            TkServiceLocator.getTimesheetService().resetTimeBlock(newTimeBlocks, tdaf.getTimesheetDocument().getAsOfDate());
664            
665            // KPME-1340
666            TkServiceLocator.getTkRuleControllerService().applyRules(TkConstants.ACTIONS.ADD_TIME_BLOCK, newTimeBlocks, leaveBlocks, tdaf.getPayCalendarDates(), tdaf.getTimesheetDocument(), TKContext.getPrincipalId());
667            TkServiceLocator.getTimeBlockService().saveTimeBlocks(newTimeBlocks);
668            TKContext.getCurrentTimesheetDocument().setTimeBlocks(newTimeBlocks);
669            
670            return mapping.findForward("basic");
671        }
672          
673      public ActionForward gotoCurrentPayPeriod(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
674              String viewPrincipal = TKUser.getCurrentTargetPersonId();
675              Date currentDate = TKUtils.getTimelessDate(null);
676          CalendarEntries pce = TkServiceLocator.getCalendarService().getCurrentCalendarDates(viewPrincipal, currentDate);
677          TimesheetDocument td = TkServiceLocator.getTimesheetService().openTimesheetDocument(viewPrincipal, pce);
678          setupDocumentOnFormContext((TimesheetActionForm)form, td);
679              return mapping.findForward("basic");
680      }
681      
682      //Triggered by changes of pay period drop down list, reload the whole page based on the selected pay period
683      public ActionForward changeCalendarYear(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
684              
685              TimeDetailActionForm tdaf = (TimeDetailActionForm) form;
686              if(request.getParameter("selectedCY") != null) {
687                      tdaf.setSelectedCalendarYear(request.getParameter("selectedCY").toString());
688              }
689              return mapping.findForward("basic");
690      }
691      
692      //Triggered by changes of pay period drop down list, reload the whole page based on the selected pay period
693      public ActionForward changePayPeriod(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
694              TimeDetailActionForm tdaf = (TimeDetailActionForm) form;
695              if(request.getParameter("selectedPP") != null) {
696                      tdaf.setSelectedPayPeriod(request.getParameter("selectedPP").toString());
697              CalendarEntries pce = TkServiceLocator.getCalendarEntriesService()
698                            .getCalendarEntries(request.getParameter("selectedPP").toString());
699                      if(pce != null) {
700                              String viewPrincipal = TKUser.getCurrentTargetPersonId();
701                              TimesheetDocument td = TkServiceLocator.getTimesheetService().openTimesheetDocument(viewPrincipal, pce);
702                              setupDocumentOnFormContext((TimesheetActionForm)form, td);
703                      }
704              }
705              return mapping.findForward("basic");
706      }
707      
708      public ActionForward deleteLeaveBlock(ActionMapping mapping,ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
709              TimeDetailActionForm tdaf = (TimeDetailActionForm) form;
710              
711              String principalId = TKContext.getPrincipalId();
712              String targetPrincipalId = TKContext.getTargetPrincipalId();
713              String documentId = tdaf.getDocumentId();
714              String leaveBlockId = tdaf.getLmLeaveBlockId();
715    
716          LeaveBlock blockToDelete = TkServiceLocator.getLeaveBlockService().getLeaveBlock(leaveBlockId);
717          if (blockToDelete != null && TkServiceLocator.getPermissionsService().canDeleteLeaveBlock(blockToDelete)) {
718                        TkServiceLocator.getLeaveBlockService().deleteLeaveBlock(leaveBlockId, TKContext.getPrincipalId());
719          
720                        generateTimesheetChangedNotification(principalId, targetPrincipalId, documentId);
721          }
722    
723          // if the leave block is NOT eligible for accrual, rerun accrual service for the leave calendar the leave block is on
724          EarnCode ec = TkServiceLocator.getEarnCodeService().getEarnCode(blockToDelete.getEarnCode(), blockToDelete.getLeaveDate());
725          if(ec != null && ec.getEligibleForAccrual().equals("N")) {
726              CalendarEntries ce = TkServiceLocator.getCalendarService()
727                                            .getCurrentCalendarDatesForLeaveCalendar(blockToDelete.getPrincipalId(), blockToDelete.getLeaveDate());
728              if(ce != null) {
729                      TkServiceLocator.getLeaveAccrualService().runAccrual(blockToDelete.getPrincipalId(), ce.getBeginPeriodDate(), ce.getEndPeriodDate(), false);
730              }
731          }
732                    
733          return mapping.findForward("basic");
734            }
735      
736            private void generateTimesheetChangedNotification(String principalId, String targetPrincipalId, String documentId) {
737                    if (!StringUtils.equals(principalId, targetPrincipalId)) {
738                            EntityNamePrincipalName person = KimApiServiceLocator.getIdentityService().getDefaultNamesForPrincipalId(principalId);
739                            if (person != null && person.getDefaultName() != null) {
740                                    String subject = "Timesheet Modification Notice";
741                                    StringBuilder message = new StringBuilder();
742                                    message.append("Your Timesheet was changed by ");
743                                    message.append(person.getDefaultName().getCompositeNameUnmasked());
744                                    message.append(" on your behalf.");
745                                    message.append(SystemUtils.LINE_SEPARATOR);
746                                    message.append(getTimesheetURL(documentId));
747                                    
748                                    TkServiceLocator.getKPMENotificationService().sendNotification(subject, message.toString(), targetPrincipalId);
749                            }
750                    }
751            }
752            
753            @SuppressWarnings("deprecation")
754            private String getTimesheetURL(String documentId) {
755                    Properties params = new Properties();
756                    params.put("documentId", documentId);
757                    return UrlFactory.parameterizeUrl(getApplicationBaseUrl() + "/TimeDetail.do", params);
758            }
759    
760    }