Adjusted monthly quotas plugin to use configurable holidays.
[timetracker.git] / plugins / MonthlyQuota.class.php
index ed1c9f8..c8559ad 100644 (file)
 <?php
+// +----------------------------------------------------------------------+
+// | Anuko Time Tracker
+// +----------------------------------------------------------------------+
+// | Copyright (c) Anuko International Ltd. (https://www.anuko.com)
+// +----------------------------------------------------------------------+
+// | LIBERAL FREEWARE LICENSE: This source code document may be used
+// | by anyone for any purpose, and freely redistributed alone or in
+// | combination with other software, provided that the license is obeyed.
+// |
+// | There are only two ways to violate the license:
+// |
+// | 1. To redistribute this code in source form, with the copyright
+// |    notice or license removed or altered. (Distributing in compiled
+// |    forms without embedded copyright notices is permitted).
+// |
+// | 2. To redistribute modified versions of this code in *any* form
+// |    that bears insufficient indications that the modifications are
+// |    not the work of the original author(s).
+// |
+// | This license applies to this document only, not any other software
+// | that it may be combined with.
+// |
+// +----------------------------------------------------------------------+
+// | Contributors:
+// | https://www.anuko.com/time_tracker/credits.htm
+// +----------------------------------------------------------------------+
 
+import('ttTimeHelper');
+
+// MontlyQuota class implements handling of work hour quotas.
 class MonthlyQuota {
-    
-    var $db;
-    var $holidays;
-    // Old style constructors are DEPRECATED in PHP 7.0, and will be removed in a future version. You should always use __construct() in new code.
-    function __construct() {
-        $this->db = getConnection();
-        $i18n = $GLOBALS['I18N'];
-        $this->holidays = $i18n->holidays;
+
+  var $db;       // Database connection.
+  var $group_id; // Group id.
+  var $org_id;   // Organization id.
+
+  function __construct() {
+    $this->db = getConnection();
+    global $user;
+    $this->group_id = $user->getGroup();
+    $this->org_id = $user->org_id;
+  }
+
+  // update - deletes a quota, then inserts a new one.
+  public function update($year, $month, $minutes) {
+    $deleteSql = "delete from tt_monthly_quotas".
+      " where year = $year and month = $month and group_id = $this->group_id and org_id = $this->org_id";
+    $this->db->exec($deleteSql);
+    if ($minutes){
+      $insertSql = "insert into tt_monthly_quotas (group_id, org_id, year, month, minutes)".
+        " values ($this->group_id, $this->org_id, $year, $month, $minutes)";
+      $affected = $this->db->exec($insertSql);
+      return (!is_a($affected, 'PEAR_Error'));
+    }
+    return true;
+  }
+
+  // get - obtains either a single month quota or an array of quotas for an entire year.
+  // Month starts with 1 for January, not 0.
+  public function get($year, $month = null) {
+    if (is_null($month)){
+      return $this->getMany($year);
+    }
+    return $this->getSingle($year, $month);
+  }
+
+  // getSingle - obtains a quota for a single month.
+  private function getSingle($year, $month) {
+    $sql = "select minutes from tt_monthly_quotas".
+      " where year = $year and month = $month and group_id = $this->group_id and org_id = $this->org_id";
+    $reader = $this->db->query($sql);
+    if (is_a($reader, 'PEAR_Error')) {
+      return false;
+    }
+
+    $row = $reader->fetchRow();
+    if ($row)
+      return $row['minutes'];
+
+    // If we did not find a record, return a calculated monthly quota.
+    $numWorkdays = $this->getNumWorkdays($month, $year);
+    global $user;
+    return $numWorkdays * $user->getWorkdayMinutes();
+  }
+
+  // getUserQuota - obtains a quota for user for a single month.
+  // This quota is adjusted by quota_percent value for user.
+  public function getUserQuota($year, $month) {
+    global $user;
+
+    $minutes = $this->getSingle($year, $month);
+    $userMinutes = (int) $minutes * $user->getQuotaPercent() / 100;
+    return $userMinutes;
+  }
+
+  // getUserQuotaFrom1st - obtains a quota for user
+  // from 1st of the month up to and inclusive of $selected_date.
+  public function getUserQuotaFrom1st($selected_date) {
+    // TODO: we may need a better algorithm here. Review.
+    $monthQuotaMinutes = $this->getUserQuota($selected_date->mYear, $selected_date->mMonth);
+    $workdaysInMonth = $this->getNumWorkdays($selected_date->mMonth, $selected_date->mYear);
+
+    // Iterate from 1st up to selected date.
+    $workdaysFrom1st = 0;
+    for ($i = 1; $i <= $selected_date->mDate; $i++) {
+      $date = ttTimeHelper::dateInDatabaseFormat($selected_date->mYear, $selected_date->mMonth, $i);
+      if (!ttTimeHelper::isWeekend($date) && !ttTimeHelper::isHoliday($date)) {
+        $workdaysFrom1st++;
+      }
+    }
+    $quotaMinutesFrom1st = (int) ($monthQuotaMinutes * $workdaysFrom1st / $workdaysInMonth);
+    return $quotaMinutesFrom1st;
+  }
+
+  // getMany - returns an array of quotas for a given year for group.
+  private function getMany($year){
+    $sql = "select month, minutes from tt_monthly_quotas".
+      " where year = $year and group_id = $this->group_id and org_id = $this->org_id";
+    $result = array();
+    $res = $this->db->query($sql);
+    if (is_a($res, 'PEAR_Error')) {
+      return false;
+    }
+
+    while ($val = $res->fetchRow()) {
+      $result[$val['month']] = $val['minutes'];
+    }
+
+    return $result;
+  }
+
+  // getNumWorkdays returns a number of work days in a given month.
+  private function getNumWorkdays($month, $year) {
+
+    $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); // Number of calendar days in month.
+
+    $workdaysInMonth = 0;
+    // Iterate through the entire month.
+    for ($i = 1; $i <= $daysInMonth; $i++) {
+      $date = "$year-$month-$i";
+      $date = ttTimeHelper::dateInDatabaseFormat($year, $month, $i);
+      if (!ttTimeHelper::isWeekend($date) && !ttTimeHelper::isHoliday($date)) {
+        $workdaysInMonth++;
+      }
+    }
+    return $workdaysInMonth;
+  }
+
+  // isValidQuota validates a localized value as an hours quota string (in hours and minutes).
+  public function isValidQuota($value) {
+
+    if (strlen($value) == 0 || !isset($value)) return true;
+
+    if (preg_match('/^[0-9]{1,3}h?$/', $value )) { // 000 - 999
+      return true;
     }
-    
-    public function update($year, $month, $quota) {
-        $deleteSql = "DELETE FROM tt_monthly_quota WHERE year = $year AND month = $month";
-        $this->db->exec($deleteSql);
-        $insertSql = "INSERT INTO tt_monthly_quota (year, month, quota) values ($year, $month, $quota)";
-        $affected = $this->db->exec($insertSql);
-        return (!is_a($affected, 'PEAR_Error'));
+
+    if (preg_match('/^[0-9]{1,3}:[0-5][0-9]$/', $value )) { // 000:00 - 999:59
+      return true;
     }
-        
-    public function get($year, $month) {
-        
-        if (is_null($month)){
-            return $this->getMany($year);
-        }
-        
-        return $this->getSingle($year, $month);
+
+    global $user;
+    $localizedPattern = '/^([0-9]{1,3})?['.$user->getDecimalMark().'][0-9]{1,4}h?$/';
+    if (preg_match($localizedPattern, $value )) { // decimal values like 000.5, 999.25h, ... .. 999.9999h (or with comma)
+      return true;
     }
-    
-    private function getSingle($year, $month) {
-        
-        $sql = "SELECT quota FROM tt_monthly_quota WHERE year = $year AND month = $month";
-        $reader = $this->db->query($sql);
-        if (is_a($reader, 'PEAR_Error')) {
-            return false;
-        }
-        
-        $row = $reader->fetchRow();
-        
-        // if we don't find a record, return calculated monthly quota
-        if (is_null($row)){
-            
-            $holidaysWithYear = array();
-            foreach ($this->holidays as $day) {
-                $parts = explode("/", $day);
-                $holiday = "$year-$parts[0]-$parts[1]";
-                array_push($holidaysWithYear, $holiday);
-            }
-            
-            $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
-            return $this->getWorkingDays("$year-$month-01", "$year-$month-$daysInMonth", $holidaysWithYear) * 8;
-        }
-        
-        return $row["quota"];  
+
+    return false;
+  }
+
+  // quotaToFloat converts a valid quota value to a float.
+  public function quotaToFloat($value) {
+
+    if (preg_match('/^[0-9]{1,3}h?$/', $value )) { // 000 - 999
+      return (float) $value;
     }
-    
-    private function getMany($year){
-        $sql = "SELECT year, month, quota FROM tt_monthly_quota WHERE year = $year";
-        $result = array();
-        $res = $this->db->query($sql);
-        if (is_a($res, 'PEAR_Error')) {
-            return false;
-        }
-        
-        while ($val = $res->fetchRow()) {
-            $result[$val["month"]] = $val["quota"];
-            // $result[] = $val;
-        }        
-        
-        return $result;
+
+    if (preg_match('/^[0-9]{1,3}:[0-5][0-9]$/', $value )) { // 000:00 - 999:59
+      $minutes = ttTimeHelper::toMinutes($value);
+      return ($minutes / 60.0);
     }
-    
-    //The function returns the no. of business days between two dates and it skips the holidays
-    private function getWorkingDays($startDate, $endDate, $holidays) {
-        // do strtotime calculations just once
-        $endDate = strtotime($endDate);
-        $startDate = strtotime($startDate);
-
-        //The total number of days between the two dates. We compute the no. of seconds and divide it to 60*60*24
-        //We add one to inlude both dates in the interval.
-        $days = ($endDate - $startDate) / 86400 + 1;
-
-        $noOfFullWeeks = floor($days / 7);
-        $noOfRemainingDays = fmod($days, 7);
-
-        //It will return 1 if it's Monday,.. ,7 for Sunday
-        $firstDayofWeek = date("N", $startDate);
-        $lastDayofWeek = date("N", $endDate);
-
-        //---->The two can be equal in leap years when february has 29 days, the equal sign is added here
-        //In the first case the whole interval is within a week, in the second case the interval falls in two weeks.
-        if ($firstDayofWeek <= $lastDayofWeek) {
-            if ($firstDayofWeek <= 6 && 6 <= $lastDayofWeek) {
-                $noOfRemainingDays--;                
-            }
-            
-            if ($firstDayofWeek <= 7 && 7 <= $lastDayofWeek) {
-                $noOfRemainingDays--;
-            }
-        }
-        else {
-            // (edit by Tokes to fix an edge case where the start day was a Sunday
-            // and the end day was NOT a Saturday)
-
-            // the day of the week for start is later than the day of the week for end
-            if ($firstDayofWeek == 7) {
-                // if the start date is a Sunday, then we definitely subtract 1 day
-                $noOfRemainingDays--;
-
-                if ($lastDayofWeek == 6) {
-                    // if the end date is a Saturday, then we subtract another day
-                    $noOfRemainingDays--;
-                }
-            }
-            else {
-                // the start date was a Saturday (or earlier), and the end date was (Mon..Fri)
-                // so we skip an entire weekend and subtract 2 days
-                $noOfRemainingDays -= 2;
-            }
-        }
-
-        //T he no. of business days is: (number of weeks between the two dates) * (5 working days) + the remainder
-        // ---->february in none leap years gave a remainder of 0 but still calculated weekends between first and last day, this is one way to fix it
-        $workingDays = $noOfFullWeeks * 5;
-        if ($noOfRemainingDays > 0 ) {
-            $workingDays += $noOfRemainingDays;
-        }
-
-        // We subtract the holidays
-        foreach($holidays as $holiday){
-            $timeStamp = strtotime($holiday);
-            // If the holiday doesn't fall in weekend
-            // TODO: add handling for countries where they move non working day to first working day if holiday is on weekends
-            if ($startDate <= $timeStamp && $timeStamp <= $endDate && date("N", $timeStamp) != 6 && date("N", $timeStamp ) != 7)
-                $workingDays--;
-        }
-
-        return $workingDays;
+
+    global $user;
+    $localizedPattern = '/^([0-9]{1,3})?['.$user->getDecimalMark().'][0-9]{1,4}h?$/';
+    if (preg_match($localizedPattern, $value )) { // decimal values like 000.5, 999.25h, ... .. 999.9999h (or with comma)
+      // Strip optional h in the end.
+      $value = trim($value, 'h');
+      if ($user->getDecimalMark() == ',')
+        $value = str_replace(',', '.', $value);
+      return (float) $value;
     }
-}
\ No newline at end of file
+
+    return null;
+  }
+}