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.clock.web;
017    
018    import java.math.BigDecimal;
019    import java.sql.Timestamp;
020    import java.text.SimpleDateFormat;
021    import java.util.ArrayList;
022    import java.util.Date;
023    import java.util.HashMap;
024    import java.util.List;
025    import java.util.Map;
026    
027    import javax.servlet.http.HttpServletRequest;
028    import javax.servlet.http.HttpServletResponse;
029    
030    import org.apache.commons.lang.StringUtils;
031    import org.apache.log4j.Logger;
032    import org.apache.struts.action.ActionForm;
033    import org.apache.struts.action.ActionForward;
034    import org.apache.struts.action.ActionMapping;
035    import org.joda.time.DateTime;
036    import org.joda.time.DateTimeZone;
037    import org.joda.time.Interval;
038    import org.json.simple.JSONArray;
039    import org.json.simple.JSONValue;
040    import org.kuali.hr.time.assignment.Assignment;
041    import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
042    import org.kuali.hr.time.clocklog.ClockLog;
043    import org.kuali.hr.time.collection.rule.TimeCollectionRule;
044    import org.kuali.hr.time.roles.TkUserRoles;
045    import org.kuali.hr.time.roles.UserRoles;
046    import org.kuali.hr.time.service.base.TkServiceLocator;
047    import org.kuali.hr.time.timeblock.TimeBlock;
048    import org.kuali.hr.time.timesheet.TimesheetDocument;
049    import org.kuali.hr.time.timesheet.web.TimesheetAction;
050    import org.kuali.hr.time.util.TKContext;
051    import org.kuali.hr.time.util.TKUser;
052    import org.kuali.hr.time.util.TKUtils;
053    import org.kuali.hr.time.util.TkConstants;
054    import org.kuali.rice.krad.exception.AuthorizationException;
055    import org.kuali.rice.krad.util.GlobalVariables;
056    
057    public class ClockAction extends TimesheetAction {
058    
059        private static final Logger LOG = Logger.getLogger(ClockAction.class);
060        public static final SimpleDateFormat SDF = new SimpleDateFormat("EEE, MMMM d yyyy HH:mm:ss, zzzz");
061        public static final String SEPERATOR = "[****]+";
062    
063        @Override
064        protected void checkTKAuthorization(ActionForm form, String methodToCall) throws AuthorizationException {
065            super.checkTKAuthorization(form, methodToCall); // Checks for read access first.
066    
067            UserRoles roles = TkUserRoles.getUserRoles(GlobalVariables.getUserSession().getPrincipalId());
068            TimesheetDocument doc = TKContext.getCurrentTimesheetDocument();
069    
070            // Check for write access to Timeblock.
071            if (StringUtils.equals(methodToCall, "clockAction") ||
072                    StringUtils.equals(methodToCall, "addTimeBlock") ||
073                    StringUtils.equals(methodToCall, "editTimeBlock") ||
074                    StringUtils.equals(methodToCall, "distributeTimeBlocks") ||
075                    StringUtils.equals(methodToCall, "saveNewTimeBlocks") ||
076                    StringUtils.equals(methodToCall, "deleteTimeBlock")) {
077                if (!roles.isDocumentWritable(doc)) {
078                    throw new AuthorizationException(roles.getPrincipalId(), "ClockAction", "");
079                }
080            }
081        }
082    
083    
084        @Override
085        public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
086            ActionForward forward = super.execute(mapping, form, request, response);
087            ClockActionForm caf = (ClockActionForm) form;
088            caf.setCurrentServerTime(String.valueOf(new Date().getTime()));
089            caf.getUserSystemOffsetServerTime();
090            caf.setShowLunchButton(TkServiceLocator.getSystemLunchRuleService().isShowLunchButton());
091            caf.setAssignmentDescriptions(TkServiceLocator.getAssignmentService().getAssignmentDescriptions(caf.getTimesheetDocument(), true));
092            if (caf.isShowLunchButton()) {
093                // We don't need to worry about the assignments and lunch rules
094                // if the global lunch rule is turned off.
095    
096                // Check for presence of department lunch rule.
097                Map<String, Boolean> assignmentDeptLunchRuleMap = new HashMap<String, Boolean>();
098                if (caf.getTimesheetDocument() != null) {
099                    for (Assignment a : caf.getTimesheetDocument().getAssignments()) {
100                        String key = AssignmentDescriptionKey.getAssignmentKeyString(a);
101                        assignmentDeptLunchRuleMap.put(key, a.getDeptLunchRule() != null);
102                    }
103                }
104                caf.setAssignmentLunchMap(assignmentDeptLunchRuleMap);
105            }
106            String principalId = TKUser.getCurrentTargetPersonId();
107            if (principalId != null) {
108                caf.setPrincipalId(principalId);
109            }
110    
111            //if there is no timesheet
112            if(caf.getTimesheetDocument() == null) {
113                //don't bother printing this message if we already have an error
114                if (!GlobalVariables.getMessageMap().hasErrors()) {
115                    caf.setErrorMessage("You do not currently have a timesheet. Clock action is not allowed.");
116                }
117                return mapping.findForward("basic");
118            }
119            //if the timesheet document is enroute aor final, don't allow clock action
120            if(caf.getTimesheetDocument().getDocumentHeader().getDocumentStatus().equals(TkConstants.ROUTE_STATUS.ENROUTE)
121                    || caf.getTimesheetDocument().getDocumentHeader().getDocumentStatus().equals(TkConstants.ROUTE_STATUS.FINAL)) {
122                caf.setErrorMessage("Your current timesheet is already submitted for Approval. Clock action is not allowed on this timesheet.");
123                return mapping.findForward("basic");
124            }
125    
126    
127            this.assignShowDistributeButton(caf);
128            // if the time sheet document is final or enroute, do not allow missed punch
129            if(caf.getTimesheetDocument().getDocumentHeader().getDocumentStatus().equals(TkConstants.ROUTE_STATUS.ENROUTE)
130                            || caf.getTimesheetDocument().getDocumentHeader().getDocumentStatus().equals(TkConstants.ROUTE_STATUS.FINAL)) {
131                    caf.setShowMissedPunchButton(false);
132            } else {
133                    caf.setShowMissedPunchButton(true);
134            }
135    
136            String tbIdString = caf.getEditTimeBlockId();
137            if (tbIdString != null) {
138                caf.setCurrentTimeBlock(TkServiceLocator.getTimeBlockService().getTimeBlock(caf.getEditTimeBlockId()));
139            }
140    
141            ClockLog lastClockLog = TkServiceLocator.getClockLogService().getLastClockLog(principalId);
142            if (lastClockLog != null) {
143                Timestamp lastClockTimestamp = lastClockLog.getClockTimestamp();
144                String lastClockZone = lastClockLog.getClockTimestampTimezone();
145                if (StringUtils.isEmpty(lastClockZone)) {
146                    lastClockZone = TKUtils.getSystemTimeZone();
147                }
148                // zone will not be null. At this point is Valid or Exception.
149                // Exception would indicate bad data stored in the system. We can wrap this, but
150                // for now, the thrown exception is probably more valuable.
151                DateTimeZone zone = DateTimeZone.forID(lastClockZone);
152                DateTime clockWithZone = new DateTime(lastClockTimestamp, zone);
153                caf.setLastClockTimeWithZone(clockWithZone.toDate());
154                caf.setLastClockTimestamp(lastClockTimestamp);
155                caf.setLastClockAction(lastClockLog.getClockAction());
156            }
157    
158            if (lastClockLog == null || StringUtils.equals(lastClockLog.getClockAction(), TkConstants.CLOCK_OUT)) {
159                caf.setCurrentClockAction(TkConstants.CLOCK_IN);
160            } else {
161    
162                if (StringUtils.equals(lastClockLog.getClockAction(), TkConstants.LUNCH_OUT) && TkServiceLocator.getSystemLunchRuleService().isShowLunchButton()) {
163                    caf.setCurrentClockAction(TkConstants.LUNCH_IN);
164                }
165    //                      else if(StringUtils.equals(lastClockLog.getClockAction(),TkConstants.LUNCH_OUT)) {
166    //                              caf.setCurrentClockAction(TkConstants.LUNCH_IN);
167    //                      }
168                else {
169                    caf.setCurrentClockAction(TkConstants.CLOCK_OUT);
170                }
171                // if the current clock action is clock out, displays only the clocked-in assignment
172                String selectedAssignment = new AssignmentDescriptionKey(lastClockLog.getJobNumber(), lastClockLog.getWorkArea(), lastClockLog.getTask()).toAssignmentKeyString();
173                caf.setSelectedAssignment(selectedAssignment);
174                Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(caf.getTimesheetDocument(), selectedAssignment);
175                Map<String, String> assignmentDesc = TkServiceLocator.getAssignmentService().getAssignmentDescriptions(assignment);
176                caf.setAssignmentDescriptions(assignmentDesc);
177    
178            }
179            
180    
181            
182            if (StringUtils.equals(GlobalVariables.getUserSession().getPrincipalId(), TKUser.getCurrentTargetPersonId())) {
183                    caf.setClockButtonEnabled(true);
184            } else {
185                boolean isApproverOrReviewerForCurrentAssignment = false;
186                String selectedAssignment = StringUtils.EMPTY;
187                if (caf.getAssignmentDescriptions() != null) {
188                        if (caf.getAssignmentDescriptions().size() == 1) {
189                            for (String assignment : caf.getAssignmentDescriptions().keySet()) {
190                                    selectedAssignment = assignment;
191                            }
192                        } else {
193                            selectedAssignment = caf.getSelectedAssignment();
194                        }
195                }
196                if (StringUtils.isNotBlank(selectedAssignment)) {
197                    Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(new AssignmentDescriptionKey(selectedAssignment), TKUtils.getCurrentDate());
198                    if (assignment != null) {
199                        UserRoles roles = TkUserRoles.getUserRoles(GlobalVariables.getUserSession().getPrincipalId());
200                        Long workArea = assignment.getWorkArea();
201                        isApproverOrReviewerForCurrentAssignment = roles.getApproverWorkAreas().contains(workArea) || roles.getReviewerWorkAreas().contains(workArea);
202                    }
203                }
204                    caf.setClockButtonEnabled(isApproverOrReviewerForCurrentAssignment);
205            }
206            
207            return forward;
208        }
209        
210        public void assignShowDistributeButton(ClockActionForm caf) {
211            caf.setShowDistrubuteButton(false);
212            
213            TimesheetDocument timesheetDocument = caf.getTimesheetDocument();
214            if (timesheetDocument != null) {
215                    int eligibleAssignmentCount = 0;
216                    for (Assignment assignment : timesheetDocument.getAssignments()) {
217                            String department = assignment.getJob() != null ? assignment.getJob().getDept() : null;
218                            Long workArea = assignment.getWorkArea();
219                            String payType = assignment.getJob() != null ? assignment.getJob().getHrPayType() : null;
220                            TimeCollectionRule rule = TkServiceLocator.getTimeCollectionRuleService().getTimeCollectionRule(department, workArea, payType, timesheetDocument.getDocEndDate());
221                            if (rule != null && rule.isHrsDistributionF()) {
222                                    eligibleAssignmentCount++;
223                            }
224                            
225                            // Only show the distribute button if there is more than one eligible assignment
226                            if (eligibleAssignmentCount > 1) {
227                                    caf.setShowDistrubuteButton(true);
228                                    break;
229                            }
230                    }
231            }
232        }
233        
234    
235        public ActionForward clockAction(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
236            ClockActionForm caf = (ClockActionForm) form;
237    
238            // TODO: Validate that clock action is valid for this user
239            // TODO: this needs to be integrated with the error tag
240            if (StringUtils.isBlank(caf.getSelectedAssignment())) {
241                caf.setErrorMessage("No assignment selected.");
242                return mapping.findForward("basic");
243            }
244            ClockLog previousClockLog = TkServiceLocator.getClockLogService().getLastClockLog(TKUser.getCurrentTargetPersonId());
245            if(previousClockLog != null && StringUtils.equals(caf.getCurrentClockAction(), previousClockLog.getClockAction())){
246                    caf.setErrorMessage("The operation is already performed.");
247                return mapping.findForward("basic");
248            }
249            String ip = TKUtils.getIPAddressFromRequest(request);
250            Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(caf.getTimesheetDocument(), caf.getSelectedAssignment());
251            
252            List<Assignment> lstAssingmentAsOfToday = TkServiceLocator.getAssignmentService().getAssignments(TKContext.getTargetPrincipalId(), TKUtils.getCurrentDate());
253            boolean foundValidAssignment = false;
254            for(Assignment assign : lstAssingmentAsOfToday){
255                    if((assign.getJobNumber().compareTo(assignment.getJobNumber()) ==0) &&
256                            (assign.getWorkArea().compareTo(assignment.getWorkArea()) == 0) &&
257                            (assign.getTask().compareTo(assignment.getTask()) == 0)){
258                            foundValidAssignment = true;
259                            break;
260                    }
261            }
262            
263            if(!foundValidAssignment){
264                    caf.setErrorMessage("Assignment is not effective as of today");
265                    return mapping.findForward("basic");
266            }
267            
268                   
269            ClockLog clockLog = TkServiceLocator.getClockLogService().processClockLog(new Timestamp(System.currentTimeMillis()), assignment, caf.getPayCalendarDates(), ip,
270                    TKUtils.getCurrentDate(), caf.getTimesheetDocument(), caf.getCurrentClockAction(), true, TKUser.getCurrentTargetPersonId());
271    
272            caf.setClockLog(clockLog);
273    
274            return mapping.findForward("basic");
275        }
276    
277        public ActionForward distributeTimeBlocks(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
278            ClockActionForm caf = (ClockActionForm) form;
279            caf.findTimeBlocksToDistribute();
280            return mapping.findForward("tb");
281        }
282    
283    
284        public ActionForward editTimeBlock(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) {
285            ClockActionForm caf = (ClockActionForm) form;
286            TimeBlock tb = caf.getCurrentTimeBlock();
287            caf.setCurrentAssignmentKey(tb.getAssignmentKey());
288    
289            ActionForward forward = mapping.findForward("et");
290    
291            return new ActionForward(forward.getPath() + "?editTimeBlockId=" + tb.getTkTimeBlockId().toString());
292    
293        }
294        public ActionForward addTimeBlock(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) {
295            ClockActionForm caf = (ClockActionForm) form;
296            TimeBlock currentTb = caf.getCurrentTimeBlock();
297            List<TimeBlock> newTimeBlocks = caf.getTimesheetDocument().getTimeBlocks();
298            List<TimeBlock> referenceTimeBlocks = new ArrayList<TimeBlock>(caf.getTimesheetDocument().getTimeBlocks().size());
299            for (TimeBlock tb : caf.getTimesheetDocument().getTimeBlocks()) {
300                referenceTimeBlocks.add(tb.copy());
301            }
302            //call persist method that only saves added/deleted/changed timeblocks
303            TkServiceLocator.getTimeBlockService().saveTimeBlocks(referenceTimeBlocks, newTimeBlocks, TKContext.getPrincipalId());
304    
305            ActionForward forward = mapping.findForward("et");
306    
307            return new ActionForward(forward.getPath() + "?editTimeBlockId=" + currentTb.getTkTimeBlockId().toString());
308        }
309        
310        public ActionForward saveNewTimeBlocks(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response){
311                    ClockActionForm caf = (ClockActionForm)form;
312                    String tbId = caf.getTbId();
313                    String timesheetDocId = caf.getTsDocId();
314    
315                    String[] assignments = caf.getNewAssignDesCol().split(SEPERATOR);
316                    String[] beginDates = caf.getNewBDCol().split(SEPERATOR);
317                    String[] beginTimes = caf.getNewBTCol().split(SEPERATOR);
318                    String[] endDates = caf.getNewEDCol().split(SEPERATOR);
319                    String[] endTimes = caf.getNewETCol().split(SEPERATOR);
320                    String[] hrs = caf.getNewHrsCol().split(SEPERATOR);
321                    String earnCode = TkServiceLocator.getTimeBlockService().getTimeBlock(tbId).getEarnCode();
322    
323                    List<TimeBlock> newTbList = new ArrayList<TimeBlock>();
324                    for(int i = 0; i < hrs.length; i++) {
325                            BigDecimal hours = new BigDecimal(hrs[i]);
326                            Timestamp beginTS = TKUtils.convertDateStringToTimestamp(beginDates[i], beginTimes[i]);
327                            Timestamp endTS = TKUtils.convertDateStringToTimestamp(endDates[i], endTimes[i]);
328                            String assignString = assignments[i];
329                            Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(assignString);
330                            
331                            TimesheetDocument tsDoc = TkServiceLocator.getTimesheetService().getTimesheetDocument(timesheetDocId);
332                            
333                            TimeBlock tb = TkServiceLocator.getTimeBlockService().createTimeBlock(tsDoc, beginTS, endTS, assignment, earnCode, hours,BigDecimal.ZERO, false, false, TKContext.getPrincipalId());
334                            newTbList.add(tb);
335                    }
336                    TkServiceLocator.getTimeBlockService().resetTimeHourDetail(newTbList);
337                    TkServiceLocator.getTimeBlockService().saveTimeBlocks(newTbList);
338                    TimeBlock oldTB = TkServiceLocator.getTimeBlockService().getTimeBlock(tbId);
339                    TkServiceLocator.getTimeBlockService().deleteTimeBlock(oldTB);
340                    return mapping.findForward("basic");
341            }
342            
343            public ActionForward validateNewTimeBlock(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response){
344                    ClockActionForm caf = (ClockActionForm)form;
345                    String tbId = caf.getTbId();
346                    String[] assignments = caf.getNewAssignDesCol().split(SEPERATOR);
347                    String[] beginDates = caf.getNewBDCol().split(SEPERATOR);
348                    String[] beginTimes = caf.getNewBTCol().split(SEPERATOR);
349                    String[] endDates = caf.getNewEDCol().split(SEPERATOR);
350                    String[] endTimes = caf.getNewETCol().split(SEPERATOR);
351                    String[] hrs = caf.getNewHrsCol().split(SEPERATOR);
352    
353                    List<Interval> newIntervals = new ArrayList<Interval>();
354                    JSONArray errorMsgList = new JSONArray();
355    
356                    // validates that all fields are available
357                    if(assignments.length != beginDates.length ||
358                                    assignments.length!= beginTimes.length ||
359                                    assignments.length != endDates.length ||
360                                    assignments.length != endTimes.length ||
361                                    assignments.length != hrs.length) {
362                            errorMsgList.add("All fields are required");
363                        caf.setOutputString(JSONValue.toJSONString(errorMsgList));
364                        return mapping.findForward("ws");
365                    }
366    
367                    for(int i = 0; i < hrs.length; i++) {
368                            String index = String.valueOf(i+1);
369    
370                            // validate the hours field
371                            BigDecimal dc = new BigDecimal(hrs[i]);
372                        if (dc.compareTo(new BigDecimal("0")) == 0) {
373                            errorMsgList.add("The entered hours for entry " + index + " is not valid.");
374                            caf.setOutputString(JSONValue.toJSONString(errorMsgList));
375                            return mapping.findForward("ws");
376                        }
377    
378                        // check if the begin / end time are valid
379                        // should not include time zone in consideration when conparing time intervals
380                        Timestamp beginTS = TKUtils.convertDateStringToTimestampWithoutZone(beginDates[i], beginTimes[i]);
381                            Timestamp endTS = TKUtils.convertDateStringToTimestampWithoutZone(endDates[i], endTimes[i]);
382                        if ((beginTS.compareTo(endTS) > 0 || endTS.compareTo(beginTS) < 0)) {
383                            errorMsgList.add("The time or date for entry " + index + " is not valid.");
384                            caf.setOutputString(JSONValue.toJSONString(errorMsgList));
385                            return mapping.findForward("ws");
386                        }
387    
388                        // check if new time blocks overlap with existing time blocks
389                        DateTime start = new DateTime(beginTS);
390                        DateTime end = new DateTime(endTS);
391                        Interval addedTimeblockInterval = new Interval(start, end);
392                        newIntervals.add(addedTimeblockInterval);
393                        for (TimeBlock timeBlock : caf.getTimesheetDocument().getTimeBlocks()) {
394                            if(timeBlock.getTkTimeBlockId().equals(tbId)) { // ignore the original time block
395                                    continue;
396                            }
397                            if(timeBlock.getHours().compareTo(BigDecimal.ZERO) == 0) { // ignore time blocks with zero hours
398                                    continue;
399                            }
400                            
401                            DateTimeZone dateTimeZone = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
402                            DateTime timeBlockBeginTimestamp = new DateTime(timeBlock.getBeginTimestamp().getTime(), dateTimeZone).withZoneRetainFields(TKUtils.getSystemDateTimeZone());
403                            DateTime timeBlockEndTimestamp = new DateTime(timeBlock.getEndTimestamp().getTime(), dateTimeZone).withZoneRetainFields(TKUtils.getSystemDateTimeZone());
404                                Interval timeBlockInterval = new Interval(timeBlockBeginTimestamp, timeBlockEndTimestamp);
405                                if (timeBlockInterval.overlaps(addedTimeblockInterval)) {
406                                    errorMsgList.add("The time block you are trying to add for entry " + index + " overlaps with an existing time block.");
407                                    caf.setOutputString(JSONValue.toJSONString(errorMsgList));
408                                    return mapping.findForward("ws");
409                                }
410                        }
411                    }
412                    // check if new time blocks overlap with each other
413                    if(newIntervals.size() > 1 ) {
414                            for(Interval intv1 : newIntervals) {
415                                    for(Interval intv2 : newIntervals) {
416                                            if(intv1.equals(intv2)) {
417                                                    continue;
418                                            }
419                                            if (intv1.overlaps(intv2)) {
420                                                    errorMsgList.add("There is time overlap between the entries.");
421                                            caf.setOutputString(JSONValue.toJSONString(errorMsgList));
422                                            return mapping.findForward("ws");
423                                            }
424                                    }
425                            }
426                    }
427    
428                caf.setOutputString(JSONValue.toJSONString(errorMsgList));
429                    return mapping.findForward("ws");
430            }
431    
432        public ActionForward closeMissedPunchDoc(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response){
433            return mapping.findForward("closeMissedPunchDoc");
434        }
435        
436    }