Time Tracker Modification

Project Description

We use Anuko Time Tracker system to track work hours.

We want an enhancement to the Lock Interval in Days feature. We have a requirement to lock the system at the end of each week. This is to force users to enter their times into the system before we process payroll.

A perfect scenario would be.


So for example:

With a lock day and time of Thursday 10:30AM - At 10:30AM on Thursday 2nd January no edits can be made for any previous dates up to and including Wednesday 1st of January but the user can always edit times for the current date and future dates.

Completion Notes

This project is currently at the "completed" stage (requirements frozen, payment secured, work completed, work accepted by customer, payment released to contractor).

Project Plan

Let's see how our project plan may look like. How about the following:


Refactoring Time Tracker

Refactoring is a required activity in any project. It is needed to keep things manageable. We do a little bit of refactoring before starting actual work for this project. We try to improve parts of the code that are directly related to what we are going to do. We keep the scope of this refactoring small related to the entire Time Tracker code base. However, the improvements are highly visible in parts that are related to this modification. For example, the line with isPluginEnabled below is shorter and communicates the intention better.

-{if in_array('iv', explode(',', $user->plugins))}
+{if $user->isPluginEnabled('iv')}


Encapsulate the Existing Locking Code

To make things neat, we write the ttUser::isDateLocked() function and put the existing locking code in it. Then we modify all parts that require locking to make use of this new function. This actually makes the parts shorter and easier to understand. In the example below the second line has e better communicated meaning, also we don't have to calculate $lockdate in each part.

-    if ($lockdate && $item_date->before($lockdate))
+    if ($user->isDateLocked($item_date))

Moving the Locking Feature into a Plugin

In this part of the project we modify the Team Profile page. The point is to move all locking configuration options into a plugin with its own UI. Currently, there is only the Lock interval in days control that needs to be moved away. So, we modify profile_edit.php and WEB-INF/templates/profile_edit.tpl to remove it. We also add the Locking plugin option to the bottom, and do the configuration page similar to how Notifications are done. When user select the plugin, the Configure link appears that allows us to go to its UI.

We write locking.php and WEB-INF/templates/locking.tpl accordingly. This is a configuration pair for the Locking plugin. At this time it only contains the Lock interval in days control.

Changing the Database Structure

There are many ways to do this, apparently. What we want is a good and flexible solution that allows for advanced locking configurations and is elegant at the same time. Instead of introducing a variety of fields, why don't we add just one that will contain a cron specification covering all possible locking combinations (daily, weekly, biweekly, monthly, yearly, and anything in between and beyond)? This looks doable, especially when considering that Time Tracker already handle cron jobs in its Notifications plugin. Here is the new structure for tt_teams table with the lock_spec field we added:

#
# Structure for table tt_teams. A team is a group of users for whom we are tracking work time.
# This table stores settings common to all team members such as language, week start day, etc.
#
CREATE TABLE `tt_teams` (
  `id` int(11) NOT NULL auto_increment,                  # team id
  `timestamp` timestamp NOT NULL,                        # modification timestamp
  `name` varchar(80) default NULL,                       # team name
  `address` varchar(255) default NULL,                   # team address, used in invoices
  `currency` varchar(7) default NULL,                    # team currency symbol
  `decimal_mark` char(1) NOT NULL default '.',           # separator in decimals
  `locktime` int(4) default '0',                         # lock interval in days
  `lang` varchar(10) NOT NULL default 'en',              # language
  `date_format` varchar(20) NOT NULL default '%Y-%m-%d', # date format
  `time_format` varchar(20) NOT NULL default '%H:%M',    # time format
  `week_start` smallint(2) NOT NULL DEFAULT '0',         # Week start day, 0 == Sunday.
  `tracking_mode` smallint(2) NOT NULL DEFAULT '1',      # tracking mode ("projects" or "projects and tasks")
  `record_type` smallint(2) NOT NULL DEFAULT '0',        # time record type ("start and finish", "duration", or both)
  `plugins` varchar(255) default NULL,                   # a list of enabled plugins for team
  `lock_spec` varchar(255) default NULL,                 # Cron specification for record locking,
                                                         # for example: "0 10 * * 1" for "weekly on Mon at 10:00".
  `custom_logo` tinyint(4) default '0',                  # whether to use a custom logo or not
  `status` tinyint(4) default '1',                       # team status
  PRIMARY KEY (`id`)
);

And below is an update statement for the dbinstall.php file:

setChange("ALTER TABLE tt_teams ADD COLUMN lock_spec varchar(255) default NULL");

We can now execute an update step utilizing dbinstall.php and have the database ready.

Adjusting ttUser Class

In this part of the project we modify ttUser class so that it maintains and keeps lock_spec for initialized user. Nothing fancy, just adding a new member variable and initializing it properly.

Creating UI Elements

Now we need to change the Locking plugin configuration UI to allow managers to supply a cron spec instead of the legacy lock interval in days. We do it similar to how Notifications are implemented. Basically, our new form is simple: it contains a Cron schedule: label, a text field for cron spec, and a What is it? link to a page that explains what to enter.

Writing Code for Read and Modify Locking Specification

This part involves modifying the parts of the project that obtain lock_spec from the database, put it into our control, and save it back when it is changed.

Reimplementing ttUser::isDateLocked() Function

Now we have to redo ttUser::isDateLocked() so that it uses cron spec. Below is the new code:

  // isDateLocked checks whether a specifc date is locked for modifications.
  function isDateLocked($date)
  {
    if ($this->isPluginEnabled('lk') && $this->lock_spec) {
      // This is legacy code...
      /*
      // Determine lock date. Entries earlier than lock date cannot be created or modified.
      $lockdate = 0;
      if ($this->lock_interval > 0) {
        $lockdate = new DateAndTime();
        $lockdate->decDay($this->lock_interval);
      }
      if($lockdate && $date->before($lockdate))
        return true;
      */

      // New code with cron specification.

      // Override for managers.
      if ($this->canManageTeam()) return false;

      require_once(LIBRARY_DIR.'/tdcron/class.tdcron.php');
      require_once(LIBRARY_DIR.'/tdcron/class.tdcron.entry.php');

      // Calculate the last occurrence of a lock.
      $last = tdCron::getLastOccurrence($this->lock_spec, mktime());
      $lockdate = new DateAndTime(DB_DATEFORMAT, strftime('%Y-%m-%d', $last));
      if ($date->before($lockdate)) {
        return true;
      }
    }
    return false;
  }
}

Because we already use cron handling elsewhere, this part is relatively straightfoward, with a simple override for managers.

Modifying Export and Import

We have to adjust WEB-INF/lib/ttImportHelper.class.php and WEB-INF/lib/ttExportHelper.class.php so that it takes care of the new cron_spec field. A big thanks to this being only one field instead of many, to keep the task small.

Changing Localization Files

For multiple language support of the Locking plugin, we have to localize title.locking in the language files located in WEB-INF/resources/

// NOTE TO TRANSLATORS: Locking is a feature to lock records from modifications (ex: weekly on Mondays we lock all previous weeks).
// It is also a name for the Locking plugin on the Team profile page.
'title.locking' => 'Locking',

Testing and Debugging

Now it's time to test and debug, and if all looks good, sit back, relax, and enjoy.

Summary

We the above changes to Time Tracker, we have implemented a flexible record locking feature that not only allows for weekly locks, but allows team managers to specify a cron format for locking. This code is open source and is part of Time Tracker 1.9.22.3470.


You can leave a comment on this project, or post a new project for consideration.