Fixed rounding errors in monthly quotas.
[timetracker.git] / WEB-INF / lib / ttTimeHelper.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 import('DateAndTime');
30
31 // The ttTimeHelper is a class to help with time-related values.
32 class ttTimeHelper {
33
34   // isWeekend determines if $date falls on weekend.
35   static function isWeekend($date) {
36     $weekDay = date('w', strtotime($date));
37     return ($weekDay == WEEKEND_START_DAY || $weekDay == (WEEKEND_START_DAY + 1) % 7);
38   }
39
40   // isHoliday determines if $date falls on a holiday.
41   static function isHoliday($date) {
42     global $user;
43     global $i18n;
44
45     if (!$user->show_holidays) return false;
46
47     // $date is expected as string in DB_DATEFORMAT.
48     $month = date('m', strtotime($date));
49     $day = date('d', strtotime($date));
50     if (in_array($month.'/'.$day, $i18n->holidays))
51       return true;
52
53     return false;
54   }
55
56   // isValidTime validates a value as a time string.
57   static function isValidTime($value) {
58     if (strlen($value)==0 || !isset($value)) return false;
59
60     // 24 hour patterns.
61     if ($value == '24:00' || $value == '2400') return true;
62
63     if (preg_match('/^([0-1]{0,1}[0-9]|[2][0-3]):?[0-5][0-9]$/', $value )) { // 0:00 - 23:59, 000 - 2359
64       return true;
65     }
66     if (preg_match('/^([0-1]{0,1}[0-9]|[2][0-4])$/', $value )) { // 0 - 24
67       return true;
68     }
69
70     // 12 hour patterns
71     if (preg_match('/^[1-9]\s?(am|AM|pm|PM)$/', $value)) { // 1 - 9 am
72       return true;
73     }
74     if (preg_match('/^(0[1-9]|1[0-2])\s?(am|AM|pm|PM)$/', $value)) { // 01 - 12 am
75       return true;
76     }
77     if (preg_match('/^[1-9]:?[0-5][0-9]\s?(am|AM|pm|PM)$/', $value)) { // 1:00 - 9:59 am, 100 - 959 am
78       return true;
79     }
80     if (preg_match('/^(0[1-9]|1[0-2]):?[0-5][0-9]\s?(am|AM|pm|PM)$/', $value)) { // 01:00 - 12:59 am, 0100 - 1259 am
81       return true;
82     }
83
84     return false;
85   }
86
87   // isValidDuration validates a value as a time duration string (in hours and minutes).
88   static function isValidDuration($value) {
89     if (strlen($value) == 0 || !isset($value)) return false;
90
91     if ($value == '24:00' || $value == '2400') return true;
92
93     if (preg_match('/^([0-1]{0,1}[0-9]|2[0-3]):?[0-5][0-9]$/', $value )) { // 0:00 - 23:59, 000 - 2359
94       return true;
95     }
96     if (preg_match('/^([0-1]{0,1}[0-9]|2[0-4])h?$/', $value )) { // 0, 1 ... 24
97       return true;
98     }
99
100     global $user;
101     $localizedPattern = '/^([0-1]{0,1}[0-9]|2[0-3])?['.$user->decimal_mark.'][0-9]{1,4}h?$/';
102     if (preg_match($localizedPattern, $value )) { // decimal values like 0.5, 1.25h, ... .. 23.9999h (or with comma)
103       return true;
104     }
105
106     return false;
107   }
108
109   // postedDurationToMinutes - converts a value representing a duration
110   // (usually enetered in a form by a user) to integer number of minutes.
111   //
112   // At the moment, we have 2 variations of duration types:
113   //   1) A duration within a day, such as in a time entry.
114   //   These are less or equal to 24 hours.
115   //
116   //   2) A duration of a monthly quota, with max value of 31*24 hours.
117   //
118   // This function is generic to be used for both types.
119   // Other functions will be used to check for specific max values.
120   //
121   // Returns false if the value cannot be converted.
122   static function postedDurationToMinutes($duration) {
123     // Handle empty value.
124     if (!isset($duration) || strlen($duration) == 0)
125       return null; // Value is not set. Caller decides whether it is valid or not.
126
127     // Handle whole hours.
128     if (preg_match('/^\d{1,3}h?$/', $duration )) { // 0 - 999, 0h - 999h
129       $minutes = 60 * trim($duration, 'h');
130       return $minutes;
131     }
132
133     // Handle a normalized duration value.
134     if (preg_match('/^\d{1,3}:[0-5][0-9]$/', $duration )) { // 0:00 - 999:59
135       $time_array = explode(':', $duration);
136       $minutes = (int)@$time_array[1] + ((int)@$time_array[0]) * 60;
137       return $minutes;
138     }
139
140     // Handle localized fractional hours.
141     global $user;
142     $localizedPattern = '/^(\d{1,3})?['.$user->decimal_mark.'][0-9]{1,4}h?$/';
143     if (preg_match($localizedPattern, $duration )) { // decimal values like .5, 1.25h, ... .. 999.9999h (or with comma)
144         if ($user->decimal_mark == ',')
145           $duration = str_replace (',', '.', $duration);
146
147         $minutes = (int)round(60 * floatval($duration));
148         return $minutes;
149     }
150
151     // Handle minutes. Some users enter durations like 10m (meaning 10 minutes).
152     if (preg_match('/^\d{1,5}m$/', $duration )) { // 0m - 99999m
153       $minutes = (int) trim($duration, 'm');
154       return $minutes;
155     }
156
157     // Everything else is not a valid duration.
158     return false;
159   }
160
161   static function durationToMinutes($duration) {
162     $minutes = ttTimeHelper::postedDurationToMinutes($duration);
163     if (false === $minutes || $minutes > 24*60)
164       return false; // $duration is not valid for a day entry.
165     return $minutes;
166   }
167
168   static function quotaToMinutes($duration) {
169     $minutes = ttTimeHelper::postedDurationToMinutes($duration);
170     if (false === $minutes || $minutes > 31*24*60)
171       return false; // $duration is not valid for a monthly quota.
172     return $minutes;
173   }
174
175   // validateDuration - a future replacement of the isValidDuration above.
176   // Validates a passed in $value as a time duration string in hours and / or minutes.
177   // Returns either a normalized duration (hh:mm) or false if $value is invalid.
178   //
179   // This is a convenience function that allows users to pass in data in a variety of formats.
180   //
181   // 3 or 3h  - means 3 hours - normalized 3:00. Note: h and m letters are not localized.
182   // 0.25 or 0.25h or .25 or .25h - means a quarter of hour - normalized 0:15.
183   // 0,25 0r 0,25h or ,25 or ,25h - means the same as above for users with comma ad decimal mark.
184   // 1:30 - means 1 hour 30 mminutes - normalized 1:30.
185   // 25m - means 25 minutes - normalized 0:25.
186   static function validateDuration($value) {
187     // Handle empty value.
188     if (!isset($value) || strlen($value) == 0)
189       return false;
190
191     // Handle whole hours.
192     if (preg_match('/^([0-1]{0,1}[0-9]|2[0-4])h?$/', $value )) { // 0, 1 ... 24
193       $normalized = trim($value, 'h');
194       $normalized .= ':00';
195       return $normalized;
196     }
197     // Handle already normalized value.
198     if (preg_match('/^([0-1]{0,1}[0-9]|2[0-3]):?[0-5][0-9]$/', $value )) { // 0:00 - 23:59, 000 - 2359
199       return $value;
200     }
201     // Handle a special case of 24:00.
202     if ($value == '24:00') {
203       return $value;
204     }
205     // Handle localized fractional hours.
206     global $user;
207     $localizedPattern = '/^([0-1]{0,1}[0-9]|2[0-3])?['.$user->decimal_mark.'][0-9]{1,4}h?$/';
208     if (preg_match($localizedPattern, $value )) { // decimal values like 0.5, 1.25h, ... .. 23.9999h (or with comma)
209         if ($user->decimal_mark == ',')
210           $value = str_replace (',', '.', $value);
211
212         $val = floatval($value);
213         $mins = round($val * 60);
214         $hours = (string)((int)($mins / 60));
215         $mins = (string)($mins % 60);
216         if (strlen($mins) == 1)
217           $mins = '0' . $mins;
218         return $hours.':'.$mins;
219     }
220     // Handle minutes.
221     if (preg_match('/^\d{1,4}m$/', $value )) { // ddddm
222       $mins = (int) trim($value, 'm');
223       if ($mins > 1440) // More minutes than an entire day could hold.
224         return false;
225       $hours = (string)((int)($mins / 60));
226       $mins = (string)($mins % 60);
227       if (strlen($mins) == 1)
228         $mins = '0' . $mins;
229       return $hours.':'.$mins;
230     }
231     return false;
232   }
233
234   // normalizeDuration - converts a valid time duration string to format 00:00.
235   static function normalizeDuration($value, $leadingZero = true) {
236     $time_value = $value;
237
238     // If we have a decimal format - convert to time format 00:00.
239     global $user;
240     if ($user->decimal_mark == ',')
241       $time_value = str_replace (',', '.', $time_value);
242
243     if((strpos($time_value, '.') !== false) || (strpos($time_value, 'h') !== false)) {
244       $val = floatval($time_value);
245       $mins = round($val * 60);
246       $hours = (string)((int)($mins / 60));
247       $mins = (string)($mins % 60);
248       if ($leadingZero && strlen($hours) == 1)
249         $hours = '0'.$hours;
250       if (strlen($mins) == 1)
251         $mins = '0' . $mins;
252       return $hours.':'.$mins;
253     }
254
255     $time_a = explode(':', $time_value);
256     $res = '';
257
258     // 0-99
259     if ((strlen($time_value) >= 1) && (strlen($time_value) <= 2) && !isset($time_a[1])) {
260       $hours = $time_a[0];
261       if ($leadingZero && strlen($hours) == 1)
262         $hours = '0'.$hours;
263        return $hours.':00';
264     }
265
266     // 000-2359 (2400)
267     if ((strlen($time_value) >= 3) && (strlen($time_value) <= 4) && !isset($time_a[1])) {
268       if (strlen($time_value)==3) $time_value = '0'.$time_value;
269       $hours = substr($time_value,0,2);
270       if ($leadingZero && strlen($hours) == 1)
271         $hours = '0'.$hours;
272       return $hours.':'.substr($time_value,2,2);
273     }
274
275     // 0:00-23:59 (24:00)
276     if ((strlen($time_value) >= 4) && (strlen($time_value) <= 5) && isset($time_a[1])) {
277       $hours = $time_a[0];
278       if ($leadingZero && strlen($hours) == 1)
279         $hours = '0'.$hours;
280       return $hours.':'.$time_a[1];
281     }
282
283     return $res;
284   }
285
286   // toMinutes - converts a time string in format 00:00 to a number of minutes.
287   static function toMinutes($value) {
288     $time_a = explode(':', $value);
289     return (int)@$time_a[1] + ((int)@$time_a[0]) * 60;
290   }
291
292   // toAbsDuration - converts a number of minutes to format 0:00
293   // even if $minutes is negative.
294   static function toAbsDuration($minutes, $abbreviate = false){
295     $hours = (string)((int)abs($minutes / 60));
296     $mins = (string) round(abs(fmod($minutes, 60)));
297     if (strlen($mins) == 1)
298       $mins = '0' . $mins;
299     if ($abbreviate && $mins == '00')
300       return $hours;
301
302     return $hours.':'.$mins;
303   }
304
305   // toDuration - calculates duration between start and finish times in 00:00 format.
306   static function toDuration($start, $finish) {
307     $duration_minutes = ttTimeHelper::toMinutes($finish) - ttTimeHelper::toMinutes($start);
308     if ($duration_minutes <= 0) return false;
309
310     return ttTimeHelper::toAbsDuration($duration_minutes);
311   }
312
313   // The to12HourFormat function converts a 24-hour time value (such as 15:23) to 12 hour format (03:23 PM).
314   static function to12HourFormat($value) {
315     if ('24:00' == $value) return '12:00 AM';
316
317     $time_a = explode(':', $value);
318     if ($time_a[0] > 12)
319       $res = (string)((int)$time_a[0] - 12).':'.$time_a[1].' PM';
320     elseif ($time_a[0] == 12)
321       $res = $value.' PM';
322     elseif ($time_a[0] == 0)
323       $res = '12:'.$time_a[1].' AM';
324     else
325       $res = $value.' AM';
326     return $res;
327   }
328
329   // The to24HourFormat function attempts to convert a string value (human readable notation of time of day)
330   // to a 24-hour time format HH:MM.
331   static function to24HourFormat($value) {
332     $res = null;
333
334     // Algorithm: use regular expressions to find a matching pattern, starting with most popular patterns first.
335     $tmp_val = trim($value);
336
337     // 24 hour patterns.
338     if (preg_match('/^([01][0-9]|2[0-3]):[0-5][0-9]$/', $tmp_val)) { // 00:00 - 23:59
339       // We already have a 24-hour format. Just return it.
340       $res = $tmp_val;
341       return $res;
342     }
343     if (preg_match('/^[0-9]:[0-5][0-9]$/', $tmp_val)) { // 0:00 - 9:59
344       // This is a 24-hour format without a leading zero. Add 0 and return.
345       $res = '0'.$tmp_val;
346       return $res;
347     }
348     if (preg_match('/^[0-9]$/', $tmp_val)) { // 0 - 9
349       // Single digit. Assuming hour number.
350       $res = '0'.$tmp_val.':00';
351       return $res;
352     }
353     if (preg_match('/^([01][0-9]|2[0-4])$/', $tmp_val)) { // 00 - 24
354       // Two digit hour number.
355       $res = $tmp_val.':00';
356       return $res;
357     }
358     if (preg_match('/^[0-9][0-5][0-9]$/', $tmp_val)) { // 000 - 959
359       // Missing colon. We'll assume the first digit is the hour, the rest is minutes.
360       $tmp_arr = str_split($tmp_val);
361       $res = '0'.$tmp_arr[0].':'.$tmp_arr[1].$tmp_arr[2];
362       return $res;
363     }
364     if (preg_match('/^([01][0-9]|2[0-3])[0-5][0-9]$/', $tmp_val)) { // 0000 - 2359
365       // Missing colon. We'll assume the first 2 digits are the hour, the rest is minutes.
366       $tmp_arr = str_split($tmp_val);
367       $res = $tmp_arr[0].$tmp_arr[1].':'.$tmp_arr[2].$tmp_arr[3];
368       return $res;
369     }
370     // Special handling for midnight.
371     if ($tmp_val == '24:00' || $tmp_val == '2400')
372       return '24:00';
373
374     // 12 hour AM patterns.
375     if (preg_match('/.(am|AM)$/', $tmp_val)) {
376
377       // The $value ends in am or AM. Strip it.
378       $tmp_val = rtrim(substr($tmp_val, 0, -2));
379
380       // Special case to handle 12, 12:MM, and 12MM AM.
381       if (preg_match('/^12:?([0-5][0-9])?$/', $tmp_val))
382         $tmp_val = '00'.substr($tmp_val, 2);
383
384       // We are ready to convert AM time.
385       if (preg_match('/^(0[0-9]|1[0-1]):[0-5][0-9]$/', $tmp_val)) { // 00:00 - 11:59
386         // We already have a 24-hour format. Just return it.
387         $res = $tmp_val;
388         return $res;
389       }
390       if (preg_match('/^[1-9]:[0-5][0-9]$/', $tmp_val)) { // 1:00 - 9:59
391         // This is a 24-hour format without a leading zero. Add 0 and return.
392         $res = '0'.$tmp_val;
393         return $res;
394       }
395       if (preg_match('/^[1-9]$/', $tmp_val)) { // 1 - 9
396         // Single digit. Assuming hour number.
397         $res = '0'.$tmp_val.':00';
398         return $res;
399       }
400       if (preg_match('/^(0[0-9]|1[0-1])$/', $tmp_val)) { // 00 - 11
401         // Two digit hour number.
402         $res = $tmp_val.':00';
403         return $res;
404       }
405       if (preg_match('/^[1-9][0-5][0-9]$/', $tmp_val)) { // 100 - 959
406         // Missing colon. Assume the first digit is the hour, the rest is minutes.
407         $tmp_arr = str_split($tmp_val);
408         $res = '0'.$tmp_arr[0].':'.$tmp_arr[1].$tmp_arr[2];
409         return $res;
410       }
411       if (preg_match('/^(0[0-9]|1[0-1])[0-5][0-9]$/', $tmp_val)) { // 0000 - 1159
412         // Missing colon. We'll assume the first 2 digits are the hour, the rest is minutes.
413         $tmp_arr = str_split($tmp_val);
414         $res = $tmp_arr[0].$tmp_arr[1].':'.$tmp_arr[2].$tmp_arr[3];
415         return $res;
416       }
417     } // AM cases handling.
418
419     // 12 hour PM patterns.
420     if (preg_match('/.(pm|PM)$/', $tmp_val)) {
421
422       // The $value ends in pm or PM. Strip it.
423       $tmp_val = rtrim(substr($tmp_val, 0, -2));
424
425       if (preg_match('/^[1-9]$/', $tmp_val)) { // 1 - 9
426         // Single digit. Assuming hour number.
427         $hour = (string)(12 + (int)$tmp_val);
428         $res = $hour.':00';
429         return $res;
430       }
431       if (preg_match('/^((0[1-9])|(1[0-2]))$/', $tmp_val)) { // 01 - 12
432         // Double digit hour.
433         if ('12' != $tmp_val)
434           $tmp_val = (string)(12 + (int)$tmp_val);
435         $res = $tmp_val.':00';
436         return $res;
437       }
438       if (preg_match('/^[1-9][0-5][0-9]$/', $tmp_val)) { // 100 - 959
439         // Missing colon. We'll assume the first digit is the hour, the rest is minutes.
440         $tmp_arr = str_split($tmp_val);
441         $hour = (string)(12 + (int)$tmp_arr[0]);
442         $res = $hour.':'.$tmp_arr[1].$tmp_arr[2];
443         return $res;
444       }
445       if (preg_match('/^(0[1-9]|1[0-2])[0-5][0-9]$/', $tmp_val)) { // 0100 - 1259
446         // Missing colon. We'll assume the first 2 digits are the hour, the rest is minutes.
447         $hour = substr($tmp_val, 0, -2);
448         $min = substr($tmp_val, 2);
449         if ('12' != $hour)
450           $hour = (string)(12 + (int)$hour);
451         $res = $hour.':'.$min;
452         return $res;
453       }
454       if (preg_match('/^[1-9]:[0-5][0-9]$/', $tmp_val)) { // 1:00 - 9:59
455         $hour = substr($tmp_val, 0, -3);
456         $min = substr($tmp_val, 2);
457         $hour = (string)(12 + (int)$hour);
458         $res = $hour.':'.$min;
459         return $res;
460       }
461       if (preg_match('/^(0[1-9]|1[0-2]):[0-5][0-9]$/', $tmp_val)) { // 01:00 - 12:59
462         $hour = substr($tmp_val, 0, -3);
463         $min = substr($tmp_val, 3);
464         if ('12' != $hour)
465           $hour = (string)(12 + (int)$hour);
466         $res = $hour.':'.$min;
467         return $res;
468       }
469     } // PM cases handling.
470
471     return $res;
472   }
473
474   // isValidInterval - checks if finish time is greater than start time.
475   static function isValidInterval($start, $finish) {
476     $start = ttTimeHelper::to24HourFormat($start);
477     $finish = ttTimeHelper::to24HourFormat($finish);
478     if ('00:00' == $finish) $finish = '24:00';
479
480     $minutesStart = ttTimeHelper::toMinutes($start);
481     $minutesFinish = ttTimeHelper::toMinutes($finish);
482     if ($minutesFinish > $minutesStart)
483       return true;
484
485     return false;
486   }
487
488   // insert - inserts a time record into log table. Does not deal with custom fields.
489   static function insert($fields)
490   {
491     $mdb2 = getConnection();
492
493     $timestamp = isset($fields['timestamp']) ? $fields['timestamp'] : '';
494     $user_id = $fields['user_id'];
495     $date = $fields['date'];
496     $start = $fields['start'];
497     $finish = $fields['finish'];
498     $duration = $fields['duration'];
499     $client = $fields['client'];
500     $project = $fields['project'];
501     $task = $fields['task'];
502     $invoice = $fields['invoice'];
503     $note = $fields['note'];
504     $billable = $fields['billable'];
505     $paid = $fields['paid'];
506     if (array_key_exists('status', $fields)) { // Key exists and may be NULL during migration of data.
507       $status_f = ', status';
508       $status_v = ', '.$mdb2->quote($fields['status']);
509     }
510
511     $start = ttTimeHelper::to24HourFormat($start);
512     if ($finish) {
513       $finish = ttTimeHelper::to24HourFormat($finish);
514       if ('00:00' == $finish) $finish = '24:00';
515     }
516     $duration = ttTimeHelper::normalizeDuration($duration);
517
518     if (!$timestamp) {
519       $timestamp = date('YmdHis'); //yyyymmddhhmmss
520       // TODO: this timestamp could be illegal if we hit inside DST switch deadzone, such as '2016-03-13 02:30:00'
521       // Anything between 2am and 3am on DST introduction date will not work if we run on a system with DST on.
522       // We need to address this properly to avoid potential complications.
523     }
524
525     if (!$billable) $billable = 0;
526     if (!$paid) $paid = 0;
527
528     if ($duration) {
529       $sql = "insert into tt_log (timestamp, user_id, date, duration, client_id, project_id, task_id, invoice_id, comment, billable, paid $status_f) ".
530         "values ('$timestamp', $user_id, ".$mdb2->quote($date).", '$duration', ".$mdb2->quote($client).", ".$mdb2->quote($project).", ".$mdb2->quote($task).", ".$mdb2->quote($invoice).", ".$mdb2->quote($note).", $billable, $paid $status_v)";
531       $affected = $mdb2->exec($sql);
532       if (is_a($affected, 'PEAR_Error'))
533         return false;
534     } else {
535       $duration = ttTimeHelper::toDuration($start, $finish);
536       if ($duration === false) $duration = 0;
537       if (!$duration && ttTimeHelper::getUncompleted($user_id)) return false;
538
539       $sql = "insert into tt_log (timestamp, user_id, date, start, duration, client_id, project_id, task_id, invoice_id, comment, billable, paid $status_f) ".
540         "values ('$timestamp', $user_id, ".$mdb2->quote($date).", '$start', '$duration', ".$mdb2->quote($client).", ".$mdb2->quote($project).", ".$mdb2->quote($task).", ".$mdb2->quote($invoice).", ".$mdb2->quote($note).", $billable, $paid $status_v)";
541       $affected = $mdb2->exec($sql);
542       if (is_a($affected, 'PEAR_Error'))
543         return false;
544     }
545
546     $id = $mdb2->lastInsertID('tt_log', 'id');
547     return $id;
548   }
549
550   // update - updates a record in log table. Does not update its custom fields.
551   static function update($fields)
552   {
553     global $user;
554     $mdb2 = getConnection();
555
556     $id = $fields['id'];
557     $date = $fields['date'];
558     $user_id = $fields['user_id'];
559     $client = $fields['client'];
560     $project = $fields['project'];
561     $task = $fields['task'];
562     $start = $fields['start'];
563     $finish = $fields['finish'];
564     $duration = $fields['duration'];
565     $note = $fields['note'];
566
567     $billable_part = '';
568     if ($user->isPluginEnabled('iv')) {
569       $billable_part = $fields['billable'] ? ', billable = 1' : ', billable = 0';
570     }
571     $paid_part = '';
572     if ($user->canManageTeam() && $user->isPluginEnabled('ps')) {
573       $paid_part = $fields['paid'] ? ', paid = 1' : ', paid = 0';
574     }
575
576     $start = ttTimeHelper::to24HourFormat($start);
577     $finish = ttTimeHelper::to24HourFormat($finish);
578     if ('00:00' == $finish) $finish = '24:00';
579     $duration = ttTimeHelper::normalizeDuration($duration);
580
581     if ($start) $duration = '';
582
583     if ($duration) {
584       $sql = "UPDATE tt_log set start = NULL, duration = '$duration', client_id = ".$mdb2->quote($client).", project_id = ".$mdb2->quote($project).", task_id = ".$mdb2->quote($task).", ".
585         "comment = ".$mdb2->quote($note)."$billable_part $paid_part, date = '$date' WHERE id = $id";
586       $affected = $mdb2->exec($sql);
587       if (is_a($affected, 'PEAR_Error'))
588         return false;
589     } else {
590       $duration = ttTimeHelper::toDuration($start, $finish);
591       if ($duration === false)
592         $duration = 0;
593       $uncompleted = ttTimeHelper::getUncompleted($user_id);
594       if (!$duration && $uncompleted && ($uncompleted['id'] != $id))
595         return false;
596
597       $sql = "UPDATE tt_log SET start = '$start', duration = '$duration', client_id = ".$mdb2->quote($client).", project_id = ".$mdb2->quote($project).", task_id = ".$mdb2->quote($task).", ".
598         "comment = ".$mdb2->quote($note)."$billable_part $paid_part, date = '$date' WHERE id = $id";
599       $affected = $mdb2->exec($sql);
600       if (is_a($affected, 'PEAR_Error'))
601         return false;
602     }
603     return true;
604   }
605
606   // delete - deletes a record from tt_log table and its associated custom field values.
607   static function delete($id, $user_id) {
608     $mdb2 = getConnection();
609
610     $sql = "update tt_log set status = NULL where id = $id and user_id = $user_id";
611     $affected = $mdb2->exec($sql);
612     if (is_a($affected, 'PEAR_Error'))
613       return false;
614
615     $sql = "update tt_custom_field_log set status = NULL where log_id = $id";
616     $affected = $mdb2->exec($sql);
617     if (is_a($affected, 'PEAR_Error'))
618       return false;
619
620     return true;
621   }
622
623   // getTimeForDay - gets total time for a user for a specific date.
624   static function getTimeForDay($user_id, $date) {
625     $mdb2 = getConnection();
626
627     $sql = "select sum(time_to_sec(duration)) as sm from tt_log where user_id = $user_id and date = '$date' and status = 1";
628     $res = $mdb2->query($sql);
629     if (!is_a($res, 'PEAR_Error')) {
630       $val = $res->fetchRow();
631       return sec_to_time_fmt_hm($val['sm']);
632     }
633     return false;
634   }
635
636   // getTimeForWeek - gets total time for a user for a given week.
637   static function getTimeForWeek($user_id, $date) {
638     import('Period');
639     $mdb2 = getConnection();
640
641     $period = new Period(INTERVAL_THIS_WEEK, $date);
642     $sql = "select sum(time_to_sec(duration)) as sm from tt_log where user_id = $user_id and date >= '".$period->getStartDate(DB_DATEFORMAT)."' and date <= '".$period->getEndDate(DB_DATEFORMAT)."' and status = 1";
643     $res = $mdb2->query($sql);
644     if (!is_a($res, 'PEAR_Error')) {
645       $val = $res->fetchRow();
646       return sec_to_time_fmt_hm($val['sm']);
647     }
648     return 0;
649   }
650
651   // getTimeForMonth - gets total time for a user for a given month.
652   static function getTimeForMonth($user_id, $date){
653     import('Period');
654     $mdb2 = getConnection();
655
656     $period = new Period(INTERVAL_THIS_MONTH, $date);
657     $sql = "select sum(time_to_sec(duration)) as sm from tt_log where user_id = $user_id and date >= '".$period->getStartDate(DB_DATEFORMAT)."' and date <= '".$period->getEndDate(DB_DATEFORMAT)."' and status = 1";
658     $res = $mdb2->query($sql);
659     if (!is_a($res, 'PEAR_Error')) {
660       $val = $res->fetchRow();
661       return sec_to_time_fmt_hm($val['sm']);
662     }
663     return 0;
664   }
665
666   // getUncompleted - retrieves an uncompleted record for user, if one exists.
667   static function getUncompleted($user_id) {
668     $mdb2 = getConnection();
669
670     $sql = "select id, start from tt_log  
671       where user_id = $user_id and start is not null and time_to_sec(duration) = 0 and status = 1";
672     $res = $mdb2->query($sql);
673     if (!is_a($res, 'PEAR_Error')) {
674       if (!$res->numRows()) {
675         return false;
676       }
677       if ($val = $res->fetchRow()) {
678         return $val;
679       }
680     }
681     return false;
682   }
683
684   // overlaps - determines if a record overlaps with an already existing record.
685   //
686   // Parameters:
687   //   $user_id - user id for whom to determine overlap
688   //   $date - date
689   //   $start - new record start time
690   //   $finish - new record finish time, may be null
691   //   $record_id - optional record id we may be editing, excluded from overlap set
692   static function overlaps($user_id, $date, $start, $finish, $record_id = null) {
693     // Do not bother checking if we allow overlaps.
694     if (defined('ALLOW_OVERLAP') && ALLOW_OVERLAP == true)
695       return false;
696
697     $mdb2 = getConnection();
698
699     $start = ttTimeHelper::to24HourFormat($start);
700     if ($finish) {
701       $finish = ttTimeHelper::to24HourFormat($finish);
702       if ('00:00' == $finish) $finish = '24:00';
703     }
704     // Handle these 3 overlap situations:
705     // - start time in existing record
706     // - end time in existing record
707     // - record fully encloses existing record
708     $sql = "select id from tt_log  
709       where user_id = $user_id and date = ".$mdb2->quote($date)."
710       and start is not null and duration is not null and status = 1 and (
711       (cast(".$mdb2->quote($start)." as time) >= start and cast(".$mdb2->quote($start)." as time) < addtime(start, duration))";
712     if ($finish) {
713       $sql .= " or (cast(".$mdb2->quote($finish)." as time) <= addtime(start, duration) and cast(".$mdb2->quote($finish)." as time) > start)
714       or (cast(".$mdb2->quote($start)." as time) < start and cast(".$mdb2->quote($finish)." as time) > addtime(start, duration))";
715     }
716     $sql .= ")";
717     if ($record_id) {
718       $sql .= " and id <> $record_id";
719     }
720     $res = $mdb2->query($sql);
721     if (!is_a($res, 'PEAR_Error')) {
722       if (!$res->numRows()) {
723         return false;
724       }
725       if ($val = $res->fetchRow()) {
726         return $val;
727       }
728     }
729     return false;
730   }
731
732   // getRecord - retrieves a time record identified by its id.
733   static function getRecord($id, $user_id) {
734     global $user;
735     $sql_time_format = "'%k:%i'"; //  24 hour format.
736     if ('%I:%M %p' == $user->time_format)
737       $sql_time_format = "'%h:%i %p'"; // 12 hour format for MySQL TIME_FORMAT function.
738
739     $mdb2 = getConnection();
740
741     $sql = "select l.id as id, l.timestamp as timestamp, TIME_FORMAT(l.start, $sql_time_format) as start,
742       TIME_FORMAT(sec_to_time(time_to_sec(l.start) + time_to_sec(l.duration)), $sql_time_format) as finish,
743       TIME_FORMAT(l.duration, '%k:%i') as duration,
744       p.name as project_name, t.name as task_name, l.comment, l.client_id, l.project_id, l.task_id, l.invoice_id, l.billable, l.paid, l.date
745       from tt_log l
746       left join tt_projects p on (p.id = l.project_id)
747       left join tt_tasks t on (t.id = l.task_id)
748       where l.id = $id and l.user_id = $user_id and l.status = 1";
749     $res = $mdb2->query($sql);
750     if (!is_a($res, 'PEAR_Error')) {
751       if (!$res->numRows()) {
752         return false;
753       }
754       if ($val = $res->fetchRow()) {
755         return $val;
756       }
757     }
758     return false;
759   }
760
761   // getAllRecords - returns all time records for a certain user.
762   static function getAllRecords($user_id) {
763     $result = array();
764
765     $mdb2 = getConnection();
766
767     $sql = "select l.id, l.timestamp, l.user_id, l.date, TIME_FORMAT(l.start, '%k:%i') as start,
768       TIME_FORMAT(sec_to_time(time_to_sec(l.start) + time_to_sec(l.duration)), '%k:%i') as finish,
769       TIME_FORMAT(l.duration, '%k:%i') as duration,
770       l.client_id, l.project_id, l.task_id, l.invoice_id, l.comment, l.billable, l.paid, l.status
771       from tt_log l where l.user_id = $user_id order by l.id";
772     $res = $mdb2->query($sql);
773     if (!is_a($res, 'PEAR_Error')) {
774       while ($val = $res->fetchRow()) {
775         $result[] = $val;
776       }
777     } else return false;
778
779     return $result;
780   }
781
782   // getRecords - returns time records for a user for a given date.
783   static function getRecords($user_id, $date) {
784     global $user;
785     $sql_time_format = "'%k:%i'"; //  24 hour format.
786     if ('%I:%M %p' == $user->time_format)
787       $sql_time_format = "'%h:%i %p'"; // 12 hour format for MySQL TIME_FORMAT function.
788
789     $result = array();
790     $mdb2 = getConnection();
791
792     $client_field = null;
793     if ($user->isPluginEnabled('cl'))
794       $client_field = ", c.name as client";
795
796     $left_joins = " left join tt_projects p on (l.project_id = p.id)".
797       " left join tt_tasks t on (l.task_id = t.id)";
798     if ($user->isPluginEnabled('cl'))
799       $left_joins .= " left join tt_clients c on (l.client_id = c.id)";
800
801     $sql = "select l.id as id, TIME_FORMAT(l.start, $sql_time_format) as start,
802       TIME_FORMAT(sec_to_time(time_to_sec(l.start) + time_to_sec(l.duration)), $sql_time_format) as finish,
803       TIME_FORMAT(l.duration, '%k:%i') as duration, p.name as project, t.name as task, l.comment, l.billable, l.invoice_id $client_field
804       from tt_log l
805       $left_joins
806       where l.date = '$date' and l.user_id = $user_id and l.status = 1
807       order by l.start, l.id";
808     $res = $mdb2->query($sql);
809     if (!is_a($res, 'PEAR_Error')) {
810       while ($val = $res->fetchRow()) {
811         if($val['duration']=='0:00')
812           $val['finish'] = '';
813         $result[] = $val;
814       }
815     } else return false;
816
817     return $result;
818   }
819 }