Fixed rounding errors in monthly quotas.
authorNik Okuntseff <support@anuko.com>
Mon, 12 Feb 2018 15:28:45 +0000 (15:28 +0000)
committerNik Okuntseff <support@anuko.com>
Mon, 12 Feb 2018 15:28:45 +0000 (15:28 +0000)
WEB-INF/lib/ttTeamHelper.class.php
WEB-INF/lib/ttTimeHelper.class.php
WEB-INF/lib/ttUser.class.php
WEB-INF/resources/it.lang.php
WEB-INF/templates/footer.tpl
dbinstall.php
mysql.sql
plugins/MonthlyQuota.class.php
quotas.php
time.php

index b308996..c0baa3d 100644 (file)
@@ -805,7 +805,7 @@ class ttTeamHelper {
     $plugins_part = '';
     $config_part = '';
     $lock_spec_part = '';
-    $workday_hours_part = '';
+    $workday_minutes_part = '';
 
     if (isset($fields['currency'])) $currency_part = ', currency = '.$mdb2->quote($fields['currency']);
     if (isset($fields['lang'])) $lang_part = ', lang = '.$mdb2->quote($fields['lang']);
@@ -820,11 +820,11 @@ class ttTeamHelper {
     if (isset($fields['plugins'])) $plugins_part = ', plugins = '.$mdb2->quote($fields['plugins']);
     if (isset($fields['config'])) $config_part = ', config = '.$mdb2->quote($fields['config']);
     if (isset($fields['lock_spec'])) $lock_spec_part = ', lock_spec = '.$mdb2->quote($fields['lock_spec']);
-    if (isset($fields['workday_hours'])) $workday_hours_part = ', workday_hours = '.$mdb2->quote($fields['workday_hours']);
+    if (isset($fields['workday_minutes'])) $workday_minutes_part = ', workday_minutes = '.$mdb2->quote($fields['workday_minutes']);
 
     $sql = "update tt_teams set $name_part $currency_part $lang_part $decimal_mark_part
       $date_format_part $time_format_part $week_start_part $tracking_mode_part $task_required_part $record_type_part
-      $uncompleted_indicators_part $bcc_email_part $plugins_part $config_part $lock_spec_part $workday_hours_part where id = $team_id";
+      $uncompleted_indicators_part $bcc_email_part $plugins_part $config_part $lock_spec_part $workday_minutes_part where id = $team_id";
     $affected = $mdb2->exec($sql);
     if (is_a($affected, 'PEAR_Error')) return false;
 
index f5c3e93..8c3d27b 100644 (file)
@@ -106,6 +106,72 @@ class ttTimeHelper {
     return false;
   }
 
+  // postedDurationToMinutes - converts a value representing a duration
+  // (usually enetered in a form by a user) to integer number of minutes.
+  //
+  // At the moment, we have 2 variations of duration types:
+  //   1) A duration within a day, such as in a time entry.
+  //   These are less or equal to 24 hours.
+  //
+  //   2) A duration of a monthly quota, with max value of 31*24 hours.
+  //
+  // This function is generic to be used for both types.
+  // Other functions will be used to check for specific max values.
+  //
+  // Returns false if the value cannot be converted.
+  static function postedDurationToMinutes($duration) {
+    // Handle empty value.
+    if (!isset($duration) || strlen($duration) == 0)
+      return null; // Value is not set. Caller decides whether it is valid or not.
+
+    // Handle whole hours.
+    if (preg_match('/^\d{1,3}h?$/', $duration )) { // 0 - 999, 0h - 999h
+      $minutes = 60 * trim($duration, 'h');
+      return $minutes;
+    }
+
+    // Handle a normalized duration value.
+    if (preg_match('/^\d{1,3}:[0-5][0-9]$/', $duration )) { // 0:00 - 999:59
+      $time_array = explode(':', $duration);
+      $minutes = (int)@$time_array[1] + ((int)@$time_array[0]) * 60;
+      return $minutes;
+    }
+
+    // Handle localized fractional hours.
+    global $user;
+    $localizedPattern = '/^(\d{1,3})?['.$user->decimal_mark.'][0-9]{1,4}h?$/';
+    if (preg_match($localizedPattern, $duration )) { // decimal values like .5, 1.25h, ... .. 999.9999h (or with comma)
+        if ($user->decimal_mark == ',')
+          $duration = str_replace (',', '.', $duration);
+
+        $minutes = (int)round(60 * floatval($duration));
+        return $minutes;
+    }
+
+    // Handle minutes. Some users enter durations like 10m (meaning 10 minutes).
+    if (preg_match('/^\d{1,5}m$/', $duration )) { // 0m - 99999m
+      $minutes = (int) trim($duration, 'm');
+      return $minutes;
+    }
+
+    // Everything else is not a valid duration.
+    return false;
+  }
+
+  static function durationToMinutes($duration) {
+    $minutes = ttTimeHelper::postedDurationToMinutes($duration);
+    if (false === $minutes || $minutes > 24*60)
+      return false; // $duration is not valid for a day entry.
+    return $minutes;
+  }
+
+  static function quotaToMinutes($duration) {
+    $minutes = ttTimeHelper::postedDurationToMinutes($duration);
+    if (false === $minutes || $minutes > 31*24*60)
+      return false; // $duration is not valid for a monthly quota.
+    return $minutes;
+  }
+
   // validateDuration - a future replacement of the isValidDuration above.
   // Validates a passed in $value as a time duration string in hours and / or minutes.
   // Returns either a normalized duration (hh:mm) or false if $value is invalid.
index f04b499..b433e6e 100644 (file)
@@ -41,7 +41,7 @@ class ttUser {
   var $date_format = null;      // Date format.
   var $time_format = null;      // Time format.
   var $week_start = 0;          // Week start day.
-  var $show_holidays = 1;       // Whether to show holidays in calendar.
+  var $show_holidays = 0;       // Whether to show holidays in calendar.
   var $tracking_mode = 0;       // Tracking mode.
   var $project_required = 0;    // Whether project selection is required on time entires.
   var $task_required = 0;       // Whether task selection is required on time entires.
@@ -54,7 +54,7 @@ class ttUser {
   var $team = null;             // Team name.
   var $custom_logo = 0;         // Whether to use a custom logo for team.
   var $lock_spec = null;        // Cron specification for record locking.
-  var $workday_hours = 8;       // Number of work hours in a regular day.
+  var $workday_minutes = 480;   // Number of work minutes in a regular day.
   var $rights = 0;              // A mask of user rights.
 
   // Constructor.
@@ -69,7 +69,7 @@ class ttUser {
     $sql = "SELECT u.id, u.login, u.name, u.team_id, u.role, u.client_id, u.email, t.name as team_name, 
       t.currency, t.lang, t.decimal_mark, t.date_format, t.time_format, t.week_start,
       t.tracking_mode, t.project_required, t.task_required, t.record_type, t.uncompleted_indicators,
-      t.bcc_email, t.plugins, t.config, t.lock_spec, t.workday_hours, t.custom_logo
+      t.bcc_email, t.plugins, t.config, t.lock_spec, t.workday_minutes, t.custom_logo
       FROM tt_users u LEFT JOIN tt_teams t ON (u.team_id = t.id) WHERE ";
     if ($id)
       $sql .= "u.id = $id";
@@ -106,7 +106,7 @@ class ttUser {
       $this->currency = $val['currency'];
       $this->plugins = $val['plugins'];
       $this->lock_spec = $val['lock_spec'];
-      $this->workday_hours = $val['workday_hours'];
+      $this->workday_minutes = $val['workday_minutes'];
       $this->custom_logo = $val['custom_logo'];
 
       // Set user config options.
index a084c76..04b63d8 100644 (file)
@@ -115,7 +115,7 @@ $i18n_key_words = array(
 'button.reset_password' => 'Reset password',
 'button.send' => 'Invia',
 'button.send_by_email' => 'Invia tramite e-mail',
-'button.create_team' => 'Crea team',
+'button.create_team' => 'Crea gruppo',
 'button.export' => 'Esporta gruppo',
 'button.import' => 'Importa gruppo',
 'button.close' => 'Chiudi',
@@ -417,7 +417,7 @@ $i18n_key_words = array(
 'form.import.success' => 'Importazione eseguita con successo.',
 
 // Teams form. See example at https://timetracker.anuko.com/admin_teams.php (login as admin first).
-'form.teams.hint' => 'Crea un nuovo gruppo creando un account gruppo manager.<br>Puoi anche importare i dati di un team da un file xml esportato da un altro server Anuko Time Tracker (non sono ammessi login duplicati).',
+'form.teams.hint' => 'Crea un nuovo gruppo creando un account gruppo manager.<br>Puoi anche importare i dati di un gruppo da un file xml esportato da un altro server Anuko Time Tracker (non sono ammessi login duplicati).',
 
 // Profile form. See example at https://timetracker.anuko.com/profile_edit.php.
 'form.profile.12_hours' => '12 ore',
index eb79446..f3e02b5 100644 (file)
@@ -12,7 +12,7 @@
       <br>
       <table cellspacing="0" cellpadding="4" width="100%" border="0">
         <tr>
-          <td align="center">&nbsp;Anuko Time Tracker 1.17.15.3982 | Copyright &copy; <a href="https://www.anuko.com/lp/tt_3.htm" target="_blank">Anuko</a> |
+          <td align="center">&nbsp;Anuko Time Tracker 1.17.15.3983 | Copyright &copy; <a href="https://www.anuko.com/lp/tt_3.htm" target="_blank">Anuko</a> |
             <a href="https://www.anuko.com/lp/tt_4.htm" target="_blank">{$i18n.footer.credits}</a> |
             <a href="https://www.anuko.com/lp/tt_5.htm" target="_blank">{$i18n.footer.license}</a> |
             <a href="https://www.anuko.com/lp/tt_7.htm" target="_blank">{$i18n.footer.improve}</a>
index 58467bc..dd36011 100755 (executable)
@@ -709,7 +709,7 @@ if ($_POST) {
     setChange("ALTER TABLE `tt_log` ADD `paid` tinyint(4) NULL default '0' AFTER `billable`");
   }
 
-  if ($_POST["convert11400to11714"]) {
+  if ($_POST["convert11400to11715"]) {
     setChange("ALTER TABLE `tt_teams` DROP `address`");
     setChange("ALTER TABLE `tt_fav_reports` ADD `report_spec` text default NULL AFTER `user_id`");
     setChange("ALTER TABLE `tt_fav_reports` ADD `paid_status` tinyint(4) default NULL AFTER `invoice`");
@@ -720,6 +720,10 @@ if ($_POST) {
     setChange("ALTER TABLE `tt_teams` ADD `config` text default NULL AFTER `custom_logo`");
     setChange("ALTER TABLE `tt_monthly_quotas` ADD `minutes` int(11) DEFAULT NULL");
     setChange("ALTER TABLE `tt_teams` ADD `workday_minutes` smallint(4) DEFAULT '480' AFTER `workday_hours`");
+    setChange("UPDATE `tt_teams` SET `workday_minutes` = 60 * `workday_hours`");
+    setChange("ALTER TABLE `tt_teams` DROP `workday_hours`");
+    setChange("UPDATE `tt_monthly_quotas` SET `minutes` = 60 * `quota`");
+    setChange("ALTER TABLE `tt_monthly_quotas` DROP `quota`");
   }
 
   if ($_POST["cleanup"]) {
@@ -764,7 +768,7 @@ if ($_POST) {
 <h2>DB Install</h2>
 <table width="80%" border="1" cellpadding="10" cellspacing="0">
   <tr>
-    <td width="80%"><b>Create database structure (v1.17.14)</b>
+    <td width="80%"><b>Create database structure (v1.17.15)</b>
     <br>(applies only to new installations, do not execute when updating)</br></td><td><input type="submit" name="crstructure" value="Create"></td>
   </tr>
 </table>
@@ -800,8 +804,8 @@ if ($_POST) {
     <td><input type="submit" name="convert1600to11400" value="Update"><br></td>
   </tr>
   <tr valign="top">
-    <td>Update database structure (v1.14 to v1.17.14)</td>
-    <td><input type="submit" name="convert11400to11714" value="Update"><br></td>
+    <td>Update database structure (v1.14 to v1.17.15)</td>
+    <td><input type="submit" name="convert11400to11715" value="Update"><br></td>
   </tr>
 </table>
 
index dc07a1d..d771126 100644 (file)
--- a/mysql.sql
+++ b/mysql.sql
@@ -376,7 +376,6 @@ CREATE TABLE `tt_monthly_quotas` (
   `team_id` int(11) NOT NULL,             # team id
   `year` smallint(5) UNSIGNED NOT NULL,   # quota year
   `month` tinyint(3) UNSIGNED NOT NULL,   # quota month
-  `quota` decimal(5,2) NOT NULL,          # number of work hours in specified month and year
   `minutes` int(11) DEFAULT NULL,         # quota in minutes in specified month and year
   PRIMARY KEY (`team_id`,`year`,`month`)
 );
index 6e420f9..a4be6ee 100644 (file)
@@ -34,7 +34,6 @@ class MonthlyQuota {
   var $db;       // Database connection.
   var $team_id;  // Team id.
 
-  // 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();
     global $user;
@@ -42,13 +41,12 @@ class MonthlyQuota {
   }
 
   // update - deletes a quota, then inserts a new one.
-  public function update($year, $month, $quota) {
+  public function update($year, $month, $minutes) {
     $team_id = $this->team_id;
     $deleteSql = "DELETE FROM tt_monthly_quotas WHERE year = $year AND month = $month AND team_id = $team_id";
     $this->db->exec($deleteSql);
-    if ($quota){
-      $float_quota = $this->quotaToFloat($quota);
-      $insertSql = "INSERT INTO tt_monthly_quotas (team_id, year, month, quota) values ($team_id, $year, $month, $float_quota)";
+    if ($minutes){
+      $insertSql = "INSERT INTO tt_monthly_quotas (team_id, year, month, minutes) values ($team_id, $year, $month, $minutes)";
       $affected = $this->db->exec($insertSql);
       return (!is_a($affected, 'PEAR_Error'));
     }
@@ -67,7 +65,7 @@ class MonthlyQuota {
   // getSingle - obtains a quota for a single month.
   private function getSingle($year, $month) {
     $team_id = $this->team_id;
-    $sql = "SELECT quota FROM tt_monthly_quotas WHERE year = $year AND month = $month AND team_id = $team_id";
+    $sql = "SELECT minutes FROM tt_monthly_quotas WHERE year = $year AND month = $month AND team_id = $team_id";
     $reader = $this->db->query($sql);
     if (is_a($reader, 'PEAR_Error')) {
       return false;
@@ -75,19 +73,18 @@ class MonthlyQuota {
 
     $row = $reader->fetchRow();
     if ($row)
-      return $row['quota'];
+      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->workday_hours; // TODO: fix a rounding issue for small values like 0:01
-                                                // Possibly with a database field type change (minutes?).
+    return $numWorkdays * $user->workday_minutes;
   }
 
   // getMany - returns an array of quotas for a given year for team.
   private function getMany($year){
     $team_id = $this->team_id;
-    $sql = "SELECT month, quota FROM tt_monthly_quotas WHERE year = $year AND team_id = $team_id";
+    $sql = "SELECT month, minutes FROM tt_monthly_quotas WHERE year = $year AND team_id = $team_id";
     $result = array();
     $res = $this->db->query($sql);
     if (is_a($res, 'PEAR_Error')) {
@@ -95,7 +92,7 @@ class MonthlyQuota {
     }
 
     while ($val = $res->fetchRow()) {
-      $result[$val['month']] = $val['quota'];
+      $result[$val['month']] = $val['minutes'];
     }
 
     return $result;
index d3e7912..15a7a49 100644 (file)
@@ -71,12 +71,12 @@ $quota = new MonthlyQuota();
 
 if ($request->isPost()){
   // Validate user input.
-  if (!ttTimeHelper::isValidDuration($request->getParameter('workdayHours')))
+  if (false === ttTimeHelper::durationToMinutes($request->getParameter('workdayHours')))
     $err->add($i18n->getKey('error.field'), $i18n->getKey('form.quota.workday_hours'));
 
   for ($i = 0; $i < count($months); $i++){
     $val = $request->getParameter($months[$i]);
-    if (!$quota->isValidQuota($val))
+    if (false === ttTimeHelper::quotaToMinutes($val))
       $err->add($i18n->getKey('error.field'), $months[$i]);
   }
   // Finished validating user input.
@@ -84,16 +84,17 @@ if ($request->isPost()){
   if ($err->no()) {
 
     // Handle workday hours.
-    $hours = $quota->quotaToFloat($request->getParameter('workdayHours'));
-    if ($hours != $user->workday_hours) {
-      if (!ttTeamHelper::update($user->team_id, array('name'=>$user->team,'workday_hours'=>$hours)))
+    $workday_minutes = ttTimeHelper::durationToMinutes($request->getParameter('workdayHours'));
+    if ($workday_minutes != $user->workday_minutes) {
+      if (!ttTeamHelper::update($user->team_id, array('name'=>$user->team,'workday_minutes'=>$workday_minutes)))
         $err->add($i18n->getKey('error.db'));
     }
 
     // Handle monthly quotas for a selected year.
     $selectedYear = (int) $request->getParameter('year');
     for ($i = 0; $i < count($months); $i++){
-      if (!$quota->update($selectedYear, $i+1, $request->getParameter($months[$i])))
+      $quota_in_minutes = ttTimeHelper::quotaToMinutes($request->getParameter($months[$i]));
+      if (!$quota->update($selectedYear, $i+1, $quota_in_minutes))
         $err->add($i18n->getKey('error.db'));
     }
 
@@ -107,7 +108,7 @@ if ($request->isPost()){
 
 // Get monthly quotas for the entire year.
 $monthsData = $quota->get($selectedYear);
-$workdayHours = ttTimeHelper::toAbsDuration($user->workday_hours * 60, true);
+$workdayHours = ttTimeHelper::toAbsDuration($user->workday_minutes, true);
 
 $form = new Form('monthlyQuotasForm');
 $form->addInput(array('type'=>'text', 'name'=>'workdayHours', 'value'=>$workdayHours, 'style'=>'width:60px'));
@@ -116,7 +117,7 @@ for ($i=0; $i < count($months); $i++) {
   $value = "";
   if (array_key_exists($i+1, $monthsData)){
     $value = $monthsData[$i+1];
-    $value = ttTimeHelper::toAbsDuration($value * 60, true);
+    $value = ttTimeHelper::toAbsDuration($value, true);
   }
   $name = $months[$i];
   $form->addInput(array('type'=>'text','name'=>$name,'maxlength'=>6,'value'=> $value,'style'=>'width:70px'));
index 25cee96..27d3cdb 100644 (file)
--- a/time.php
+++ b/time.php
@@ -66,9 +66,9 @@ if ($user->isPluginEnabled('cf')) {
 if ($user->isPluginEnabled('mq')){
   require_once('plugins/MonthlyQuota.class.php');
   $quota = new MonthlyQuota();
-  $month_quota = $quota->get($selected_date->mYear, $selected_date->mMonth);
+  $month_quota_minutes = $quota->get($selected_date->mYear, $selected_date->mMonth);
   $month_total = ttTimeHelper::getTimeForMonth($user->getActiveUser(), $selected_date);
-  $minutes_left = round(60*$month_quota) - ttTimeHelper::toMinutes($month_total);
+  $minutes_left = $month_quota_minutes - ttTimeHelper::toMinutes($month_total);
   
   $smarty->assign('month_total', $month_total);
   $smarty->assign('over_quota', $minutes_left < 0);