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