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 }