TTi18n::gettext( 'In' ), 20 => TTi18n::gettext( 'Out' ), ]; break; case 'type': $retval = [ 10 => TTi18n::gettext( 'Normal' ), 20 => TTi18n::gettext( 'Lunch' ), 30 => TTi18n::gettext( 'Break' ), ]; break; case 'transfer': $retval = [ 0 => TTi18n::gettext( 'No' ), 1 => TTi18n::gettext( 'Yes' ), ]; break; case 'columns': $retval = [ '-1000-first_name' => TTi18n::gettext( 'First Name' ), '-1002-last_name' => TTi18n::gettext( 'Last Name' ), //'-1005-user_status' => TTi18n::gettext('Employee Status'), '-1010-title' => TTi18n::gettext( 'Title' ), '-1039-group' => TTi18n::gettext( 'Group' ), '-1040-default_branch' => TTi18n::gettext( 'Default Branch' ), '-1050-default_department' => TTi18n::gettext( 'Default Department' ), '-1160-branch' => TTi18n::gettext( 'Branch' ), '-1170-department' => TTi18n::gettext( 'Department' ), '-1200-type' => TTi18n::gettext( 'Type' ), '-1202-status' => TTi18n::gettext( 'Status' ), '-1210-date_stamp' => TTi18n::gettext( 'Date' ), '-1220-time_stamp' => TTi18n::gettext( 'Time' ), '-1224-actual_time_stamp' => TTi18n::gettext( 'Time (Actual)' ), '-1225-actual_time_diff' => TTi18n::gettext( 'Actual Time Difference' ), '-1230-tainted' => TTi18n::gettext( 'Tainted' ), '-1310-station_station_id' => TTi18n::gettext( 'Station ID' ), '-1320-station_type' => TTi18n::gettext( 'Station Type' ), '-1330-station_source' => TTi18n::gettext( 'Station Source' ), '-1340-station_description' => TTi18n::gettext( 'Station Description' ), '-2000-created_by' => TTi18n::gettext( 'Created By' ), '-2010-created_date' => TTi18n::gettext( 'Created Date' ), '-2020-updated_by' => TTi18n::gettext( 'Updated By' ), '-2030-updated_date' => TTi18n::gettext( 'Updated Date' ), ]; $retval = $this->getCustomFieldsColumns( $retval, null ); if ( getTTProductEdition() >= TT_PRODUCT_CORPORATE ) { $retval = array_merge( [ '-1180-job' => TTi18n::gettext( 'Job' ), '-1190-job_item' => TTi18n::gettext( 'Task' ), '-1155-default_job' => TTi18n::gettext( 'Default Job' ), '-1156-default_job_item' => TTi18n::gettext( 'Default Task' ), '-1228-punch_tag' => TTi18n::gettext( 'Punch Tags' ), ], $retval ); ksort( $retval ); } break; case 'list_columns': $retval = Misc::arrayIntersectByKey( $this->getOptions( 'default_display_columns' ), Misc::trimSortPrefix( $this->getOptions( 'columns' ) ) ); break; case 'default_display_columns': //Columns that are displayed by default. $retval = [ 'first_name', 'last_name', 'type', 'status', 'date_stamp', 'time_stamp', ]; break; } return $retval; } /** * @param $data * @return array */ function _getVariableToFunctionMap( $data ) { $variable_function_map = [ 'id' => 'ID', 'user_id' => false, //This is coming from PunchControl factory. 'split_punch_control' => false, //Must come before transfer, so a punch control can be split to allow a transfer to occur. 'transfer' => 'Transfer', 'type_id' => 'Type', 'type' => false, 'status_id' => 'Status', 'status' => false, 'time_stamp' => 'TimeStamp', 'raw_time_stamp' => false, 'punch_date' => false, 'punch_time' => false, 'punch_control_id' => 'PunchControlID', 'actual_time_stamp' => 'ActualTimeStamp', 'actual_time_diff' => false, 'original_time_stamp' => 'OriginalTimeStamp', 'schedule_id' => 'ScheduleID', 'station_id' => 'Station', 'station_station_id' => false, 'station_type_id' => false, 'station_type' => false, 'station_source' => false, 'station_description' => false, 'longitude' => 'Longitude', 'latitude' => 'Latitude', 'position_accuracy' => 'PositionAccuracy', 'first_name' => false, 'last_name' => false, 'user_status_id' => false, 'user_status' => false, 'group_id' => false, 'group' => false, 'title_id' => false, 'title' => false, 'default_branch_id' => false, 'default_branch' => false, 'default_department_id' => false, 'default_department' => false, 'default_job_id' => false, 'default_job' => false, 'default_job_item_id' => false, 'default_job_item' => false, 'date_stamp' => false, 'user_date_id' => false, 'pay_period_id' => false, 'total_time' => false, //Used for Map, Distance tab. 'branch_id' => false, 'branch' => false, 'department_id' => false, 'department' => false, 'job_id' => false, 'job' => false, 'job_manual_id' => false, 'job_item_id' => false, 'job_item' => false, 'job_item_manual_id' => false, 'punch_tag_id' => false, 'punch_tag' => false, 'quantity' => false, 'bad_quantity' => false, 'meal_policy_id' => false, 'note' => false, 'tainted' => 'Tainted', 'has_image' => 'HasImage', 'deleted' => 'Deleted', ]; return $variable_function_map; } /** * @return bool */ function getPunchControlObject() { return $this->getGenericObject( 'PunchControlListFactory', $this->getPunchControlID(), 'punch_control_obj' ); } /** * @return bool */ function getScheduleObject() { return $this->getGenericObject( 'ScheduleListFactory', $this->getScheduleID(), 'schedule_obj' ); } /** * @return bool */ function getUserObject() { return $this->getGenericObject( 'UserListFactory', $this->getUser(), 'user_obj' ); } /** * @return bool */ function getStationObject() { return $this->getGenericObject( 'StationListFactory', $this->getStation(), 'station_obj' ); } /** * @param int $epoch EPOCH * @param bool $user_id * @param bool $ignore_future_punches * @return bool|null */ function getPreviousPunchObject( $epoch, $user_id = false, $ignore_future_punches = false ) { if ( $user_id == '' ) { $user_id = $this->getUser(); } if ( is_object( $this->previous_punch_obj ) ) { return $this->previous_punch_obj; } else { //Use getShiftData() to better detect the previous punch based on the shift time. //This should make our maximum shift time setting based on the shift start time rather then the last punch that happens to exist. //If no Normal In punch exists in the shift, use the first punch time to base the Maximum Shift Time on. $ppslf = new PayPeriodScheduleListFactory(); $ppslf->getByUserId( $user_id ); if ( $ppslf->getRecordCount() == 1 ) { $pps_obj = $ppslf->getCurrent(); $maximum_shift_time = $pps_obj->getMaximumShiftTime(); } else { $pps_obj = TTnew( 'PayPeriodScheduleFactory' ); /** @var PayPeriodScheduleFactory $pps_obj */ $maximum_shift_time = ( 3600 * 16 ); $pps_obj->setMaximumShiftTime( $maximum_shift_time ); } $shift_data = $pps_obj->getShiftData( null, $user_id, $epoch, 'nearest_shift', null, null, null, null, $ignore_future_punches ); unset( $pps_obj ); $last_punch_id = false; if ( isset( $shift_data ) && is_array( $shift_data ) ) { //If we check against the first punch, then split shifts like: 10AM -> 11AM, then 11PM -> 8AM (next day) won't match properly, // as the 8AM would need an almost 24hr maximum shift time, when the shift was only 1hr prior to last out punch. // Instead maybe check from the last punch minus the maximum shift time minus the total time of the shift, that way when the 8AM // punch out specified in the above case is being entered it would be 10hr maximum shift time, not a 22hr maximum shift time. // if ( isset($shift_data['punches']) AND $shift_data['punches'][0]['time_stamp'] >= ( $epoch - $maximum_shift_time ) ) { // $last_punch_id = $shift_data['punches'][( count($shift_data['punches']) - 1 )]['id']; // } if ( isset( $shift_data['punches'] ) && isset( $shift_data['last_punch_key'] ) && isset( $shift_data['punches'][$shift_data['last_punch_key']] ) && $shift_data['punches'][$shift_data['last_punch_key']]['time_stamp'] >= ( $epoch - ( $maximum_shift_time - $shift_data['total_time'] ) ) ) { $last_punch_id = $shift_data['punches'][$shift_data['last_punch_key']]['id']; } else { Debug::Text( ' Shift didnt start within maximum shift time...', __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::Text( ' No shift data...', __FILE__, __LINE__, __METHOD__, 10 ); } //Debug::Arr($shift_data, ' Shift Data: Last Punch ID: '. $last_punch_id, __FILE__, __LINE__, __METHOD__, 10); if ( TTUUID::isUUID( $last_punch_id ) && $last_punch_id != TTUUID::getZeroID() && $last_punch_id != TTUUID::getNotExistID() ) { $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getById( $last_punch_id ); if ( $plf->getRecordCount() > 0 ) { $previous_punch_obj = $plf->getCurrent(); return $previous_punch_obj; } } return false; } } /** * @return bool|mixed */ function getNextPunchControlID() { //This is normally the PREVIOUS punch, //so if it was IN (10), return its punch control ID //so the next OUT punch is a new punch_control_id. if ( $this->getStatus() == 10 ) { return $this->getPunchControlID(); } return false; } /** * @return bool|mixed */ function getUser() { return $this->getGenericDataValue( 'user_id' ); } /** * @param $value * @return bool */ function setUser( $value ) { return $this->setGenericDataValue( 'user_id', TTUUID::castUUID( $value ) );//Make sure this isn't an array. } /** * @return bool|mixed */ function findPunchControlID() { if ( $this->getPunchControlID() != false ) { $retval = $this->getPunchControlID(); } else { $pclf = TTnew( 'PunchControlListFactory' ); /** @var PunchControlListFactory $pclf */ Debug::Text( 'Checking for incomplete punch control... User: ' . $this->getUser() . ' TimeStamp: ' . $this->getTimeStamp() . ' Status: ' . $this->getStatus(), __FILE__, __LINE__, __METHOD__, 10 ); //Need to make sure the punch is rounded before we can get the proper punch_control_id. However // roundTimeStamp requires punch_control_id before it can round properly. $retval = $pclf->getInCompletePunchControlIdByUserIdAndEpoch( $this->getUser(), $this->getTimeStamp(), $this->getStatus() ); if ( $retval == false ) { Debug::Text( 'Couldnt find already existing PunchControlID, generating new one...', __FILE__, __LINE__, __METHOD__, 10 ); $retval = $pclf->getNextInsertId(); } } Debug::Text( 'Punch Control ID: ' . $retval, __FILE__, __LINE__, __METHOD__, 10 ); return $retval; } /** * @return bool|mixed */ function getPunchControlID() { return $this->getGenericDataValue( 'punch_control_id' ); } /** * @param $value * @return bool */ function setPunchControlID( $value ) { $value = trim( $value ); //Can't check to make sure the PunchControl row exists, as it may be inserted later. So just //make sure its an non-zero INT. /* if ( $this->Validator->isResultSetWithRows( 'punch_control', $pclf->getByID($id), TTi18n::gettext('Invalid Punch Control ID') ) ) { $this->setGenericDataValue( 'punch_control_id', $id ); return TRUE; } */ return $this->setGenericDataValue( 'punch_control_id', $value ); } /** * @return bool */ function getTransfer() { return $this->fromBool( $this->getGenericDataValue( 'transfer' ) ); } /** * @param $value * @param null $time_stamp * @return bool */ function setTransfer( $value, $time_stamp = null ) { //If a timestamp is passed, check for the previous punch, if one does NOT exist, transfer can not be enabled. if ( $value == true && $time_stamp != '' && $this->isNew() && $this->getEnableSplitPunchControl() == false ) { //If the punch isn't a new one, always accept the transfer flag so we don't mistakenly round punches that are transfer punches when an administrator edits them. $prev_punch_obj = $this->getPreviousPunchObject( $time_stamp, $this->getUser(), true ); //Make sure we check that the previous punch wasn't an out punch from the last shift. if ( !is_object( $prev_punch_obj ) || ( is_object( $prev_punch_obj ) && $prev_punch_obj->getStatus() == 20 ) ) { Debug::Text( 'Previous punch does not exist, or it was an OUT punch, transfer cannot be enabled. EPOCH: ' . $time_stamp, __FILE__, __LINE__, __METHOD__, 10 ); return false; } } return $this->setGenericDataValue( 'transfer', $this->toBool( $value ) ); } /** * @return int */ function getNextStatus() { if ( $this->getStatus() == 10 ) { return 20; } return 10; } /** * @return bool|int */ function getStatus() { return $this->getGenericDataValue( 'status_id' ); } /** * @param $value * @return bool */ function setStatus( $value ) { $value = (int)trim( $value ); Debug::text( ' Status: ' . $value, __FILE__, __LINE__, __METHOD__, 10 ); return $this->setGenericDataValue( 'status_id', $value ); } /** * @param int $epoch EPOCH * @return int */ function getNextType( $epoch = null ) { if ( $this->getStatus() == 10 ) { //In $next_type = 10; //Normal } else { //20 = Out $next_type = $this->getType(); } //$this object should always be the previous punch. if ( $epoch > 0 && TTUUID::isUUID( $this->getUser() ) && $this->getUser() != TTUUID::getZeroID() && $this->getUser() != TTUUID::getNotExistID() ) { Debug::Text( ' Previous Punch Type: ' . $this->getType() . ' Status: ' . $this->getStatus() . ' Epoch: ' . $epoch . ' User ID: ' . $this->getUser(), __FILE__, __LINE__, __METHOD__, 10 ); //Optimization to only call findScheduleID() once. if ( ( $next_type != 30 && $this->getType() != 30 ) || ( $next_type != 20 && $this->getType() != 20 ) ) { //Make sure we send $status_id=FALSE so when outside of the schedule start/end time we can still match to a schedule. // Otherwise findScheduleID() uses $this->getStatus() and since when calling getNextType() its often on the previous punch object, // the status will be 10=In when the user is attempting to Punch Out now. $this->setScheduleID( $this->findScheduleID( $epoch, null, false ) ); } //Check for break policy window. //With Time Window auto-detection, ideally we would filter on $this->getStatus() != 20, so we don't try to detect explicit OUT punches. //However with Punch Time auto-detection, ideally we would filter on $this->getStatus() != 10 so we don't try to detect explicity IN punches. //So because of the above, we can't filter based on status at all until we know what the break/lunch policy requires. //Do the filter in inBreakPolicyWindow()/inMealPolicyWindow(). //if ( $next_type != 30 AND ( $this->getStatus() != 20 AND $this->getType() != 30 ) ) { if ( $next_type != 30 && $this->getType() != 30 ) { if ( $this->inBreakPolicyWindow( $epoch, $this->getTimeStamp(), $this->getStatus() ) == true ) { Debug::Text( ' Setting Type to Break...', __FILE__, __LINE__, __METHOD__, 10 ); $next_type = 30; } } //Check for meal policy window. //if ( $next_type != 20 AND ( $this->getStatus() != 20 AND $this->getType() != 20 ) ) { if ( $next_type != 20 && $this->getType() != 20 ) { if ( $this->inMealPolicyWindow( $epoch, $this->getTimeStamp(), $this->getStatus() ) == true ) { Debug::Text( ' Setting Type to Lunch...', __FILE__, __LINE__, __METHOD__, 10 ); $next_type = 20; } } //To help with punch reminder notifications, try to be smarter about what might likely be a break/lunch punch when the user has already started their shift and is punching out. // For example if the user is punching out near the beginning or end of their shift, assume it might be a break? A break policy must be specified too. // If they are punching near the middle of their shift assume it might be a lunch? A meal policy must be specified too. // // Do this by trying to predict each punch in the day based on the policies, for example: // Start: 8AM // Break 1: 10:00AM // Break 1: 10:15AM // Lunch: 12PM // Lunch: 1PM // Break 2: 3:00PM // Break 2: 3:15PM // End: 5PM // Then try to figure out which punch we are closest too and use that as the next punch type. if ( $next_type == 10 && ( ( $this->getStatus() == 10 ) //Previous Punch is Normal/Lunch/Break In -- Required to handle cases where they maybe went for two breaks in a row. || ( $this->getStatus() == 20 && ( $this->getType() == 20 || $this->getType() == 30 ) ) //Previous Punch is Break/Lunch Out ) ) { //If no meal/break policies exist, it can't be any type other than a normal punch by default. $meal_policy_arr = $this->getMealPolicies(); $mplf = $meal_policy_arr['lf']; Debug::Text( 'Meal Policy Record Count: ' . $mplf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); $break_policy_arr = $this->getBreakPolicies(); $bplf = $break_policy_arr['lf']; Debug::Text( 'Break Policy Record Count: ' . $bplf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $mplf->getRecordCount() > 0 || $bplf->getRecordCount() > 0 ) { $time_stamps_to_match = []; //Start time should be the shift start time, not the previous punch start time. //Get shift data here. if ( is_object( $this->getPunchControlObject() ) ) { $this->getPunchControlObject()->setPunchObject( $this ); $shift_data = $this->getPunchControlObject()->getShiftData(); if ( is_array( $shift_data ) && isset( $shift_data['first_in'] ) ) { Debug::Text( ' Shift First In Punch: ' . TTDate::getDate( 'DATE+TIME', $shift_data['first_in']['time_stamp'] ), __FILE__, __LINE__, __METHOD__, 10 ); $start_epoch = $shift_data['first_in']['time_stamp']; } } if ( is_object( $this->getScheduleObject() ) ) { if ( !isset( $start_epoch ) ) { $start_epoch = $this->getScheduleObject()->getStartTime(); //Keep these here in case PunchControlObject can't be determined. } $time_stamps_to_match[] = [ 'time_stamp' => $this->getScheduleObject()->getStartTime(), 'type_id' => 10 ]; //10=Normal $time_stamps_to_match[] = [ 'time_stamp' => $this->getScheduleObject()->getEndTime(), 'type_id' => 10 ]; //10=Normal //These are needed to determine if we are outside the schedule time. $shift_start_time = $this->getScheduleObject()->getStartTime(); $shift_end_time = $this->getScheduleObject()->getEndTime(); } else { if ( !isset( $start_epoch ) ) { $start_epoch = $epoch; //Keep these here in case PunchControlObject can't be determined. } //No Schedule found, try to figure out common shifts that the employee has worked in the past. $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $start_shift_most_common_data = $plf->getMostCommonPunchDataByCompanyIdAndUserAndTypeAndStatusAndStartDateAndEndDate( $this->getUserObject()->getCompany(), $this->getUser(), 10, 10, TTDate::getBeginWeekEpoch( $epoch ), $epoch, true ); //Normal In if ( count( (array)$start_shift_most_common_data ) == 0 ) { //Extend the date range to find some value. Debug::Text( 'No punches to get common default from, extend range back one more week...', __FILE__, __LINE__, __METHOD__, 10 ); $start_shift_most_common_data = $plf->getMostCommonPunchDataByCompanyIdAndUserAndTypeAndStatusAndStartDateAndEndDate( $this->getUserObject()->getCompany(), $this->getUser(), 10, 10, TTDate::getBeginWeekEpoch( ( TTDate::getMiddleDayEpoch( $epoch ) - ( 86400 * 7 ) ) ), $epoch, true ); //Normal In } if ( isset( $start_shift_most_common_data['time_stamp'] ) ) { $shift_start_time = TTDate::getTimeLockedDate( TTDate::strtotime( $start_shift_most_common_data['time_stamp'] ), $epoch ); //These are needed to determine if we are outside the schedule time. $time_stamps_to_match[] = [ 'time_stamp' => $shift_start_time, 'type_id' => 10 ]; //10=Normal } else { $shift_start_time = $shift_data['first_in']['time_stamp']; $time_stamps_to_match[] = [ 'time_stamp' => $shift_start_time, 'type_id' => 10 ]; //10=Normal } $end_shift_most_common_data = $plf->getMostCommonPunchDataByCompanyIdAndUserAndTypeAndStatusAndStartDateAndEndDate( $this->getUserObject()->getCompany(), $this->getUser(), 10, 20, TTDate::getBeginWeekEpoch( $epoch ), $epoch, true ); //Normal Out if ( count( (array)$end_shift_most_common_data ) == 0 ) { //Extend the date range to find some value. Debug::Text( 'No punches to get common default from, extend range back one more week...', __FILE__, __LINE__, __METHOD__, 10 ); $end_shift_most_common_data = $plf->getMostCommonPunchDataByCompanyIdAndUserAndTypeAndStatusAndStartDateAndEndDate( $this->getUserObject()->getCompany(), $this->getUser(), 10, 20, TTDate::getBeginWeekEpoch( ( TTDate::getMiddleDayEpoch( $epoch ) - ( 86400 * 7 ) ) ), $epoch, true ); //Normal Out } if ( isset( $end_shift_most_common_data['time_stamp'] ) ) { $shift_end_time = TTDate::getTimeLockedDate( TTDate::strtotime( $end_shift_most_common_data['time_stamp'] ), $epoch ); //These are needed to determine if we are outside the schedule time. $time_stamps_to_match[] = [ 'time_stamp' => $shift_end_time, 'type_id' => 10 ]; //10=Normal } else if ( isset( $shift_data['first_in']['time_stamp'] ) ) { //No past out shifts at all, so just assume an 8hr + 1hr lunch = 9hr shift. // If we don't have an end of shift estimate, than a meal/break policy in the middle the day will always be the closet matches and throw off the next type detection. $shift_end_time = ( $shift_data['first_in']['time_stamp'] + ( 3600 * 9 ) ); //These are needed to determine if we are outside the schedule time. $time_stamps_to_match[] = [ 'time_stamp' => $shift_end_time, 'type_id' => 10 ]; //10=Normal } unset( $plf, $start_shift_most_common_data, $end_shift_most_common_data ); } //Find a schedule first and determine if we are inside the start/end time. // If Yes: // Lookup meal/break policies and determine which we are closest too. // If No: // No need to lookup meal/break policies, just assume Type=Normal if ( $epoch >= $shift_start_time && $epoch <= $shift_end_time ) { $has_include_multiple_break_policies = false; $min_threshold_for_non_break = false; //Check break policies that apply and see if it autodetects by Punch Time. if ( $bplf->getRecordCount() > 0 ) { //Do one pass first to see if we need to ever combine multiple breaks. foreach( $bplf as $bp_obj ) { if ( $bp_obj->getIncludeMultipleBreaks() == true ) { $has_include_multiple_break_policies = true; break; } } //Only consider breaks if multiple breaks are combined, or the user hasn't punched out for enough breaks yet. //Once a user has taken a break, remove it from the list for detection. if ( $has_include_multiple_break_policies == true || ( isset( $shift_data ) && isset( $shift_data['break']['total'] ) && $shift_data['break']['total'] < $bplf->getRecordCount() ) ) { $b = 1; foreach ( $bplf as $bp_obj ) { //How do we handle cases where the employee is never expected to punch in/out for break, but it should just be auto-deducted every shift. // There may be cases where the employee ends their shift early and it will detect that as a Break Out punch, when the employee should really never be punching for break. // This is where 20=ProActive and 25=Reactive detection types come into play. if ( $has_include_multiple_break_policies == true || ( isset( $shift_data ) && isset( $shift_data['break']['total'] ) && $shift_data['break']['total'] < $b ) ) { if ( $bp_obj->getAutoDetectType() == 20 && $bp_obj->getAmount() > 0 && $bp_obj->getMaximumPunchTime() > 0 ) { //20=Auto-Detect by Punch Time (ProActive) -- See inBreakPolicyWindow() for how other break policies are detected. $time_stamps_to_match[] = [ 'time_stamp' => ( $start_epoch + $bp_obj->getTriggerTime() ), 'type_id' => 30 ]; //30=Break -- Start $time_stamps_to_match[] = [ 'time_stamp' => ( $start_epoch + $bp_obj->getTriggerTime() + $bp_obj->getMaximumPunchTime() ), 'type_id' => 30 ]; //30=Break -- End if ( $has_include_multiple_break_policies == true ) { $min_threshold_for_non_break = ( $bp_obj->getMaximumPunchTime() * 2 ); //Double the maximum punch time for break. Mainly needed in case no meal policy exists. } Debug::Text( ' Break Window Time Stamp: Start: ' . TTDate::getDate( 'DATE+TIME', ( $start_epoch + $bp_obj->getTriggerTime() ) ) . ' End: ' . TTDate::getDate( 'DATE+TIME', ( $start_epoch + $bp_obj->getTriggerTime() + $bp_obj->getMaximumPunchTime() ) ) .' Min Threshold: '. $min_threshold_for_non_break, __FILE__, __LINE__, __METHOD__, 10 ); } } $b++; } } } unset( $bplf ); //If lunch has already been taken, don't bother trying to detect it again. if ( isset( $shift_data ) && isset( $shift_data['lunch']['total'] ) && $shift_data['lunch']['total'] == 0 ) { //Check meal policy that applies and see if it autodetects by Punch Time. if ( $mplf->getRecordCount() > 0 ) { foreach( $mplf as $mp_obj ) { /** @var MealPolicyFactory $mp_obj */ //How do we handle cases where the employee is never expected to punch in/out for lunch, but it should just be auto-deducted every shift. // There may be cases where the employee ends their shift early and it will detect that as a Lunch Out punch, when the employee should really never be punching for lunch. // This is where 20=ProActive and 25=Reactive detection types come into play. if ( $mp_obj->getAutoDetectType() == 20 && $mp_obj->getAmount() > 0 && $mp_obj->getMaximumPunchTime() > 0 ) { //20=Auto-Detect by Punch Time (ProActive) -- See inMealPolicyWindow() for how other meal policies are detected. $time_stamps_to_match[] = [ 'time_stamp' => ( $start_epoch + $mp_obj->getTriggerTime() ), 'type_id' => 20 ]; //20=Lunch -- Start $time_stamps_to_match[] = [ 'time_stamp' => ( $start_epoch + $mp_obj->getTriggerTime() + $mp_obj->getMaximumPunchTime() ), 'type_id' => 20 ]; //20=Lunch -- End if ( $has_include_multiple_break_policies == true ) { $min_threshold_for_non_break = ( $mp_obj->getMaximumPunchTime() / 2 ); //One half of the maximum punch time allowed for lunch. } Debug::Text( ' Lunch Window Time Stamp: Start: ' . TTDate::getDate( 'DATE+TIME', ( $start_epoch + $mp_obj->getTriggerTime() ) ) . ' End: ' . TTDate::getDate( 'DATE+TIME', ( $start_epoch + $mp_obj->getTriggerTime() + $mp_obj->getMaximumPunchTime() ) ) .' Min Threshold: '. $min_threshold_for_non_break, __FILE__, __LINE__, __METHOD__, 10 ); } } } unset( $mplf ); } $time_stamps_to_match = Sort::Multisort( $time_stamps_to_match, 'time_stamp', null, 'asc', null ); Debug::Arr( $time_stamps_to_match, 'TimeStamps to Match: Epoch: '. TTDate::getDate('DATE+TIME', $epoch ) .' Min Threshold: '. $min_threshold_for_non_break, __FILE__, __LINE__, __METHOD__, 10 ); $best_match_arr = false; $best_diff = false; foreach( $time_stamps_to_match as $key => $time_stamp_match_arr ) { $next_key = ($key + 1); $diff = abs( $epoch - $time_stamp_match_arr['time_stamp'] ); if ( isset( $time_stamps_to_match[$next_key] ) ) { //This is not the last timestamp. $next_time_stamp_match_arr = $time_stamps_to_match[$next_key]; if ( in_array( $time_stamp_match_arr['type_id'], [ 20, 30 ] ) && $time_stamp_match_arr['type_id'] == $next_time_stamp_match_arr ) { //Next timestamp matches same type as this one, so its likely the end of the break/shift if ( $epoch >= $time_stamp_match_arr['time_stamp'] && $epoch <= $next_time_stamp_match_arr['time_stamp'] ) { $diff = 0; //Within break/meal time. } } } if ( $min_threshold_for_non_break === false || $time_stamp_match_arr['type_id'] == 30 || ( $time_stamp_match_arr['type_id'] != 30 && $diff <= $min_threshold_for_non_break ) ) { if ( $best_diff === false || $diff < $best_diff ) { $best_diff = $diff; $best_match_arr = $time_stamp_match_arr; } } else { Debug::Text( ' Difference is outside min. threshold: Diff: '. $diff .' Threshold: ' . $min_threshold_for_non_break .' Type: '. $time_stamp_match_arr['type_id'], __FILE__, __LINE__, __METHOD__, 10 ); } } if ( is_array( $best_match_arr ) ) { Debug::Text( 'Best Match Data: TimeStamp: '. TTDate::getDate('DATE+TIME', $best_match_arr['time_stamp'] ) .' Type: '. $best_match_arr['type_id'], __FILE__, __LINE__, __METHOD__, 10 ); $next_type = $best_match_arr['type_id']; } } else { //Outside schedule start/end time, so assume its a normal punch. $next_type = 10; //10=Normal } } else { Debug::Text( 'No Meal/Break Policies, assuming Type: Normal...', __FILE__, __LINE__, __METHOD__, 10 ); } } } Debug::Text( ' Next Type: '. $next_type, __FILE__, __LINE__, __METHOD__, 10 ); return (int)$next_type; } /** * @return bool|string */ function getTypeCode() { if ( $this->getType() != 10 ) { $options = $this->getOptions( 'type' ); return substr( $options[$this->getType()], 0, 1 ); } return false; } /** * @return bool|int */ function getType() { return $this->getGenericDataValue( 'type_id' ); } /** * @param $value * @return bool */ function setType( $value ) { $value = (int)trim( $value ); return $this->setGenericDataValue( 'type_id', $value ); } /** * @return bool|mixed */ function getStation() { return $this->getGenericDataValue( 'station_id' ); } /** * @param $value * @return bool */ function setStation( $value ) { $value = TTUUID::castUUID( $value ); return $this->setGenericDataValue( 'station_id', $value ); } /** * @param int $epoch EPOCH * @return bool|int|mixed */ function roundTimeStamp( $epoch ) { if ( $epoch == '' ) { return false; } $original_epoch = $epoch; //$is_transfer = ( ( $this->getTransfer() == TRUE OR $this->getEnableAutoTransfer() ) ? TRUE : FALSE ); //This broke rounding on timeclocks for the first IN punch of the day when a Branch/Department/Job/Task was specified. $is_transfer = $this->getTransfer(); //Punch control is no longer used for rounding. //Check for rounding policies. $riplf = TTnew( 'RoundIntervalPolicyListFactory' ); /** @var RoundIntervalPolicyListFactory $riplf */ $type_id = $riplf->getPunchTypeFromPunchStatusAndType( $this->getStatus(), $this->getType(), $is_transfer ); Debug::text( ' Rounding Timestamp: ' . TTDate::getDate( 'DATE+TIME', $epoch ) . '(' . $epoch . ') Status ID: ' . $this->getStatus() . ' Type ID: ' . $this->getType() . ' Round Policy Type: ' . $type_id . ' Is Transfer: ' . (int)$is_transfer, __FILE__, __LINE__, __METHOD__, 10 ); $riplf->getByPolicyGroupUserIdAndTypeId( $this->getUser(), $type_id ); Debug::text( ' Round Interval Punch Type: ' . $type_id . ' User: ' . $this->getUser() . ' Total Records: ' . $riplf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $riplf->getRecordCount() > 0 ) { $is_rounded_timestamp = false; //Loop over each rounding policy testing the conditionals and rounding the punch if necessary. foreach ( $riplf as $round_policy_obj ) { Debug::text( ' Found Rounding Policy: ' . $round_policy_obj->getName() . '(' . $round_policy_obj->getId() . ') Punch Type: ' . $round_policy_obj->getPunchType() . ' Conditional Type: ' . $round_policy_obj->getConditionType(), __FILE__, __LINE__, __METHOD__, 10 ); //FIXME: It will only do proper total rounding if they edit the Lunch Out punch. //We need to account for cases when they edit just the Lunch In Punch. if ( $round_policy_obj->getPunchType() == 100 ) { Debug::text( 'Lunch Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); //On Lunch Punch In (back from lunch) do the total rounding. if ( $this->getStatus() == 10 && $this->getType() == 20 ) { Debug::text( 'bLunch Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); //If strict is set, round to scheduled lunch time? //Find Lunch Punch In. //Make sure we include both lunch and normal punches when searching for the previous punch, because with Punch Time detection meal policies //the previous punch will never be lunch, just normal, but with Time Window meal policies it will be lunch. This is critical for Lunch rounding //with Punch Time detection meal policies. //There was a bug where if Lunch Total rounding is enabled, and auto-detect punches by Punch Time is also enabled, //this won't round the Lunch In punch because the Lunch Out punch hasn't been designated until changePreviousPunchType() is called in PunchControlFactory::preSave(). //which doesn't happen until later. $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getPreviousPunchByUserIdAndStatusAndTypeAndEpoch( $this->getUser(), 20, [ 10, 20 ], $epoch ); if ( $plf->getRecordCount() == 1 ) { Debug::text( 'Found Lunch Punch Out: ' . TTDate::getDate( 'DATE+TIME', $plf->getCurrent()->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); $total_lunch_time = ( $epoch - $plf->getCurrent()->getTimeStamp() ); Debug::text( 'Total Lunch Time: ' . $total_lunch_time, __FILE__, __LINE__, __METHOD__, 10 ); //Test rounding condition, needs to happen after we attempt to get the schedule at least. if ( $round_policy_obj->isConditionTrue( $total_lunch_time, false ) == false ) { continue; } //Set the ScheduleID $has_schedule = $this->setScheduleID( $this->findScheduleID( $epoch ) ); //Combine all break policies together. $meal_policy_time = 0; if ( is_object( $this->getScheduleObject() ) && is_object( $this->getScheduleObject()->getSchedulePolicyObject() ) ) { $meal_policy_ids = $this->getScheduleObject()->getSchedulePolicyObject()->getMealPolicy(); if ( is_array( $meal_policy_ids ) ) { $meal_policy_data = []; foreach ( $meal_policy_ids as $meal_policy_id ) { $meal_policy_obj = $this->getScheduleObject()->getSchedulePolicyObject()->getMealPolicyObject( $meal_policy_id ); if ( is_object( $meal_policy_obj ) ) { $meal_policy_data[$meal_policy_obj->getTriggerTime()] = $meal_policy_obj->getAmount(); } } krsort( $meal_policy_data ); if ( is_array( $meal_policy_data ) ) { foreach ( $meal_policy_data as $meal_policy_trigger_time => $tmp_meal_policy_time ) { Debug::text( 'Checking Meal Policy Trigger Time: ' . $meal_policy_trigger_time . ' Schedule Time: ' . $this->getScheduleObject()->getTotalTime(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $this->getScheduleObject()->getTotalTime() >= $meal_policy_trigger_time ) { $meal_policy_time = $tmp_meal_policy_time; break; } } } } } unset( $meal_policy_id, $meal_policy_ids, $meal_policy_data, $meal_policy_trigger_time, $tmp_meal_policy_time ); Debug::text( 'Meal Policy Time: ' . $meal_policy_time, __FILE__, __LINE__, __METHOD__, 10 ); if ( $has_schedule == true && $round_policy_obj->getGrace() > 0 ) { Debug::text( ' Applying Grace Period: ', __FILE__, __LINE__, __METHOD__, 10 ); $total_lunch_time = TTDate::graceTime( $total_lunch_time, $round_policy_obj->getGrace(), $meal_policy_time ); Debug::text( 'After Grace: ' . $total_lunch_time, __FILE__, __LINE__, __METHOD__, 10 ); } if ( $round_policy_obj->getInterval() > 0 ) { Debug::Text( ' Rounding to interval: ' . $round_policy_obj->getInterval(), __FILE__, __LINE__, __METHOD__, 10 ); $total_lunch_time = TTDate::roundTime( $total_lunch_time, $round_policy_obj->getInterval(), $round_policy_obj->getRoundType(), $round_policy_obj->getGrace() ); Debug::text( 'After Rounding: ' . $total_lunch_time, __FILE__, __LINE__, __METHOD__, 10 ); } if ( $has_schedule == true && $round_policy_obj->getStrict() == true ) { Debug::Text( ' Snap Time: Round Type: ' . $round_policy_obj->getRoundType(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $round_policy_obj->getRoundType() == 10 ) { Debug::Text( ' Snap Time DOWN ', __FILE__, __LINE__, __METHOD__, 10 ); $total_lunch_time = TTDate::snapTime( $total_lunch_time, $meal_policy_time, 'DOWN' ); } else if ( $round_policy_obj->getRoundType() == 30 ) { Debug::Text( ' Snap Time UP', __FILE__, __LINE__, __METHOD__, 10 ); $total_lunch_time = TTDate::snapTime( $total_lunch_time, $meal_policy_time, 'UP' ); } else { Debug::Text( ' Not Snaping Time', __FILE__, __LINE__, __METHOD__, 10 ); } } $epoch = ( $plf->getCurrent()->getTimeStamp() + $total_lunch_time ); Debug::text( 'Epoch after total rounding is: ' . $epoch . ' - ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); $is_rounded_timestamp = true; } else { Debug::text( 'DID NOT Find Lunch Punch Out: ' . TTDate::getDate( 'DATE+TIME', $plf->getCurrent()->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::text( 'Skipping Lunch Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); } } else if ( $round_policy_obj->getPunchType() == 110 ) { //Break Total Debug::text( 'Break Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); //On break Punch In (back from break) do the total rounding. if ( $this->getStatus() == 10 && $this->getType() == 30 ) { Debug::text( 'bbreak Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); //If strict is set, round to scheduled break time? //Find break Punch In. //Make sure we include both break and normal punches when searching for the previous punch, because with Punch Time detection meal policies //the previous punch will never be break, just normal, but with Time Window meal policies it will be break. This is critical for break rounding //with Punch Time detection meal policies. $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getPreviousPunchByUserIdAndStatusAndTypeAndEpoch( $this->getUser(), 20, [ 10, 30 ], $epoch ); if ( $plf->getRecordCount() == 1 ) { Debug::text( 'Found break Punch Out: ' . TTDate::getDate( 'DATE+TIME', $plf->getCurrent()->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); $total_break_time = ( $epoch - $plf->getCurrent()->getTimeStamp() ); Debug::text( 'Total break Time: ' . $total_break_time, __FILE__, __LINE__, __METHOD__, 10 ); //Test rounding condition, needs to happen after we attempt to get the schedule at least. if ( $round_policy_obj->isConditionTrue( $total_break_time, false ) == false ) { continue; } //Set the ScheduleID $has_schedule = $this->setScheduleID( $this->findScheduleID( $epoch ) ); //Combine all break policies together. $break_policy_time = 0; if ( is_object( $this->getScheduleObject() ) && is_object( $this->getScheduleObject()->getSchedulePolicyObject() ) ) { $break_policy_ids = $this->getScheduleObject()->getSchedulePolicyObject()->getBreakPolicy(); if ( is_array( $break_policy_ids ) ) { foreach ( $break_policy_ids as $break_policy_id ) { if ( is_object( $this->getScheduleObject()->getSchedulePolicyObject()->getBreakPolicyObject( $break_policy_id ) ) ) { $break_policy_time += $this->getScheduleObject()->getSchedulePolicyObject()->getBreakPolicyObject( $break_policy_id )->getAmount(); } } } } unset( $break_policy_id, $break_policy_ids ); Debug::text( 'Break Policy Time: ' . $break_policy_time, __FILE__, __LINE__, __METHOD__, 10 ); if ( $has_schedule == true && $round_policy_obj->getGrace() > 0 ) { Debug::text( ' Applying Grace Period: ', __FILE__, __LINE__, __METHOD__, 10 ); $total_break_time = TTDate::graceTime( $total_break_time, $round_policy_obj->getGrace(), $break_policy_time ); Debug::text( 'After Grace: ' . $total_break_time, __FILE__, __LINE__, __METHOD__, 10 ); } if ( $round_policy_obj->getInterval() > 0 ) { Debug::Text( ' Rounding to interval: ' . $round_policy_obj->getInterval(), __FILE__, __LINE__, __METHOD__, 10 ); $total_break_time = TTDate::roundTime( $total_break_time, $round_policy_obj->getInterval(), $round_policy_obj->getRoundType(), $round_policy_obj->getGrace() ); Debug::text( 'After Rounding: ' . $total_break_time, __FILE__, __LINE__, __METHOD__, 10 ); } if ( $has_schedule == true && $round_policy_obj->getStrict() == true ) { Debug::Text( ' Snap Time: Round Type: ' . $round_policy_obj->getRoundType(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $round_policy_obj->getRoundType() == 10 ) { Debug::Text( ' Snap Time DOWN ', __FILE__, __LINE__, __METHOD__, 10 ); $total_break_time = TTDate::snapTime( $total_break_time, $break_policy_time, 'DOWN' ); } else if ( $round_policy_obj->getRoundType() == 30 ) { Debug::Text( ' Snap Time UP', __FILE__, __LINE__, __METHOD__, 10 ); $total_break_time = TTDate::snapTime( $total_break_time, $break_policy_time, 'UP' ); } else { Debug::Text( ' Not Snaping Time', __FILE__, __LINE__, __METHOD__, 10 ); } } $epoch = ( $plf->getCurrent()->getTimeStamp() + $total_break_time ); Debug::text( 'Epoch after total rounding is: ' . $epoch . ' - ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); $is_rounded_timestamp = true; } else { Debug::text( 'DID NOT Find break Punch Out: ' . TTDate::getDate( 'DATE+TIME', $plf->getCurrent()->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::text( 'Skipping break Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); } } else if ( $round_policy_obj->getPunchType() == 120 ) { //Day Total Rounding Debug::text( 'Day Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); if ( $this->getStatus() == 20 && $this->getType() == 10 ) { //Out, Type Normal Debug::text( 'bDay Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); //If strict is set, round to scheduled time? $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getPreviousPunchByUserIdAndEpochAndNotPunchIDAndMaximumShiftTime( $this->getUser(), $epoch, $this->getId() ); if ( $plf->getRecordCount() == 1 ) { Debug::text( 'Found Previous Punch In: ' . TTDate::getDate( 'DATE+TIME', $plf->getCurrent()->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); //Get day total time prior to this punch control. $pclf = TTnew( 'PunchControlListFactory' ); /** @var PunchControlListFactory $pclf */ $pclf->getByUserIdAndDateStamp( $this->getUser(), $plf->getCurrent()->getPunchControlObject()->getDateStamp() ); if ( $pclf->getRecordCount() > 0 ) { $day_total_time = ( $epoch - $plf->getCurrent()->getTimeStamp() ); Debug::text( 'aDay Total Time: ' . $day_total_time . ' Current Punch Control ID: ' . $this->getPunchControlID(), __FILE__, __LINE__, __METHOD__, 10 ); foreach ( $pclf as $pc_obj ) { if ( $plf->getCurrent()->getPunchControlID() != $pc_obj->getID() ) { Debug::text( 'Punch Control Total Time: ' . $pc_obj->getTotalTime() . ' ID: ' . $pc_obj->getId(), __FILE__, __LINE__, __METHOD__, 10 ); $day_total_time += $pc_obj->getTotalTime(); } } //Take into account paid meal/breaks when doing day total rounding... // Since we don't know what time will be added/deducted when creating a new punch, but it has already happened when editing an existing punch, // the calculations should be based on the RAW time taken only. So we need to add back in any meal/break time taken but not auto-deducted/added. // **If the Meal/Break policy active after setting is greater than the first part of the shift before lunch, then when creating a new punch the rounding will not work, because the policy hasn't been calculated yet. // So if the rounding isn't being calculated correctly, they may need to set the active after setting lower, to ensure the meal policy is calculated before the last punch of the day. // See testRoundingDayTotal*() unit tests. $meal_and_break_adjustment = 0; $udtlf = TTnew( 'UserDateTotalListFactory' ); /** @var UserDateTotalListFactory $udtlf */ $udtlf->getByUserIdAndDateStampAndObjectType( $this->getUser(), $plf->getCurrent()->getPunchControlObject()->getDateStamp(), [ 100, 110 ] ); //Only consider time calculated from meal/break policies. Not time taken. if ( $udtlf->getRecordCount() > 0 ) { foreach ( $udtlf as $udt_obj ) { $meal_and_break_adjustment += $udt_obj->getTotalTime(); } Debug::text( 'Meal and Break Adjustment: ' . $meal_and_break_adjustment . ' Records: ' . $udtlf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); } $day_total_time += $meal_and_break_adjustment; $original_day_total_time = $day_total_time; Debug::text( 'cDay Total Time: ' . $day_total_time, __FILE__, __LINE__, __METHOD__, 10 ); if ( $day_total_time > 0 ) { //Need to handle split shifts properly, so just like we get all punches for the user_date_id, get all schedules too. $has_schedule = false; $schedule_day_total_time = 0; //Test rounding condition, needs to happen after we attempt to get the schedule at least. if ( $round_policy_obj->isConditionTrue( $day_total_time, false ) == false ) { continue; } $slf = TTnew( 'ScheduleListFactory' ); /** @var ScheduleListFactory $slf */ //$slf->getByUserDateId( $plf->getCurrent()->getPunchControlObject()->getUserDateID() ); $slf->getByUserIdAndDateStamp( $this->getUser(), $plf->getCurrent()->getPunchControlObject()->getDateStamp() ); if ( $slf->getRecordCount() > 0 ) { $has_schedule = true; foreach ( $slf as $s_obj ) { //Because auto-deduct meal/break policies are already accounted for in the total schedule time, they will be automatically //deducted once the punch is saved. So if we don't add them back in here they will be deducted twice. //The above happens when adding new punches, but editing existing punches need to account for any already deducted meal/break time. // This was causing problems with auto-deduct meal policies where the user punched for part of the meal time, and it auto-deducted the rest. // Since we don't know what time will be added/deducted when creating a new punch, but it has already happened when editing an existing punch, // the calculations should be based on the RAW time only. //$schedule_day_total_time += ( $s_obj->getTotalTime() + abs($s_obj->getMealPolicyDeductTime( $s_obj->calcRawTotalTime(), 10 )) + abs($s_obj->getBreakPolicyDeductTime( $s_obj->calcRawTotalTime(), 10 )) + $meal_and_break_adjustment ); $schedule_day_total_time += $s_obj->calcRawTotalTime(); } Debug::text( 'Before Grace: ' . $day_total_time . ' Schedule Day Total: ' . $schedule_day_total_time, __FILE__, __LINE__, __METHOD__, 10 ); $day_total_time = TTDate::graceTime( $day_total_time, $round_policy_obj->getGrace(), $schedule_day_total_time ); Debug::text( 'After Grace: ' . $day_total_time, __FILE__, __LINE__, __METHOD__, 10 ); } unset( $slf, $s_obj ); if ( $round_policy_obj->getInterval() > 0 ) { Debug::Text( ' Rounding to interval: ' . $round_policy_obj->getInterval(), __FILE__, __LINE__, __METHOD__, 10 ); $day_total_time = TTDate::roundTime( $day_total_time, $round_policy_obj->getInterval(), $round_policy_obj->getRoundType(), $round_policy_obj->getGrace() ); Debug::text( 'After Rounding: ' . $day_total_time, __FILE__, __LINE__, __METHOD__, 10 ); } if ( $has_schedule == true && $round_policy_obj->getStrict() == true && $schedule_day_total_time > 0 ) { Debug::Text( ' Snap Time: Round Type: ' . $round_policy_obj->getRoundType(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $round_policy_obj->getRoundType() == 10 ) { Debug::Text( ' Snap Time DOWN ', __FILE__, __LINE__, __METHOD__, 10 ); $day_total_time = TTDate::snapTime( $day_total_time, $schedule_day_total_time, 'DOWN' ); } else if ( $round_policy_obj->getRoundType() == 30 ) { Debug::Text( ' Snap Time UP', __FILE__, __LINE__, __METHOD__, 10 ); $day_total_time = TTDate::snapTime( $day_total_time, $schedule_day_total_time, 'UP' ); } else { Debug::Text( ' Not Snaping Time', __FILE__, __LINE__, __METHOD__, 10 ); } } Debug::text( 'cDay Total Time: ' . $day_total_time, __FILE__, __LINE__, __METHOD__, 10 ); $day_total_time_diff = ( $day_total_time - $original_day_total_time ); Debug::text( 'Day Total Diff: ' . $day_total_time_diff, __FILE__, __LINE__, __METHOD__, 10 ); $epoch = ( $original_epoch + $day_total_time_diff ); $is_rounded_timestamp = true; } } } else { Debug::text( 'DID NOT Find Normal Punch Out: ' . TTDate::getDate( 'DATE+TIME', $plf->getCurrent()->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::text( 'Skipping Lunch Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::text( 'NOT Total Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); $in_schedule_window = $this->inScheduleStartStopWindow( $epoch, $this->getStatus() ); //Test rounding condition, needs to happen after we attempt to get the schedule at least, which inScheduleStartStopWindow() does for us. // But this also needs to ahppen before we apply the grace period below, // otherwise the epoch will already be adjusted, and makes it hard to predict what the condition will be testing against. if ( $round_policy_obj->isConditionTrue( $epoch, $this->getScheduleWindowTime() ) == false ) { continue; } if ( $in_schedule_window == true && $round_policy_obj->getGrace() > 0 ) { Debug::text( ' Applying Grace Period: ', __FILE__, __LINE__, __METHOD__, 10 ); $epoch = TTDate::graceTime( $epoch, $round_policy_obj->getGrace(), $this->getScheduleWindowTime() ); Debug::text( 'After Grace: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); } unset( $in_schedule_window ); $grace_time = $round_policy_obj->getGrace(); //If strict scheduling is enabled, handle grace times differently. //Only apply them above if we are near the schedule start/stop time. //This allows for grace time to apply if an employee punches in late, //but afterwards not apply at all. if ( $round_policy_obj->getStrict() == true ) { $grace_time = 0; } if ( $round_policy_obj->getInterval() > 0 ) { Debug::Text( ' Rounding to interval: ' . $round_policy_obj->getInterval(), __FILE__, __LINE__, __METHOD__, 10 ); $epoch = TTDate::roundTime( $epoch, $round_policy_obj->getInterval(), $round_policy_obj->getRoundType(), $grace_time ); Debug::text( 'After Rounding: ' . TTDate::getDate( 'DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10 ); } //ONLY perform strict rounding on Normal punches, not break/lunch punches? //Modify the UI to restrict this as well perhaps? if ( $round_policy_obj->getStrict() == true && $this->getScheduleWindowTime() !== false ) { Debug::Text( ' Snap Time: Round Type: ' . $round_policy_obj->getRoundType(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $round_policy_obj->getRoundType() == 10 ) { Debug::Text( ' Snap Time DOWN ', __FILE__, __LINE__, __METHOD__, 10 ); $epoch = TTDate::snapTime( $epoch, $this->getScheduleWindowTime(), 'DOWN' ); } else if ( $round_policy_obj->getRoundType() == 30 ) { Debug::Text( ' Snap Time UP', __FILE__, __LINE__, __METHOD__, 10 ); $epoch = TTDate::snapTime( $epoch, $this->getScheduleWindowTime(), 'UP' ); } else { //If its an In Punch, snap up, if its out punch, snap down? Debug::Text( ' Average rounding type, automatically determining snap direction.', __FILE__, __LINE__, __METHOD__, 10 ); if ( $this->getStatus() == 10 ) { Debug::Text( ' Snap Time UP', __FILE__, __LINE__, __METHOD__, 10 ); $epoch = TTDate::snapTime( $epoch, $this->getScheduleWindowTime(), 'UP' ); } else { Debug::Text( ' Snap Time DOWN ', __FILE__, __LINE__, __METHOD__, 10 ); $epoch = TTDate::snapTime( $epoch, $this->getScheduleWindowTime(), 'DOWN' ); } } } $is_rounded_timestamp = true; } //In cases where employees transfer between jobs, then have rounding on just In or Out punches, //its possible for a punch in to be at 3:04PM and a later Out punch at 3:07PM to be rounded down to 3:00PM, //causing a conflict and the punch not to be saved at all. //In these cases don't round the punch. //Don't implement just yet... /* $plf = TTnew( 'PunchListFactory' ); $plf->getPreviousPunchByUserIdAndStatusAndTypeAndEpoch( $this->getUser(), 10, array(10, 20, 30), $original_epoch ); if ( $plf->getRecordCount() == 1 ) { if ( $epoch <= $plf->getCurrent()->getTimeStamp() ) { Debug::text(' Rounded TimeStamp is before previous punch, not rounding at all! Previous Punch: '. TTDate::getDate('DATE+TIME', $plf->getCurrent()->getTimeStamp() ) .' Rounded Time: '. TTDate::getDate('DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10); $epoch = $original_epoch; } } unset($plf, $p_obj); */ } //In cases where there may be one or more conditional rounding policies, if none of the conditions match, the punch still needs to be rounded down to the nearest minute, //otherwise its recorded to the nearest second when they may not be expecting that. // So when we know there are rounding policies, if none of them get applied, treat it as if there aren't any policies at all and still round to the minute. if ( $is_rounded_timestamp == false ) { Debug::text( ' Rounding Policy(s) found, but due to conditions none applied...', __FILE__, __LINE__, __METHOD__, 10 ); $epoch = TTDate::roundTime( $epoch, 60, 10 ); //Round down. } } else { Debug::text( ' NO Rounding Policy(s) Found', __FILE__, __LINE__, __METHOD__, 10 ); //Even when rounding policies don't exist, // Always round *down* to the nearest minute, no matter what. Even on a transfer punch. // We used to round to the nearest minute (average), however in v10.5.0 this was changed. // Instead this mimics what a wall clock would show, for example 8:00:59 shows as 8:00, so if we do average rounding it would record as 8:01 which they may not expect. // So if we always round down its still fair and consistent, for both IN and OUT punches, but it more closely matches a clock that they may be looking at, assuming its close to the server time at least. // // **To disable rounding completely and record punches to the exact second, they must create a rounding policy with a interval of 0 and no grace time. // // This is also done in setTimeStamp() when rounding policies are disabled. $epoch = TTDate::roundTime( $epoch, 60, 10 ); //Round down. } Debug::text( ' Rounded TimeStamp: ' . TTDate::getDate( 'DATE+TIME', $epoch ) . '(' . $epoch . ') Original TimeStamp: ' . TTDate::getDate( 'DATE+TIME', $original_epoch ), __FILE__, __LINE__, __METHOD__, 10 ); return $epoch; } /** * @param bool $raw * @return bool|int */ function getTimeStamp( $raw = false ) { $value = $this->getGenericDataValue( 'time_stamp' ); if ( $value !== false ) { if ( $raw === true ) { return $value; } else { //return $this->db->UnixTimeStamp( $this->data['start_date'] ); //strtotime is MUCH faster than UnixTimeStamp //Must use ADODB for times pre-1970 though. return TTDate::strtotime( $value ); } } return false; } /** * @param $value * @param bool $enable_rounding * @return bool */ function setTimeStamp( $value, $enable_rounding = true ) { $value = trim( $value ); //We can't disable rounding if its the first IN punch and no transfer actually needs to occur. //Have setTransfer check to see if there is a previous punch and if not, don't allow it to be set. if ( $enable_rounding == true ) { $value = $this->roundTimeStamp( $value ); } else { Debug::text( ' Rounding policies disabled... ', __FILE__, __LINE__, __METHOD__, 10 ); //When rounding is disabled, that just disables rounding policies. // Still always round *down* to the nearest minute, no matter what. Even on a transfer punch. // We used to round to the nearest minute (average), however in v10.5.0 this was changed. // Instead this mimics what a wall clock would show, for example 8:00:59 shows as 8:00, so if we do average rounding it would record as 8:01 which they may not expect. // So if we always round down its still fair and consistent, for both IN and OUT punches, but it more closely matches a clock that they may be looking at, assuming its close to the server time at least. // // **To disable rounding completely and record punches to the exact second, they must create a rounding policy with a interval of 0 and no grace time. // // This is also done in roundTimeStamp() when no rounding policies apply. $value = TTDate::roundTime( $value, 60, 10 ); //Round down. } Debug::text( ' Set: ' . $value, __FILE__, __LINE__, __METHOD__, 10 ); return $this->setGenericDataValue( 'time_stamp', $value ); } /** * @param bool $raw * @return bool|int */ function getOriginalTimeStamp( $raw = false ) { $value = $this->getGenericDataValue( 'original_time_stamp' ); if ( $value !== false ) { if ( $raw === true ) { return $value; } else { //return $this->db->UnixTimeStamp( $this->data['start_date'] ); //strtotime is MUCH faster than UnixTimeStamp //Must use ADODB for times pre-1970 though. return TTDate::strtotime( $value ); } } return false; } /** * @param $value * @return bool */ function setOriginalTimeStamp( $value ) { $value = ( !is_int( $value ) && $value !== null ) ? trim( $value ) : $value;//Dont trim integer values, as it changes them to strings. return $this->setGenericDataValue( 'original_time_stamp', $value ); } /** * @param bool $raw * @return bool|int */ function getActualTimeStamp( $raw = false ) { $value = $this->getGenericDataValue( 'actual_time_stamp' ); if ( $value !== false ) { if ( $raw === true ) { return $value; } else { //return $this->db->UnixTimeStamp( $this->data['start_date'] ); //strtotime is MUCH faster than UnixTimeStamp //Must use ADODB for times pre-1970 though. return TTDate::strtotime( $value ); } } return false; } /** * @param $value * @return bool */ function setActualTimeStamp( $value ) { $value = ( !is_int( $value ) && $value !== null ) ? trim( $value ) : $value;//Dont trim integer values, as it changes them to strings. return $this->setGenericDataValue( 'actual_time_stamp', $value ); } /** * @return bool|float */ function getLongitude() { return Misc::removeTrailingZeros( $this->getGenericDataValue( 'longitude' ) ); } /** * @param $value * @return bool */ function setLongitude( $value ) { if ( is_numeric( $value ) && $value != 0 ) { //Since we don't obtain coordinates via GEO coding, its just passed from the device, ignore 0 values as some versions of the mobile app pass in 0 values. $value = Misc::removeTrailingZeros( round( (float)$value, 6 ) ); //Always use 6 decimal places as that is to 0.11m accuracy, this also prevents audit logging 0 vs 0.000000000 -- Don't use parseFloat() here as it should never be a user input value with commas as decimal symbols. } else { $value = null; //Allow $value=NULL so the coordinates can be cleared. Also make sure if FALSE is passed in here we assume NULL so it doesn't get cast to integer and saved in DB. } return $this->setGenericDataValue( 'longitude', $value ); } /** * @return bool|float */ function getLatitude() { return Misc::removeTrailingZeros( $this->getGenericDataValue( 'latitude' ) ); } /** * @param $value * @return bool */ function setLatitude( $value ) { if ( is_numeric( $value ) && $value != 0 ) { //Since we don't obtain coordinates via GEO coding, its just passed from the device, ignore 0 values as some versions of the mobile app pass in 0 values. $value = Misc::removeTrailingZeros( round( (float)$value, 6 ) ); //Always use 6 decimal places as that is to 0.11m accuracy, this also prevents audit logging 0 vs 0.000000000 -- Don't use parseFloat() here as it should never be a user input value with commas as decimal symbols. } else { $value = null; //Allow $value=NULL so the coordinates can be cleared. Also make sure if FALSE is passed in here we assume NULL so it doesn't get cast to integer and saved in DB. } return $this->setGenericDataValue( 'latitude', $value ); } /** * @return bool|float */ function getPositionAccuracy() { return $this->getGenericDataValue( 'position_accuracy' ); } /** * @param $value * @return bool */ function setPositionAccuracy( $value ) { if ( is_numeric( $value ) ) { $value = round( (float)trim( $value ) ); //This in whole meters. } else { $value = null; //If no position accuracy is sent, leave NULL. } return $this->setGenericDataValue( 'position_accuracy', $value ); } /** * @return bool */ function getScheduleID() { return $this->getGenericTempDataValue( 'schedule_id' ); } /** * @param $value * @return bool */ function setScheduleID( $value ) { if ( $value != false ) { //Each time this is called, clear the ScheduleObject() cache. $this->schedule_obj = null; return $this->setGenericTempDataValue( 'schedule_id', $value ); } return false; } /** * @param int $epoch EPOCH * @param string $user_id UUID * @return bool */ function findScheduleID( $epoch = null, $user_id = null, $status_id = null ) { //Debug::text(' aFinding SchedulePolicyID for this Punch: '. $epoch .' User ID: '. $user_id, __FILE__, __LINE__, __METHOD__, 10); if ( $epoch == '' ) { $epoch = $this->getTimeStamp(); } if ( $epoch == false ) { return false; } if ( $user_id == '' && $this->getUser() == '' ) { Debug::text( ' User ID not specified, cant find schedule... ', __FILE__, __LINE__, __METHOD__, 10 ); return false; } else if ( $user_id == '' ) { $user_id = $this->getUser(); } //Debug::text(' bFinding SchedulePolicyID for this Punch: '. $epoch .' User ID: '. $user_id, __FILE__, __LINE__, __METHOD__, 10); if ( $status_id === null ) { $status_id = $this->getStatus(); } //Check to see if this punch is within the start/stop window for the schedule. //We need to make sure we get schedules within about a 24hr //window of this punch, because if punch is at 11:55AM and the schedule starts at 12:30AM it won't //be found by a user_date_id. //In cases where an absence shift ends at the exact same time as working shift begins (Absence: 11:30PM to 7:00AM, WORKING: 7:00AM-3:00PM), //order the working shift first so its used instead of the absence shift. //These two functions are almost identical: PunchFactory::findScheduleId() and ScheduleListFactory::getScheduleObjectByUserIdAndEpoch() $slf = TTnew( 'ScheduleListFactory' ); /** @var ScheduleListFactory $slf */ $slf->getByUserIdAndStartDateAndEndDate( $user_id, ( $epoch - 43200 ), ( $epoch + 43200 ), null, [ 'a.date_stamp' => 'asc', 'a.status_id' => 'asc', 'a.start_time' => 'desc' ] ); if ( $slf->getRecordCount() > 0 ) { $retval = false; $best_diff = false; //Check for schedule policy foreach ( $slf as $s_obj ) { Debug::text( ' Checking Schedule ID: ' . $s_obj->getID() . ' Start: ' . TTDate::getDate( 'DATE+TIME', $s_obj->getStartTime() ) . ' End: ' . TTDate::getDate( 'DATE+TIME', $s_obj->getEndTime() ), __FILE__, __LINE__, __METHOD__, 10 ); //If the Start/Stop window is large (ie: 6-8hrs) we need to find the closest schedule. $schedule_diff = $s_obj->inScheduleDifference( $epoch, $status_id ); if ( $schedule_diff === 0 ) { Debug::text( ' Within schedule times. ', __FILE__, __LINE__, __METHOD__, 10 ); return $s_obj->getId(); } else { if ( $schedule_diff > 0 && ( $best_diff === false || $schedule_diff < $best_diff ) ) { Debug::text( ' Within schedule start/stop time by: ' . $schedule_diff . ' Prev Best Diff: ' . $best_diff, __FILE__, __LINE__, __METHOD__, 10 ); $best_diff = $schedule_diff; $retval = $s_obj->getId(); } } } Debug::text( ' Final Schedule ID: ' . $retval, __FILE__, __LINE__, __METHOD__, 10 ); return $retval; } else { Debug::text( ' Did not find Schedule...', __FILE__, __LINE__, __METHOD__, 10 ); } return false; } /** * @return bool */ function getEnableCalcSystemTotalTime() { if ( isset( $this->calc_system_total_time ) ) { return $this->calc_system_total_time; } return false; } /** * @param $bool * @return bool */ function setEnableCalcSystemTotalTime( $bool ) { $this->calc_system_total_time = $bool; return true; } /** * @return bool */ function getEnableCalcWeeklySystemTotalTime() { if ( isset( $this->calc_weekly_system_total_time ) ) { return $this->calc_weekly_system_total_time; } return false; } /** * @param $bool * @return bool */ function setEnableCalcWeeklySystemTotalTime( $bool ) { $this->calc_weekly_system_total_time = $bool; return true; } /** * @return bool */ function getEnableCalcException() { if ( isset( $this->calc_exception ) ) { return $this->calc_exception; } return false; } /** * @param $bool * @return bool */ function setEnableCalcException( $bool ) { $this->calc_exception = $bool; return true; } /** * @return bool */ function getEnablePreMatureException() { if ( isset( $this->premature_exception ) ) { return $this->premature_exception; } return false; } /** * @param $bool * @return bool */ function setEnablePreMatureException( $bool ) { $this->premature_exception = $bool; return true; } /** * @return bool */ function getEnableCalcUserDateTotal() { if ( isset( $this->calc_user_date_total ) ) { return $this->calc_user_date_total; } return false; } /** * @param $bool * @return bool */ function setEnableCalcUserDateTotal( $bool ) { $this->calc_user_date_total = $bool; return true; } /** * @return bool */ function getEnableCalcUserDateID() { if ( isset( $this->calc_user_date_id ) ) { return $this->calc_user_date_id; } return false; } /** * @param $bool * @return bool */ function setEnableCalcUserDateID( $bool ) { $this->calc_user_date_id = $bool; return true; } /** * @return bool */ function getEnableCalcTotalTime() { if ( isset( $this->calc_total_time ) ) { return $this->calc_total_time; } return false; } /** * @param $bool * @return bool */ function setEnableCalcTotalTime( $bool ) { $this->calc_total_time = $bool; return true; } /** * @return bool */ function getEnableAutoTransfer() { if ( isset( $this->auto_transfer ) ) { return $this->auto_transfer; } return false; //Default to FALSE otherwise roundTimeStamp() will treat it as a transfer punch and only apply the transfer rounding policy type to it. //return TRUE; } /** * @param $bool * @return bool */ function setEnableAutoTransfer( $bool ) { $this->auto_transfer = $bool; return true; } /** * @return bool */ function getEnableSplitAtMidnight() { if ( isset( $this->split_at_midnight ) ) { return $this->split_at_midnight; } return true; } /** * @param $bool * @return bool */ function setEnableSplitAtMidnight( $bool ) { $this->split_at_midnight = $bool; return true; } /** * @return bool */ function getEnableSplitPunchControl() { if ( isset( $this->split_punch_control ) ) { return $this->split_punch_control; } return false; } /** * @param $bool * @return bool */ function setEnableSplitPunchControl( $bool ) { $this->split_punch_control = $bool; return true; } /** * @return bool */ function getScheduleWindowTime() { return $this->getGenericTempDataValue( 'schedule_window_time' ); } /** * @param int $epoch EPOCH * @param int $status_id * @return bool */ function inScheduleStartStopWindow( $epoch, $status_id ) { if ( $epoch == '' ) { return false; } $this->setScheduleID( $this->findScheduleID( $epoch ) ); if ( $this->getScheduleObject() == false ) { return false; } //If the Start/Stop window is excessively long (like 6-8hrs) with strict rounding and an user punches in AND out within that time, //we have to return the schedule time in accordance to the punch status (In/Out) to prevent rounding Out punches to the schedule start time if ( $status_id == 10 && $this->getScheduleObject()->inStartWindow( $epoch ) == true ) { //Consider In punches only. Debug::text( ' Within Start window... Schedule Policy ID: ' . $this->getScheduleObject()->getSchedulePolicyID(), __FILE__, __LINE__, __METHOD__, 10 ); $this->setGenericTempDataValue( 'schedule_window_time', $this->getScheduleObject()->getStartTime() ); return true; } else if ( $status_id == 20 && $this->getScheduleObject()->inStopWindow( $epoch ) == true ) { //Consider Out punches only. Debug::text( ' Within Start window... Schedule Policy ID: ' . $this->getScheduleObject()->getSchedulePolicyID(), __FILE__, __LINE__, __METHOD__, 10 ); $this->setGenericTempDataValue( 'schedule_window_time', $this->getScheduleObject()->getEndTime() ); return true; } else { Debug::text( ' NOT Within Start/Stop window.', __FILE__, __LINE__, __METHOD__, 10 ); } return false; } /** * Gets any break policies that may apply to this punch. * @return array * @throws ReflectionException */ function getMealPolicies() { if ( $this->meal_policy_arr !== null ) { return $this->meal_policy_arr; } else { $is_schedule = false; $mplf = TTnew( 'MealPolicyListFactory' ); /** @var MealPolicyListFactory $mplf */ if ( is_object( $this->getScheduleObject() ) && is_object( $this->getScheduleObject()->getSchedulePolicyObject() ) && $this->getScheduleObject()->getSchedulePolicyObject()->isUsePolicyGroupMealPolicy() == false ) { $policy_group_meal_policy_ids = $this->getScheduleObject()->getSchedulePolicyObject()->getMealPolicy(); $mplf->getByIdAndCompanyId( $policy_group_meal_policy_ids, $this->getUserObject()->getCompany() ); $is_schedule = true; } else { $mplf->getByPolicyGroupUserId( $this->getUser() ); } $this->meal_policy_arr = [ 'lf' => $mplf, 'is_schedule' => $is_schedule ]; return $this->meal_policy_arr; } } /** * Run this function on the previous punch object normally. * @param int $current_epoch EPOCH * @param int $previous_epoch EPOCH * @param null $previous_punch_status * @return bool */ function inMealPolicyWindow( $current_epoch, $previous_epoch, $previous_punch_status = null ) { Debug::Text( ' Checking if we are in meal policy window/punch time...', __FILE__, __LINE__, __METHOD__, 10 ); if ( $current_epoch == '' ) { return false; } if ( $previous_epoch == '' ) { return false; } $meal_policy_arr = $this->getMealPolicies(); $mplf = $meal_policy_arr['lf']; if ( $meal_policy_arr['is_schedule'] == true ) { $start_epoch = $this->getScheduleObject()->getStartTime(); //Keep these here in case PunchControlObject can't be determined. } else { $start_epoch = $previous_epoch; //Keep these here in case PunchControlObject can't be determined. } Debug::Text( 'Meal Policy Record Count: ' . $mplf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $mplf->getRecordCount() > 0 ) { $mp_obj = $mplf->getCurrent(); } unset( $mplf ); //Start time should be the shift start time, not the previous punch start time. //Get shift data here. if ( is_object( $this->getPunchControlObject() ) ) { $this->getPunchControlObject()->setPunchObject( $this ); $shift_data = $this->getPunchControlObject()->getShiftData(); if ( is_array( $shift_data ) && isset( $shift_data['first_in'] ) ) { Debug::Text( ' Shift First In Punch: ' . TTDate::getDate( 'DATE+TIME', $shift_data['first_in']['time_stamp'] ), __FILE__, __LINE__, __METHOD__, 10 ); $start_epoch = $shift_data['first_in']['time_stamp']; } } if ( isset( $mp_obj ) && is_object( $mp_obj ) ) { if ( $mp_obj->getAutoDetectType() == 10 ) { //Meal window Debug::Text( ' Auto Detect Type: Meal Window...', __FILE__, __LINE__, __METHOD__, 10 ); //Make we sure ignore meals if the previous punch status was OUT. if ( $previous_punch_status != 20 && $current_epoch >= ( $start_epoch + $mp_obj->getStartWindow() ) && $current_epoch <= ( $start_epoch + $mp_obj->getStartWindow() + $mp_obj->getWindowLength() ) ) { Debug::Text( ' aPunch is in meal policy window! Current Epoch: ' . TTDate::getDate( 'DATE+TIME', $current_epoch ), __FILE__, __LINE__, __METHOD__, 10 ); return true; } } else { //Punch time. Debug::Text( ' Auto Detect Type: Punch Time...', __FILE__, __LINE__, __METHOD__, 10 ); //Make we sure ignore meals if the previous punch status was IN. if ( $previous_punch_status != 10 && ( $current_epoch - $previous_epoch ) >= $mp_obj->getMinimumPunchTime() && ( $current_epoch - $previous_epoch ) <= $mp_obj->getMaximumPunchTime() ) { Debug::Text( ' bPunch is in meal policy window!', __FILE__, __LINE__, __METHOD__, 10 ); return true; } } } else { Debug::Text( ' Unable to find meal policy object...', __FILE__, __LINE__, __METHOD__, 10 ); } return false; } /** * Gets any break policies that may apply to this punch. * @return array * @throws ReflectionException */ function getBreakPolicies() { if ( $this->break_policy_arr !== null ) { return $this->break_policy_arr; } else { $is_schedule = false; $bplf = TTnew( 'BreakPolicyListFactory' ); /** @var BreakPolicyListFactory $bplf */ if ( is_object( $this->getScheduleObject() ) && is_object( $this->getScheduleObject()->getSchedulePolicyObject() ) && $this->getScheduleObject()->getSchedulePolicyObject()->isUsePolicyGroupBreakPolicy() == false ) { $policy_group_break_policy_ids = $this->getScheduleObject()->getSchedulePolicyObject()->getBreakPolicy(); $bplf->getByIdAndCompanyId( $policy_group_break_policy_ids, $this->getUserObject()->getCompany() ); $is_schedule = true; } else { $bplf->getByPolicyGroupUserId( $this->getUser() ); } $this->break_policy_arr = [ 'lf' => $bplf, 'is_schedule' => $is_schedule ]; return $this->break_policy_arr; } } /** * Run this function on the previous punch object normally. * @param int $current_epoch EPOCH * @param int $previous_epoch EPOCH * @param null $previous_punch_status * @return bool */ function inBreakPolicyWindow( $current_epoch, $previous_epoch, $previous_punch_status = null ) { Debug::Text( ' Checking if we are in break policy window/punch time... Current: ' . TTDate::getDate( 'DATE+TIME', $current_epoch ) . ' Previous: ' . TTDate::getDate( 'DATE+TIME', $previous_epoch ), __FILE__, __LINE__, __METHOD__, 10 ); if ( $current_epoch == '' ) { return false; } if ( $previous_epoch == '' ) { return false; } $break_policy_arr = $this->getBreakPolicies(); $bplf = $break_policy_arr['lf']; if ( $break_policy_arr['is_schedule'] == true ) { $start_epoch = $this->getScheduleObject()->getStartTime(); //Keep these here in case PunchControlObject can't be determined. } else { $start_epoch = $previous_epoch; //Keep these here in case PunchControlObject can't be determined. } $bp_objs = []; if ( $bplf->getRecordCount() > 0 ) { foreach ( $bplf as $bp_obj ) { $bp_objs[] = $bp_obj; } } unset( $bplf ); //Start time should be the shift start time, not the previous punch start time. //Get shift data here. if ( is_object( $this->getPunchControlObject() ) ) { $this->getPunchControlObject()->setPunchObject( $this ); $shift_data = $this->getPunchControlObject()->getShiftData(); if ( is_array( $shift_data ) && isset( $shift_data['first_in'] ) ) { Debug::Text( ' Shift First In Punch: ' . TTDate::getDate( 'DATE+TIME', $shift_data['first_in']['time_stamp'] ), __FILE__, __LINE__, __METHOD__, 10 ); $start_epoch = $shift_data['first_in']['time_stamp']; } } if ( empty( $bp_objs ) == false ) { foreach ( $bp_objs as $bp_obj ) { if ( $bp_obj->getAutoDetectType() == 10 ) { //Break window Debug::Text( ' Auto Detect Type: Break Window... Start Epoch: ' . TTDate::getDate( 'DATE+TIME', $start_epoch ), __FILE__, __LINE__, __METHOD__, 10 ); //Make we sure ignore breaks if the previous punch status was OUT. if ( $previous_punch_status != 20 && $current_epoch >= ( $start_epoch + $bp_obj->getStartWindow() ) && $current_epoch <= ( $start_epoch + $bp_obj->getStartWindow() + $bp_obj->getWindowLength() ) ) { Debug::Text( ' aPunch is in break policy (ID:' . $bp_obj->getId() . ') window!', __FILE__, __LINE__, __METHOD__, 10 ); return true; } } else { //Punch time. //Make we sure ignore breaks if the previous punch status was IN. Debug::Text( ' Auto Detect Type: Punch Time...', __FILE__, __LINE__, __METHOD__, 10 ); if ( $previous_punch_status != 10 && ( $current_epoch - $previous_epoch ) >= $bp_obj->getMinimumPunchTime() && ( $current_epoch - $previous_epoch ) <= $bp_obj->getMaximumPunchTime() ) { Debug::Text( ' bPunch is in break policy (ID:' . $bp_obj->getId() . ') window!', __FILE__, __LINE__, __METHOD__, 10 ); return true; } } } } else { Debug::Text( ' Unable to find break policy object...', __FILE__, __LINE__, __METHOD__, 10 ); } return false; } /** * @param $user_id UUID * @param $id UUID * @return bool * @throws ReflectionException */ function isAllowedBranch( $user_id, $id ) { $lf = TTNew( 'BranchListFactory' ); $lf->getById( $id ); if ( $lf->getRecordCount() > 0 ) { $retval = $lf->getCurrent()->isAllowedUser( $user_id ); } else { $retval = false; } if ( $retval == false ) { Debug::Text( ' User is not allowed on Branch: ' . $id, __FILE__, __LINE__, __METHOD__, 10 ); } return $retval; } /** * @param $user_id UUID * @param $branch_id UUID * @param $id UUID * @return bool * @throws ReflectionException */ function isAllowedDepartment( $user_id, $branch_id, $id ) { $lf = TTNew( 'DepartmentListFactory' ); $lf->getById( $id ); if ( $lf->getRecordCount() > 0 ) { $retval = $lf->getCurrent()->isAllowedUser( $user_id, $branch_id ); } else { $retval = false; } if ( $retval == false ) { Debug::Text( ' User is not allowed on Department: ' . $id, __FILE__, __LINE__, __METHOD__, 10 ); } return $retval; } /** * @param $user_id UUID * @param $branch_id UUID * @param $department_id UUID * @param $id UUID * @return bool * @throws ReflectionException */ function isAllowedJob( $user_id, $branch_id, $department_id, $id ) { $lf = TTNew( 'JobListFactory' ); $lf->getById( $id ); if ( $lf->getRecordCount() > 0 ) { $retval = $lf->getCurrent()->isAllowedUser( $user_id, $branch_id, $department_id ); } else { $retval = false; } if ( $retval == false ) { Debug::Text( ' User is not allowed on Job: ' . $id, __FILE__, __LINE__, __METHOD__, 10 ); } return $retval; } /** * @param $user_id UUID * @param $job_id UUID * @param $id UUID * @return bool * @throws ReflectionException */ function isAllowedJobItem( $user_id, $job_id, $id ) { $lf = TTNew( 'JobListFactory' ); $lf->getById( $job_id ); if ( $lf->getRecordCount() > 0 ) { $retval = $lf->getCurrent()->isAllowedItem( $id ); } else { $retval = false; } if ( $retval == false ) { Debug::Text( ' Task is not allowed on job: Job: '. $job_id .' Task: ' . $id, __FILE__, __LINE__, __METHOD__, 10 ); } return $retval; } /** * @param $user_id * @param $branch_id * @param $department_id * @param $job_id * @param $job_item_id * @param $id * @return array|false * @throws ReflectionException */ function isAllowedPunchTag( $user_id, $branch_id, $department_id, $job_id, $job_item_id, $id ) { $lf = TTNew( 'PunchTagListFactory' ); $lf->getById( $id ); if ( $lf->getRecordCount() > 0 ) { foreach( $lf as $punch_tag_obj ) { if ( $punch_tag_obj->isAllowedUser( $user_id, $branch_id, $department_id, $job_id, $job_item_id ) == true ) { $retval[] = $punch_tag_obj->getId(); } else { Debug::Text( ' Tag is not allowed on Branch/Department/Job/Task: Tag: ' . $punch_tag_obj->getId(), __FILE__, __LINE__, __METHOD__, 10 ); } } } else { $retval = false; } return $retval; } /** * @param object $user_obj * @param int $epoch EPOCH * @param object $station_obj * @param object $permission_obj * @param float $latitude * @param float $longitude * @param float $position_accuracy * @return array */ function getDefaultPunchSettings( $user_obj, $epoch, $station_obj = null, $permission_obj = null, $latitude = null, $longitude = null, $position_accuracy = null ) { $branch_id = $department_id = $job_id = $job_item_id = TTUUID::getZeroID(); $punch_tag_id = []; $transfer = false; $is_previous_punch = false; //Ignore future punches, so auto-punch shifts in the future don't mess up default punch settings. $prev_punch_obj = $this->getPreviousPunchObject( $epoch, $user_obj->getId(), true ); /** @var PunchFactory $prev_punch_obj */ if ( is_object( $prev_punch_obj ) ) { $is_previous_punch = true; $prev_punch_obj->setUser( $user_obj->getId() ); Debug::Text( ' Found Previous Punch within Continuous Time from now: ' . TTDate::getDate( 'DATE+TIME', $prev_punch_obj->getTimeStamp() ) . ' ID: ' . $prev_punch_obj->getID(), __FILE__, __LINE__, __METHOD__, 10 ); //Due to split shifts or multiple schedules on a single day that are close to one another, we have to be smarter about how we default punch settings. //We only base default punch settings on the previous punch if it was *NOT* a Normal Out punch, with the idea that the employee //would likely want to continue working on the same job after they come back from lunch/break, or if they haven't punched out for the end of this shift yet. //if ( ( is_object( $prev_punch_obj ) AND ( ( $prev_punch_obj->getStatus() == 10 AND $prev_punch_obj->getType() == 10 ) OR ( $prev_punch_obj->getStatus() == 20 AND $prev_punch_obj->getType() > 10 ) ) ) ) { if ( is_object( $prev_punch_obj ) && !( $prev_punch_obj->getStatus() == 20 && $prev_punch_obj->getType() == 10 ) ) { $branch_id = $prev_punch_obj->getPunchControlObject()->getBranch(); $department_id = $prev_punch_obj->getPunchControlObject()->getDepartment(); $job_id = $prev_punch_obj->getPunchControlObject()->getJob(); $job_item_id = $prev_punch_obj->getPunchControlObject()->getJobItem(); $punch_tag_id = $prev_punch_obj->getPunchControlObject()->getPunchTag(); } else { Debug::Text( ' Not using Previous Punch settings... Prev Status: ' . $prev_punch_obj->getStatus() . ' Type: ' . $prev_punch_obj->getType(), __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::Text( ' DID NOT Find Previous Punch within Continuous Time from now: ', __FILE__, __LINE__, __METHOD__, 10 ); } if ( $branch_id == '' || empty( $branch_id ) || $branch_id == TTUUID::getZeroID() || $department_id == '' || empty( $department_id ) || $department_id == TTUUID::getZeroID() || $job_id == '' || empty( $job_id ) || $job_id == TTUUID::getZeroID() || $job_item_id == '' || empty( $job_item_id ) || $job_item_id == TTUUID::getZeroID() || $punch_tag_id == '' || empty( $punch_tag_id ) || $punch_tag_id == TTUUID::getZeroID() ) { Debug::Text( ' NULL values: Branch: ' . $branch_id . ' Department: ' . $department_id . ' Job: ' . $job_id . ' Task: ' . $job_item_id, __FILE__, __LINE__, __METHOD__, 10 ); $slf = TTnew( 'ScheduleListFactory' ); /** @var ScheduleListFactory $slf */ $s_obj = $slf->getScheduleObjectByUserIdAndEpoch( $user_obj->getId(), $epoch ); //Try to handle cases where the employee forgets to punch in at 8AM then they punch at 5PM (as that should be the OUT punch) and it would become a 5PM IN punch instead. // Then the next morning for their 8AM punch it thinks they are continuing their shift from yesterday, and the issue persists across multiple days. // // However we also need to handle cases where a schedule like this exists: // Absent: 1a - 8a Working: 8a - 5pm and the employee punches at 7:45AM, it should be a IN punch assigned to the working schedule, not assigned to the absent shift. // This would exist if they had recurring schedules and swapped an employee from day shift to night shift (where they don't overlap) // and have to make one of them an absent (with no absence policy) to get it off the schedule. // There is also the on-call case of: // Working: 8a - 5p, Absent (oncall): 5p - 8a, and the employee getting called in around 11p or around 6a? // We want to default branch/department/job/task to that of the oncall schedule not the working schedule if at all possible. if ( is_object( $s_obj ) ) { //Determine if we are closer to the schedule start time, or end time. if ( abs( $epoch - $s_obj->getEndTime() ) < abs( $epoch - $s_obj->getStartTime() ) && abs( $epoch - $s_obj->getEndTime() ) <= $s_obj->getStartStopWindow() ) { //Closer to end time. Debug::Text( ' Current punch is closer to schedule end time and within the schedule start/stop window. Schedule: Start: ' . TTDate::getDate( 'DATE+TIME', $s_obj->getStartTime() ) .' End: ' . TTDate::getDate( 'DATE+TIME', $s_obj->getEndTime() ) .' Window: '. $s_obj->getStartStopWindow(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $is_previous_punch == false ) { Debug::Text( ' NOTICE: No previous punch exists, defaulting to OUT.', __FILE__, __LINE__, __METHOD__, 10 ); $next_status_id = 20; //Out } elseif ( isset( $prev_punch_obj ) && is_object( $prev_punch_obj ) && $prev_punch_obj->getTimeStamp() < ( $s_obj->getStartTime() - $s_obj->getStartStopWindow() ) && TTDate::getDayOfYear( $prev_punch_obj->getTimeStamp() ) != TTDate::getDayOfYear( $epoch ) ) { Debug::Text( ' NOTICE: Previous punch exists before the schedule start time - window, assuming missed IN punch and defaulting to Out.', __FILE__, __LINE__, __METHOD__, 10 ); $next_status_id = 20; //Out $prev_punch_obj = false; //Clear previous punch object so none of its data carries over to the current punch. } } else { //Closer to start time. Debug::Text( ' Current punch is closer to schedule start time and within the schedule start/stop window. Schedule: Start: ' . TTDate::getDate( 'DATE+TIME', $s_obj->getStartTime() ) .' End: ' . TTDate::getDate( 'DATE+TIME', $s_obj->getEndTime() ) .' Window: '. $s_obj->getStartStopWindow(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $is_previous_punch == false ) { Debug::Text( ' NOTICE: No previous punch exists, defaulting to IN.', __FILE__, __LINE__, __METHOD__, 10 ); $next_status_id = 10; //In } elseif ( isset( $prev_punch_obj ) && is_object( $prev_punch_obj ) && $prev_punch_obj->getTimeStamp() < ( $s_obj->getStartTime() - $s_obj->getStartStopWindow() ) && TTDate::getDayOfYear( $prev_punch_obj->getTimeStamp() ) != TTDate::getDayOfYear( $epoch ) ) { Debug::Text( ' NOTICE: Previous punch exists before the schedule start time - window, assuming missed OUT punch and defaulting to IN.', __FILE__, __LINE__, __METHOD__, 10 ); $next_status_id = 10; //In $prev_punch_obj = false; //Clear previous punch object so none of its data carries over to the current punch. } } } //Get all GEO fences that this coordinates fall within. if ( getTTProductEdition() >= TT_PRODUCT_CORPORATE ) { $gflf = TTnew( 'GEOFenceListFactory' ); /** @var GEOFenceListFactory $gflf */ $geo_fence_default_object_ids = $gflf->getBranchAndDepartmentAndJobAndJobItemAndPunchTagArrayByCompanyIdAndGEOLocation( $user_obj->getCompany(), $latitude, $longitude, $position_accuracy ); } if ( $branch_id == '' || empty( $branch_id ) || $branch_id == TTUUID::getZeroID() ) { if ( is_object( $station_obj ) && $station_obj->getDefaultBranch() !== false && $station_obj->getDefaultBranch() != TTUUID::getZeroID() ) { $branch_id = $station_obj->getDefaultBranch(); //Debug::Text(' aOverriding branch to: '. $branch_id, __FILE__, __LINE__, __METHOD__, 10); } else if ( isset( $geo_fence_default_object_ids ) && isset( $geo_fence_default_object_ids['branch_id'] ) && isset( $geo_fence_default_object_ids['branch_id'][0] ) && count( $geo_fence_default_object_ids['branch_id'] ) == 1 && $this->isAllowedBranch( $user_obj->getId(), $geo_fence_default_object_ids['branch_id'][0] ) ) { //If more than one record matches GEO coordinates, ignore it since we will never know which one to choose. $branch_id = $geo_fence_default_object_ids['branch_id'][0]; //Debug::Text(' bOverriding branch to: '. $branch_id, __FILE__, __LINE__, __METHOD__, 10); } else if ( is_object( $s_obj ) && $s_obj->getBranch() != TTUUID::getZeroID() ) { $branch_id = $s_obj->getBranch(); //Debug::Text(' cOverriding branch to: '. $branch_id, __FILE__, __LINE__, __METHOD__, 10); } else if ( $user_obj->getDefaultBranch() != TTUUID::getZeroID() ) { $branch_id = $user_obj->getDefaultBranch(); //Debug::Text(' dOverriding branch to: '. $branch_id, __FILE__, __LINE__, __METHOD__, 10); } Debug::Text( ' Overriding branch to: ' . $branch_id, __FILE__, __LINE__, __METHOD__, 10 ); } if ( $department_id == '' || empty( $department_id ) || $department_id == TTUUID::getZeroID() ) { if ( is_object( $station_obj ) && $station_obj->getDefaultDepartment() !== false && $station_obj->getDefaultDepartment() != TTUUID::getZeroID() ) { $department_id = $station_obj->getDefaultDepartment(); } else if ( isset( $geo_fence_default_object_ids ) && isset( $geo_fence_default_object_ids['department_id'] ) && isset( $geo_fence_default_object_ids['department_id'][0] ) && count( $geo_fence_default_object_ids['department_id'] ) == 1 && $this->isAllowedDepartment( $user_obj->getId(), $branch_id, $geo_fence_default_object_ids['department_id'][0] ) ) { $department_id = $geo_fence_default_object_ids['department_id'][0]; } else if ( is_object( $s_obj ) && $s_obj->getDepartment() != TTUUID::getZeroID() ) { $department_id = $s_obj->getDepartment(); } else if ( $user_obj->getDefaultDepartment() != TTUUID::getZeroID() ) { $department_id = $user_obj->getDefaultDepartment(); } Debug::Text( ' Overriding department to: ' . $department_id, __FILE__, __LINE__, __METHOD__, 10 ); } if ( $job_id == '' || empty( $job_id ) || $job_id == TTUUID::getZeroID() ) { if ( is_object( $station_obj ) && $station_obj->getDefaultJob() !== false && $station_obj->getDefaultJob() != TTUUID::getZeroID() ) { $job_id = $station_obj->getDefaultJob(); } else if ( isset( $geo_fence_default_object_ids ) && isset( $geo_fence_default_object_ids['job_id'] ) && isset( $geo_fence_default_object_ids['job_id'][0] ) && count( $geo_fence_default_object_ids['job_id'] ) == 1 && $this->isAllowedJob( $user_obj->getId(), $branch_id, $department_id, $geo_fence_default_object_ids['job_id'][0] ) ) { $job_id = $geo_fence_default_object_ids['job_id'][0]; } else if ( is_object( $s_obj ) && $s_obj->getJob() != TTUUID::getZeroID() ) { $job_id = $s_obj->getJob(); } else if ( $user_obj->getDefaultJob() != TTUUID::getZeroID() ) { $job_id = $user_obj->getDefaultJob(); } Debug::Text( ' Overriding job to: ' . $job_id, __FILE__, __LINE__, __METHOD__, 10 ); } if ( $job_item_id == '' || empty( $job_item_id ) || $job_item_id == TTUUID::getZeroID() ) { if ( is_object( $station_obj ) && $station_obj->getDefaultJobItem() !== false && $station_obj->getDefaultJobItem() != TTUUID::getZeroID() ) { $job_item_id = $station_obj->getDefaultJobItem(); } else if ( isset( $geo_fence_default_object_ids ) && isset( $geo_fence_default_object_ids['job_item_id'] ) && isset( $geo_fence_default_object_ids['job_item_id'][0] ) && count( $geo_fence_default_object_ids['job_item_id'] ) == 1 && $this->isAllowedJobItem( $user_obj->getId(), $job_id, $geo_fence_default_object_ids['job_item_id'][0] ) ) { $job_item_id = $geo_fence_default_object_ids['job_item_id'][0]; } else if ( is_object( $s_obj ) && $s_obj->getJobItem() != TTUUID::getZeroID() ) { $job_item_id = $s_obj->getJobItem(); } else if ( $user_obj->getDefaultJobItem() != TTUUID::getZeroID() ) { $job_item_id = $user_obj->getDefaultJobItem(); } Debug::Text( ' Overriding task to: ' . $job_item_id, __FILE__, __LINE__, __METHOD__, 10 ); } if ( $punch_tag_id == '' || empty( $punch_tag_id ) || $punch_tag_id == TTUUID::getZeroID() ) { if ( is_object( $station_obj ) && empty( $station_obj->getDefaultPunchTag() ) != true && $station_obj->getDefaultPunchTag() !== false && $station_obj->getDefaultPunchTag() != TTUUID::getZeroID() ) { $punch_tag_id = $station_obj->getDefaultPunchTag(); } else if ( isset( $geo_fence_default_object_ids ) && isset( $geo_fence_default_object_ids['punch_tag_id'] ) && isset( $geo_fence_default_object_ids['punch_tag_id'] ) && is_array( $geo_fence_default_object_ids['punch_tag_id'] ) && is_array( $allowed_punch_tags = $this->isAllowedPunchTag( $user_obj->getId(), $branch_id, $department_id, $job_id, $job_item_id, $geo_fence_default_object_ids['punch_tag_id'] ) ) ) { $punch_tag_id = $allowed_punch_tags; //Allowed punch tags is defined in the IF statement above, to avoid running isAllowedPunchTag() twice, as its fairly resource intensive. } else if ( is_object( $s_obj ) && empty( $s_obj->getPunchTag() ) !== true && $s_obj->getPunchTag() != TTUUID::getZeroID() ) { $punch_tag_id = $s_obj->getPunchTag(); } else if ( empty( $user_obj->getDefaultPunchTag() ) !== true && $user_obj->getDefaultPunchTag() !== TTUUID::getZeroID() ) { $punch_tag_id = $user_obj->getDefaultPunchTag(); } Debug::Arr( $punch_tag_id, ' Overriding punch tag to: ', __FILE__, __LINE__, __METHOD__, 10 ); } } if ( $is_previous_punch == true && is_object( $prev_punch_obj ) ) { //Don't enable transfer by default if the previous punch was any OUT punch. //Transfer does the OUT punch for them, so if the previous punch is an OUT punch //we don't gain anything anyways. $default_transfer_on = ( is_object( $permission_obj ) ) ? $permission_obj->Check( 'punch', 'default_transfer', $user_obj->getId(), $user_obj->getCompany() ) : false; //User/Company need to be passed into here otherwise its always false if called from APIClientStationUnAuthenticated //Check for "Disable: Default Transfer On" station flag and set default_transfer_on = FALSE if it is set. if ( $default_transfer_on == true ) { $mode_flags = $station_obj->getModeFlag(); if ( is_array( $mode_flags ) && in_array( 16384, $mode_flags ) ) { $default_transfer_on = false; Debug::Text( ' Turning off Transfer due to Station Flag: Disable: Default Transfer On ...', __FILE__, __LINE__, __METHOD__, 10 ); } } if ( $default_transfer_on == true && ( isset( $prev_punch_obj ) && is_object( $prev_punch_obj ) && $prev_punch_obj->getStatus() == 10 ) ) { //Check to see if the employee is scheduled, if they are past their scheduled out time, then don't default to transfer. //If they are not scheduled default to transfer though. if ( !isset( $s_obj ) ) { $slf = TTnew( 'ScheduleListFactory' ); /** @var ScheduleListFactory $slf */ $s_obj = $slf->getScheduleObjectByUserIdAndEpoch( $user_obj->getId(), $epoch ); } if ( !is_object( $s_obj ) || ( is_object( $s_obj ) && $epoch < $s_obj->getEndTime() ) ) { $transfer = true; } } else { Debug::Text( ' Not setting Transfer: On...', __FILE__, __LINE__, __METHOD__, 10 ); } $next_type = (int)$prev_punch_obj->getNextType( $epoch ); //Detects breaks/lunches too. if ( $prev_punch_obj->getNextStatus() == 10 ) { //In punch - Carry over just certain data $data = [ 'user_id' => $user_obj->getId(), //'user_full_name' => $user_obj->getFullName(), //'time_stamp' => $epoch, //'date_stamp' => $epoch, 'transfer' => $transfer, 'branch_id' => TTUUID::castUUID( $branch_id ), 'department_id' => TTUUID::castUUID( $department_id ), 'job_id' => TTUUID::castUUID( $job_id ), 'job_item_id' => TTUUID::castUUID( $job_item_id ), 'punch_tag_id' => $punch_tag_id, 'quantity' => 0, 'bad_quantity' => 0, 'status_id' => (int)$prev_punch_obj->getNextStatus(), 'type_id' => (int)$next_type, 'punch_control_id' => $prev_punch_obj->getNextPunchControlID(), 'note' => '', //Must be null. ]; $pcf = TTNew('PunchControlFactory'); /** @var PunchControlFactory $pcf */ $data = $pcf->getCustomFieldsDefaultData( $user_obj->getCompany(), $data, true ); } else { //Out punch $data = [ 'user_id' => $user_obj->getId(), //'user_full_name' => $user_obj->getFullName(), //'time_stamp' => $epoch, //'date_stamp' => $epoch, 'transfer' => $transfer, 'branch_id' => TTUUID::castUUID( $branch_id ), 'department_id' => TTUUID::castUUID( $department_id ), 'job_id' => TTUUID::castUUID( $job_id ), 'job_item_id' => TTUUID::castUUID( $job_item_id ), 'punch_tag_id' => $punch_tag_id, 'quantity' => (float)$prev_punch_obj->getPunchControlObject()->getQuantity(), 'bad_quantity' => (float)$prev_punch_obj->getPunchControlObject()->getBadQuantity(), 'status_id' => (int)$prev_punch_obj->getNextStatus(), 'type_id' => (int)$next_type, 'punch_control_id' => $prev_punch_obj->getNextPunchControlID(), 'note' => (string)$prev_punch_obj->getPunchControlObject()->getNote(), //Must be null. ]; $data = $prev_punch_obj->getPunchControlObject()->getCustomFields( $user_obj->getCompany(), $data ); } } else { $data = [ 'user_id' => $user_obj->getId(), //'user_full_name' => $user_obj->getFullName(), //'time_stamp' => $epoch, //'date_stamp' => $epoch, 'transfer' => false, 'branch_id' => TTUUID::castUUID( $branch_id ), 'department_id' => TTUUID::castUUID( $department_id ), 'job_id' => TTUUID::castUUID( $job_id ), 'job_item_id' => TTUUID::castUUID( $job_item_id ), 'punch_tag_id' => $punch_tag_id, 'quantity' => 0, 'bad_quantity' => 0, 'status_id' => ( ( isset($next_status_id) && $next_status_id != '' ) ? $next_status_id : 10 ), //In 'type_id' => 10, //Normal 'note' => '', //Must be null. ]; $pcf = TTNew('PunchControlFactory'); /** @var PunchControlFactory $pcf */ $data = $pcf->getCustomFieldsDefaultData( $user_obj->getCompany(), $data, true ); } Debug::Arr( $data, ' Default Punch Settings: ', __FILE__, __LINE__, __METHOD__, 10 ); return $data; } /** * Determine if the punch was manually created (without punching in/out) or modified by someone other than the person who punched in/out. * Allow for employees manually entering in their own punches (and editing) without that being marked as tainted. * * @return bool */ function getTainted() { if ( $this->getColumn( 'tainted' ) !== false ) { return (bool)$this->getColumn( 'tainted' ); } return false; } /** * @return bool */ function getHasImage() { return $this->fromBool( $this->getGenericDataValue( 'has_image' ) ); } /** * @param $value * @return bool */ function setHasImage( $value ) { return $this->setGenericDataValue( 'has_image', $this->toBool( $value ) ); } /** * @param string $company_id UUID * @param string $user_id UUID * @param string $punch_id UUID * @return bool */ function isImageExists( $company_id = null, $user_id = null, $punch_id = null, $file_name = null ) { if ( empty( $file_name ) ) { $file_name = $this->getImageFileName( $company_id, $user_id, $punch_id ); } if ( $this->getHasImage() && file_exists( $file_name ) ) { return true; } return false; } /** * @param string $company_id UUID * @param string $user_id UUID * @param string $punch_id UUID * @return bool|int */ function saveImage( $company_id = null, $user_id = null, $punch_id = null ) { $image_data = $this->getImage(); if ( $image_data != '' ) { $file_name = $this->getImageFileName( $company_id, $user_id, $punch_id ); if ( $file_name != '' ) { //Use retry loop because sometimes transient file system errors may be encountered. $retry_function = function () use ( $image_data, $file_name ) { $dir = dirname( $file_name ); if ( file_exists( $dir ) == false ) { $mkdir_result = @mkdir( $dir, 0700, true ); if ( $mkdir_result == false ) { Debug::Text( 'ERROR: Unable to create storage file directory: ' . $dir .' Message: '. Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 ); throw new Exception( 'ERROR: Unable to create storage file directory: ' . $dir .' Message: '. Debug::getLastPHPErrorMessage() ); } } Debug::Text( 'Saving Image File Name: ' . $file_name, __FILE__, __LINE__, __METHOD__, 10 ); $file_put_contents_result = file_put_contents( $file_name, $image_data ); if ( $file_put_contents_result == true ) { return true; } else { throw new Exception( 'ERROR: Unable to save data to file: ' . $file_name ); } }; return Misc::Retry( $retry_function, 3, 0.25 ); //If needed, could potentially continue on error here and the punch would get saved without an image. } } else { //Debug::Arr( $image_data, 'NOT Saving Image File Name: ' . $file_name, __FILE__, __LINE__, __METHOD__, 10 ); Debug::Text( ' No Punch Image recevied...', __FILE__, __LINE__, __METHOD__, 10 ); } return false; } /** * @param string $company_id UUID * @param string $user_id UUID * @param string $punch_id UUID * @return bool|string */ function getImageFileName( $company_id = null, $user_id = null, $punch_id = null ) { if ( $company_id == '' && is_object( $this->getUserObject() ) ) { $company_id = $this->getUserObject()->getCompany(); } if ( $user_id == '' && $this->getUser() != '' ) { $user_id = $this->getUser(); } if ( $punch_id == '' ) { $punch_id = $this->getID(); } if ( $company_id == '' || TTUUID::isUUID( $company_id ) == false ) { Debug::Text( 'No Company... Company ID: ' . $company_id . ' User ID: ' . $user_id . ' Punch ID: ' . $punch_id, __FILE__, __LINE__, __METHOD__, 10 ); return false; } if ( $user_id == '' || TTUUID::isUUID( $user_id ) == false ) { Debug::Text( 'No User... Company ID: ' . $company_id . ' User ID: ' . $user_id . ' Punch ID: ' . $punch_id, __FILE__, __LINE__, __METHOD__, 10 ); return false; } if ( $punch_id == '' || TTUUID::isUUID( $punch_id ) == false ) { Debug::Text( 'No Punch... Company ID: ' . $company_id . ' User ID: ' . $user_id . ' Punch ID: ' . $punch_id, __FILE__, __LINE__, __METHOD__, 10 ); return false; } $hash_dir = []; $hash = crc32( $company_id . $user_id . $punch_id ); $hash_dir[0] = substr( $hash, 0, 2 ); $hash_dir[1] = substr( $hash, 2, 2 ); $hash_dir[2] = substr( $hash, 4, 2 ); $base_name = Environment::getStorageBasePath() . DIRECTORY_SEPARATOR . 'punch_images' . DIRECTORY_SEPARATOR . $company_id . DIRECTORY_SEPARATOR . $user_id . DIRECTORY_SEPARATOR . $hash_dir[0] . DIRECTORY_SEPARATOR . $hash_dir[1] . DIRECTORY_SEPARATOR . $hash_dir[2] . DIRECTORY_SEPARATOR; $punch_image_file_name = $base_name . $punch_id . '.jpg'; //Should be JPEG 75% quality, about 10K in size. //Debug::Text( 'Punch Image File Name: ' . $punch_image_file_name . ' Company ID: ' . $company_id . ' User ID: ' . $user_id . ' Punch ID: ' . $punch_id . ' CRC32: ' . $hash, __FILE__, __LINE__, __METHOD__, 10 ); return $punch_image_file_name; } /** * @param string $company_id UUID * @param string $user_id UUID * @param string $punch_id UUID * @return bool */ function cleanStoragePath( $company_id = null, $user_id = null, $punch_id = null ) { $file_name = $this->getImageFileName( $company_id, $user_id, $punch_id ); if ( $file_name != '' && $this->isImageExists( $company_id, $user_id, $punch_id, $file_name ) ) { Debug::Text( 'Deleting Image... File Name: ' . $file_name, __FILE__, __LINE__, __METHOD__, 10 ); @unlink( $file_name ); Misc::deleteEmptyParentDirectory( dirname( $file_name ), 4 ); //Recurse to $user_id parent level and remove empty directories. } return true; } /** * @param string $company_id UUID * @param string $user_id UUID * @param string $punch_id UUID * @return bool|string */ function getImage( $company_id = null, $user_id = null, $punch_id = null ) { $value = $this->getGenericTempDataValue( 'image' ); if ( $value !== false && $value != '' ) { return $value; } else if ( !isset( $this->is_new ) || $this->is_new == false ) { //Don't bother checking if image exists on the file system when its a new punch. $file_name = $this->getImageFileName( $company_id, $user_id, $punch_id ); if ( $this->isImageExists() ) { return file_get_contents( $file_name ); } } return false; } /** * @param $data * @return bool */ function setImage( $data ) { if ( $data != '' ) { $this->setGenericTempDataValue( 'image', $data ); $this->setHasImage( true ); return true; } Debug::Text( 'Not setting Image data...', __FILE__, __LINE__, __METHOD__, 10 ); return false; } /** * @param bool $ignore_warning * @return bool */ function Validate( $ignore_warning = true ) { // // BELOW: Validation code moved from set*() functions. // // Punch Control ID $this->Validator->isUUID( 'punch_control', $this->getPunchControlID(), TTi18n::gettext( 'Invalid Punch Control ID' ) ); // Status $this->Validator->inArrayKey( 'status_id', $this->getStatus(), TTi18n::gettext( 'Incorrect Status' ), $this->getOptions( 'status' ) ); // Type $this->Validator->inArrayKey( 'type_id', $this->getType(), TTi18n::gettext( 'Incorrect Type' ), $this->getOptions( 'type' ) ); // Time stamp $this->Validator->isDate( 'punch_time', $this->getTimeStamp(), TTi18n::gettext( 'Incorrect time stamp' ) ); // Original time stamp if ( $this->getOriginalTimeStamp() !== false ) { $this->Validator->isDate( 'original_time_stamp', $this->getOriginalTimeStamp(), TTi18n::gettext( 'Incorrect original time stamp' ) ); } // Actual time stamp if ( $this->getActualTimeStamp() !== false ) { $this->Validator->isDate( 'actual_time_stamp', $this->getActualTimeStamp(), TTi18n::gettext( 'Incorrect actual time stamp' ) ); } // Longitude if ( $this->getLongitude() != '' ) { $this->Validator->isFloat( 'longitude', $this->getLongitude(), TTi18n::gettext( 'Longitude is invalid' ) ); } // Latitude if ( $this->getLatitude() != '' ) { $this->Validator->isFloat( 'latitude', $this->getLatitude(), TTi18n::gettext( 'Latitude is invalid' ) ); } // Position Accuracy if ( $this->getPositionAccuracy() != '' ) { $this->Validator->isNumeric( 'position_accuracy', (int)$this->getPositionAccuracy(), TTi18n::gettext( 'Position Accuracy is invalid' ) ); } // // ABOVE: Validation code moved from set*() functions. // if ( $this->getStatus() == false ) { $this->Validator->isTRUE( 'status_id', false, TTi18n::getText( 'Status not specified' ) ); } if ( $this->getType() == false ) { $this->Validator->isTRUE( 'type_id', false, TTi18n::getText( 'Type not specified' ) ); } if ( $this->Validator->hasError( 'time_stamp' ) == false && $this->getTimeStamp() == false ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::getText( 'Time stamp not specified' ) ); } if ( $this->Validator->hasError( 'punch_control' ) == false && $this->getPunchControlID() == false ) { $this->Validator->isTRUE( 'punch_control', false, TTi18n::getText( 'Invalid Punch Control ID' ) ); } if ( is_object( $this->getPunchControlObject() ) && is_object( $this->getPunchControlObject()->getPayPeriodObject() ) && $this->getPunchControlObject()->getPayPeriodObject()->getIsLocked() == true ) { $this->Validator->isTRUE( 'pay_period', false, TTi18n::getText( 'Pay Period is Currently Locked' ) ); } //Make sure two punches with the same status are not in the same punch pair. //This has to be done here rather than PunchControlFactory because of the unique index and punches are saved before the PunchControl record. if ( is_object( $this->getPunchControlObject() ) ) { $plf = $this->getPunchControlObject()->getPLFByPunchControlID(); if ( $plf->getRecordCount() > 0 ) { foreach ( $plf as $p_obj ) { if ( $p_obj->getId() !== $this->getID() ) { if ( $p_obj->getStatus() == $this->getStatus() ) { if ( $p_obj->getStatus() == 10 ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'In punches cannot occur twice in the same punch pair, you may want to make this an out punch instead' ) ); } else { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'Out punches cannot occur twice in the same punch pair, you may want to make this an in punch instead' ) ); } } } } } unset( $plf, $p_obj ); } /* @formatter:off */ if ( $this->Validator->isValid() == TRUE && $this->isNew() == TRUE ) { $obj_class = "\124\124\114\x69\x63\x65\x6e\x73\x65"; $obj_function = "\166\x61\154\x69\144\x61\164\145\114\x69\x63\145\x6e\x73\x65"; $obj_error_msg_function = "\x67\x65\x74\x46\x75\154\154\105\162\x72\x6f\x72\115\x65\x73\163\141\x67\x65"; @$obj = new $obj_class; $retval = $obj->{$obj_function}(); if ( $retval !== TRUE ) { $this->Validator->isTrue( 'lic_obj', FALSE, $obj->{$obj_error_msg_function}($retval) ); } } /* @formatter:on */ return TRUE; } /** * Saves the punch object when splitting punches at midnight. * @param $epoch int EPOCH * @param $status_id int * @param $punch_control_id string UUID * @return bool * @throws DBError * @throws GeneralError * @throws ReflectionException */ function saveSplitAtMidnightPunch( $epoch, $status_id, $punch_control_id ) { //Since we need to change the PunchControlID, copy the current punch_control object to work around getGenericObject() checking the //IDs and not returning the object anymore. $tmp_punch_control_obj = $this->getPunchControlObject(); // //Punch out just before midnight //The issue with this is that if rounding is enabled this will ignore it, and the shift for this day may total: 3.98hrs. //when they want it to total 4.00hrs. Why don't we split shifts at exactly midnight with no gap at all? //Split shifts right at midnight causes additional issues when editing those punches, TimeTrex will want to combine them on the same day again. $pf = TTnew( 'PunchFactory' ); /** @var PunchFactory $pf */ $pf->setUser( $this->getUser() ); $pf->setEnableSplitAtMidnight( false ); $pf->setTransfer( false ); $pf->setEnableAutoTransfer( false ); $pf->setType( 10 ); //Normal $pf->setStatus( $status_id ); //We used to have to make this punch 60seconds before midnight, but getShiftData() was modified to support punch at exactly midnight. $midnight_timestamp = ( TTDate::getBeginDayEpoch( $epoch ) ); $pf->setTimeStamp( $midnight_timestamp, false ); //Disable rounding. $pf->setActualTimeStamp( $midnight_timestamp ); //$pf->setOriginalTimeStamp( $midnight_timestamp ); //set in preSave() Debug::text( ' Split Punch: Punching in/out at midnight: '. TTDate::getDate('DATE+TIME', $midnight_timestamp ), __FILE__, __LINE__, __METHOD__, 10 ); $pf->setPunchControlID( $punch_control_id ); if ( $pf->isValid() ) { if ( $pf->Save( false ) == true ) { $pcf = TTnew( 'PunchControlFactory' ); /** @var PunchControlFactory $pcf */ $pcf->setId( $pf->getPunchControlID() ); $pcf->setPunchObject( $pf ); $pcf->setBranch( $tmp_punch_control_obj->getBranch() ); $pcf->setDepartment( $tmp_punch_control_obj->getDepartment() ); $pcf->setJob( $tmp_punch_control_obj->getJob() ); $pcf->setJobItem( $tmp_punch_control_obj->getJobItem() ); $pcf->setPunchTag( $tmp_punch_control_obj->getPunchTag() ); $pcf->setCustomFields( $tmp_punch_control_obj->getCustomFields( $tmp_punch_control_obj->getUserObject()->getCompany(), [] ) ); $pcf->setQuantity( $tmp_punch_control_obj->getQuantity() ); $pcf->setBadQuantity( $tmp_punch_control_obj->getBadQuantity() ); $pcf->setNote( $tmp_punch_control_obj->getNote() ); $pcf->setEnableStrictJobValidation( true ); $pcf->setEnableCalcUserDateID( true ); $pcf->setEnableCalcTotalTime( true ); $pcf->setEnableCalcSystemTotalTime( true ); $pcf->setEnableCalcWeeklySystemTotalTime( true ); $pcf->setEnableCalcUserDateTotal( true ); $pcf->setEnableCalcException( true ); if ( $pcf->isValid() == true ) { $pcf->Save( false, true ); //Force isNEW() lookup. } } } unset( $pf, $pcf ); return true; } /** * @return bool */ function preSave() { $this->is_new = false; if ( $this->isNew() == true ) { $this->is_new = true; } if ( $this->is_new ) { Debug::text( ' Setting Original TimeStamp: ' . $this->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10 ); $this->setOriginalTimeStamp( $this->getTimeStamp() ); } if ( $this->getDeleted() == false ) { if ( $this->is_new && $this->getEnableSplitPunchControl() == true ) { Debug::text( ' Split Punch Control to insert inbetween if needed...', __FILE__, __LINE__, __METHOD__, 10 ); $plf = TTNew('PunchListFactory'); $tmp_punch_control_id = $plf->getCompletePunchControlIdByUserIdAndEpoch( $this->getUser(), $this->getTimeStamp() ); if ( TTUUID::isUUID( $tmp_punch_control_id ) ) { Debug::text( ' Found Punch Control ID to split: '. $tmp_punch_control_id, __FILE__, __LINE__, __METHOD__, 10 ); $is_punch_control_split = PunchControlFactory::splitPunchControl( $tmp_punch_control_id ); if ( $is_punch_control_split == true ) { $this->setPunchControlID( false ); //Clear PunchControlID so findPunchControlId() will actually execute. $this->setPunchControlID( $this->findPunchControlID() ); Debug::text( ' Split Punch Control ID: '. $tmp_punch_control_id .' Result: '. (int)$is_punch_control_split, __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::text( ' No Punch Control to split...', __FILE__, __LINE__, __METHOD__, 10 ); } unset( $plf, $tmp_punch_control_id, $is_punch_control_split ); } if ( $this->is_new && $this->getTransfer() == true && $this->getEnableAutoTransfer() == true ) { Debug::text( ' Transfer is Enabled, automatic punch out of last punch pair...', __FILE__, __LINE__, __METHOD__, 10 ); //Use actual time stamp, not rounded timestamp. This should only be called on new punches as well, otherwise Actual Time Stamp could be incorrect. //$prev_punch_obj = $this->getPreviousPunchObject( $this->getActualTimeStamp() ); $prev_punch_obj = $this->getPreviousPunchObject( $this->getActualTimeStamp(), $this->getUser(), true ); //Ignore future punches, so mass adding transfer punches at the end of the day can still work. if ( is_object( $prev_punch_obj ) ) { Debug::text( ' Found Last Punch: ID: '. $prev_punch_obj->getID() .' Timestamp: '. TTDate::getDate('DATE+TIME', $prev_punch_obj->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); if ( $prev_punch_obj->getStatus() == 10 ) { Debug::text( ' Last Punch was in. Auto Punch Out now: ', __FILE__, __LINE__, __METHOD__, 10 ); //Make sure the current punch status is IN $this->setStatus( 10 ); //In $this->setType( 10 ); //Normal (can't transfer in/out of lunches?) $pf = TTnew( 'PunchFactory' ); /** @var PunchFactory $pf */ $pf->setUser( $this->getUser() ); $pf->setEnableAutoTransfer( false ); $pf->setPunchControlID( $prev_punch_obj->getPunchControlID() ); $pf->setTransfer( true ); $pf->setType( $prev_punch_obj->getNextType() ); $pf->setStatus( 20 ); //Out $pf->setTimeStamp( $this->getTimeStamp() ); //Use the exact timestamp from the previous punch so they always match now that transfer punches can round. $pf->setActualTimeStamp( $this->getTimeStamp() ); //$pf->setOriginalTimeStamp( $this->getTimeStamp() ); //set in preSave() $pf->setLongitude( $this->getLongitude() ); $pf->setLatitude( $this->getLatitude() ); $pf->setPositionAccuracy( $this->getPositionAccuracy() ); if ( $pf->isValid() ) { if ( $pf->Save( false ) == true ) { $prev_punch_obj->getPunchControlObject()->setPunchObject( $pf ); $prev_punch_obj->getPunchControlObject()->setEnableCalcTotalTime( true ); $prev_punch_obj->getPunchControlObject()->setEnableCalcSystemTotalTime( true ); $prev_punch_obj->getPunchControlObject()->setEnableCalcUserDateTotal( true ); $prev_punch_obj->getPunchControlObject()->setEnableCalcException( true ); $prev_punch_obj->getPunchControlObject()->setEnablePreMatureException( true ); if ( $prev_punch_obj->getPunchControlObject()->isValid() ) { $prev_punch_obj->getPunchControlObject()->Save(); } else { Debug::text( ' aError saving auto out punch...', __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::text( ' bError saving auto out punch...', __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::text( ' cError saving auto out punch...', __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::text( ' Last Punch was out. No Auto Punch out needed, removing transfer flag from this punch...', __FILE__, __LINE__, __METHOD__, 10 ); $this->setTransfer( false ); } } unset( $prev_punch_obj, $pf ); } //Split punch at midnight. //This has to be an Out punch, and the previous punch has to be an in punch in order for the split to occur. //Check to make sure there is an open punch pair. //Make sure this punch isn't right at midnight either, as no point in splitting a punch at that time. //FIXME: What happens if a supervisor edits a 11:30PM punch and makes it 5:00AM the next day? // We can't split punches when editing existing punches, because we have to split punch_control_ids prior to saving etc... // But we can split when supervisors are adding new punches. //Debug::text('Split at Midnight Enabled: '. $this->getEnableSplitAtMidnight() .' IsNew: '. $this->is_new .' Status: '. $this->getStatus() .' TimeStamp: '. $this->getTimeStamp() .' Punch Control ID: '. $this->getPunchControlID(), __FILE__, __LINE__, __METHOD__, 10); if ( $this->is_new == true && $this->getStatus() == 20 && $this->getEnableSplitAtMidnight() == true && $this->getTimeStamp() != TTDate::getBeginDayEpoch( $this->getTimeStamp() ) && ( is_object( $this->getPunchControlObject() ) && is_object( $this->getPunchControlObject()->getPayPeriodScheduleObject() ) && $this->getPunchControlObject()->getPayPeriodScheduleObject()->getShiftAssignedDay() == 40 ) ) { $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getPreviousPunchByUserIdAndEpoch( $this->getUser(), $this->getTimeStamp() ); if ( $plf->getRecordCount() > 0 ) { $prev_punch_obj = $plf->getCurrent(); Debug::text( ' Found Last Punch... ID: ' . $prev_punch_obj->getId() . ' Timestamp: ' . $prev_punch_obj->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $prev_punch_obj->getStatus() == 10 && TTDate::doesRangeSpanMidnight( $this->getTimeStamp(), $prev_punch_obj->getTimeStamp() ) ) { Debug::text( ' Last Punch was in and this is an out punch that spans midnight. Split Punch at midnight now: ', __FILE__, __LINE__, __METHOD__, 10 ); //Make sure the current punch status is OUT //But we can split LUNCH/Break punches, because someone could punch in at 8PM, then out for lunch at 1:00AM, this would need to be split. $this->setStatus( 20 ); //Out //Reduce the out punch by 60 seconds, and increase the current punch by 60seconds so no time is lost. $this->setTimeStamp( $this->getTimeStamp() ); //FIXME: May need to use ActualTimeStamp here so we aren't double rounding. $split_range_at_midnight = TTDate::splitDateRangeAtMidnight( $prev_punch_obj->getTimeStamp(), $this->getTimeStamp() ); if ( is_array( $split_range_at_midnight ) ) { $i = 0; $max = ( count( $split_range_at_midnight ) - 1 ); foreach( $split_range_at_midnight as $split_range_arr ) { Debug::Text( ' Split Range: Start: '. TTDate::getDate('DATE+TIME', $split_range_arr['start_time_stamp'] ) .' End: '. TTDate::getDate('DATE+TIME', $split_range_arr['end_time_stamp'] ), __FILE__, __LINE__, __METHOD__, 10 ); if ( $i == 0 ) { Debug::text( ' Split Punch: Punching out just before midnight after previous punch...', __FILE__, __LINE__, __METHOD__, 10 ); $this->saveSplitAtMidnightPunch( $split_range_arr['end_time_stamp'], 20, $prev_punch_obj->getPunchControlID() ); //Out } if ( $i > 0 && $i < $max ) { $new_punch_control_id = $this->getPunchControlObject()->getNextInsertId(); Debug::text( ' Split Punch: Punching in just before midnight on a middle day...', __FILE__, __LINE__, __METHOD__, 10 ); $this->saveSplitAtMidnightPunch( $split_range_arr['start_time_stamp'], 10, $new_punch_control_id ); //In Debug::text( ' Split Punch: Punching out just before midnight on a middle day...', __FILE__, __LINE__, __METHOD__, 10 ); $this->saveSplitAtMidnightPunch( $split_range_arr['end_time_stamp'], 20, $new_punch_control_id ); //Out unset( $new_punch_control_id ); } if ( $i == $max ) { $new_punch_control_id = $this->getPunchControlObject()->getNextInsertId(); Debug::text( ' Split Punch: Punching in just at midnight on last day...', __FILE__, __LINE__, __METHOD__, 10 ); $this->saveSplitAtMidnightPunch( $split_range_arr['end_time_stamp'], 10, $new_punch_control_id ); //In $this->setPunchControlID( $new_punch_control_id ); unset( $new_punch_control_id ); } $i++; } } unset( $split_range_at_midnight, $split_range_arr ); } else { Debug::text( ' Last Punch was out. No Auto Punch ', __FILE__, __LINE__, __METHOD__, 10 ); } } unset( $plf, $prev_punch_obj ); } } return true; } /** * @return bool */ function postSave() { if ( $this->getDeleted() == true ) { $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getByPunchControlId( $this->getPunchControlID() ); if ( $plf->getRecordCount() === 0 ) { //=== is needed here to ensure its not FALSE. //Check to see if any other punches are assigned to this punch_control_id Debug::text( ' Deleted Last Punch for Punch Control Object: ' . $this->getPunchControlObject()->getId(), __FILE__, __LINE__, __METHOD__, 10 ); $this->getPunchControlObject()->setDeleted( true ); } //Make sure we recalculate system time. $this->getPunchControlObject()->setPunchObject( $this ); $this->getPunchControlObject()->setEnableCalcUserDateID( true ); $this->getPunchControlObject()->setEnableCalcSystemTotalTime( $this->getEnableCalcSystemTotalTime() ); $this->getPunchControlObject()->setEnableCalcWeeklySystemTotalTime( $this->getEnableCalcWeeklySystemTotalTime() ); $this->getPunchControlObject()->setEnableCalcException( $this->getEnableCalcException() ); $this->getPunchControlObject()->setEnablePreMatureException( $this->getEnablePreMatureException() ); $this->getPunchControlObject()->setEnableCalcUserDateTotal( $this->getEnableCalcUserDateTotal() ); $this->getPunchControlObject()->setEnableCalcTotalTime( $this->getEnableCalcTotalTime() ); if ( $this->getPunchControlObject()->isValid() ) { //Saving the punch control object clears it, so even if the punch was Save(FALSE) the punch control object will be cleared and not accessible. //This can affect things like drag and drop. $this->getPunchControlObject()->Save(); Debug::text( ' Punch Control Object Saved...', __FILE__, __LINE__, __METHOD__, 10 ); } else { //Something went wrong, rollback the entire transaction. $this->FailTransaction(); } if ( $this->getHasImage() == true ) { $this->cleanStoragePath(); } } else { $this->saveImage(); $this->handleReminderNotifications(); $this->handleFutureTimeSheetRecalculationForExceptions(); } return true; } function handleFutureTimeSheetRecalculationForExceptions() { global $config_vars; if ( isset( $config_vars['other']['enable_job_queue'] ) && $config_vars['other']['enable_job_queue'] != true ) { return false; } if ( $this->is_new != true ) { Debug::text( ' Not a new punch, ignore reminder notifications...', __FILE__, __LINE__, __METHOD__, 10 ); return false; } if ( $this->getEnablePreMatureException() == false ) { Debug::text( ' User is not punching themselves, ignore reminder notifications...', __FILE__, __LINE__, __METHOD__, 10 ); return false; } //Make sure the current punch isn't in the past, or too far in the future. if ( $this->getTimeStamp() > ( TTDate::getTime() + 7200 ) || $this->getTimeStamp() < ( TTDate::getTime() - 7200 ) ) { Debug::text( ' Punch does not appear to be real-time, ignore reminder notifications...', __FILE__, __LINE__, __METHOD__, 10 ); return false; } $this->setScheduleID( $this->findScheduleID( $this->getTimeStamp() ) ); $delay_after_trigger_time = 1; //1 second. $start_date = $this->getTimeStamp(); $end_date = $start_date; $eplf = TTNew('ExceptionPolicyListFactory'); $eplf->getByPolicyGroupUserIdAndTypeAndActive( $this->getUser(), [ 'C1', 'L4' ], true ); if ( $eplf->getRecordCount() > 0 ) { switch( $this->getStatus() ) { case 10: //In //Handle 'C1' on every IN punch, so when punching back in from Lunch/Break it triggers within the check-in grace time. foreach( $eplf as $ep_obj ) { /** @var ExceptionFactory $ep_obj */ switch ( $ep_obj->getType() ) { case 'C1': $effective_date = ( $this->getTimeStamp() + $ep_obj->getGrace() + $delay_after_trigger_time ); Debug::text( ' Exception Type: '. $ep_obj->getType() .' Effective Date: '. TTDate::getDate('DATE+TIME', $effective_date ) .' Punch Time: '. TTDate::getDate('DATE+TIME', $this->getTimeStamp() ) .' Grace: '. $ep_obj->getGrace(), __FILE__, __LINE__, __METHOD__, 10 ); SystemJobQueue::Add( TTi18n::getText( 'ReCalculate Quick Exceptions' ), $this->getId(), 'CalculatePolicy', 'reCalculateForJobQueue', [ $this->getUser(), 'calcQuickExceptions', $start_date, $end_date ], 10, null, $effective_date, $this->getUser() ); break; case 'L4': if ( $this->getType() == 10 && $this->getTransfer() == false ) { //Only schedule recalculation for Normal In punches that are *not* transfer punches. $meal_policy_arr = $this->getMealPolicies(); $mplf = $meal_policy_arr['lf']; if ( $mplf->getRecordCount() > 0 ) { $mplf->rewind(); //Other code above loops through meal policies, so we always want to rewind to the start and get the first one. $mp_obj = $mplf->getCurrent(); $effective_date = ( $this->getTimeStamp() + $ep_obj->getGrace() + $mp_obj->getTriggerTime() ); Debug::text( ' Exception Type: '. $ep_obj->getType() .' Effective Date: '. TTDate::getDate('DATE+TIME', $effective_date ) .' Punch Time: '. TTDate::getDate('DATE+TIME', $this->getTimeStamp() ) .' Grace: '. $ep_obj->getGrace(), __FILE__, __LINE__, __METHOD__, 10 ); SystemJobQueue::Add( TTi18n::getText( 'ReCalculate Quick Exceptions' ), $this->getId(), 'CalculatePolicy', 'reCalculateForJobQueue', [ $this->getUser(), 'calcQuickExceptions', $start_date, $end_date ], 10, null, $effective_date, $this->getUser() ); } unset( $meal_policy_arr, $mplf, $mp_obj ); } break; } } //switch( $this->getType() ) { // case 10: //Normal // break; // case 20: //Lunch // //When returning from lunch/break, its possible that the previous punch (lunch/break out) could be the wrong status and needs to be changed. So we need to clear all lunch/break/transfer punch reminders here. // break; // case 30: //Break // //When returning from lunch/break, its possible that the previous punch (lunch/break out) could be the wrong status and needs to be changed. So we need to clear all lunch/break/transfer punch reminders here. // break; //} break; case 20: //Out //switch( $this->getType() ) { // case 10: //Normal // //Clear any end shift notifications in the future. As well as any lunch/break reminders that still might be pending. // // **NOTE: This is *not* called when a transfer punch is saved and the Out punch is being created. // break; // case 20: //Lunch // break; // case 30: //Break // break; //} break; } } return true; } /** * Adds a transfer punch reminder notification * @return bool */ function addPunchReminderTransfer( $payload ) { //Add Transfer Punch Reminder Debug::text( ' Add post-dated notification for transfer punch at: '. TTDate::getDate('DATE+TIME', $this->getTimeStamp() ) .' plus delay.', __FILE__, __LINE__, __METHOD__, 10 ); $notification_subject = TTi18n::getText( 'Reminder: Transfer Punch.' ); $notification_body = TTi18n::getText('Transfer punch.' ); $notification_data = [ 'object_id' => $this->getId(), 'user_id' => $this->getUser(), 'priority_id' => 2, //High - Don't use 1 here, as we shouldn't send a "must get attention" reminder for transfer as its a guess at best. 'type_id' => 'reminder_punch_transfer', 'object_type_id' => 120, //120=PunchFactory 'effective_date' => $this->getTimeStamp(), 'title_short' => $notification_subject, 'body_short' => $notification_body, 'payload' => $payload, ]; return Notification::sendNotification( $notification_data ); } /** * Adds/Removes notifications for punch reminders based on the current action. * @return false * @throws ReflectionException */ function handleReminderNotifications() { if ( getTTProductEdition() == TT_PRODUCT_COMMUNITY ) { return false; } if ( $this->is_new != true ) { Debug::text( ' Not a new punch, ignore reminder notifications...', __FILE__, __LINE__, __METHOD__, 10 ); return false; } if ( $this->getEnablePreMatureException() == false ) { Debug::text( ' User is not punching themselves, ignore reminder notifications...', __FILE__, __LINE__, __METHOD__, 10 ); return false; } //Make sure the current punch isn't in the past, or too far in the future. if ( $this->getTimeStamp() > ( TTDate::getTime() + 7200 ) || $this->getTimeStamp() < ( TTDate::getTime() - 7200 ) ) { Debug::text( ' Punch does not appear to be real-time, ignore reminder notifications...', __FILE__, __LINE__, __METHOD__, 10 ); return false; } $this->setScheduleID( $this->findScheduleID( $this->getTimeStamp() ) ); $payload = [ 'timetrex' => [ 'event' => [ [ 'type' => 'open_view', 'data' => [], 'view_name' => 'InOut', 'action_name' => 'add' ] ] ] ]; //Open In/Out view for punching. switch( $this->getStatus() ) { case 10: //In switch( $this->getType() ) { case 10: //Normal Notification::deletePendingNotifications( [ 'reminder_punch_normal_in', 'reminder_punch_transfer' ], $this->getUser(), null, $this->getTimeStamp() ); //Add Transfer Punch Reminder $this->addPunchReminderTransfer( $payload ); if ( $this->getTransfer() == false ) { Notification::deletePendingNotifications( [ 'reminder_punch_normal_out', 'reminder_punch_lunch_out', 'reminder_punch_break_out' ], $this->getUser(), null, $this->getTimeStamp() ); //Add reminder for end of shift. $slf = TTnew( 'ScheduleListFactory' ); /** @var ScheduleListFactory $slf */ $s_obj = $slf->getScheduleObjectByUserIdAndEpoch( $this->getUser(), $this->getTimeStamp() ); /** @var ScheduleFactory $s_obj */ if ( is_object( $s_obj ) ) { Debug::text( ' Add post-dated notification for end of shift at: '. TTDate::getDate('DATE+TIME', $s_obj->getEndTime() ), __FILE__, __LINE__, __METHOD__, 10 ); $notification_title_short = TTi18n::getText( 'Reminder: Punch Out.' ); $notification_title_long = TTi18n::getText( 'Reminder: Punch Out by %1', TTDate::getDate('TIME', $s_obj->getEndTime() ) ); $notification_body = TTi18n::getText( 'Punch out for the end of your shift by '. TTDate::getDate('TIME', $s_obj->getEndTime() ) ); $notification_data = [ 'object_id' => $this->getId(), 'user_id' => $this->getUser(), 'priority_id' => 2, //High - Don't use 1 here, as we shouldn't send a "must get attention" reminder for end of shift, as often people will work past. 'type_id' => 'reminder_punch_normal_out', 'object_type_id' => 120, //120=PunchFactory 'effective_date' => $s_obj->getEndTime(), 'title_short' => $notification_title_short, 'title_long' => $notification_title_long, 'body_short' => $notification_body, 'payload' => $payload, ]; Notification::sendNotification( $notification_data ); } else { Debug::text( ' No schedule, unable to add post-dated reminder notification for end of shift...', __FILE__, __LINE__, __METHOD__, 10 ); } //Add reminder for start of lunch. $meal_policy_arr = $this->getMealPolicies(); $mplf = $meal_policy_arr['lf']; Debug::Text( 'Meal Policy Record Count: ' . $mplf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $mplf->getRecordCount() > 0 ) { //FIXME: If they have multiple meal policies, how do we determine which one to send a reminder on? foreach( $mplf as $mp_obj ) { $start_lunch_time = ( $this->getTimeStamp() + $mp_obj->getTriggerTime() ); Debug::text( ' Add post-dated notification for start of lunch at: '. TTDate::getDate('DATE+TIME', $start_lunch_time ), __FILE__, __LINE__, __METHOD__, 10 ); $notification_title_short = TTi18n::getText( 'Reminder: Punch Out for Lunch.' ); $notification_title_long = TTi18n::getText( 'Reminder: Punch Out for Lunch by %1', TTDate::getDate('TIME', $start_lunch_time ) ); $notification_body = TTi18n::getText( 'Punch out for lunch by '. TTDate::getDate('TIME', $start_lunch_time ) ); $notification_data = [ 'object_id' => $this->getId(), 'user_id' => $this->getUser(), 'priority_id' => 2, //High 'type_id' => 'reminder_punch_lunch_out', 'object_type_id' => 120, //120=PunchFactory 'effective_date' => $start_lunch_time, 'title_short' => $notification_title_short, 'title_long' => $notification_title_long, 'body_short' => $notification_body, 'payload' => $payload, ]; Notification::sendNotification( $notification_data ); } } else { Debug::text( ' No meal policy, unable to add post-dated reminder notification for start of lunch...', __FILE__, __LINE__, __METHOD__, 10 ); } unset( $mplf ); //Add reminder for start of break(s). $break_policy_arr = $this->getBreakPolicies(); $bplf = $break_policy_arr['lf']; Debug::Text( 'Break Policy Record Count: ' . $bplf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $bplf->getRecordCount() > 0 ) { $prev_start_break_time = null; foreach( $bplf as $bp_obj ) { //If they include multiple breaks, there is likely too much flexibility for this to be useful, so just disable reminders in this case. if ( $bp_obj->getIncludeMultipleBreaks() == true ) { continue; } $start_break_time = ( $this->getTimeStamp() + $bp_obj->getTriggerTime() ); //Skip duplicate notifications. if ( $prev_start_break_time == $start_break_time ) { Debug::text( ' Skipping duplicate break notification at: '. $start_break_time, __FILE__, __LINE__, __METHOD__, 10 ); continue; } Debug::text( ' Add post-dated notification for start of break at: '. TTDate::getDate('DATE+TIME', $start_break_time ), __FILE__, __LINE__, __METHOD__, 10 ); $notification_title_short = TTi18n::getText( 'Reminder: Punch Out for Break.' ); $notification_title_long = TTi18n::getText( 'Reminder: Punch Out for Break by %1', TTDate::getDate('TIME', $start_break_time ) ); $notification_body = TTi18n::getText( 'Punch out for break by '. TTDate::getDate('TIME', $start_break_time ) ); $notification_data = [ 'object_id' => $this->getId(), 'user_id' => $this->getUser(), 'priority_id' => 2, //High 'type_id' => 'reminder_punch_break_out', 'object_type_id' => 120, //120=PunchFactory 'effective_date' => $start_break_time, 'title_short' => $notification_title_short, 'title_long' => $notification_title_long, 'body_short' => $notification_body, 'payload' => $payload, ]; Notification::sendNotification( $notification_data ); $prev_start_break_time = $start_break_time; } } else { Debug::text( ' No break policy, unable to add post-dated reminder notification for start of break(s)...', __FILE__, __LINE__, __METHOD__, 10 ); } unset( $bplf ); } break; case 20: //Lunch //When returning from lunch/break, its possible that the previous punch (lunch/break out) could be the wrong status and needs to be changed. So we need to clear all lunch/break/transfer punch reminders here. Debug::text( ' Delete post-dated notification for end of lunch, end of break, and punch transfer...', __FILE__, __LINE__, __METHOD__, 10 ); Notification::deletePendingNotifications( [ 'reminder_punch_lunch_in', 'reminder_punch_break_in', 'reminder_punch_transfer' ], $this->getUser(), null, $this->getTimeStamp() ); $this->addPunchReminderTransfer( $payload ); break; case 30: //Break //When returning from lunch/break, its possible that the previous punch (lunch/break out) could be the wrong status and needs to be changed. So we need to clear all lunch/break/transfer punch reminders here. Debug::text( ' Delete post-dated notification for end of break, end of lunch, and punch transfer...', __FILE__, __LINE__, __METHOD__, 10 ); Notification::deletePendingNotifications( [ 'reminder_punch_lunch_in', 'reminder_punch_break_in', 'reminder_punch_transfer' ], $this->getUser(), null, $this->getTimeStamp() ); $this->addPunchReminderTransfer( $payload ); break; } break; case 20: //Out switch( $this->getType() ) { case 10: //Normal //Clear any end shift notifications in the future. As well as any lunch/break reminders that still might be pending. // **NOTE: This is *not* called when a transfer punch is saved and the Out punch is being created. Debug::text( ' Delete post-dated notification for end of shift at...', __FILE__, __LINE__, __METHOD__, 10 ); Notification::deletePendingNotifications( [ 'reminder_punch_normal_out', 'reminder_punch_lunch_out', 'reminder_punch_break_out', 'reminder_punch_transfer' ], $this->getUser(), null, $this->getTimeStamp() ); break; case 20: //Lunch Notification::deletePendingNotifications( [ 'reminder_punch_lunch_out', 'reminder_punch_transfer' ], $this->getUser(), null, $this->getTimeStamp() ); //Add reminder for end of lunch. $meal_policy_arr = $this->getMealPolicies(); $mplf = $meal_policy_arr['lf']; Debug::Text( 'Meal Policy Record Count: ' . $mplf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $mplf->getRecordCount() > 0 ) { $mp_obj = $mplf->getCurrent(); $end_lunch_time = ( $this->getTimeStamp() + $mp_obj->getAmount() ); Debug::text( ' Add post-dated notification for end of lunch by: '. TTDate::getDate('DATE+TIME', $end_lunch_time ), __FILE__, __LINE__, __METHOD__, 10 ); $notification_title_short = TTi18n::getText( 'Reminder: Punch In from Lunch.' ); $notification_title_long = TTi18n::getText( 'Reminder: Punch In from Lunch by %1', TTDate::getDate('TIME', $end_lunch_time )); $notification_body = TTi18n::getText( 'Punch in from lunch at '. TTDate::getDate('TIME', $end_lunch_time ) ); $notification_data = [ 'object_id' => $this->getId(), 'user_id' => $this->getUser(), 'priority_id' => 1, //Critical, likely a missed punch scenario, try to get attention. 'type_id' => 'reminder_punch_lunch_in', 'object_type_id' => 120, //120=PunchFactory 'effective_date' => $end_lunch_time, 'title_short' => $notification_title_short, 'title_long' => $notification_title_long, 'body_short' => $notification_body, 'payload' => $payload, ]; Notification::sendNotification( $notification_data ); } else { Debug::text( ' No meal policy, unable to add post-dated reminder notification for end of lunch...', __FILE__, __LINE__, __METHOD__, 10 ); } unset( $mplf ); break; case 30: //Break Notification::deletePendingNotifications( [ 'reminder_punch_break_out', 'reminder_punch_transfer' ], $this->getUser(), null, $this->getTimeStamp() ); //Add reminder for end of break. $break_policy_arr = $this->getBreakPolicies(); $bplf = $break_policy_arr['lf']; //FIXME: How do we handle multiple breaks of different lengths, or combined breaks? if ( $bplf->getRecordCount() > 0 ) { foreach ( $bplf as $bp_obj ) { if ( $bp_obj->getAutoDetectType() == 20 ) { //20=Punch Time $end_break_time = ( $this->getTimeStamp() + $bp_obj->getMaximumPunchTime() ); //Max punch time could be different than break time, so this instead of getAmount(). } else { $end_break_time = ( $this->getTimeStamp() + $bp_obj->getAmount() ); } Debug::text( ' Add post-dated notification for end of break at: '. TTDate::getDate('DATE+TIME', $end_break_time ), __FILE__, __LINE__, __METHOD__, 10 ); $notification_title_short = TTi18n::getText( 'Reminder: Punch in from Break.' ); $notification_title_long = TTi18n::getText( 'Reminder: Punch in from Break by %1', TTDate::getDate('TIME', $end_break_time ) ); $notification_body = TTi18n::getText( 'Punch in from break by '. TTDate::getDate('TIME', $end_break_time ) ); $notification_data = [ 'object_id' => $this->getId(), 'user_id' => $this->getUser(), 'priority_id' => 1, //Critical, likely a missed punch scenario, try to get attention. 'type_id' => 'reminder_punch_break_in', 'object_type_id' => 120, //120=PunchFactory 'effective_date' => $end_break_time, 'title_short' => $notification_title_short, 'title_long' => $notification_title_long, 'body_short' => $notification_body, 'payload' => $payload, ]; Notification::sendNotification( $notification_data ); break; //Stop after first break policy. } } else { Debug::text( ' No break policy, unable to add post-dated reminder notification for end of break...', __FILE__, __LINE__, __METHOD__, 10 ); } unset( $bplf ); break; } break; } } /** * Takes Punch rows and calculates the total breaks/lunches and how long each is. * @param $data * @return array|bool */ static function calcMealAndBreakTotalTime( $data ) { if ( is_array( $data ) && count( $data ) > 0 ) { $date_break_totals = array(); $tmp_date_break_totals = array(); //Sort data by date_stamp at the top, so it works for multiple days at a time. foreach ( $data as $row ) { if ( $row['type_id'] != 10 ) { if ( $row['status_id'] == 20 ) { $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['prev'] = $row['raw_time_stamp']; } else if ( isset( $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['prev'] ) ) { if ( !isset( $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_time'] ) ) { $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_time'] = 0; } $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_time'] = bcadd( $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_time'], bcsub( $row['raw_time_stamp'], $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['prev'] ) ); if ( !isset( $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_breaks'] ) ) { $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_breaks'] = 0; } $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_breaks']++; if ( $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_time'] > 0 ) { if ( $row['type_id'] == 20 ) { $break_name = TTi18n::gettext( 'Lunch (Taken)' ); } else { $break_name = TTi18n::gettext( 'Break (Taken)' ); } $date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ] = array( 'break_name' => $break_name, 'total_time' => $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_time'], 'total_breaks' => $tmp_date_break_totals[ $row['date_stamp'] ][ $row['type_id'] ]['total_breaks'], ); unset( $break_name ); } } } } if ( empty( $date_break_totals ) == false ) { return $date_break_totals; } } return false; } /** * @param $data * @return bool */ function setObjectFromArray( $data ) { if ( is_array( $data ) ) { //We need to set the UserID as soon as possible. if ( isset( $data['user_id'] ) && $data['user_id'] != '' ) { Debug::text( 'Setting User ID: ' . $data['user_id'], __FILE__, __LINE__, __METHOD__, 10 ); $this->setUser( $data['user_id'] ); } /* //We need to set the UserDate as soon as possible. if ( isset($data['user_id']) AND $data['user_id'] != '' AND isset($data['date_stamp']) AND $data['date_stamp'] != '' AND isset($data['start_time']) AND $data['start_time'] != '' ) { Debug::text('Setting User Date ID based on User ID:'. $data['user_id'] .' Date Stamp: '. $data['date_stamp'] .' Start Time: '. $data['start_time'], __FILE__, __LINE__, __METHOD__, 10); $this->setUserDate( $data['user_id'], TTDate::parseDateTime( $data['date_stamp'].' '.$data['start_time'] ) ); } elseif ( isset( $data['user_date_id'] ) AND $data['user_date_id'] > 0 ) { Debug::text(' Setting UserDateID: '. $data['user_date_id'], __FILE__, __LINE__, __METHOD__, 10); $this->setUserDateID( $data['user_date_id'] ); } else { Debug::text(' NOT CALLING setUserDate or setUserDateID!', __FILE__, __LINE__, __METHOD__, 10); } if ( isset($data['overwrite']) ) { $this->setEnableOverwrite( TRUE ); } */ /* ORDER IS EXTREMELY IMPORTANT FOR THIS FUNCTION: 1. $pf->setUser(); 1b. $pf->setTransfer() //include timestamp for this. 2. $pf->setType(); 3. $pf->setStatus(); 4. $pf->setTimeStamp(); 5. $pf->setPunchControlID(); All these related fields MUST be passed to this function as well, even if they are blank. */ //Parse time stamp above loop so we don't have to do it twice. if ( isset( $data['epoch'] ) && $data['epoch'] != '' ) { $full_time_stamp = $data['epoch']; } else if ( isset( $data['punch_date'] ) && $data['punch_date'] != '' && isset( $data['punch_time'] ) && $data['punch_time'] != '' ) { $full_time_stamp = TTDate::parseDateTime( $data['punch_date'] . ' ' . $data['punch_time'] ); //Debug::text('Setting Punch Time/Date: Date Stamp: '. $data['punch_date'] .' Time Stamp: '. $data['punch_time'] .' Full Time Stamp: '. $data['full_time_stamp'] .' Parsed: '. TTDate::getDate('DATE+TIME', $full_time_stamp ), __FILE__, __LINE__, __METHOD__, 10); } else if ( isset( $data['time_stamp'] ) && $data['time_stamp'] != '' ) { $full_time_stamp = TTDate::parseDateTime( $data['time_stamp'] ); } else { $full_time_stamp = null; } $variable_function_map = $this->getVariableToFunctionMap(); foreach ( $variable_function_map as $key => $function ) { if ( isset( $data[ $key ] ) ) { $function = 'set' . $function; switch ( $key ) { case 'transfer': $this->$function( $data[ $key ], $full_time_stamp ); //Assume time_stamp contains date as well. if ( $this->getTransfer() == true ) { $this->setEnableAutoTransfer( true ); } break; case 'split_punch_control': $this->setEnableSplitPunchControl( $data[ $key ] ); break; case 'time_stamp': if ( method_exists( $this, $function ) ) { if ( isset( $data['disable_rounding'] ) && $data['disable_rounding'] == true ) { $enable_rounding = false; } else { $enable_rounding = true; } $this->$function( $full_time_stamp, $enable_rounding ); //Assume time_stamp contains date as well. } break; case 'actual_time_stamp': //Ignore actual/original timestamps. case 'original_time_stamp': break; case 'punch_control_id': //If this is a new punch or punch_contol_id is not being set, find a new one to use. if ( $data['punch_control_id'] == '' || $data['punch_control_id'] == TTUUID::getZeroID() ) { $this->setPunchControlID( $this->findPunchControlID() ); Debug::text( 'Setting Punch Control ID: ' . $this->getPunchControlID() . ' Was passed: ' . $data['punch_control_id'], __FILE__, __LINE__, __METHOD__, 10 ); } else { Debug::text( 'Valid Punch Control ID passed...', __FILE__, __LINE__, __METHOD__, 10 ); $this->$function( $data[ $key ] ); } break; default: if ( method_exists( $this, $function ) ) { $this->$function( $data[ $key ] ); } break; } } } //Handle actual/original timestamp at the end, as we need to make sure we have the full_time_stamp set first. if ( $this->isNew() == true && $full_time_stamp != null ) { Debug::text( 'Setting actual/original timestamp: ' . $full_time_stamp, __FILE__, __LINE__, __METHOD__, 10 ); $this->setActualTimeStamp( $full_time_stamp ); //$this->setOriginalTimeStamp( $this->getTimeStamp() ); //set in preSave() } else { Debug::text( 'NOT setting actual/original timestamp...', __FILE__, __LINE__, __METHOD__, 10 ); } $this->setCreatedAndUpdatedColumns( $data ); return true; } return false; } /** * @param null $include_columns * @param bool $permission_children_ids * @return array */ function getObjectAsArray( $include_columns = null, $permission_children_ids = false ) { $sf = TTnew( 'StationFactory' ); /** @var StationFactory $sf */ $data = array(); $variable_function_map = $this->getVariableToFunctionMap(); if ( is_array( $variable_function_map ) ) { foreach ( $variable_function_map as $variable => $function_stub ) { if ( $include_columns == null || ( isset( $include_columns[ $variable ] ) && $include_columns[ $variable ] == true ) ) { $function = 'get' . $function_stub; switch ( $variable ) { case 'user_id': case 'first_name': case 'last_name': case 'user_status_id': case 'group_id': case 'group': case 'title_id': case 'title': case 'default_branch_id': case 'default_branch': case 'default_department_id': case 'default_department': case 'default_job_id': case 'default_job': case 'default_job_item_id': case 'default_job_item': case 'pay_period_id': case 'total_time': //Used for Map, Distance tab. case 'branch_id': case 'branch': case 'department_id': case 'department': case 'job_id': case 'job': case 'job_manual_id': case 'job_item_id': case 'job_item': case 'job_item_manual_id': case 'quantity': case 'bad_quantity': case 'user_date_id': case 'meal_policy_id': case 'note': case 'station_type_id': case 'station_station_id': case 'station_source': case 'station_description': $data[ $variable ] = $this->getColumn( $variable ); break; case 'punch_tag_id': $data[ $variable ] = json_decode( $this->getColumn( $variable ), true ); break; case 'punch_tag': //Punch Tags for display purposes. //These functions are slow to obtain (especially in a large loop), so make sure the column is requested explicitly before we include it. //Flex currently doesn't specify these fields in the Edit view though, so this breaks Flex. if ( getTTProductEdition() >= TT_PRODUCT_CORPORATE && $this->isClientFriendly() == true || ( isset( $include_columns[$variable] ) AND $include_columns[$variable] == TRUE ) ) { $data[ $variable ] = PunchTagListFactory::getStringByIDs( json_decode( $this->getColumn( 'punch_tag_id' ), true ) ); } break; case 'status': case 'type': $function = 'get' . $variable; if ( method_exists( $this, $function ) ) { $data[ $variable ] = Option::getByKey( $this->$function(), $this->getOptions( $variable ) ); } break; case 'date_stamp': //Date the punch falls on for timesheet generation. The punch itself may have a different date. $data[ $variable ] = TTDate::getAPIDate( 'DATE', TTDate::strtotime( $this->getColumn( 'date_stamp' ) ) ); break; case 'time_stamp': //Full date/time of the punch itself. $data[ $variable ] = TTDate::getAPIDate( 'DATE+TIME', TTDate::strtotime( $this->getColumn( 'time_stamp' ) ) ); break; case 'raw_time_stamp': //Need a epoch value of the timestamp, so PunchFactory::calcMealAndBreakTotalTime() can use it for calculating Lunch/Break (Taken) when seconds are involved. // Otherwise returning getAPIDate() excludes the seconds if the users time format doesn't include them, and they can mismatch. $data[ $variable ] = TTDate::strtotime( $this->getColumn( 'time_stamp' ) ); break; case 'punch_date': //Just date portion of the punch $data[ $variable ] = TTDate::getAPIDate( 'DATE', TTDate::strtotime( $this->getColumn( 'time_stamp' ) ) ); break; case 'punch_time': //Just the time portion of the punch $data[ $variable ] = TTDate::getAPIDate( 'TIME', TTDate::strtotime( $this->getColumn( 'time_stamp' ) ) ); break; case 'original_time_stamp': $data[ $variable ] = TTDate::getAPIDate( 'DATE+TIME', TTDate::strtotime( $this->getColumn( 'original_time_stamp' ) ) ); break; case 'actual_time_stamp': $data[ $variable ] = TTDate::getAPIDate( 'DATE+TIME', TTDate::strtotime( $this->getColumn( 'actual_time_stamp' ) ) ); break; case 'actual_time': $data[ $variable ] = TTDate::getAPIDate( 'TIME', TTDate::strtotime( $this->getColumn( 'actual_time_stamp' ) ) ); break; case 'actual_time_diff': $data[ $variable ] = TTDate::getTimeUnit( $this->getColumn( 'actual_time_diff' ) ); break; case 'station_type': $data[ $variable ] = Option::getByKey( $this->getColumn( 'station_type_id' ), $sf->getOptions( 'type' ) ); break; default: if ( method_exists( $this, $function ) ) { $data[ $variable ] = $this->$function(); } break; } } } $this->getPermissionColumns( $data, $this->getColumn( 'user_id' ), $this->getCreatedBy(), $permission_children_ids, $include_columns ); $this->getCreatedAndUpdatedColumns( $data, $include_columns ); $data = $this->getCustomFields( $this->getUserObject()->getCompany(), $data, $include_columns ); } return $data; } /** * @param $log_action * @return bool */ function addLog( $log_action ) { return TTLog::addEntry( $this->getId(), $log_action, TTi18n::getText( 'Punch - Employee' ) . ': ' . UserListFactory::getFullNameById( $this->getUser() ) . ' ' . TTi18n::getText( 'Timestamp' ) . ': ' . TTDate::getDate( 'DATE+TIME', $this->getTimeStamp() ), null, $this->getTable(), $this ); } static function convertSetUserPunchToGetPunch( $queued_punch_data ) { $pf = new PunchFactory(); $retarr = [ 'id' => TTUUID::getNotExistID(), 'user_id' => $queued_punch_data['user_id'], 'transfer' => $queued_punch_data['transfer'], 'type_id' => $queued_punch_data['type_id'], 'type' => Option::getByKey( $queued_punch_data['type_id'], $pf->getOptions( 'type' ) ), 'status_id' => $queued_punch_data['status_id'], 'status' => Option::getByKey( $queued_punch_data['status_id'], $pf->getOptions( 'status' ) ), 'time_stamp' => TTDate::getDate('DATE+TIME', $queued_punch_data['epoch'] ), 'raw_time_stamp' => $queued_punch_data['epoch'], 'punch_date' => TTDate::getDate('DATE', $queued_punch_data['epoch'] ), 'punch_time' => TTDate::getDate('TIME', $queued_punch_data['epoch'] ), 'punch_control_id' => ( isset( $queued_punch_data['punch_control_id'] ) ) ? $queued_punch_data['punch_control_id'] : null, 'longitude' => null, 'latitude' => null, 'position_accuracy' => null, 'date_stamp' => TTDate::getDate('DATE', $queued_punch_data['epoch'] ), 'pay_period_id' => null, 'note' => $queued_punch_data['note'], 'tainted' => false, 'has_image' => false, 'branch_id' => $queued_punch_data['branch_id'], 'department_id' => $queued_punch_data['department_id'], 'job_id' => $queued_punch_data['job_id'], 'job_item_id' => $queued_punch_data['job_item_id'], ]; return $retarr; } /** * Save real-time punches through the JobQueue * @param $data * @param $validate_only * @param $ignore_warning * @param $validator_stats * @param $validator * @param $save_result * @param $key * @return array * @throws DBError * @throws ReflectionException */ static function setUserPunchForJobQueue( $data, $validate_only, $ignore_warning, $validator_stats, $validator, $save_result, $key ) { //**IMPORTANT** The users timezone/preferences should all be set before this is called. Currently that is done in SystemJobQueueFactory::run() Debug::Text( ' Saving Punch from job queue...', __FILE__, __LINE__, __METHOD__, 10 ); $pf = TTnew('PunchFactory'); [ $validator, $validator_stats, $key, $save_result ] = $pf->setUserPunch( $data, $validate_only, $ignore_warning, $validator_stats, $validator, $save_result, $key ); //Since we are saving the punch in the background, we need to send a notification if their is a validation failure. if ( !( $validator_stats['valid_records'] > 0 && $validator_stats['total_records'] == $validator_stats['valid_records'] ) ) { Debug::Arr( $validator, ' Punch failed validation check!', __FILE__, __LINE__, __METHOD__, 10 ); $validator_obj = new Validator(); $notification_data = [ 'object_id' => TTUUID::getZeroID(), 'user_id' => $data['user_id'], 'type_id' => 'punch', 'object_type_id' => 120, 'priority' => 2, //2=High 'title_short' => TTi18n::getText( 'Punch Save Failed: %1', [ TTDate::getDate('DATE+TIME', $data['epoch'] ) ] ), 'body_short' => $validator_obj->getTextErrors( false, $validator ), 'payload' => [ 'link' => Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getDefaultInterfaceBaseURL() . 'html5/#!m=TimeSheet' ], ]; Notification::sendNotification( $notification_data ); } //Send a direct background notification to tell the browser to refresh the job queue. Do this even if the punch fails to save. SystemJobQueue::sendNotificationToBrowser( $data['user_id'] ); return [ $validator, $validator_stats, $key, $save_result ]; } /** * Helper function to save real-time punches from both the API and the JobQueue. * @param $data * @param $validate_only * @param $ignore_warning * @param $validator_stats * @param $validator * @param $save_result * @param $key * @return mixed|null * @throws DBError */ function setUserPunch( $data, $validate_only, $ignore_warning, $validator_stats, $validator, $save_result, $key ) { $transaction_function = function () use ( $data, $validate_only, $ignore_warning, $validator_stats, $validator, $save_result, $key ) { $lf = TTnew( 'PunchFactory' ); /** @var PunchFactory $lf */ if ( $validate_only == false ) { //Only switch into serializable mode when actually saving the record. $lf->setTransactionMode( 'REPEATABLE READ' ); //Required to help prevent duplicate simulataneous HTTP requests from causing incorrect calculations in user_date_total table. } $lf->StartTransaction(); //We can't set the PunchControlID from $data, otherwise findPunchControlID() will never work. //This causes transfer punches to fail. if ( isset( $data['punch_control_id'] ) ) { $tmp_punch_control_id = $data['punch_control_id']; unset( $data['punch_control_id'] ); } $lf->setObjectFromArray( $data ); //Checking the station here prevents the user from bypassing the station allowed checks. // It also delays updating the station record allowed_date until the punch is saved, turning getUserPunch() into a 100% read-only SQL queries and deferring all writes to the job queue on save in high load cases. $is_station_allowed = false; if ( is_object( $lf->getStationObject() ) && isset( $data['_server_remote_addr'] ) ) { $_SERVER['REMOTE_ADDR'] = $data['_server_remote_addr']; //Overwrite this global variable so Misc::getRemoteIPAddress() can obtain it in $station_obj->checkAllowed() below. $station_obj = $lf->getStationObject(); //Don't update allowed_date due to concurrency issues with many users from the same station record. // Also for PC stations its not that helpful anyways. // postgres9 error: [-29: ERROR: could not serialize access due to concurrent update] in EXECUTE("UPDATE station as b set allowed_date = ? WHERE EXISTS ( SELECT null FROM station as a where b.id = a.id and b.id = ? FOR UPDATE OF A SKIP LOCKED )") if ( $station_obj->checkAllowed( $lf->getUser(), $station_obj->getStation(), $station_obj->getType(), false ) == true ) { $is_station_allowed = true; } } if ( $is_station_allowed == true ) { if ( isset( $data['status_id'] ) && $data['status_id'] == 20 && isset( $tmp_punch_control_id ) && $tmp_punch_control_id != '' ) { $lf->setPunchControlID( $tmp_punch_control_id ); } else { $lf->setPunchControlID( $lf->findPunchControlID() ); } unset( $tmp_punch_control_id ); $lf->setEnablePreMatureException( true ); //Enable pre-mature exceptions at this point. $key = 0; $is_valid = $lf->isValid( $ignore_warning ); if ( $is_valid == true ) { Debug::Text( 'Saving data...', __FILE__, __LINE__, __METHOD__, 10 ); if ( $validate_only == true ) { $save_result[$key] = true; $validator_stats['valid_records']++; } else { //Save Punch object and start on PunchControl if ( $save_result[$key] = $lf->Save( false ) == true ) { unset( $data['id'] ); //ID must be removed so it doesn't get confused with PunchControlID Debug::Text( 'Saving PCF data... Punch Control ID: ' . $lf->getPunchControlID(), __FILE__, __LINE__, __METHOD__, 10 ); $pcf = TTnew( 'PunchControlFactory' ); /** @var PunchControlFactory $pcf */ $pcf->setId( $lf->getPunchControlID() ); $pcf->setPunchObject( $lf ); $pcf->setObjectFromArray( $data ); $pcf->setEnableStrictJobValidation( true ); $pcf->setEnableCalcUserDateID( true ); $pcf->setEnableCalcTotalTime( true ); $pcf->setEnableCalcSystemTotalTime( true ); $pcf->setEnableCalcWeeklySystemTotalTime( true ); $pcf->setEnableCalcUserDateTotal( true ); $pcf->setEnableCalcException( true ); $pcf->setEnablePreMatureException( true ); //Enable pre-mature exceptions at this point. Debug::Arr( $lf->data, 'Punch Object: ', __FILE__, __LINE__, __METHOD__, 10 ); Debug::Arr( $pcf->data, 'Punch Control Object: ', __FILE__, __LINE__, __METHOD__, 10 ); if ( $pcf->isValid() ) { $validator_stats['valid_records']++; if ( $pcf->Save( true, true ) != true ) { //Force isNew() lookup. $is_valid = $pcf_valid = false; } } else { $is_valid = $pcf_valid = false; } } } } if ( $is_valid == false ) { Debug::Text( 'Data is Invalid...', __FILE__, __LINE__, __METHOD__, 10 ); $lf->FailTransaction(); //Just rollback this single record, continue on to the rest. Debug::Text( 'PF Data is Invalid...', __FILE__, __LINE__, __METHOD__, 10 ); $validator[$key] = $lf->Validator->getErrorsArray(); //Merge PCF validation errors onto array. if ( isset( $pcf ) && $pcf_valid == false ) { Debug::Text( 'PCF Data is Invalid...', __FILE__, __LINE__, __METHOD__, 10 ); $validator[$key] += $pcf->Validator->getErrorsArray(); } } } else { Debug::Text( 'Station IS NOT Allowed! ID: ' . $lf->getStation() . ' User ID: ' . $lf->getUser(), __FILE__, __LINE__, __METHOD__, 10 ); $lf->Validator->isTrue( 'user_name', false, TTi18n::gettext( 'You are not authorized to punch in or out from this station!' ) ); $validator[$key] = $lf->Validator->getErrorsArray(); } $lf->CommitTransaction(); $lf->setTransactionMode(); //Back to default isolation level. return [ $validator, $validator_stats, $key, $save_result ]; }; return $this->RetryTransaction( $transaction_function ); } } ?>