Some refactoring and commenting.
[timetracker.git] / plugins / MonthlyQuota.class.php
1 <?php
2 // +----------------------------------------------------------------------+
3 // | Anuko Time Tracker
4 // +----------------------------------------------------------------------+
5 // | Copyright (c) Anuko International Ltd. (https://www.anuko.com)
6 // +----------------------------------------------------------------------+
7 // | LIBERAL FREEWARE LICENSE: This source code document may be used
8 // | by anyone for any purpose, and freely redistributed alone or in
9 // | combination with other software, provided that the license is obeyed.
10 // |
11 // | There are only two ways to violate the license:
12 // |
13 // | 1. To redistribute this code in source form, with the copyright
14 // |    notice or license removed or altered. (Distributing in compiled
15 // |    forms without embedded copyright notices is permitted).
16 // |
17 // | 2. To redistribute modified versions of this code in *any* form
18 // |    that bears insufficient indications that the modifications are
19 // |    not the work of the original author(s).
20 // |
21 // | This license applies to this document only, not any other software
22 // | that it may be combined with.
23 // |
24 // +----------------------------------------------------------------------+
25 // | Contributors:
26 // | https://www.anuko.com/time_tracker/credits.htm
27 // +----------------------------------------------------------------------+
28
29 class MonthlyQuota {
30     
31     var $db;       // Database connection.
32     var $holidays; // Array of holidays from localization file.
33     var $team_id;  // Team id.
34
35     // 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.
36     function __construct() {
37         $this->db = getConnection();
38         $i18n = $GLOBALS['I18N'];
39         $this->holidays = $i18n->holidays;
40         global $user;
41         $this->team_id = $user->team_id;
42     }
43
44     // update - deletes a quota, then inserts a new one.
45     public function update($year, $month, $quota) {
46         $teamId = $this->team_id;
47         $deleteSql = "DELETE FROM tt_monthly_quotas WHERE year = $year AND month = $month AND team_id = $teamId";
48         $this->db->exec($deleteSql);
49         if ($quota){
50             $insertSql = "INSERT INTO tt_monthly_quotas (team_id, year, month, quota) values ($teamId, $year, $month, $quota)";
51             $affected = $this->db->exec($insertSql);
52             return (!is_a($affected, 'PEAR_Error'));
53         }
54         return true;
55     }
56
57     // get - obains either a single month quota or an array of quotas for an entire year.
58     public function get($year, $month) {
59         if (is_null($month)){
60             return $this->getMany($year);
61         }
62         return $this->getSingle($year, $month);
63     }
64
65     // getWorkdayHours - obtains workday_hours value for a team from the database.
66     public function getWorkdayHours(){
67         $teamId = $this->team_id;
68         $sql = "SELECT workday_hours FROM tt_teams where id = $teamId";
69         $reader = $this->db->query($sql);
70         if (is_a($reader, 'PEAR_Error')) {
71             return false;
72         }
73
74         $row = $reader->fetchRow();
75         return $row['workday_hours'];
76     }
77
78     // getSingle - obtains a quota for a single month.
79     private function getSingle($year, $month) {
80         $teamId = $this->team_id;
81         $sql = "SELECT quota FROM tt_monthly_quotas WHERE year = $year AND month = $month AND team_id = $teamId";
82         $reader = $this->db->query($sql);
83         if (is_a($reader, 'PEAR_Error')) {
84             return false;
85         }
86         
87         $row = $reader->fetchRow();
88         if ($row)
89           return $row['quota'];
90         
91         // If we did not find a record, return a calculated monthly quota.
92         $holidaysWithYear = array();
93         foreach ($this->holidays as $day) {
94             $parts = explode("/", $day);
95             $holiday = "$year-$parts[0]-$parts[1]";
96             array_push($holidaysWithYear, $holiday);
97         }
98     
99         $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
100         return $this->getWorkingDays("$year-$month-01", "$year-$month-$daysInMonth", $holidaysWithYear) * $this->getWorkdayHours();
101     }
102
103     // getMany - returns an array of quotas for a given year for team.
104     private function getMany($year){
105         $teamId = $this->team_id;
106         $sql = "SELECT month, quota FROM tt_monthly_quotas WHERE year = $year AND team_id = $teamId";
107         $result = array();
108         $res = $this->db->query($sql);
109         if (is_a($res, 'PEAR_Error')) {
110             return false;
111         }
112         
113         while ($val = $res->fetchRow()) {
114             $result[$val['month']] = $val['quota'];
115         }        
116         
117         return $result;
118     }
119     
120     // The function returns the number of business days between two dates skipping holidays.
121     private function getWorkingDays($startDate, $endDate, $holidays) {
122         // TODO: this function needs a fix, as it assumes Sat + Sun weekends, 
123         // and will not work properly for other week types (ex. Arabic weeks).
124
125         // do strtotime calculations just once
126         $endDate = strtotime($endDate);
127         $startDate = strtotime($startDate);
128
129         //The total number of days between the two dates. We compute the no. of seconds and divide it to 60*60*24
130         //We add one to inlude both dates in the interval.
131         $days = ($endDate - $startDate) / 86400 + 1;
132
133         $noOfFullWeeks = floor($days / 7);
134         $noOfRemainingDays = fmod($days, 7);
135
136         //It will return 1 if it's Monday,.. ,7 for Sunday
137         $firstDayofWeek = date("N", $startDate);
138         $lastDayofWeek = date("N", $endDate);
139
140         //---->The two can be equal in leap years when february has 29 days, the equal sign is added here
141         //In the first case the whole interval is within a week, in the second case the interval falls in two weeks.
142         if ($firstDayofWeek <= $lastDayofWeek) {
143             if ($firstDayofWeek <= 6 && 6 <= $lastDayofWeek) {
144                 $noOfRemainingDays--;                
145             }
146             
147             if ($firstDayofWeek <= 7 && 7 <= $lastDayofWeek) {
148                 $noOfRemainingDays--;
149             }
150         }
151         else {
152             // (edit by Tokes to fix an edge case where the start day was a Sunday
153             // and the end day was NOT a Saturday)
154
155             // the day of the week for start is later than the day of the week for end
156             if ($firstDayofWeek == 7) {
157                 // if the start date is a Sunday, then we definitely subtract 1 day
158                 $noOfRemainingDays--;
159
160                 if ($lastDayofWeek == 6) {
161                     // if the end date is a Saturday, then we subtract another day
162                     $noOfRemainingDays--;
163                 }
164             }
165             else {
166                 // the start date was a Saturday (or earlier), and the end date was (Mon..Fri)
167                 // so we skip an entire weekend and subtract 2 days
168                 $noOfRemainingDays -= 2;
169             }
170         }
171
172         //T he no. of business days is: (number of weeks between the two dates) * (5 working days) + the remainder
173         // ---->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
174         $workingDays = $noOfFullWeeks * 5;
175         if ($noOfRemainingDays > 0 ) {
176             $workingDays += $noOfRemainingDays;
177         }
178
179         // We subtract the holidays
180         foreach($holidays as $holiday){
181             $timeStamp = strtotime($holiday);
182             // If the holiday doesn't fall in weekend
183             // TODO: add handling for countries where they move non working day to first working day if holiday is on weekends
184             if ($startDate <= $timeStamp && $timeStamp <= $endDate && date("N", $timeStamp) != 6 && date("N", $timeStamp ) != 7)
185                 $workingDays--;
186         }
187
188         return $workingDays;
189     }
190 }