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 }