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 return forward; 180 } 181 182 public void assignShowDistributeButton(ClockActionForm caf) { 183 caf.setShowDistrubuteButton(false); 184 185 TimesheetDocument timesheetDocument = caf.getTimesheetDocument(); 186 if (timesheetDocument != null) { 187 int eligibleAssignmentCount = 0; 188 for (Assignment assignment : timesheetDocument.getAssignments()) { 189 String department = assignment.getJob() != null ? assignment.getJob().getDept() : null; 190 Long workArea = assignment.getWorkArea(); 191 String payType = assignment.getJob() != null ? assignment.getJob().getHrPayType() : null; 192 TimeCollectionRule rule = TkServiceLocator.getTimeCollectionRuleService().getTimeCollectionRule(department, workArea, payType, timesheetDocument.getDocEndDate()); 193 if (rule != null && rule.isHrsDistributionF()) { 194 eligibleAssignmentCount++; 195 } 196 197 // Only show the distribute button if there is more than one eligible assignment 198 if (eligibleAssignmentCount > 1) { 199 caf.setShowDistrubuteButton(true); 200 break; 201 } 202 } 203 } 204 } 205 206 207 public ActionForward clockAction(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { 208 ClockActionForm caf = (ClockActionForm) form; 209 210 // TODO: Validate that clock action is valid for this user 211 // TODO: this needs to be integrated with the error tag 212 if (StringUtils.isBlank(caf.getSelectedAssignment())) { 213 caf.setErrorMessage("No assignment selected."); 214 return mapping.findForward("basic"); 215 } 216 ClockLog previousClockLog = TkServiceLocator.getClockLogService().getLastClockLog(TKUser.getCurrentTargetPersonId()); 217 if(previousClockLog != null && StringUtils.equals(caf.getCurrentClockAction(), previousClockLog.getClockAction())){ 218 caf.setErrorMessage("The operation is already performed."); 219 return mapping.findForward("basic"); 220 } 221 String ip = TKUtils.getIPAddressFromRequest(request); 222 Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(caf.getTimesheetDocument(), caf.getSelectedAssignment()); 223 224 List<Assignment> lstAssingmentAsOfToday = TkServiceLocator.getAssignmentService().getAssignments(TKContext.getTargetPrincipalId(), TKUtils.getCurrentDate()); 225 boolean foundValidAssignment = false; 226 for(Assignment assign : lstAssingmentAsOfToday){ 227 if((assign.getJobNumber().compareTo(assignment.getJobNumber()) ==0) && 228 (assign.getWorkArea().compareTo(assignment.getWorkArea()) == 0) && 229 (assign.getTask().compareTo(assignment.getTask()) == 0)){ 230 foundValidAssignment = true; 231 break; 232 } 233 } 234 235 if(!foundValidAssignment){ 236 caf.setErrorMessage("Assignment is not effective as of today"); 237 return mapping.findForward("basic"); 238 } 239 240 241 ClockLog clockLog = TkServiceLocator.getClockLogService().processClockLog(new Timestamp(System.currentTimeMillis()), assignment, caf.getPayCalendarDates(), ip, 242 TKUtils.getCurrentDate(), caf.getTimesheetDocument(), caf.getCurrentClockAction(), TKUser.getCurrentTargetPersonId()); 243 244 caf.setClockLog(clockLog); 245 246 return mapping.findForward("basic"); 247 } 248 249 public ActionForward distributeTimeBlocks(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { 250 ClockActionForm caf = (ClockActionForm) form; 251 caf.findTimeBlocksToDistribute(); 252 return mapping.findForward("tb"); 253 } 254 255 256 public ActionForward editTimeBlock(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { 257 ClockActionForm caf = (ClockActionForm) form; 258 TimeBlock tb = caf.getCurrentTimeBlock(); 259 caf.setCurrentAssignmentKey(tb.getAssignmentKey()); 260 261 ActionForward forward = mapping.findForward("et"); 262 263 return new ActionForward(forward.getPath() + "?editTimeBlockId=" + tb.getTkTimeBlockId().toString()); 264 265 } 266 public ActionForward addTimeBlock(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { 267 ClockActionForm caf = (ClockActionForm) form; 268 TimeBlock currentTb = caf.getCurrentTimeBlock(); 269 List<TimeBlock> newTimeBlocks = caf.getTimesheetDocument().getTimeBlocks(); 270 List<TimeBlock> referenceTimeBlocks = new ArrayList<TimeBlock>(caf.getTimesheetDocument().getTimeBlocks().size()); 271 for (TimeBlock tb : caf.getTimesheetDocument().getTimeBlocks()) { 272 referenceTimeBlocks.add(tb.copy()); 273 } 274 //call persist method that only saves added/deleted/changed timeblocks 275 TkServiceLocator.getTimeBlockService().saveTimeBlocks(referenceTimeBlocks, newTimeBlocks, TKContext.getPrincipalId()); 276 277 ActionForward forward = mapping.findForward("et"); 278 279 return new ActionForward(forward.getPath() + "?editTimeBlockId=" + currentTb.getTkTimeBlockId().toString()); 280 } 281 282 public ActionForward saveNewTimeBlocks(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response){ 283 ClockActionForm caf = (ClockActionForm)form; 284 String tbId = caf.getTbId(); 285 String timesheetDocId = caf.getTsDocId(); 286 287 String[] assignments = caf.getNewAssignDesCol().split(SEPERATOR); 288 String[] beginDates = caf.getNewBDCol().split(SEPERATOR); 289 String[] beginTimes = caf.getNewBTCol().split(SEPERATOR); 290 String[] endDates = caf.getNewEDCol().split(SEPERATOR); 291 String[] endTimes = caf.getNewETCol().split(SEPERATOR); 292 String[] hrs = caf.getNewHrsCol().split(SEPERATOR); 293 String earnCode = TkServiceLocator.getTimeBlockService().getTimeBlock(tbId).getEarnCode(); 294 295 List<TimeBlock> newTbList = new ArrayList<TimeBlock>(); 296 for(int i = 0; i < hrs.length; i++) { 297 BigDecimal hours = new BigDecimal(hrs[i]); 298 Timestamp beginTS = TKUtils.convertDateStringToTimestamp(beginDates[i], beginTimes[i]); 299 Timestamp endTS = TKUtils.convertDateStringToTimestamp(endDates[i], endTimes[i]); 300 String assignString = assignments[i]; 301 Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(assignString); 302 303 TimesheetDocument tsDoc = TkServiceLocator.getTimesheetService().getTimesheetDocument(timesheetDocId); 304 305 TimeBlock tb = TkServiceLocator.getTimeBlockService().createTimeBlock(tsDoc, beginTS, endTS, assignment, earnCode, hours,BigDecimal.ZERO, false, false, TKContext.getPrincipalId()); 306 newTbList.add(tb); 307 } 308 TkServiceLocator.getTimeBlockService().resetTimeHourDetail(newTbList); 309 TkServiceLocator.getTimeBlockService().saveTimeBlocks(newTbList); 310 TimeBlock oldTB = TkServiceLocator.getTimeBlockService().getTimeBlock(tbId); 311 TkServiceLocator.getTimeBlockService().deleteTimeBlock(oldTB); 312 return mapping.findForward("basic"); 313 } 314 315 public ActionForward validateNewTimeBlock(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response){ 316 ClockActionForm caf = (ClockActionForm)form; 317 String tbId = caf.getTbId(); 318 String[] assignments = caf.getNewAssignDesCol().split(SEPERATOR); 319 String[] beginDates = caf.getNewBDCol().split(SEPERATOR); 320 String[] beginTimes = caf.getNewBTCol().split(SEPERATOR); 321 String[] endDates = caf.getNewEDCol().split(SEPERATOR); 322 String[] endTimes = caf.getNewETCol().split(SEPERATOR); 323 String[] hrs = caf.getNewHrsCol().split(SEPERATOR); 324 325 List<Interval> newIntervals = new ArrayList<Interval>(); 326 JSONArray errorMsgList = new JSONArray(); 327 328 // validates that all fields are available 329 if(assignments.length != beginDates.length || 330 assignments.length!= beginTimes.length || 331 assignments.length != endDates.length || 332 assignments.length != endTimes.length || 333 assignments.length != hrs.length) { 334 errorMsgList.add("All fields are required"); 335 caf.setOutputString(JSONValue.toJSONString(errorMsgList)); 336 return mapping.findForward("ws"); 337 } 338 339 for(int i = 0; i < hrs.length; i++) { 340 String index = String.valueOf(i+1); 341 342 // validate the hours field 343 BigDecimal dc = new BigDecimal(hrs[i]); 344 if (dc.compareTo(new BigDecimal("0")) == 0) { 345 errorMsgList.add("The entered hours for entry " + index + " is not valid."); 346 caf.setOutputString(JSONValue.toJSONString(errorMsgList)); 347 return mapping.findForward("ws"); 348 } 349 350 // check if the begin / end time are valid 351 // should not include time zone in consideration when conparing time intervals 352 Timestamp beginTS = TKUtils.convertDateStringToTimestampWithoutZone(beginDates[i], beginTimes[i]); 353 Timestamp endTS = TKUtils.convertDateStringToTimestampWithoutZone(endDates[i], endTimes[i]); 354 if ((beginTS.compareTo(endTS) > 0 || endTS.compareTo(beginTS) < 0)) { 355 errorMsgList.add("The time or date for entry " + index + " is not valid."); 356 caf.setOutputString(JSONValue.toJSONString(errorMsgList)); 357 return mapping.findForward("ws"); 358 } 359 360 // check if new time blocks overlap with existing time blocks 361 DateTime start = new DateTime(beginTS); 362 DateTime end = new DateTime(endTS); 363 Interval addedTimeblockInterval = new Interval(start, end); 364 newIntervals.add(addedTimeblockInterval); 365 for (TimeBlock timeBlock : caf.getTimesheetDocument().getTimeBlocks()) { 366 if(timeBlock.getTkTimeBlockId().equals(tbId)) { // ignore the original time block 367 continue; 368 } 369 if(timeBlock.getHours().compareTo(BigDecimal.ZERO) == 0) { // ignore time blocks with zero hours 370 continue; 371 } 372 Interval timeBlockInterval = new Interval(timeBlock.getBeginTimestamp().getTime(), timeBlock.getEndTimestamp().getTime()); 373 if (timeBlockInterval.overlaps(addedTimeblockInterval)) { 374 errorMsgList.add("The time block you are trying to add for entry " + index + " overlaps with an existing time block."); 375 caf.setOutputString(JSONValue.toJSONString(errorMsgList)); 376 return mapping.findForward("ws"); 377 } 378 } 379 } 380 // check if new time blocks overlap with each other 381 if(newIntervals.size() > 1 ) { 382 for(Interval intv1 : newIntervals) { 383 for(Interval intv2 : newIntervals) { 384 if(intv1.equals(intv2)) { 385 continue; 386 } 387 if (intv1.overlaps(intv2)) { 388 errorMsgList.add("There is time overlap between the entries."); 389 caf.setOutputString(JSONValue.toJSONString(errorMsgList)); 390 return mapping.findForward("ws"); 391 } 392 } 393 } 394 } 395 396 caf.setOutputString(JSONValue.toJSONString(errorMsgList)); 397 return mapping.findForward("ws"); 398 } 399 400 public ActionForward closeMissedPunchDoc(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response){ 401 return mapping.findForward("closeMissedPunchDoc"); 402 } 403 404 }