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