Work in progress creating a repo
authorNik Okuntseff <support@anuko.com>
Mon, 29 Feb 2016 03:39:25 +0000 (19:39 -0800)
committerNik Okuntseff <support@anuko.com>
Mon, 29 Feb 2016 03:39:25 +0000 (19:39 -0800)
Added files in root dir, subdirectories remain...

55 files changed:
charts.php [new file with mode: 0644]
client_add.php [new file with mode: 0644]
client_delete.php [new file with mode: 0644]
client_edit.php [new file with mode: 0644]
clients.php [new file with mode: 0644]
cron.php [new file with mode: 0644]
dbinstall.php [new file with mode: 0644]
default.css [new file with mode: 0644]
expense_delete.php [new file with mode: 0644]
expense_edit.php [new file with mode: 0644]
expenses.php [new file with mode: 0644]
export.php [new file with mode: 0644]
favicon.ico [new file with mode: 0644]
import.php [new file with mode: 0644]
index.php [new file with mode: 0644]
initialize.php [new file with mode: 0644]
invoice_add.php [new file with mode: 0644]
invoice_delete.php [new file with mode: 0644]
invoice_send.php [new file with mode: 0644]
invoice_view.php [new file with mode: 0644]
invoices.php [new file with mode: 0644]
license.txt [new file with mode: 0644]
login.php [new file with mode: 0644]
logout.php [new file with mode: 0644]
mysql.sql [new file with mode: 0644]
notification_add.php [new file with mode: 0644]
notification_delete.php [new file with mode: 0644]
notification_edit.php [new file with mode: 0644]
notifications.php [new file with mode: 0644]
password_change.php [new file with mode: 0644]
password_reset.php [new file with mode: 0644]
phpinfo.php [new file with mode: 0644]
profile_edit.php [new file with mode: 0644]
project_add.php [new file with mode: 0644]
project_delete.php [new file with mode: 0644]
project_edit.php [new file with mode: 0644]
projects.php [new file with mode: 0644]
readme.txt [new file with mode: 0644]
register.php [new file with mode: 0644]
report.php [new file with mode: 0644]
report_send.php [new file with mode: 0644]
reports.php [new file with mode: 0644]
rtl.css [new file with mode: 0644]
task_add.php [new file with mode: 0644]
task_delete.php [new file with mode: 0644]
task_edit.php [new file with mode: 0644]
tasks.php [new file with mode: 0644]
time.php [new file with mode: 0644]
time_delete.php [new file with mode: 0644]
time_edit.php [new file with mode: 0644]
tofile.php [new file with mode: 0644]
user_add.php [new file with mode: 0644]
user_delete.php [new file with mode: 0644]
user_edit.php [new file with mode: 0644]
users.php [new file with mode: 0644]

diff --git a/charts.php b/charts.php
new file mode 100644 (file)
index 0000000..5e26978
--- /dev/null
@@ -0,0 +1,230 @@
+<?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
+// +----------------------------------------------------------------------+
+
+// Note: This script uses Lichart PHP library and requires GD 2.0.1 or later.
+
+require_once('initialize.php');
+import('form.Form');
+import('DateAndTime');
+import('ttChartHelper');
+import('ttSysConfig');
+import('PieChartEx');
+import('ttUserHelper');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_view_charts)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Initialize and store date in session.
+$cl_date = $request->getParameter('date', @$_SESSION['date']);
+if(!$cl_date) {
+  $now = new DateAndTime(DB_DATEFORMAT);
+  $cl_date = $now->toString(DB_DATEFORMAT);
+}
+$_SESSION['date'] = $cl_date;
+
+// Initialize chart interval.
+$cl_interval = $_SESSION['chart_interval'];
+if (!$cl_interval) {
+  $sc = new ttSysConfig($user->id);
+  $cl_interval = $sc->getValue(SYSC_CHART_INTERVAL);
+}
+if (!$cl_interval) $cl_interval = INTERVAL_THIS_MONTH;
+$_SESSION['chart_interval'] = $cl_interval;
+
+// Initialize chart type.
+$cl_type = $_SESSION['chart_type'];
+if (!$cl_type) {
+  $sc = new ttSysConfig($user->id);
+  $cl_type = $sc->getValue(SYSC_CHART_TYPE);
+}
+if (MODE_TIME == $user->tracking_mode) {
+  if (in_array('cl', explode(',', $user->plugins)))
+    $cl_type = CHART_CLIENTS;
+} else {
+  if ($cl_type == CHART_CLIENTS) {
+    if (!in_array('cl', explode(',', $user->plugins)))
+      $cl_type = CHART_PROJECTS;       
+  } else if ($cl_type == CHART_TASKS) {
+    if (MODE_PROJECTS_AND_TASKS != $user->tracking_mode)
+      $cl_type = CHART_PROJECTS;
+  }
+}
+if (!$cl_type) $cl_type = CHART_PROJECTS;
+$_SESSION['chart_type'] = $cl_type;
+
+// Who do we draw charts for?
+$on_behalf_id = $request->getParameter('onBehalfUser', (isset($_SESSION['behalf_id'])? $_SESSION['behalf_id'] : $user->id));
+
+if ($request->getMethod( )== 'POST') {    
+  // If chart interval changed - save it.
+  $cl_interval = $request->getParameter('interval');
+  if ($cl_interval) {
+       // Save in the session
+    $_SESSION['chart_interval'] = $cl_interval;
+    // and permanently.
+       $sc = new ttSysConfig($user->id);
+    $sc->setValue(SYSC_CHART_INTERVAL, $cl_interval);
+  }
+  // If chart type changed - save it.  
+  $cl_type = $request->getParameter('type');
+  if ($cl_type) {
+    // Save in the session
+    $_SESSION['chart_type'] = $cl_type;
+    // and permanently.
+    $sc = new ttSysConfig($user->id);
+    $sc->setValue(SYSC_CHART_TYPE, $cl_type);  
+  }
+  // If user has changed - set behalf_id accordingly in the session.
+  if ($request->getParameter('onBehalfUser')) {
+    if($user->canManageTeam()) {
+      unset($_SESSION['behalf_id']);
+      unset($_SESSION['behalf_name']);
+       
+      if($on_behalf_id != $user->id) {
+        $_SESSION['behalf_id'] = $on_behalf_id;
+       $_SESSION['behalf_name'] = ttUserHelper::getUserName($on_behalf_id);                    
+      }
+      header('Location: charts.php');
+      exit();
+    }
+  }
+}
+
+// Elements of chartForm.
+$chart_form = new Form('chartForm');
+
+// User dropdown. Changes the user "on behalf" of whom we are working. 
+if ($user->canManageTeam()) {
+  $user_list = ttTeamHelper::getActiveUsers(array('putSelfFirst'=>true));
+  if (count($user_list) > 1) {
+    $chart_form->addInput(array('type'=>'combobox',
+      'onchange'=>'this.form.submit();',
+      'name'=>'onBehalfUser',
+      'value'=>$on_behalf_id,
+      'data'=>$user_list,
+      'datakeys'=>array('id','name'),
+    ));
+    $smarty->assign('on_behalf_control', 1);
+  }
+}
+
+// Chart interval options.
+$intervals = array();
+$intervals[INTERVAL_THIS_DAY] = $i18n->getKey('dropdown.this_day');
+$intervals[INTERVAL_THIS_WEEK] = $i18n->getKey('dropdown.this_week');
+$intervals[INTERVAL_THIS_MONTH] = $i18n->getKey('dropdown.this_month');
+$intervals[INTERVAL_THIS_YEAR] = $i18n->getKey('dropdown.this_year');
+$intervals[INTERVAL_ALL_TIME] = $i18n->getKey('dropdown.all_time');
+
+// Chart interval dropdown.
+$chart_form->addInput(array('type' => 'combobox',
+  'onchange' => 'if(this.form) this.form.submit();',
+  'name' => 'interval',
+  'value' => $cl_interval,
+  'data' => $intervals
+));
+
+// Chart type options.
+$chart_selector = (MODE_PROJECTS_AND_TASKS == $user->tracking_mode
+  || in_array('cl', explode(',', $user->plugins)));
+if ($chart_selector) {
+  $types = array();
+  if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode)
+    $types[CHART_PROJECTS] = $i18n->getKey('dropdown.projects');
+  if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode)
+    $types[CHART_TASKS] = $i18n->getKey('dropdown.tasks');
+  if (in_array('cl', explode(',', $user->plugins)))
+    $types[CHART_CLIENTS] = $i18n->getKey('dropdown.clients');
+       
+  // Add chart type dropdown.
+  $chart_form->addInput(array('type' => 'combobox',
+    'onchange' => 'if(this.form) this.form.submit();',
+    'name' => 'type',
+    'value' => $cl_type,
+    'data' => $types
+  ));
+}
+
+// Calendar.
+$chart_form->addInput(array('type'=>'calendar','name'=>'date','value'=>$cl_date)); // calendar
+
+// Get data for our chart.
+$totals = ttChartHelper::getTotals($on_behalf_id, $cl_type, $cl_date, $cl_interval);
+$smarty->assign('totals', $totals);   
+
+// Prepare chart for drawing.
+/*
+ * We use libchart.php library to draw chart images. It can draw chart labels, too (embed in the image).
+ * But quality of such auto-scaled text is not good. Therefore, we only use libchart to draw a pie-chart picture with
+ * auto-calculated percentage markers around it. We print labels (to the side of the picture) ourselves,
+ * using the same colors libchart is using. For labels printout, the $totals array (which is used for picture points)
+ * is also passed to charts.tpl Smarty template.
+ * 
+ * To make all of the above possible with only one database call to obtain $totals we have to print the chart image
+ * to a file here (see code below). Once the image is available as a .png file, the charts.tpl can render it.
+ *
+ * PieChartEx class is a little extension to libchart-provided PieChart class. It allows us to print the chart
+ * without title, logo, and labels.
+ */
+$chart = new PieChartEx(300, 300);
+$data_set = new XYDataSet();
+foreach($totals as $total) {
+  $data_set->addPoint(new Point( $total['name'], $total['time']));
+}
+$chart->setDataSet($data_set); 
+
+// Prepare a file name.
+$img_dir = TEMPLATE_DIR.'_c/'; // Directory.
+$file_name = uniqid('chart_').'.png'; // Short file name. Unique ID here is to avoid problems with browser caching.
+$img_ref = 'WEB-INF/templates_c/'.$file_name; // Image reference for html.
+$file_name = $img_dir.$file_name; // Full file name. 
+
+// Clean up the file system from older images.
+$img_files = glob($img_dir.'chart_*.png');
+foreach($img_files as $file) {
+  // If the create time of file is older than 1 minute, delete it.
+  if (filemtime($file) < (time() - 60)) {
+    unlink($file);
+  }
+}
+
+// Write chart image to file system.
+$chart->renderEx(array('fileName'=>$file_name,'hideLogo'=>true,'hideTitle'=>true,'hideLabel'=>true));
+// At this point libchart usage is complete and we have chart image on disk.
+
+$smarty->assign('img_file_name', $img_ref);
+$smarty->assign('chart_selector', $chart_selector);
+$smarty->assign('forms', array($chart_form->getName() => $chart_form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.charts'));
+$smarty->assign('content_page_name', 'charts.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/client_add.php b/client_add.php
new file mode 100644 (file)
index 0000000..503b940
--- /dev/null
@@ -0,0 +1,89 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttClientHelper');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$projects = ttTeamHelper::getActiveProjects($user->team_id);
+
+if ($request->getMethod() == 'POST') {
+  $cl_name = trim($request->getParameter('name'));
+  $cl_address = trim($request->getParameter('address'));
+  $cl_tax = $request->getParameter('tax');
+  $cl_projects = $request->getParameter('projects');
+} else {
+  // Do not assign all projects to a new client by default. This should help to reduce clutter.
+  // foreach ($projects as $project_item)
+  //    $cl_projects[] = $project_item['id'];
+}
+
+$form = new Form('clientForm');
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'name','style'=>'width: 350px;','value'=>$cl_name));
+$form->addInput(array('type'=>'textarea','name'=>'address','maxlength'=>'255','style'=>'width: 350px;','cols'=>'55','rows'=>'5','value'=>$cl_address));
+$form->addInput(array('type'=>'floatfield','name'=>'tax','size'=>'10','format'=>'.2','value'=>$cl_tax));
+$form->addInput(array('type'=>'checkboxgroup','name'=>'projects','data'=>$projects,'layout'=>'H','datakeys'=>array('id','name'),'value'=>$cl_projects));
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.add')));
+       
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.client_name'));
+  if (!ttValidString($cl_address, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.client_address'));
+  if (!ttValidFloat($cl_tax, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.tax'));
+
+  if ($errors->isEmpty()) {
+    if (!ttClientHelper::getClientByName($cl_name)) {
+      if (ttClientHelper::insert(array(
+        'team_id' => $user->team_id,
+        'name' => $cl_name,
+        'address' => $cl_address,
+        'tax' => $cl_tax,
+        'projects' => $cl_projects,
+        'status' => ACTIVE))) {
+        header('Location: clients.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));       
+     } else
+       $errors->add($i18n->getKey('error.client_exists'));
+  }
+} // post
+       
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.clientForm.name.focus()"');
+$smarty->assign('title', $i18n->getKey('title.add_client'));
+$smarty->assign('content_page_name', 'client_add.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/client_delete.php b/client_delete.php
new file mode 100644 (file)
index 0000000..d03ef9c
--- /dev/null
@@ -0,0 +1,76 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttClientHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$id = (int)$request->getParameter('id');
+$client = ttClientHelper::getClient($id);
+
+$client_to_delete = $client['name'];
+       
+$form = new Form('clientDeleteForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$id));
+$form->addInput(array('type'=>'combobox',
+    'name'=>'delete_client_entries',
+    'data'=>array('0'=>$i18n->getKey('dropdown.do_not_delete'),'1'=>$i18n->getKey('dropdown.delete')),
+));
+$form->addInput(array('type'=>'submit','name'=>'btn_delete','value'=>$i18n->getKey('label.delete')));
+$form->addInput(array('type'=>'submit','name'=>'btn_cancel','value'=>$i18n->getKey('button.cancel')));
+
+if ($request->getMethod() == 'POST') {
+  if(ttClientHelper::getClient($id)) {
+    if ($request->getParameter('btn_delete')) {
+      if (ttClientHelper::delete($id, $request->getParameter('delete_client_entries'))) {
+        header('Location: clients.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    }
+  } else 
+      $errors->add($i18n->getKey('error.db'));
+
+  if ($request->getParameter('btn_cancel')) {
+       header('Location: clients.php');
+       exit();
+  }
+} // post
+
+$smarty->assign('client_to_delete', $client_to_delete);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.delete_client'));
+$smarty->assign('content_page_name', 'client_delete.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/client_edit.php b/client_edit.php
new file mode 100644 (file)
index 0000000..400286d
--- /dev/null
@@ -0,0 +1,121 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttClientHelper');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$cl_id = (int) $request->getParameter('id');
+
+$projects = ttTeamHelper::getActiveProjects($user->team_id);
+
+if ($request->getMethod() == 'POST') {
+  $cl_name = trim($request->getParameter('name'));
+  $cl_address = trim($request->getParameter('address'));
+  $cl_tax = trim($request->getParameter('tax'));
+  $cl_status = $request->getParameter('status');
+  $cl_projects = $request->getParameter('projects');
+} else {
+  $client = ttClientHelper::getClient($cl_id, true);
+  $cl_name = $client['name'];
+  $cl_address = $client['address'];
+  $cl_tax = $client['tax'];
+  $cl_status = $client['status'];
+  $assigned_projects = ttClientHelper::getAssignedProjects($cl_id);
+  foreach($assigned_projects as $project_item) {
+    $cl_projects[] = $project_item['id'];
+  }
+}
+
+$form = new Form('clientForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_id));
+$form->addInput(array('type'=>'text','name'=>'name','maxlength'=>'100','style'=>'width: 350px;','value'=>$cl_name));
+$form->addInput(array('type'=>'textarea','name'=>'address','maxlength'=>'255','style'=>'width: 350px;','cols'=>'55','rows'=>'5','value'=>$cl_address));
+$form->addInput(array('type'=>'floatfield','name'=>'tax','size'=>'10','format'=>'.2','value'=>$cl_tax));
+$form->addInput(array('type'=>'combobox','name'=>'status','value'=>$cl_status,
+  'data'=>array(ACTIVE=>$i18n->getKey('dropdown.status_active'),INACTIVE=>$i18n->getKey('dropdown.status_inactive'))));
+$form->addInput(array('type'=>'checkboxgroup','name'=>'projects','data'=>$projects,'datakeys'=>array('id','name'),'layout'=>'H','value'=>$cl_projects));
+$form->addInput(array('type'=>'submit','name'=>'btn_save','value'=>$i18n->getKey('button.save')));
+$form->addInput(array('type'=>'submit','name'=>'btn_copy','value'=>$i18n->getKey('button.copy')));
+       
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.client_name'));
+  if (!ttValidString($cl_address, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.client_address'));
+  if (!ttValidFloat($cl_tax, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.tax'));
+               
+  if ($errors->isEmpty()) {
+       if ($request->getParameter('btn_save')) {
+      $client = ttClientHelper::getClientByName($cl_name);
+      if (($client && ($cl_id == $client['id'])) || !$client) {
+        if (ttClientHelper::update(array(
+          'id' => $cl_id,
+          'name' => $cl_name,
+          'address' => $cl_address,
+          'tax' => $cl_tax,
+          'status' => $cl_status,
+          'projects' => $cl_projects))) {
+          header('Location: clients.php');
+          exit();
+        } else
+          $errors->add($i18n->getKey('error.db'));     
+      } else
+        $errors->add($i18n->getKey('error.client_exists'));
+       }
+       
+    if ($request->getParameter('btn_copy')) {
+      if (!ttClientHelper::getClientByName($cl_name)) {
+        if (ttClientHelper::insert(array(
+          'team_id' => $user->team_id,
+          'name' => $cl_name,
+          'address' => $cl_address,
+          'tax' => $cl_tax,
+          'status' => $cl_status,
+          'projects' => $cl_projects))) {
+          header('Location: clients.php');
+          exit();
+        } else
+          $errors->add($i18n->getKey('error.db'));
+      } else
+        $errors->add($i18n->getKey('error.client_exists'));
+    }
+  }
+} // post
+       
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.edit_client'));
+$smarty->assign('content_page_name', 'client_edit.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/clients.php b/clients.php
new file mode 100644 (file)
index 0000000..61c6658
--- /dev/null
@@ -0,0 +1,44 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$smarty->assign('active_clients', ttTeamHelper::getActiveClients($user->team_id, true));
+$smarty->assign('inactive_clients', ttTeamHelper::getInactiveClients($user->team_id, true));
+$smarty->assign('title', $i18n->getKey('title.clients'));
+$smarty->assign('content_page_name', 'clients.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/cron.php b/cron.php
new file mode 100644 (file)
index 0000000..22b4991
--- /dev/null
+++ b/cron.php
@@ -0,0 +1,84 @@
+<?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
+// +----------------------------------------------------------------------+
+
+/*
+ * cron.php - this file is an entry point to execute scheduled jobs in Time Tracker.
+ * It must be called externally (for example, from the system cron or task scheduler).
+ * 
+ * Internally, we store scheduled jobs in tt_cron table in db. The cron_spec field is in cron format.
+ * Along with it, we store last and next timestamps for jobs, we use them as an execute condition.
+ * 
+ * Although cron_spec follows 5-field cron specification precisely, actual job timing depends on
+ * how often cron.php is called. For example, an hourly ping will execute jobs no more than once
+ * each hour, even if they are due more often. Configure whatever calls this file accordingly.
+ */
+
+require_once('initialize.php');
+require_once(LIBRARY_DIR.'/tdcron/class.tdcron.php');
+require_once(LIBRARY_DIR.'/tdcron/class.tdcron.entry.php');
+import('ttFavReportHelper');
+import('ttReportHelper');
+
+$mdb2 = getConnection();
+$now = mktime();
+
+$sql = "select * from tt_cron where $now >= next 
+        and status = 1 and report_id is not null and email is not null";
+$res = $mdb2->query($sql);
+if (is_a($res, 'PEAR_Error'))
+  exit;
+
+while ($val = $res->fetchRow()) {
+  // We have jobs to execute in user language.
+  
+  // Get favorite report details.
+  $report = ttFavReportHelper::getReport($val['report_id']);
+  if (!$report) continue;
+  
+  // Recycle global $user and $i18n objects, as user settings and language are specific for each report.
+  $user = new ttUser(null, $report['user_id']);
+  $i18n->load($user->lang);
+
+  // Email report.
+  if (ttReportHelper::sendFavReport($report, $val['email']))
+    echo "Report ".$val['report_id']. " sent to ".$val['email']."<br>";
+  else
+    echo "Error while emailing report...<br>";
+    
+  // Calculate next execution time.
+  $next = tdCron::getNextOccurrence($val['cron_spec'], $now); 
+
+  // Update last and next values in tt_cron.
+  $sql = "update tt_cron set last = $now, next = $next where id = ".$val['id'];
+  $affected = $mdb2->exec($sql);
+  if (is_a($affected, 'PEAR_Error')) continue;
+}
+
+echo "Done!";
+
+?>
\ No newline at end of file
diff --git a/dbinstall.php b/dbinstall.php
new file mode 100644 (file)
index 0000000..c65358a
--- /dev/null
@@ -0,0 +1,675 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('WEB-INF/config.php');
+require_once('WEB-INF/lib/common.lib.php');
+if (!require_once('MDB2.php')) {
+       die ("Unable to require MDB2 module. Please check it<br>\n");
+}
+require_once('initialize.php');
+import('ttUserHelper');
+import('ttTaskHelper');
+
+function setChange($sql) {
+       print "<pre>".$sql."</pre>";
+       $mdb2 = getConnection();
+       $affected = $mdb2->exec($sql);
+       if (is_a($affected, 'PEAR_Error')) {
+               print "error: ".$affected->getMessage()."<br>";
+       } else {
+           print "successful update<br>\n";
+       }
+}
+
+
+  if ($_POST) {
+       print "Processing...<br>\n";
+
+       if ($_POST["crstructure"]) {
+                 $sqlQuery = join("\n", file("mysql.sql"));
+                 $sqlQuery = str_replace("TYPE=MyISAM","",$sqlQuery);
+                 $queries  = explode(";",$sqlQuery);
+                 if (is_array($queries)) {
+                         foreach ($queries as $query) {
+                               $query = trim($query);
+                               if (strlen($query)>0) {
+                                       setChange($query);
+                               }
+                         }
+                 }
+       }
+
+    if ($_POST["convert5to7"]) {
+      setChange("alter table `activity_log` CHANGE al_comment al_comment BLOB");
+         setChange("CREATE TABLE `sysconfig` (`sysc_id` int(11) unsigned NOT NULL auto_increment,`sysc_name` varchar(32) NOT NULL default '',`sysc_value` varchar(70) default NULL, PRIMARY KEY  (`sysc_id`), UNIQUE KEY `sysc_id` (`sysc_id`), UNIQUE KEY `sysc_name` (`sysc_name`))");
+         setChange("alter table `companies` add c_locktime int(4) default -1");
+         setChange("alter table `activity_log` add al_billable tinyint(4) default 0");
+         setChange("alter table `sysconfig` drop INDEX `sysc_name`");
+         setChange("alter table `sysconfig` add sysc_id_u int(4)");
+         setChange("alter table `report_filter_set` add rfs_billable VARCHAR(10)");
+      setChange("ALTER TABLE clients MODIFY clnt_id int(11) NOT NULL AUTO_INCREMENT");
+      setChange("ALTER TABLE `users` ADD `u_show_pie` smallint(2) DEFAULT '1'");
+      setChange("alter table `users` ADD `u_pie_mode` smallint(2) DEFAULT '1'");
+      setChange("alter table users drop `u_aprojects`");
+       }
+
+  if ($_POST["convert7to133"]) {
+    setChange("ALTER TABLE users ADD COLUMN u_lang VARCHAR(20) DEFAULT NULL");
+    setChange("ALTER TABLE users ADD COLUMN u_email VARCHAR(100) DEFAULT NULL");
+    setChange("ALTER TABLE `activity_log` drop `al_proof`");
+    setChange("ALTER TABLE `activity_log` drop `al_charge`");
+    setChange("ALTER TABLE `activities` drop `a_project_id`");
+    setChange("DROP TABLE `activity_status_list`");
+    setChange("DROP TABLE `project_status_list`");
+    setChange("DROP TABLE `user_status_list`");
+    setChange("DROP TABLE `companies_c_id_seq`");
+    setChange("ALTER TABLE projects ADD COLUMN p_activities TEXT");
+  }
+
+  // The update_projects function updates p_activities field in the projects table so that we could
+  // improve performance of the application by using this field instead of activity_bind table.
+  if ($_POST["update_projects"]) {
+    $mdb2 = getConnection();
+    // $sql = "select p_id from projects where p_status = 1 and p_activities is NULL";
+    $sql = "select p_id from projects where p_status = 1";
+    $res = $mdb2->query($sql);
+    if (is_a($res, 'PEAR_Error')) {
+      die($res->getMessage());
+    }
+    // Iterate through projects.
+    while ($val = $res->fetchRow()) {
+       $project_id = $val['p_id'];
+       
+        // Get activity binds for project (old way).
+        // $sql = "select ab_id_a from activity_bind where ab_id_p = $project_id";
+        $sql = "select ab_id_a, a_id, a_name from activity_bind
+          inner join activities on (ab_id_a = a_id)
+          where ab_id_p = $project_id
+          order by a_name";
+        
+        $result = $mdb2->query($sql);
+        if (is_a($result, 'PEAR_Error')) {
+          die($result->getMessage());
+        }
+        $activity_arr = array();
+        while ($value = $result->fetchRow()) {
+          $activity_arr[] = $value['ab_id_a']; 
+        }
+        $a_comma_separated = implode(",", $activity_arr); // This is a comma-separated list of associated activity ids.
+             
+        // Re-bind the project to activities (new way).
+        $sql = "update projects set p_activities = ".$mdb2->quote($a_comma_separated)." where p_id = $project_id";
+        $affected = $mdb2->exec($sql);
+        if (is_a($affected, 'PEAR_Error')) {
+          die($affected->getMessage());
+        }
+    }
+  }
+  
+  if ($_POST["convert133to1340"]) {
+    setChange("ALTER TABLE companies ADD COLUMN c_show_pie smallint(2) DEFAULT 1");
+    setChange("ALTER TABLE companies ADD COLUMN c_pie_mode smallint(2) DEFAULT 1");
+    setChange("ALTER TABLE companies ADD COLUMN c_lang varchar(20) default NULL");
+  }
+  
+  // The update_companies function sets up c_show_pie, c_pie_mode, and c_lang
+  // fields in the companies table from the corresponding manager fields. 
+  if ($_POST["update_companies"]) {
+    $mdb2 = getConnection();
+    // Get all active managers.
+    $sql = "select u_company_id, u_show_pie, u_pie_mode, u_lang from users
+      where u_manager_id is NULL and u_login <> 'admin' and u_company_id is not NULL and u_active = 1"; 
+    $res = $mdb2->query($sql);
+    if (is_a($res, 'PEAR_Error')) {
+      die($res->getMessage());
+    }
+    // Iterate through managers and set fields in the companies table.
+    while ($val = $res->fetchRow()) {
+      $company_id = $val['u_company_id'];
+      $show_pie = $val['u_show_pie'];
+      $pie_mode = $val['u_pie_mode'];
+      $lang = $val['u_lang'];
+      
+      $sql = "update companies set
+        c_show_pie = $show_pie, c_pie_mode = $pie_mode, c_lang = ".$mdb2->quote($lang).
+        " where c_id = $company_id";
+      
+      $result = $mdb2->query($sql);
+      if (is_a($result, 'PEAR_Error')) {
+        die($result->getMessage());
+      }
+    }
+  }
+    
+  if ($_POST["convert1340to1485"]) {
+    setChange("ALTER TABLE users DROP u_show_pie");
+    setChange("ALTER TABLE users DROP u_pie_mode");
+    setChange("ALTER TABLE users DROP u_lang");  
+    setChange("ALTER TABLE `users` modify u_login varchar(100) NOT NULL");
+    setChange("ALTER TABLE `users` modify u_active smallint(6) default '1'");
+    setChange("drop index u_login_idx on users");
+    setChange("create unique index u_login_idx on users(u_login, u_active)");
+       setChange("ALTER TABLE companies MODIFY `c_lang` varchar(20) NOT NULL default 'en'");
+       setChange("ALTER TABLE companies ADD COLUMN `c_date_format` varchar(20) NOT NULL default '%Y-%m-%d'");
+       setChange("ALTER TABLE companies ADD COLUMN `c_time_format` varchar(20) NOT NULL default '%H:%M'");
+    setChange("ALTER TABLE companies ADD COLUMN `c_week_start` smallint(2) NOT NULL DEFAULT '0'");
+       setChange("ALTER TABLE clients MODIFY `clnt_status` smallint(6) default '1'");
+    setChange("create unique index clnt_name_idx on clients(clnt_id_um, clnt_name, clnt_status)");
+       setChange("ALTER TABLE projects modify p_status smallint(6) default '1'");
+       setChange("update projects set p_status = NULL where p_status = 1000");
+       setChange("drop index p_manager_idx on projects");
+       setChange("create unique index p_name_idx on projects(p_manager_id, p_name, p_status)");
+       setChange("ALTER TABLE activities modify a_status smallint(6) default '1'");
+       setChange("update activities set a_status = NULL where a_status = 1000");
+       setChange("drop index a_manager_idx on activities");
+       setChange("create unique index a_name_idx on activities(a_manager_id, a_name, a_status)");
+       setChange("RENAME TABLE companies TO teams");
+       setChange("RENAME TABLE teams TO att_teams");
+       setChange("ALTER TABLE att_teams CHANGE c_id id int(11) NOT NULL auto_increment");
+       setChange("RENAME TABLE users TO att_users");
+    setChange("update att_users set u_company_id = 0 where u_company_id is NULL");    
+    setChange("ALTER TABLE att_users CHANGE u_company_id team_id int(11) NOT NULL");
+    setChange("RENAME TABLE att_teams TO tt_teams");
+    setChange("RENAME TABLE att_users TO tt_users");
+    setChange("ALTER TABLE tt_teams CHANGE c_name name varchar(80) NOT NULL");
+    setChange("ALTER TABLE `tt_teams` drop `c_www`");
+    setChange("ALTER TABLE `tt_teams` MODIFY `name` varchar(80) default NULL");
+    setChange("ALTER TABLE clients ADD COLUMN `your_name` varchar(255) default NULL");
+    setChange("ALTER TABLE tt_teams ADD COLUMN `address` varchar(255) default NULL");
+    setChange("ALTER TABLE invoice_header ADD COLUMN `client_name` varchar(255) default NULL");
+    setChange("ALTER TABLE invoice_header ADD COLUMN `client_addr` varchar(255) default NULL");
+    setChange("ALTER TABLE report_filter_set ADD COLUMN `rfs_cb_cost` tinyint(4) default '0'");
+    setChange("ALTER TABLE activity_log DROP primary key");    
+       setChange("ALTER TABLE activity_log ADD COLUMN `id` bigint NOT NULL auto_increment primary key"); 
+    setChange("CREATE TABLE `tt_custom_fields` (`id` int(11) NOT NULL auto_increment, `team_id` int(11) NOT NULL, `type` tinyint(4) NOT NULL default '0', `label` varchar(32) NOT NULL default '', PRIMARY KEY (`id`))");
+    setChange("CREATE TABLE `tt_custom_field_options` (`id` int(11) NOT NULL auto_increment, `field_id` int(11) NOT NULL, `value` varchar(32) NOT NULL default '', PRIMARY KEY (`id`))");
+    setChange("CREATE TABLE `tt_custom_field_log` (`id` bigint NOT NULL auto_increment, `al_id` bigint NOT NULL, `field_id` int(11) NOT NULL, `value` varchar(255) default NULL, PRIMARY KEY (`id`))");
+       setChange("ALTER TABLE tt_users DROP u_level"); 
+       setChange("ALTER TABLE tt_custom_fields ADD COLUMN `status` tinyint(4) default '1'"); 
+    setChange("ALTER TABLE report_filter_set ADD COLUMN `rfs_cb_cf_1` tinyint(4) default '0'");
+    setChange("ALTER TABLE tt_teams ADD COLUMN `plugins` varchar(255) default NULL");
+    setChange("ALTER TABLE tt_teams MODIFY c_locktime int(4) default '0'");
+    setChange("ALTER TABLE clients DROP your_name");
+    setChange("ALTER TABLE clients DROP clnt_addr_your");
+    setChange("ALTER TABLE `tt_custom_fields` ADD COLUMN `required` tinyint(4) default '0'");
+    setChange("ALTER TABLE tt_teams DROP c_pie_mode");
+    setChange("RENAME TABLE report_filter_set TO tt_fav_reports");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_id id int(11) unsigned NOT NULL auto_increment");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_name name varchar(200) NOT NULL");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_id_u user_id int(11) NOT NULL");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_id_p project_id int(11) default NULL");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_id_a task_id int(11) default NULL");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_users users text default NULL");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_period period tinyint(4) default NULL");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_period_start period_start date default NULL");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_period_finish period_end date default NULL");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_project show_project tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_activity show_task tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_note show_note tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_start show_start tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_finish show_end tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_duration show_duration tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_cost show_cost tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_cf_1 show_custom_field_1 tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_idle show_empty_days tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_cb_totals_only show_totals_only tinyint(4) NOT NULL default '0'");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_groupby group_by varchar(20) default NULL");
+    setChange("ALTER TABLE tt_fav_reports CHANGE rfs_billable billable tinyint(4) default NULL");
+    setChange("ALTER TABLE projects CHANGE p_activities tasks text default NULL");
+    setChange("ALTER TABLE tt_teams CHANGE c_currency currency varchar(7) default NULL");
+    setChange("ALTER TABLE tt_teams CHANGE c_locktime locktime int(4) default '0'");
+    setChange("ALTER TABLE tt_teams CHANGE c_show_pie show_pie smallint(2) DEFAULT '1'");
+    setChange("ALTER TABLE tt_teams CHANGE c_lang lang varchar(10) NOT NULL default 'en'");
+    setChange("ALTER TABLE tt_teams CHANGE c_date_format date_format varchar(20) NOT NULL default '%Y-%m-%d'");
+    setChange("ALTER TABLE tt_teams CHANGE c_time_format time_format varchar(20) NOT NULL default '%H:%M'");
+    setChange("ALTER TABLE tt_teams CHANGE c_week_start week_start smallint(2) NOT NULL DEFAULT '0'");
+    setChange("ALTER TABLE tt_users CHANGE u_id id int(11) NOT NULL auto_increment");
+    setChange("ALTER TABLE tt_users CHANGE u_timestamp timestamp timestamp NOT NULL");
+    setChange("ALTER TABLE tt_users CHANGE u_login login varchar(50) NOT NULL");
+    setChange("drop index u_login_idx on tt_users");  
+    setChange("create unique index login_idx on tt_users(login, u_active)");
+    setChange("ALTER TABLE tt_users CHANGE u_password password varchar(50) default NULL");
+    setChange("ALTER TABLE tt_users CHANGE u_name name varchar(100) default NULL");
+    setChange("ALTER TABLE tt_users CHANGE u_email email varchar(100) default NULL");
+    setChange("ALTER TABLE tt_users CHANGE u_rate rate float(6,2) NOT NULL default '0.00'");
+    setChange("update tt_users set u_active = NULL where u_active = 1000");
+    setChange("ALTER TABLE tt_users CHANGE u_active status tinyint(4) default '1'");
+    setChange("ALTER TABLE tt_teams ADD COLUMN status tinyint(4) default '1'");
+    setChange("ALTER TABLE tt_users ADD COLUMN role int(11) default '4'");
+    setChange("update tt_users set role = 1024 where login = 'admin'");
+    setChange("update tt_users set role = 68 where u_comanager = 1");
+    setChange("update tt_users set role = 324 where u_manager_id is null and login != 'admin'");
+    setChange("ALTER TABLE user_bind CHANGE ub_checked status tinyint(4) default '1'");
+    setChange("ALTER TABLE activities ADD COLUMN team_id int(11) NOT NULL");
+    setChange("ALTER TABLE clients ADD COLUMN team_id int(11) NOT NULL");
+    setChange("ALTER TABLE projects ADD COLUMN team_id int(11) NOT NULL");                     
+  }
+  
+  // The update_to_team_id function sets team_id field projects, activities, and clients tables.
+  if ($_POST["update_to_team_id"]) {
+    $mdb2 = getConnection();
+    
+    // Update projects.   
+    $sql = "select p_id, p_manager_id from projects where team_id = 0 limit 1000";
+    $res = $mdb2->query($sql);
+    if (is_a($res, 'PEAR_Error')) {
+      die($res->getMessage());
+    }
+    // Iterate through projects.
+    $projects_updated = 0;
+    while ($val = $res->fetchRow()) {
+      $project_id = $val['p_id'];
+      $manager_id = $val['p_manager_id'];
+      
+      $sql = "select team_id from tt_users where id = $manager_id";
+      $res2 = $mdb2->query($sql);
+      if (is_a($res2, 'PEAR_Error')) {
+        die($res2->getMessage());
+      }
+      $val2 = $res2->fetchRow();
+      $team_id = $val2['team_id'];
+      
+      if ($team_id) {
+        $sql = "update projects set team_id = $team_id where p_id = $project_id";
+        $affected = $mdb2->exec($sql);
+        if (is_a($affected, 'PEAR_Error')) {
+          die($affected->getMessage());
+        }
+        $projects_updated += $affected;        
+      }
+    }
+    print "Updated $projects_updated projects...<br>\n";
+    
+    // Update tasks.   
+    $sql = "select a_id, a_manager_id from activities where team_id = 0 limit 1000";
+    $res = $mdb2->query($sql);
+    if (is_a($res, 'PEAR_Error')) {
+      die($res->getMessage());
+    }
+    // Iterate through tasks.
+    $tasks_updated = 0;
+    while ($val = $res->fetchRow()) {
+      $task_id = $val['a_id'];
+      $manager_id = $val['a_manager_id'];
+      
+      $sql = "select team_id from tt_users where id = $manager_id";
+      $res2 = $mdb2->query($sql);
+      if (is_a($res2, 'PEAR_Error')) {
+        die($res2->getMessage());
+      }
+      $val2 = $res2->fetchRow();
+      $team_id = $val2['team_id'];
+      
+      if ($team_id) {
+        $sql = "update activities set team_id = $team_id where a_id = $task_id";
+        $affected = $mdb2->exec($sql);
+        if (is_a($affected, 'PEAR_Error')) {
+          die($affected->getMessage());
+        }
+        $tasks_updated += $affected;   
+      }
+    }
+    print "Updated $tasks_updated tasks...<br>\n";
+    
+    // Update clients.   
+    $sql = "select clnt_id, clnt_id_um from clients where team_id = 0 limit 1000";
+    $res = $mdb2->query($sql);
+    if (is_a($res, 'PEAR_Error')) {
+      die($res->getMessage());
+    }
+    // Iterate through clients.
+    $clients_updated = 0;
+    while ($val = $res->fetchRow()) {
+      $client_id = $val['clnt_id'];
+      $manager_id = $val['clnt_id_um'];
+      
+      $sql = "select team_id from tt_users where id = $manager_id";
+      $res2 = $mdb2->query($sql);
+      if (is_a($res2, 'PEAR_Error')) {
+        die($res2->getMessage());
+      }
+      $val2 = $res2->fetchRow();
+      $team_id = $val2['team_id'];
+      
+      if ($team_id) {
+        $sql = "update clients set team_id = $team_id where clnt_id = $client_id";
+        $affected = $mdb2->exec($sql);
+        if (is_a($affected, 'PEAR_Error')) {
+          die($affected->getMessage());
+        }
+        $clients_updated += $affected; 
+      }
+    }
+    print "Updated $clients_updated clients...<br>\n";
+  }
+    
+  if ($_POST["convert1485to1579"]) {
+    setChange("ALTER TABLE tt_fav_reports MODIFY id int(11) NOT NULL auto_increment");
+       setChange("RENAME TABLE clients TO tt_clients");
+    setChange("ALTER TABLE tt_clients CHANGE clnt_id id int(11) NOT NULL AUTO_INCREMENT");
+    setChange("ALTER TABLE tt_clients CHANGE clnt_status status tinyint(4) default '1'");
+    setChange("ALTER TABLE tt_clients DROP clnt_id_um");       
+    setChange("ALTER TABLE tt_clients CHANGE clnt_name name varchar(80) NOT NULL");
+    setChange("drop index clnt_name_idx on tt_clients");
+    setChange("drop index client_name_idx on tt_clients");   
+    setChange("create unique index client_name_idx on tt_clients(team_id, name, status)");
+    setChange("ALTER TABLE tt_teams ADD COLUMN `timestamp` timestamp NOT NULL");
+    setChange("ALTER TABLE tt_clients CHANGE clnt_addr_cust address varchar(255) default NULL");
+    setChange("ALTER TABLE tt_clients DROP clnt_discount");
+    setChange("ALTER TABLE tt_clients DROP clnt_comment");
+    setChange("ALTER TABLE tt_clients DROP clnt_fsubtotals");
+    setChange("ALTER TABLE tt_clients CHANGE clnt_tax tax float(6,2) NOT NULL default '0.00'");
+    setChange("ALTER TABLE activity_log ADD COLUMN client_id int(11) default NULL");
+    setChange("ALTER TABLE tt_teams DROP show_pie");
+    setChange("ALTER TABLE tt_fav_reports CHANGE group_by sort_by varchar(20) default 'date'");
+    setChange("RENAME TABLE tmp_refs TO tt_tmp_refs");
+    setChange("ALTER TABLE tt_tmp_refs CHANGE tr_created timestamp timestamp NOT NULL");
+    setChange("ALTER TABLE tt_tmp_refs CHANGE tr_code ref char(32) NOT NULL default ''");
+    setChange("ALTER TABLE tt_tmp_refs CHANGE tr_userid user_id int(11) NOT NULL");
+    setChange("RENAME TABLE projects TO tt_projects");
+    setChange("ALTER TABLE tt_projects CHANGE p_id id int(11) NOT NULL auto_increment");
+    setChange("ALTER TABLE tt_projects DROP p_timestamp");
+    setChange("ALTER TABLE tt_projects CHANGE p_name name varchar(80) NOT NULL");
+    setChange("ALTER TABLE tt_projects CHANGE p_status status tinyint(4) default '1'");
+    setChange("drop index p_name_idx on tt_projects");
+    setChange("create unique index project_idx on tt_projects(team_id, name, status)");
+    setChange("RENAME TABLE activities TO tt_tasks");
+    setChange("ALTER TABLE tt_tasks CHANGE a_id id int(11) NOT NULL auto_increment");
+    setChange("ALTER TABLE tt_tasks DROP a_timestamp");
+    setChange("ALTER TABLE tt_tasks CHANGE a_name name varchar(80) NOT NULL");
+    setChange("ALTER TABLE tt_tasks CHANGE a_status status tinyint(4) default '1'");
+    setChange("drop index a_name_idx on tt_tasks");
+    setChange("create unique index task_idx on tt_tasks(team_id, name, status)");
+       setChange("RENAME TABLE invoice_header TO tt_invoice_headers");
+       setChange("ALTER TABLE tt_invoice_headers CHANGE ih_user_id user_id int(11) NOT NULL");
+       setChange("ALTER TABLE tt_invoice_headers CHANGE ih_number number varchar(20) default NULL");
+       setChange("ALTER TABLE tt_invoice_headers DROP ih_addr_your");
+       setChange("ALTER TABLE tt_invoice_headers DROP ih_addr_cust");
+       setChange("ALTER TABLE tt_invoice_headers CHANGE ih_comment comment varchar(255) default NULL");
+       setChange("ALTER TABLE tt_invoice_headers CHANGE ih_tax tax float(6,2) default '0.00'");
+       setChange("ALTER TABLE tt_invoice_headers CHANGE ih_discount discount float(6,2) default '0.00'");
+       setChange("ALTER TABLE tt_invoice_headers CHANGE ih_fsubtotals subtotals tinyint(4) NOT NULL default '0'");
+       setChange("ALTER TABLE tt_users DROP u_comanager");
+       setChange("ALTER TABLE tt_tasks DROP a_manager_id");
+       setChange("ALTER TABLE tt_projects DROP p_manager_id");
+       setChange("ALTER TABLE tt_users DROP u_manager_id");
+       setChange("ALTER TABLE activity_bind DROP ab_id");
+       setChange("RENAME TABLE activity_bind TO tt_project_task_binds");
+       setChange("ALTER TABLE tt_project_task_binds CHANGE ab_id_p project_id int(11) NOT NULL");
+       setChange("ALTER TABLE tt_project_task_binds CHANGE ab_id_a task_id int(11) NOT NULL");
+       setChange("RENAME TABLE user_bind TO tt_user_project_binds");
+       setChange("ALTER TABLE tt_user_project_binds CHANGE ub_rate rate float(6,2) NOT NULL default '0.00'");
+       setChange("ALTER TABLE tt_user_project_binds CHANGE ub_id_p project_id int(11) NOT NULL");
+       setChange("ALTER TABLE tt_user_project_binds CHANGE ub_id_u user_id int(11) NOT NULL");
+       setChange("ALTER TABLE tt_user_project_binds CHANGE ub_id id int(11) NOT NULL auto_increment");
+       setChange("CREATE TABLE `tt_client_project_binds` (`client_id` int(11) NOT NULL, `project_id` int(11) NOT NULL)");
+       setChange("ALTER TABLE tt_user_project_binds MODIFY rate float(6,2) default '0.00'");
+       setChange("ALTER TABLE tt_clients MODIFY tax float(6,2) default '0.00'");
+       setChange("RENAME TABLE activity_log TO tt_log");
+       setChange("ALTER TABLE tt_log CHANGE al_timestamp timestamp timestamp NOT NULL");
+       setChange("ALTER TABLE tt_log CHANGE al_user_id user_id int(11) NOT NULL");
+       setChange("ALTER TABLE tt_log CHANGE al_date date date NOT NULL");
+       setChange("drop index al_date_idx on tt_log");
+       setChange("create index date_idx on tt_log(date)");
+       setChange("ALTER TABLE tt_log CHANGE al_from start time default NULL");
+       setChange("ALTER TABLE tt_log CHANGE al_duration duration time default NULL");
+       setChange("ALTER TABLE tt_log CHANGE al_project_id project_id int(11) NOT NULL");
+       setChange("ALTER TABLE tt_log MODIFY project_id int(11) default NULL");
+       setChange("ALTER TABLE tt_log CHANGE al_activity_id task_id int(11) default NULL");
+       setChange("ALTER TABLE tt_log CHANGE al_comment comment blob");
+       setChange("ALTER TABLE tt_log CHANGE al_billable billable tinyint(4) default '0'");
+       setChange("drop index al_user_id_idx on tt_log");
+       setChange("drop index al_project_id_idx on tt_log");
+       setChange("drop index al_activity_id_idx on tt_log");
+       setChange("create index user_idx on tt_log(user_id)");
+       setChange("create index project_idx on tt_log(project_id)");
+       setChange("create index task_idx on tt_log(task_id)");
+       setChange("ALTER TABLE tt_custom_field_log CHANGE al_id log_id bigint NOT NULL");
+       setChange("RENAME TABLE sysconfig TO tt_config");
+       setChange("ALTER TABLE tt_config DROP sysc_id");
+       setChange("ALTER TABLE tt_config CHANGE sysc_id_u user_id int(11) NOT NULL");
+       setChange("ALTER TABLE tt_config CHANGE sysc_name param_name varchar(32) NOT NULL");
+       setChange("ALTER TABLE tt_config CHANGE sysc_value param_value varchar(80) default NULL");
+       setChange("create unique index param_idx on tt_config(user_id, param_name)");
+       setChange("ALTER TABLE tt_log ADD COLUMN invoice_id int(11) default NULL");
+       setChange("ALTER TABLE tt_projects ADD COLUMN description varchar(255) default NULL");
+    setChange("CREATE TABLE `tt_invoices` (`id` int(11) NOT NULL auto_increment, `team_id` int(11) NOT NULL, `number` varchar(20) default NULL, `client_name` varchar(255) default NULL, `client_addr` varchar(255) default NULL, `comment` varchar(255) default NULL, `tax` float(6,2) default '0.00', `discount` float(6,2) default '0.00', PRIMARY KEY (`id`))");
+    setChange("ALTER TABLE tt_invoices drop number");
+    setChange("ALTER TABLE tt_invoices drop client_name");
+    setChange("ALTER TABLE tt_invoices drop client_addr");
+    setChange("ALTER TABLE tt_invoices drop comment");
+    setChange("ALTER TABLE tt_invoices drop tax");    
+    setChange("ALTER TABLE tt_invoices ADD COLUMN name varchar(80) NOT NULL");
+    setChange("ALTER TABLE tt_invoices ADD COLUMN client_id int(11) NOT NULL");
+    setChange("ALTER TABLE tt_invoices ADD COLUMN start_date date NOT NULL");
+    setChange("ALTER TABLE tt_invoices ADD COLUMN end_date date NOT NULL");
+    setChange("create unique index name_idx on tt_invoices(team_id, name)");
+    setChange("drop index ub_id_u on tt_user_project_binds");
+    setChange("create unique index bind_idx on tt_user_project_binds(user_id, project_id)");
+    setChange("create index client_idx on tt_log(client_id)");
+    setChange("create index invoice_idx on tt_log(invoice_id)");
+  }
+  
+  if ($_POST["convert1579to1600"]) {
+    setChange("ALTER TABLE tt_invoices ADD COLUMN date date NOT NULL");
+    setChange("ALTER TABLE tt_teams ADD COLUMN custom_logo tinyint(4) default '0'");
+    setChange("ALTER TABLE tt_tasks ADD COLUMN description varchar(255) default NULL");
+    setChange("ALTER TABLE tt_projects MODIFY name varchar(80) COLLATE utf8_bin NOT NULL");
+    setChange("ALTER TABLE tt_users MODIFY login varchar(50) COLLATE utf8_bin NOT NULL");
+    setChange("ALTER TABLE tt_tasks MODIFY name varchar(80) COLLATE utf8_bin NOT NULL");
+    setChange("ALTER TABLE tt_invoices MODIFY name varchar(80) COLLATE utf8_bin NOT NULL");
+    setChange("ALTER TABLE tt_clients MODIFY name varchar(80) COLLATE utf8_bin NOT NULL");
+    setChange("ALTER TABLE tt_clients ADD COLUMN projects text default NULL");
+    setChange("ALTER TABLE tt_custom_field_log ADD COLUMN option_id int(11) default NULL");
+    setChange("ALTER TABLE tt_teams ADD COLUMN tracking_mode smallint(2) NOT NULL DEFAULT '2'");
+    setChange("ALTER TABLE tt_teams ADD COLUMN record_type smallint(2) NOT NULL DEFAULT '0'");
+    setChange("ALTER TABLE tt_invoices DROP start_date");
+    setChange("ALTER TABLE tt_invoices DROP end_date");
+  }
+  
+  if ($_POST["convert1600to1900"]) {
+       setChange("DROP TABLE IF EXISTS tt_invoice_headers");
+       setChange("ALTER TABLE tt_fav_reports ADD COLUMN `client_id` int(11) default NULL");
+       setChange("ALTER TABLE tt_fav_reports ADD COLUMN `cf_1_option_id` int(11) default NULL");
+       setChange("ALTER TABLE tt_fav_reports ADD COLUMN `show_client` tinyint(4) NOT NULL default '0'");
+       setChange("ALTER TABLE tt_fav_reports ADD COLUMN `show_invoice` tinyint(4) NOT NULL default '0'");
+       setChange("ALTER TABLE tt_fav_reports ADD COLUMN `group_by` varchar(20) default NULL");
+       setChange("CREATE TABLE `tt_expense_items` (`id` bigint NOT NULL auto_increment, `date` date NOT NULL, `user_id` int(11) NOT NULL, `client_id` int(11) default NULL, `project_id` int(11) default NULL, `name` varchar(255) NOT NULL, `cost` decimal(10,2) default '0.00', `invoice_id` int(11) default NULL, PRIMARY KEY  (`id`))");
+       setChange("create index date_idx on tt_expense_items(date)");
+       setChange("create index user_idx on tt_expense_items(user_id)");
+       setChange("create index client_idx on tt_expense_items(client_id)");
+       setChange("create index project_idx on tt_expense_items(project_id)");
+       setChange("create index invoice_idx on tt_expense_items(invoice_id)");
+       setChange("ALTER TABLE tt_fav_reports DROP sort_by");
+       setChange("ALTER TABLE tt_fav_reports DROP show_empty_days");
+       setChange("ALTER TABLE tt_invoices DROP discount");
+       setChange("ALTER TABLE tt_users ADD COLUMN `client_id` int(11) default NULL");
+       setChange("ALTER TABLE tt_teams ADD COLUMN `decimal_mark` char(1) NOT NULL default '.'");
+       setChange("ALTER TABLE tt_fav_reports ADD COLUMN `invoice` tinyint(4) default NULL");
+       setChange("CREATE TABLE `tt_cron` (`id` int(11) NOT NULL auto_increment, `cron_spec` varchar(255) NOT NULL, `last` int(11) default NULL, `next` int(11) default NULL, `report_id` int(11) default NULL, `email` varchar(100) default NULL, `status` tinyint(4) default '1', PRIMARY KEY (`id`))");
+    setChange("ALTER TABLE tt_cron ADD COLUMN `team_id` int(11) NOT NULL");
+    setChange("create index client_idx on tt_client_project_binds(client_id)");
+    setChange("create index project_idx on tt_client_project_binds(project_id)");
+    setChange("ALTER TABLE tt_log ADD COLUMN status tinyint(4) default '1'");
+    setChange("ALTER TABLE tt_custom_field_log ADD COLUMN status tinyint(4) default '1'");
+    setChange("ALTER TABLE tt_expense_items ADD COLUMN status tinyint(4) default '1'");
+    setChange("ALTER TABLE tt_invoices ADD COLUMN status tinyint(4) default '1'");
+    setChange("DROP INDEX name_idx on tt_invoices");
+    setChange("create unique index name_idx on tt_invoices(team_id, name, status)");
+  }    
+   
+  // The update_clients function updates projects field in tt_clients table.
+  if ($_POST["update_clients"]) {
+    $mdb2 = getConnection();
+    $sql = "select id from tt_clients where status = 1 or status = 0";
+    $res = $mdb2->query($sql);
+    if (is_a($res, 'PEAR_Error')) {
+      die($res->getMessage());
+    }
+    
+    $clients_updated = 0;
+    // Iterate through clients.
+    while ($val = $res->fetchRow()) {
+      $client_id = $val['id'];
+      
+      // Get projects binds for client.
+      $sql = "select cpb.project_id from tt_client_project_binds cpb
+        left join tt_projects p on (p.id = cpb.project_id)
+        where cpb.client_id = $client_id order by p.name";
+      
+      $result = $mdb2->query($sql);
+      if (is_a($result, 'PEAR_Error'))
+        die($result->getMessage());
+      
+      $project_arr = array();
+      while ($value = $result->fetchRow()) {
+        $project_arr[] = $value['project_id']; 
+      }
+      $comma_separated = implode(',', $project_arr); // This is a comma-separated list of associated project ids.
+      
+      // Update the projects field.
+      $sql = "update tt_clients set projects = ".$mdb2->quote($comma_separated)." where id = $client_id";
+      $affected = $mdb2->exec($sql);
+      if (is_a($affected, 'PEAR_Error'))
+        die($affected->getMessage());
+      $clients_updated += $affected;   
+    }
+    print "Updated $clients_updated clients...<br>\n";
+  }
+  
+  // The update_custom_fields function updates option_id field field in tt_custom_field_log table.
+  if ($_POST['update_custom_fields']) {
+    $mdb2 = getConnection();
+    $sql = "update tt_custom_field_log set option_id = value where field_id in (select id from tt_custom_fields where type = 2)";
+    $affected = $mdb2->exec($sql);
+    if (is_a($affected, 'PEAR_Error'))
+      die($affected->getMessage());
+    
+    print "Updated $affected custom fields...<br>\n";
+  }
+
+  // The update_tracking_mode function sets the tracking_mode field in tt_teams table to 2 (== MODE_PROJECTS_AND_TASKS).
+  if ($_POST['update_tracking_mode']) {
+    $mdb2 = getConnection();
+    $sql = "update tt_teams set tracking_mode = 2 where tracking_mode = 0";
+    $affected = $mdb2->exec($sql);
+    if (is_a($affected, 'PEAR_Error'))
+      die($affected->getMessage());
+    
+    print "Updated $affected teams...<br>\n";
+  }
+
+  if ($_POST["cleanup"]) {
+     
+    $mdb2 = getConnection();
+       $inactive_teams = ttTeamHelper::getInactiveTeams();
+    
+    $count = count($inactive_teams);
+    print "$count inactive teams found...<br>\n";
+    for ($i = 0; $i < $count; $i++) {
+      print "  deleting team ".$inactive_teams[$i]."<br>\n";           
+      $res = ttTeamHelper::delete($inactive_teams[$i]);
+    }
+
+    setChange("OPTIMIZE TABLE tt_client_project_binds");
+    setChange("OPTIMIZE TABLE tt_clients");
+    setChange("OPTIMIZE TABLE tt_config");
+    setChange("OPTIMIZE TABLE tt_custom_field_log");
+    setChange("OPTIMIZE TABLE tt_custom_field_options");   
+    setChange("OPTIMIZE TABLE tt_custom_fields"); 
+    setChange("OPTIMIZE TABLE tt_expense_items");
+    setChange("OPTIMIZE TABLE tt_fav_reports");
+    setChange("OPTIMIZE TABLE tt_invoices");    
+    setChange("OPTIMIZE TABLE tt_log");
+    setChange("OPTIMIZE TABLE tt_project_task_binds");
+    setChange("OPTIMIZE TABLE tt_projects");            
+    setChange("OPTIMIZE TABLE tt_tasks");
+    setChange("OPTIMIZE TABLE tt_teams");
+    setChange("OPTIMIZE TABLE tt_tmp_refs");
+    setChange("OPTIMIZE TABLE tt_user_project_binds");    
+    setChange("OPTIMIZE TABLE tt_users");
+  }
+  
+  print "done.<br>\n";
+}
+?>
+<html>
+<body>
+<div align="center">
+<form method="POST">
+<h2>DB Install</h2>
+<table width="80%" border="1" cellpadding="10" cellspacing="0">
+<tr>
+       <td width="80%"><b>Create database structure (v1.9)</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>
+
+<h2>Updates</h2>
+
+<table width="80%" border="1" cellpadding="10" cellspacing="0">
+<tr valign="top">
+       <td>Update database structure (v0.5 to v0.7)</td><td><input type="submit" name="convert5to7" value="Update"></td>
+</tr>
+<tr valign="top">
+       <td>Update database structure (v0.7 to v1.3.3)</td>
+       <td><input type="submit" name="convert7to133" value="Update"><br><input type="submit" name="update_projects" value="Update projects"></td>
+</tr>
+<tr valign="top">
+  <td>Update database structure (v1.3.3 to v1.3.40)</td>
+  <td><input type="submit" name="convert133to1340" value="Update"><br><input type="submit" name="update_companies" value="Update companies"></td>
+</tr>
+<tr valign="top">
+  <td>Update database structure (v1.3.40 to v1.4.85)</td>
+  <td><input type="submit" name="convert1340to1485" value="Update"><br><input type="submit" name="update_to_team_id" value="Update team_id"></td>
+</tr>
+<tr valign="top">
+  <td>Update database structure (v1.4.85 to v1.5.79)</td>
+  <td><input type="submit" name="convert1485to1579" value="Update"></td>
+</tr>
+<tr valign="top">
+  <td>Update database structure (v1.5.79 to v1.6)</td>
+  <td><input type="submit" name="convert1579to1600" value="Update"><br><input type="submit" name="update_clients" value="Update clients"><br><input type="submit" name="update_custom_fields" value="Update custom fields"><br><input type="submit" name="update_tracking_mode" value="Update tracking mode"></td>
+</tr>
+<tr valign="top">
+  <td>Update database structure (v1.6 to v1.9)</td>
+  <td><input type="submit" name="convert1600to1900" value="Update"><br></td>
+</tr>
+</table>
+
+<h2>DB Maintenance</h2>
+<table width="80%" border="1" cellpadding="10" cellspacing="0">
+<tr>
+       <td width="80%">Clean up DB from inactive teams</td><td><input type="submit" name="cleanup" value="Clean up"></td>
+</tr>
+</table>
+
+</form>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/default.css b/default.css
new file mode 100644 (file)
index 0000000..da54b7e
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+// +----------------------------------------------------------------------+
+// | 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
+// +----------------------------------------------------------------------+
+*/
+
+a { color: blue; text-decoration: none; }
+
+a:visited { text-decoration: none; }
+
+a:hover { text-decoration: underline; }
+
+body {
+  font-size: 10pt;
+  font-family: verdana;
+  background-color: white;
+}
+
+table { font-size: 10pt; font-family: verdana; }
+
+input, button { font-size: 10pt; font-family: verdana; }
+
+textarea { font-size: 10pt; font-family: verdana; }
+
+select{ font-size: 10pt; font-family: verdana; }
+
+.pageTitle {
+  font-size: 12pt;
+  font-weight: bold;
+  color: silver;
+}
+
+.systemMenu {
+  font-size: 12pt;
+  font-weight: bold;
+  color: #ffffff;
+  background-color: #000000;
+}
+
+.mainMenu {
+  font-size: 12pt;
+  color: #444444;
+}
+
+.tableHeader {
+  font-weight: bold;
+  text-align: left;
+  color: #000000;
+  background-color: #a6ccf7;
+}
+
+.tableHeaderCentered {
+  font-weight: bold;
+  text-align: center;
+  color: #000000;
+  background-color: #a6ccf7;
+}
+
+.rowReportItem {
+  background-color: #ccccce;
+}
+
+.rowReportItemAlt {
+  background-color: #f5f5f5;
+}
+
+.rowReportSubtotal {
+  background-color: #e0e0e0;
+}
+
+.cellLeftAligned {
+  text-align: left;
+  vertical-align: top;
+}
+
+.cellRightAligned {
+  text-align: right;
+  vertical-align: top;
+}
+
+.cellLeftAlignedSubtotal {
+  font-weight: bold;
+  vertical-align: top;
+}
+
+.cellRightAlignedSubtotal {
+  font-weight: bold;
+  text-align: right;
+  vertical-align: top;
+}
+
+.sectionHeader {
+  font-weight: bold;
+  border-bottom: 1px solid silver;
+}
+
+.sectionHeaderNoBorder {
+  font-weight: bold;
+}
+
+.error {
+  font-weight: bold;
+  color: red;
+}
+
+.info_message {
+  font-weight: bold;
+  color: #0000c0;
+}
+
+div#LoginAboutText { width:400px; }
+
diff --git a/expense_delete.php b/expense_delete.php
new file mode 100644 (file)
index 0000000..e8828e0
--- /dev/null
@@ -0,0 +1,90 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('DateAndTime');
+import('ttExpenseHelper');
+
+// Access check.
+if (!ttAccessCheck(right_data_entry)) {
+  header('Location: access_denied.php');
+  exit();
+}
+  
+$cl_id = $request->getParameter('id');
+$expense_item = ttExpenseHelper::getItem($cl_id, $user->getActiveUser());
+
+// Prohibit deleting invoiced records.
+if ($expense_item['invoice_id']) die($i18n->getKey('error.sys'));
+  
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('delete_button'))  {  // Delete button pressed.
+  
+    // Determine if it's okay to delete the record.
+
+       // Determine lock date.
+    $lock_interval = $user->lock_interval;
+    $lockdate = 0;
+    if ($lock_interval > 0) {
+      $lockdate = new DateAndTime();
+      $lockdate->decDay($lock_interval);
+    }
+    if ($lockdate) {
+      $item_date = new DateAndTime(DB_DATEFORMAT);
+      $item_date->parseVal($expense_item['date'], DB_DATEFORMAT);
+      if ($item_date->before($lockdate))
+        $errors->add($i18n->getKey('error.period_locked'));
+    }
+           
+    if ($errors->isEmpty()) {
+      // Mark the record as deleted.
+      if (ttExpenseHelper::markDeleted($cl_id, $user->getActiveUser())) {
+        header('Location: expenses.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    }
+  }
+  if ($request->getParameter('cancel_button')) { // Cancel button pressed.
+       header('Location: expenses.php');
+       exit();
+  }
+}
+               
+$form = new Form('expenseItemForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_id));
+$form->addInput(array('type'=>'submit','name'=>'delete_button','value'=>$i18n->getKey('label.delete')));
+$form->addInput(array('type'=>'submit','name'=>'cancel_button','value'=>$i18n->getKey('button.cancel')));
+
+$smarty->assign('expense_item', $expense_item);
+$smarty->assign('forms', array($form->getName() => $form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.delete_expense'));
+$smarty->assign('content_page_name', 'expense_delete.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/expense_edit.php b/expense_edit.php
new file mode 100644 (file)
index 0000000..f9c94dd
--- /dev/null
@@ -0,0 +1,218 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+import('DateAndTime');
+import('ttExpenseHelper');
+
+// Access check.
+if (!ttAccessCheck(right_data_entry)) {
+  header('Location: access_denied.php');
+  exit();
+}
+  
+$cl_id = $request->getParameter('id');
+
+// Get the expense item we are editing.
+$expense_item = ttExpenseHelper::getItem($cl_id, $user->getActiveUser());
+
+// Prohibit editing invoiced items.
+if ($expense_item['invoice_id']) die($i18n->getKey('error.sys'));
+  
+$item_date = new DateAndTime(DB_DATEFORMAT, $expense_item['date']);
+
+// Initialize variables.
+$cl_date = $cl_client = $cl_project = $cl_item_name = $cl_cost = null;
+if ($request->getMethod() == 'POST') {
+  $cl_date = trim($request->getParameter('date'));
+  $cl_client = $request->getParameter('client');
+  $cl_project = $request->getParameter('project');
+  $cl_item_name = trim($request->getParameter('item_name'));
+  $cl_cost = trim($request->getParameter('cost'));
+} else {
+  $cl_date = $item_date->toString($user->date_format);
+  $cl_client = $expense_item['client_id'];
+  $cl_project = $expense_item['project_id'];
+  $cl_item_name = $expense_item['name'];
+  $cl_cost = $expense_item['cost'];
+}
+
+// Initialize elements of 'expenseItemForm'.
+$form = new Form('expenseItemForm');
+
+// Dropdown for clients in MODE_TIME. Use all active clients.
+if (MODE_TIME == $user->tracking_mode && in_array('cl', explode(',', $user->plugins))) {
+    $active_clients = ttTeamHelper::getActiveClients($user->team_id, true);
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'fillProjectDropdown(this.value);',
+      'name'=>'client',
+      'style'=>'width: 250px;',
+      'value'=>$cl_client,
+      'data'=>$active_clients,
+      'datakeys'=>array('id', 'name'),
+      'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+    ));
+  // Note: in other modes the client list is filtered to relevant clients only. See below.
+}
+
+if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+  // Dropdown for projects assigned to user.
+  $project_list = $user->getAssignedProjects();
+  $form->addInput(array('type'=>'combobox',
+    'name'=>'project',
+    'style'=>'width: 250px;',
+    'value'=>$cl_project,
+    'data'=>$project_list,
+    'datakeys'=>array('id','name'),
+    'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+  ));
+
+  // Dropdown for clients if the clients plugin is enabled.
+  if (in_array('cl', explode(',', $user->plugins))) {
+    $active_clients = ttTeamHelper::getActiveClients($user->team_id, true);
+    // We need an array of assigned project ids to do some trimming. 
+    foreach($project_list as $project)
+      $projects_assigned_to_user[] = $project['id'];
+
+    // Build a client list out of active clients. Use only clients that are relevant to user.
+    // Also trim their associated project list to only assigned projects (to user).
+    foreach($active_clients as $client) {
+         $projects_assigned_to_client = explode(',', $client['projects']);
+         $intersection = array_intersect($projects_assigned_to_client, $projects_assigned_to_user);
+         if ($intersection) {
+        $client['projects'] = implode(',', $intersection);
+           $client_list[] = $client;
+         }
+    }
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'fillProjectDropdown(this.value);',
+      'name'=>'client',
+      'style'=>'width: 250px;',
+      'value'=>$cl_client,
+      'data'=>$client_list,
+      'datakeys'=>array('id', 'name'),
+      'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+    ));
+  }
+}
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'item_name','style'=>'width: 250px;','value'=>$cl_item_name));
+$form->addInput(array('type'=>'text','maxlength'=>'40','name'=>'cost','style'=>'width: 100px;','value'=>$cl_cost));
+$form->addInput(array('type'=>'datefield','name'=>'date','maxlength'=>'20','value'=>$cl_date));
+// Hidden control for record id.
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_id));
+$form->addInput(array('type'=>'hidden','name'=>'browser_today','value'=>'')); // User current date, which gets filled in on btn_save or btn_copy click.
+$form->addInput(array('type'=>'submit','name'=>'btn_save','onclick'=>'browser_today.value=get_date()','value'=>$i18n->getKey('button.save')));
+$form->addInput(array('type'=>'submit','name'=>'btn_copy','onclick'=>'browser_today.value=get_date()','value'=>$i18n->getKey('button.copy')));
+$form->addInput(array('type'=>'submit','name'=>'btn_delete','value'=>$i18n->getKey('label.delete')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (in_array('cl', explode(',', $user->plugins)) && in_array('cm', explode(',', $user->plugins)) && !$cl_client)
+    $errors->add($i18n->getKey('error.client'));
+  if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+    if (!$cl_project) $errors->add($i18n->getKey('error.project'));
+  }
+  if (!ttValidString($cl_item_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.item'));
+  if (!ttValidFloat($cl_cost)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.cost'));
+  if (!ttValidDate($cl_date)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.date'));
+  
+  // Determine lock date.
+  $lock_interval = $user->lock_interval;
+  $lockdate = 0;
+  if ($lock_interval > 0) {
+    $lockdate = new DateAndTime();
+    $lockdate->decDay($lock_interval);
+  }
+
+  // This is a new date for the expense item.
+  $new_date = new DateAndTime($user->date_format, $cl_date);
+  
+  // Prohibit creating entries in future.
+  if (defined('FUTURE_ENTRIES') && !isTrue(FUTURE_ENTRIES)) {
+    $browser_today = new DateAndTime(DB_DATEFORMAT, $request->getParameter('browser_today', null));
+    if ($new_date->after($browser_today))
+      $errors->add($i18n->getKey('error.future_date'));
+  }
+  
+  // Save record.
+  if ($request->getParameter('btn_save')) {
+    // We need to:
+    // 1) Prohibit updating locked entries (that are in locked interval).
+    // 2) Prohibit saving unlocked entries into locked interval.
+      
+    // Now, step by step.
+    // 1) Prohibit updating locked entries.
+    if($lockdate && $item_date->before($lockdate))
+      $errors->add($i18n->getKey('error.period_locked'));        
+    // 2) Prohibit saving completed unlocked entries into locked interval.
+    if($errors->isEmpty() && $lockdate && $new_date->before($lockdate))
+      $errors->add($i18n->getKey('error.period_locked'));        
+    
+    // Now, an update.
+    if ($errors->isEmpty()) {
+      if (ttExpenseHelper::update(array('id'=>$cl_id,'date'=>$new_date->toString(DB_DATEFORMAT),'user_id'=>$user->getActiveUser(),
+          'client_id'=>$cl_client,'project_id'=>$cl_project,'name'=>$cl_item_name,'cost'=>$cl_cost))) {
+        header('Location: expenses.php?date='.$new_date->toString(DB_DATEFORMAT));
+        exit();
+      }
+    }
+  }
+      
+  // Save as new record.
+  if ($request->getParameter('btn_copy')) {
+    // We need to prohibit saving into locked interval.
+    if($lockdate && $new_date->before($lockdate))
+      $errors->add($i18n->getKey('error.period_locked'));
+    
+    // Now, a new insert.
+    if ($errors->isEmpty()) {
+      if (ttExpenseHelper::insert(array('date'=>$new_date->toString(DB_DATEFORMAT),'user_id'=>$user->getActiveUser(),
+        'client_id'=>$cl_client,'project_id'=>$cl_project,'name'=>$cl_item_name,'cost'=>$cl_cost,'status'=>1))) {
+        header('Location: expenses.php?date='.$new_date->toString(DB_DATEFORMAT));
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    }
+  }
+  
+  if ($request->getParameter('btn_delete')) {
+    header("Location: expense_delete.php?id=$cl_id");
+    exit();
+  }
+} // End of if ($request->getMethod() == "POST")
+
+$smarty->assign('client_list', $client_list);
+$smarty->assign('project_list', $project_list);
+$smarty->assign('task_list', $task_list);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.edit_expense'));
+$smarty->assign('content_page_name', 'expense_edit.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/expenses.php b/expenses.php
new file mode 100644 (file)
index 0000000..9a08375
--- /dev/null
@@ -0,0 +1,206 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttUserHelper');
+import('ttTeamHelper');
+import('DateAndTime');
+import('ttExpenseHelper');
+
+// Access check.
+if (!ttAccessCheck(right_data_entry)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Initialize and store date in session.
+$cl_date = $request->getParameter('date', @$_SESSION['date']);
+$selected_date = new DateAndTime(DB_DATEFORMAT, $cl_date);
+if($selected_date->isError())
+  $selected_date = new DateAndTime(DB_DATEFORMAT);
+if(!$cl_date)
+  $cl_date = $selected_date->toString(DB_DATEFORMAT);
+$_SESSION['date'] = $cl_date;
+
+// Initialize variables.
+$on_behalf_id = $request->getParameter('onBehalfUser', (isset($_SESSION['behalf_id']) ? $_SESSION['behalf_id'] : $user->id));
+$cl_client = $request->getParameter('client', ($request->getMethod()=='POST' ? null : @$_SESSION['client']));
+$_SESSION['client'] = $cl_client;
+$cl_project = $request->getParameter('project', ($request->getMethod()=='POST' ? null : @$_SESSION['project']));
+$_SESSION['project'] = $cl_project;
+$cl_item_name = $request->getParameter('item_name');
+$cl_cost = $request->getParameter('cost');
+
+// Elements of expensesForm.
+$form = new Form('expensesForm');
+
+if ($user->canManageTeam()) {
+  $user_list = ttTeamHelper::getActiveUsers(array('putSelfFirst'=>true));
+  if (count($user_list) > 1) {
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'this.form.submit();',
+      'name'=>'onBehalfUser',
+      'style'=>'width: 250px;',
+      'value'=>$on_behalf_id,
+      'data'=>$user_list,
+      'datakeys'=>array('id','name'),
+    ));
+    $smarty->assign('on_behalf_control', 1);
+  }
+}
+
+// Dropdown for clients in MODE_TIME. Use all active clients.
+if (MODE_TIME == $user->tracking_mode && in_array('cl', explode(',', $user->plugins))) {
+    $active_clients = ttTeamHelper::getActiveClients($user->team_id, true);
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'fillProjectDropdown(this.value);',
+      'name'=>'client',
+      'style'=>'width: 250px;',
+      'value'=>$cl_client,
+      'data'=>$active_clients,
+      'datakeys'=>array('id', 'name'),
+      'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+    ));
+  // Note: in other modes the client list is filtered to relevant clients only. See below.
+}
+
+if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+  // Dropdown for projects assigned to user.
+  $project_list = $user->getAssignedProjects();
+  $form->addInput(array('type'=>'combobox',
+    // 'onchange'=>'fillTaskDropdown(this.value);',
+    'name'=>'project',
+    'style'=>'width: 250px;',
+    'value'=>$cl_project,
+    'data'=>$project_list,
+    'datakeys'=>array('id','name'),
+    'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+  ));
+
+  // Dropdown for clients if the clients plugin is enabled.
+  if (in_array('cl', explode(',', $user->plugins))) {
+    $active_clients = ttTeamHelper::getActiveClients($user->team_id, true);
+    // We need an array of assigned project ids to do some trimming. 
+    foreach($project_list as $project)
+      $projects_assigned_to_user[] = $project['id'];
+
+    // Build a client list out of active clients. Use only clients that are relevant to user.
+    // Also trim their associated project list to only assigned projects (to user).
+    foreach($active_clients as $client) {
+      $projects_assigned_to_client = explode(',', $client['projects']);
+         $intersection = array_intersect($projects_assigned_to_client, $projects_assigned_to_user);
+         if ($intersection) {
+           $client['projects'] = implode(',', $intersection);
+           $client_list[] = $client;
+         }
+    }
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'fillProjectDropdown(this.value);',
+      'name'=>'client',
+      'style'=>'width: 250px;',
+      'value'=>$cl_client,
+      'data'=>$client_list,
+      'datakeys'=>array('id', 'name'),
+      'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+    ));
+  }
+}
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'item_name','style'=>'width: 250px;','value'=>$cl_item_name));
+$form->addInput(array('type'=>'text','maxlength'=>'40','name'=>'cost','style'=>'width: 100px;','value'=>$cl_cost));
+$form->addInput(array('type'=>'calendar','name'=>'date','highlight'=>'expenses','value'=>$cl_date)); // calendar
+$form->addInput(array('type'=>'hidden','name'=>'browser_today','value'=>'')); // User current date, which gets filled in on btn_submit click.
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','onclick'=>'browser_today.value=get_date()','value'=>$i18n->getKey('button.submit')));
+
+// Determine lock date. Time entries earlier than lock date cannot be created or modified. 
+$lock_interval = $user->lock_interval;
+$lockdate = 0;
+if ($lock_interval > 0) {
+  $lockdate = new DateAndTime();
+  $lockdate->decDay($lock_interval);
+}
+
+// Submit.
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('btn_submit')) {
+    // Validate user input.
+    if (in_array('cl', explode(',', $user->plugins)) && in_array('cm', explode(',', $user->plugins)) && !$cl_client)
+      $errors->add($i18n->getKey('error.client'));
+    if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+      if (!$cl_project) $errors->add($i18n->getKey('error.project'));          
+    }
+    if (!ttValidString($cl_item_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.item'));
+    if (!ttValidFloat($cl_cost)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.cost'));
+      
+    // Prohibit creating entries in future.
+    if (defined('FUTURE_ENTRIES') && !isTrue(FUTURE_ENTRIES)) {
+      $browser_today = new DateAndTime(DB_DATEFORMAT, $request->getParameter('browser_today', null));
+      if ($selected_date->after($browser_today))
+        $errors->add($i18n->getKey('error.future_date'));
+    }
+    // Finished validating input data.
+    
+    // Prohibit creating time entries in locked interval.
+    if($lockdate && $selected_date->before($lockdate))
+      $errors->add($i18n->getKey('error.period_locked'));
+    
+    // Insert record.
+    if ($errors->isEmpty()) {
+      if (ttExpenseHelper::insert(array('date'=>$cl_date,'user_id'=>$user->getActiveUser(),
+        'client_id'=>$cl_client,'project_id'=>$cl_project,'name'=>$cl_item_name,'cost'=>$cl_cost,'status'=>1))) {
+        header('Location: expenses.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    }
+  }
+  else if ($request->getParameter('onBehalfUser')) {
+    if($user->canManageTeam()) {
+      unset($_SESSION['behalf_id']);
+      unset($_SESSION['behalf_name']);
+       
+      if($on_behalf_id != $user->id) {
+        $_SESSION['behalf_id'] = $on_behalf_id;
+       $_SESSION['behalf_name'] = ttUserHelper::getUserName($on_behalf_id);                    
+      }
+      header('Location: expenses.php');
+      exit();
+    }
+  }
+}
+
+$smarty->assign('day_total', ttExpenseHelper::getTotalForDay($user->getActiveUser(), $cl_date));
+$smarty->assign('expense_items', ttExpenseHelper::getItems($user->getActiveUser(), $cl_date));
+$smarty->assign('client_list', $client_list);
+$smarty->assign('project_list', $project_list);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('timestring', $selected_date->toString($user->date_format));
+$smarty->assign('title', $i18n->getKey('title.expenses'));
+$smarty->assign('content_page_name', 'expenses.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/export.php b/export.php
new file mode 100644 (file)
index 0000000..4bb9db3
--- /dev/null
@@ -0,0 +1,84 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('ttExportHelper');
+import('form.Form');
+
+// Access check.
+if (!ttAccessCheck(right_export_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$cl_compression = $request->getParameter('compression');
+$compressors = array('' => $i18n->getKey('form.export.compression_none'));
+if (function_exists('bzcompress'))
+  $compressors['bzip'] = $i18n->getKey('form.export.compression_bzip');
+
+$form = new Form('exportForm');
+$form->addInput(array('type'=>'combobox','name'=>'compression','value'=>$cl_compression,'data'=>$compressors));
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.export')));
+
+if ($request->getMethod() == 'POST') {
+
+  $filename = 'team_data.xml';
+  $mime_type = 'text/xml';
+  $compress = false;
+  if ('bzip' == $cl_compression) {
+    $compress = true;
+    $filename  .= '.bz2';
+    $mime_type = 'application/x-bzip2';
+  }
+
+  $exportHelper = new ttExportHelper();
+  if ($exportHelper->createDataFile($compress)) {
+       header('Pragma: public'); // This is needed for IE8 to download files over https.
+       header('Content-Type: '.$mime_type);
+       header('Expires: '.gmdate('D, d M Y H:i:s').' GMT');
+       header('Content-Disposition: attachment; filename="'.$filename.'"');
+       header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+       header('Cache-Control: private', false);
+       
+    if ($file_pointer = fopen($exportHelper->getFileName(), 'r')) {
+      while ($data = fread($file_pointer, 4096)) {
+       echo $data;
+      }
+      fclose($file_pointer);
+      unlink($exportHelper->getFileName());
+    }
+    exit;
+  } else
+    $errors->add($i18n->getKey('error.sys'));
+}
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.export'));
+$smarty->assign('content_page_name', 'export.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/favicon.ico b/favicon.ico
new file mode 100644 (file)
index 0000000..56e4007
Binary files /dev/null and b/favicon.ico differ
diff --git a/import.php b/import.php
new file mode 100644 (file)
index 0000000..47493db
--- /dev/null
@@ -0,0 +1,56 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('ttImportHelper');
+import('form.Form');
+
+// Access check.
+if (!ttAccessCheck(right_administer_site)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$form = new Form('importForm');
+$form->addInput(array('type'=>'upload','name'=>'xmlfile','value'=>'browse','maxsize'=>16777216)); // 16 MB file upload limit.
+// Note: for the above limit to work make sure to set upload_max_filesize and post_max_size in php.ini to at least 16M. 
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.import')));
+
+if ($request->getMethod() == 'POST') {
+
+  $import = new ttImportHelper($errors);
+  $import->importXml();
+  if ($errors->isEmpty())
+    $messages->add($i18n->getKey('form.import.success'));
+}
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()) );
+$smarty->assign('title', $i18n->getKey('title.import'));
+$smarty->assign('content_page_name', 'import.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/index.php b/index.php
new file mode 100644 (file)
index 0000000..d390ebf
--- /dev/null
+++ b/index.php
@@ -0,0 +1,81 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+
+// Redirects for admin and client roles.
+if ($auth->isAuthenticated()) {
+  if ($user->isAdmin()) {
+    header('Location: admin_teams.php');
+    exit();
+  }
+  else if ($user->isClient()) {
+    header('Location: reports.php');
+    exit();    
+  }
+}
+
+// Redirect to time.php or mobile/time.php for other roles.
+
+// Regexp to determine mobile browser was taken from detectmobilebrowsers.com on Aug 24, 2012. 
+$useragent = $_SERVER['HTTP_USER_AGENT'];
+$mobileBrowser = preg_match('/android.+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|meego.+mobile|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i',$useragent)||preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i',substr($useragent,0,4));
+
+if ($mobileBrowser) {
+?>
+
+<html>
+  <script src="js/strftime.js"></script>
+  <script>
+    location.href = "mobile/time.php?date="+(new Date()).strftime('<?php print DB_DATEFORMAT;?>');
+  </script>
+  <noscript>
+    <p>Anuko Time Tracker is a simple, easy to use, open source, web-based time tracking system.</p>
+    <p>Your browser does not support JavaScript. Time Tracker will not work without it.</p>
+  </noscript>
+</html>
+
+<?php 
+} else {
+?>
+
+<html>
+  <script src="js/strftime.js"></script>
+  <script>
+    location.href = "time.php?date="+(new Date()).strftime('<?php print DB_DATEFORMAT;?>');
+  </script>
+  <noscript>
+    <p>Anuko Time Tracker is a simple, easy to use, open source, web-based time tracking system.</p>
+    <p>Your browser does not support JavaScript. Time Tracker will not work without it.</p>
+  </noscript>
+</html>
+
+<?php 
+}
+?>
+
diff --git a/initialize.php b/initialize.php
new file mode 100644 (file)
index 0000000..ce6acdf
--- /dev/null
@@ -0,0 +1,251 @@
+<?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
+// +----------------------------------------------------------------------+
+
+// Report all errors except E_NOTICE and E_STRICT.
+// Ignoring E_STRICT is here because PEAR 1.9.4 that we use is not E_STRICT compliant.
+if (!defined('E_STRICT')) define('E_STRICT', 2048);
+// if (!defined('E_DEPRECATED')) define('E_DEPRECATED', 8192);
+error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT); // & ~E_DEPRECATED);
+// E_ALL tends to change as PHP evloves, therefore we use & here instead of exclusive OR (^).
+
+// Disable displaying errors on screen.
+ini_set('display_errors', 'Off');
+
+// require_once('init_auth.php');
+define("APP_DIR", dirname(__FILE__));
+define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib");
+define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates");
+// Date format for database and URI parameters.
+define('DB_DATEFORMAT', '%Y-%m-%d');
+
+require_once(LIBRARY_DIR.'/common.lib.php');
+
+// Require the configuration file with application settings.
+if (!file_exists(APP_DIR."/WEB-INF/config.php")) die ("WEB-INF/config.php file does not exist.");
+require_once("WEB-INF/config.php");
+// Check whether DSN is defined.
+if (!defined("DSN")) {
+  die ("DSN value is not defined. Check your config.php file.");
+}
+
+// Depending on DSN, require either mysqli or mysql extensions.
+if (strrpos(DSN, 'mysqli://', -strlen(DSN)) !== FALSE) {
+  check_extension('mysqli'); // DSN starts with mysqli:// - require mysqli extension.
+}
+if (strrpos(DSN, 'mysql://', -strlen(DSN)) !== FALSE) {
+  check_extension('mysql');  // DSN starts with mysql:// - require mysql extension.
+}
+
+// Require other extensions.
+check_extension('mbstring');
+
+// If auth params are not defined (in config.php) - initialize with an empty array.
+if (!isset($GLOBALS['AUTH_MODULE_PARAMS']) || !is_array($GLOBALS['AUTH_MODULE_PARAMS']))
+  $GLOBALS['AUTH_MODULE_PARAMS'] = array();
+  
+// Smarty initialization.
+import('smarty.Smarty');
+$smarty = new Smarty;
+$smarty->use_sub_dirs = false;
+$smarty->template_dir = TEMPLATE_DIR;
+$smarty->compile_dir  = TEMPLATE_DIR.'_c';
+$GLOBALS['SMARTY'] = &$smarty;
+
+// Note: these 3 settings below used to be in .htaccess file. Moved them here to eliminate "error 500" problems
+// with some shared hostings that do not have AllowOverride Options or AllowOverride All in their apache configurations.
+// Change http cache expiration time to 1 minute.
+session_cache_expire(1);
+
+$phpsessid_ttl = defined('PHPSESSID_TTL') ? PHPSESSID_TTL : 60*60*24;
+// Set lifetime for garbage collection.
+ini_set('session.gc_maxlifetime', $phpsessid_ttl);
+// Set session cookie lifetime.
+session_set_cookie_params($phpsessid_ttl);
+if (isset($_COOKIE['tt_PHPSESSID'])) {
+  // Extend PHP session cookie lifetime by PHPSESSID_TTL (if defined, otherwise 24 hours) 
+  // so that users don't have to re-login during this period from now. 
+  setcookie('tt_PHPSESSID', $_COOKIE['tt_PHPSESSID'],  time() + $phpsessid_ttl, '/');
+}
+
+// Start or resume PHP session.
+session_name('tt_PHPSESSID'); // "tt_" prefix is to avoid sharing session with other PHP apps that do not name session.  
+@session_start();
+
+// Authorization.
+import('Auth');
+$auth = Auth::factory(AUTH_MODULE, $GLOBALS['AUTH_MODULE_PARAMS']);
+
+// Some defines we'll need.
+//
+define('RESOURCE_DIR', APP_DIR.'/WEB-INF/resources');
+define('COOKIE_EXPIRE', 60*60*24*30); // Cookies expire in 30 days.
+
+// Status values for projects, users, etc.
+define('ACTIVE', 1);
+define('INACTIVE', 0);
+// define('DELETED', -1); // DELETED items should have a NULL status. This allows us to have duplicate NULL status entries with existing indexes.
+
+// Definitions for tracking mode types.
+define('MODE_TIME', 0); // Tracking time only. There are no projects or tasks.
+define('MODE_PROJECTS', 1); // Tracking time per projects. There are no tasks.
+define('MODE_PROJECTS_AND_TASKS', 2); // Tracking time for projects and tasks.
+
+// Definitions of types for time records.
+define('TYPE_ALL', 0); // Time record can be specified with either duration or start and finish times.
+define('TYPE_START_FINISH', 1); // Time record has start and finish times.
+define('TYPE_DURATION', 2); // Time record has only duration, no start and finish times.
+
+// User access rights - bits that collectively define an access mask to the system (a role).
+// We'll have some bits here (1,2, etc...) reserved for future use.
+define('right_data_entry', 4);     // Right to enter work hours and expenses.
+define('right_view_charts', 8);    // Right to view charts.
+define('right_view_reports', 16);  // Right to view reports.
+define('right_view_invoices', 32); // Right to view invoices.
+define('right_manage_team', 64);   // Right to manage team. Note that this is not full access to team.
+define('right_assign_roles', 128); // Right to assign user roles.
+define('right_export_team', 256);  // Right to export team data to a file.
+define('right_administer_site', 1024); // Admin account right to manage the application as a whole. 
+
+// User roles.
+define('ROLE_USER', 4);          // Regular user.
+define('ROLE_CLIENT', 16);       // Client (to view reports and invoices).
+define('ROLE_COMANAGER', 68);    // Team co-manager. Can do many things but not as much as team manager.
+define('ROLE_MANAGER', 324);     // Team manager. Can do everything for a team.
+define('ROLE_SITE_ADMIN', 1024); // Site administrator.
+
+
+define('CHARSET', 'utf-8');
+
+date_default_timezone_set(@date_default_timezone_get());
+
+// Strip auto-inserted extra slashes when magic_quotes ON for PHP versions prior to 5.4.0.
+if (get_magic_quotes_gpc())
+  magic_quotes_off();
+
+// Initialize global objects that are needed for the application.
+import('html.HttpRequest');
+$request = new ttHttpRequest();
+
+import('form.ActionErrors');
+$errors = new ActionErrors();
+$messages = new ActionErrors();
+
+// Create an instance of ttUser class. This gets us most of user details.
+import('ttUser');
+$user = new ttUser(null, $auth->getUserId());
+if ($user->custom_logo) {
+  $smarty->assign('custom_logo', 'images/'.$user->team_id.'.png');
+  $smarty->assign('mobile_custom_logo', '../images/'.$user->team_id.'.png');
+}
+$smarty->assign('user', $user);
+
+// Localization.
+import('I18n');
+$i18n = new I18n();
+
+// Determine the language to use.
+$lang = $user->lang;
+if (!$lang) {
+  if (defined('LANG_DEFAULT'))
+    $lang = LANG_DEFAULT;
+  
+  // If we still do not have the language get it from the browser.
+  if (!$lang) {
+    $lang = $i18n->getBrowserLanguage();
+        
+    // Finally - English is the default.
+    if (!$lang) {
+      $lang = 'en';
+    }
+  }
+}
+
+// Load i18n file.
+$i18n->load($lang);
+$GLOBALS['I18N'] = &$i18n;
+
+$GLOBALS['USER'] = &$user;
+
+// Assign things for smarty to use in template files.
+$smarty->assign('i18n', $i18n->keys);
+$smarty->assign('errors', $errors);
+$smarty->assign('messages', $messages);
+
+// TODO: move this code out of here to the files that use it. 
+  
+// We use js/strftime.js to print dates in JavaScript (in DateField controls).
+// One of our date formats (%d.%m.%Y %a) prints a localized short weekday name (%a).
+// The init_js_date_locale function iniitializes Date.ext.locales array in js/strftime.js for our language
+// so that we could print localized short weekday names.
+//
+// JavaScript usage (see http://hacks.bluesmoon.info/strftime/localisation.html).
+//
+// var d = new Date();
+// d.locale = "fr";           // Remember to initialize locale.
+// d.strftime("%d.%m.%Y %a"); // This will output a localized %a as in "31.05.2013 Ven"
+
+// Initialize date locale for JavaScript.
+init_js_date_locale();
+
+function init_js_date_locale()
+{
+  global $i18n, $smarty;
+  $lang = $i18n->lang;
+
+  $days = $i18n->weekdayNames;
+  $short_day_names = array();
+  foreach($days as $k => $v) {
+    $short_day_names[$k] = mb_substr($v, 0, 3, 'utf-8');
+  }
+
+  /*
+  $months = $i18n->monthNames;
+  $short_month_names = array();
+  foreach ($months as $k => $v) {
+    $short_month_names[$k] = mb_substr($v, 0, 3, 'utf-8');
+  }
+  $js = "Date.ext.locales['$lang'] = {
+      a: ['" . join("', '", $short_day_names) . "'],
+      A: ['" . join("', '", $days) . "'],
+      b: ['" . join("', '", $short_month_names) . "'],
+      B: ['" . join("', '", $months) . "'],
+      c: '%a %d %b %Y %T %Z',
+      p: ['', ''],
+      P: ['', ''],
+      x: '%Y-%m-%d',
+      X: '%T'
+    };"; */
+  // We use %a in one of date formats. Therefore, simplified code here (instead of the above block).
+  // %p is also used on the Profile page in 12-hour time format example. Note that %p is not localized.
+  $js = "Date.ext.locales['$lang'] = {
+      a: ['" . join("', '", $short_day_names) . "'],
+      p: ['AM', 'PM']
+    };";
+  $smarty->assign('js_date_locale', $js);
+}
+?>
\ No newline at end of file
diff --git a/invoice_add.php b/invoice_add.php
new file mode 100644 (file)
index 0000000..4a25ddc
--- /dev/null
@@ -0,0 +1,99 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+import('ttInvoiceHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+if ($request->getMethod() == 'POST') {
+  $cl_date = $request->getParameter('date');
+  $cl_client = $request->getParameter('client');
+  $cl_project = $request->getParameter('project');
+  $cl_number = trim($request->getParameter('number'));
+  $cl_start = $request->getParameter('start');
+  $cl_finish = $request->getParameter('finish');
+}
+
+$form = new Form('invoiceForm');
+$form->addInput(array('type'=>'datefield','name'=>'date','size'=>'20','value'=>$cl_date));
+
+// Dropdown for clients if the clients plugin is enabled.
+if (in_array('cl', explode(',', $user->plugins))) {
+  $clients = ttTeamHelper::getActiveClients($user->team_id);
+  $form->addInput(array('type'=>'combobox','name'=>'client','style'=>'width: 250px;','data'=>$clients,'datakeys'=>array('id','name'),'value'=>$cl_client,'empty'=>array(''=>$i18n->getKey('dropdown.select'))));
+}
+// Dropdown for projects.
+if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+  $projects = ttTeamHelper::getActiveProjects($user->team_id);
+  $form->addInput(array('type'=>'combobox','name'=>'project','style'=>'width: 250px;','data'=>$projects,'datakeys'=>array('id','name'),'value'=>$cl_project,'empty'=>array(''=>$i18n->getKey('dropdown.all'))));
+}
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'number','style'=>'width: 250px;','value'=>$cl_number));
+$form->addInput(array('type'=>'datefield','maxlength'=>'20','name'=>'start','value'=>$cl_start));
+$form->addInput(array('type'=>'datefield','maxlength'=>'20','name'=>'finish','value'=>$cl_finish));
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.add')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_number)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.invoice.number'));
+  if (!ttValidDate($cl_date)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.date'));
+  if (!$cl_client) $errors->add($i18n->getKey('error.client'));
+  if (!ttValidDate($cl_start)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.start_date'));
+  if (!ttValidDate($cl_finish)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.end_date'));
+
+  $fields = array('date'=>$cl_date,'name'=>$cl_number,'client_id'=>$cl_client,'project_id'=>$cl_project,'start_date'=>$cl_start,'end_date'=>$cl_finish);
+  if ($errors->isEmpty()) {
+    if (ttInvoiceHelper::getInvoiceByName($cl_number))
+      $errors->add($i18n->getKey('error.invoice_exists'));
+      
+    if (!ttInvoiceHelper::invoiceableItemsExist($fields))
+      $errors->add($i18n->getKey('error.no_invoiceable_items'));    
+  }
+
+  if ($errors->isEmpty()) {
+       // Now we can go ahead and create our invoice.
+    if (ttInvoiceHelper::createInvoice($fields)) {
+      header('Location: invoices.php');
+      exit();
+    }
+      $errors->add($i18n->getKey('error.db')); 
+  }
+} // post
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.invoiceForm.number.focus()"');
+$smarty->assign('title', $i18n->getKey('title.add_invoice'));
+$smarty->assign('content_page_name', 'invoice_add.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/invoice_delete.php b/invoice_delete.php
new file mode 100644 (file)
index 0000000..9fb7c43
--- /dev/null
@@ -0,0 +1,74 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttInvoiceHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$cl_invoice_id = (int)$request->getParameter('id');
+$invoice = ttInvoiceHelper::getInvoice($cl_invoice_id);
+$invoice_to_delete = $invoice['name'];
+
+$form = new Form('invoiceDeleteForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_invoice_id));
+$form->addInput(array('type'=>'combobox',
+    'name'=>'delete_invoice_entries',
+    'data'=>array('0'=>$i18n->getKey('dropdown.do_not_delete'),'1'=>$i18n->getKey('dropdown.delete')),
+));
+$form->addInput(array('type'=>'submit','name'=>'btn_delete','value'=>$i18n->getKey('label.delete')));
+$form->addInput(array('type'=>'submit','name'=>'btn_cancel','value'=>$i18n->getKey('button.cancel')));
+
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('btn_delete')) {
+    if (ttInvoiceHelper::getInvoice($cl_invoice_id)) {
+      if (ttInvoiceHelper::delete($cl_invoice_id, $request->getParameter('delete_invoice_entries'))) {
+        header('Location: invoices.php');
+       exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    } else
+      $errors->add($i18n->getKey('error.db'));
+  } else if ($request->getParameter('btn_cancel')) {
+    header('Location: invoices.php');
+    exit();
+  }
+} // post
+
+$smarty->assign('invoice_to_delete', $invoice_to_delete);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.invoiceDeleteForm.btn_cancel.focus()"');
+$smarty->assign('title', $i18n->getKey('title.delete_invoice'));
+$smarty->assign('content_page_name', 'invoice_delete.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/invoice_send.php b/invoice_send.php
new file mode 100644 (file)
index 0000000..1dd2323
--- /dev/null
@@ -0,0 +1,102 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttInvoiceHelper');
+import('ttSysConfig');
+
+// Access check.
+if (!ttAccessCheck(right_view_invoices)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$cl_invoice_id = (int)$request->getParameter('id');
+$invoice = ttInvoiceHelper::getInvoice($cl_invoice_id); 
+$sc = new ttSysConfig($user->id);
+
+// Security check.
+if (!$cl_invoice_id || !$invoice)
+  die ($i18n->getKey('error.sys'));
+
+if ($request->getMethod() == 'POST') {
+  $cl_receiver = trim($request->getParameter('receiver'));
+  $cl_cc = trim($request->getParameter('cc'));
+  $cl_subject = trim($request->getParameter('subject'));
+  $cl_comment = trim($request->getParameter('comment'));
+} else {
+  $cl_receiver = $sc->getValue(SYSC_LAST_INVOICE_EMAIL);
+  $cl_cc = $sc->getValue(SYSC_LAST_INVOICE_CC);
+  $cl_subject = $i18n->getKey('title.invoice').' '.$invoice['name'].', '.$user->team;
+}
+
+$form = new Form('mailForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_invoice_id));
+$form->addInput(array('type'=>'text','name'=>'receiver','style'=>'width: 300px;','value'=>$cl_receiver));
+$form->addInput(array('type'=>'text','name'=>'cc','style'=>'width: 300px;','value'=>$cl_cc));
+$form->addInput(array('type'=>'text','name'=>'subject','style'=>'width: 300px;','value'=>$cl_subject));
+$form->addInput(array('type'=>'textarea','name'=>'comment','maxlength'=>'250','style'=>'width: 300px; height: 60px;'));
+$form->addInput(array('type'=>'submit','name'=>'btn_send','value'=>$i18n->getKey('button.send')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidEmailList($cl_receiver)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.mail.to'));
+  if (!ttValidEmailList($cl_cc, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.mail.cc'));
+  if (!ttValidString($cl_subject)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.mail.subject'));
+  if (!ttValidString($cl_comment, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.comment'));
+  
+  if ($errors->isEmpty()) {
+    // Save last invoice emails for future use.
+    $sc->setValue(SYSC_LAST_INVOICE_EMAIL, $cl_receiver);
+       $sc->setValue(SYSC_LAST_INVOICE_CC, $cl_cc);
+               
+       $body = ttInvoiceHelper::prepareInvoiceBody($cl_invoice_id, $cl_comment);
+       
+    import('mail.Mailer');
+    $mailer = new Mailer();
+    $mailer->setCharSet(CHARSET);
+    $mailer->setContentType('text/html');
+    $mailer->setSender(SENDER);
+    $mailer->setReceiver($cl_receiver);
+    if (isset($cl_cc))
+      $mailer->setReceiverCC($cl_cc);
+    $mailer->setSendType(MAIL_MODE);
+    if ($mailer->send($cl_subject, $body))
+      $messages->add($i18n->getKey('form.mail.invoice_sent'));
+    else
+      $errors->add($i18n->getKey('error.mail_send'));
+  }
+}
+
+$smarty->assign('title', $i18n->getKey('title.send_invoice'));
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.mailForm.'.($cl_receiver?'comment':'receiver').'.focus()"');
+$smarty->assign('content_page_name', 'mail.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/invoice_view.php b/invoice_view.php
new file mode 100644 (file)
index 0000000..a91ddf1
--- /dev/null
@@ -0,0 +1,90 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('DateAndTime');
+import('ttInvoiceHelper');
+import('ttClientHelper');
+
+// Access check.
+if (!ttAccessCheck(right_view_invoices)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$invoice_id = (int)$request->getParameter('id');
+$invoice = ttInvoiceHelper::getInvoice($invoice_id);
+$invoice_date = new DateAndTime(DB_DATEFORMAT, $invoice['date']);
+$client = ttClientHelper::getClient($invoice['client_id'], true);
+if (!$client) // In case client was deleted.
+  $client = ttClientHelper::getDeletedClient($invoice['client_id']);
+
+$invoice_items = ttInvoiceHelper::getInvoiceItems($invoice_id);
+$tax_percent = $client['tax'];
+
+$subtotal = 0;
+$tax = 0;
+foreach($invoice_items as $item)
+  $subtotal += $item['cost'];
+if ($tax_percent) {
+  $tax_expenses = in_array('et', explode(',', $user->plugins));
+  foreach($invoice_items as $item) {
+       if ($item['type'] == 2 && !$tax_expenses)
+         continue;
+    $tax += round($item['cost'] * $tax_percent / 100, 2);      
+  }
+}
+$total = $subtotal + $tax; 
+
+$smarty->assign('subtotal', $user->currency.' '.str_replace('.', $user->decimal_mark, sprintf('%8.2f', round($subtotal, 2))));
+if ($tax) $smarty->assign('tax', $user->currency.' '.str_replace('.', $user->decimal_mark, sprintf('%8.2f', round($tax, 2))));
+$smarty->assign('total', $user->currency.' '.str_replace('.', $user->decimal_mark, sprintf('%8.2f', round($total, 2))));
+
+if ('.' != $user->decimal_mark) {
+  foreach ($invoice_items as &$item)
+       $item['cost'] = str_replace('.', $user->decimal_mark, $item['cost']);
+}
+
+// Calculate colspan for invoice summary.
+$colspan = 4;
+if (MODE_PROJECTS == $user->tracking_mode)
+  $colspan++;
+else if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode)
+  $colspan += 2;
+
+$smarty->assign('invoice_id', $invoice_id);
+$smarty->assign('invoice_name', $invoice['name']);
+$smarty->assign('invoice_date', $invoice_date->toString($user->date_format));
+$smarty->assign('client_name', $client['name']);
+$smarty->assign('client_address', $client['address']);
+$smarty->assign('invoice_items', $invoice_items);
+$smarty->assign('colspan', $colspan);
+$smarty->assign('title', $i18n->getKey('title.view_invoice'));
+$smarty->assign('content_page_name', 'invoice_view.tpl');
+$smarty->display('index.tpl'); 
+?>
\ No newline at end of file
diff --git a/invoices.php b/invoices.php
new file mode 100644 (file)
index 0000000..f495292
--- /dev/null
@@ -0,0 +1,45 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_view_invoices)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$invoices = ttTeamHelper::getActiveInvoices();
+
+$smarty->assign('invoices', $invoices);
+$smarty->assign('title', $i18n->getKey('title.invoices'));
+$smarty->assign('content_page_name', 'invoices.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/license.txt b/license.txt
new file mode 100644 (file)
index 0000000..eaa424c
--- /dev/null
@@ -0,0 +1,20 @@
+Anuko Time Tracker
+
+Copyright 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. 
diff --git a/login.php b/login.php
new file mode 100644 (file)
index 0000000..c9ddc81
--- /dev/null
+++ b/login.php
@@ -0,0 +1,96 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+import('ttUser');
+
+$cl_login = $request->getParameter('login');
+$cl_password = $request->getParameter('password');
+if ($cl_login == null && $request->getMethod() == 'GET')
+  $cl_login = @$_COOKIE['tt_login'];
+
+$form = new Form('loginForm');
+$form->addInput(array('type'=>'text','size'=>'25','maxlength'=>'100','name'=>'login','style'=>'width: 220px;','value'=>$cl_login));
+$form->addInput(array('type'=>'text','size'=>'25','maxlength'=>'50','name'=>'password','style'=>'width: 220px;','aspassword'=>true,'value'=>$cl_password));
+$form->addInput(array('type'=>'hidden','name'=>'browser_today','value'=>'')); // User current date, which gets filled in on btn_login click.
+$form->addInput(array('type'=>'submit','name'=>'btn_login','onclick'=>'browser_today.value=get_date()','value'=>$i18n->getKey('button.login')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_login)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.login'));
+  if (!ttValidString($cl_password)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.password'));
+  
+  if ($errors->isEmpty()) {
+       // Use the "limit" plugin if we have one. Ignore include errors.
+    // The "limit" plugin is not required for normal operation of Time Tracker.
+    @include('plugins/limit/access_check.php');
+       
+    if ($auth->doLogin($cl_login, $cl_password)) {
+      // Set current user date (as determined by user browser) into session.
+      $current_user_date = $request->getParameter('browser_today', null);
+      if ($current_user_date)
+        $_SESSION['date'] = $current_user_date;
+          
+      // Remember user login in a cookie.
+      setcookie('tt_login', $cl_login, time() + COOKIE_EXPIRE, '/');
+      
+      $user = new ttUser(null, $auth->getUserId());
+      // Redirect, depending on user role.
+      if ($user->isAdmin()) {
+        header('Location: admin_teams.php');
+        exit();
+      }
+      else if ($user->isClient()) {
+        header('Location: reports.php');
+        exit();        
+      }
+      else {
+        header('Location: time.php');
+        exit();
+      }
+    } else
+      $errors->add($i18n->getKey('error.auth'));
+  }
+}
+
+if(!isTrue(MULTITEAM_MODE) && !ttTeamHelper::getTeams())
+  $errors->add($i18n->getKey('error.no_teams'));
+
+// Determine whether to show login hint. It is currently used only for Windows LDAP authentication.
+$show_hint = ('ad' == $GLOBALS['AUTH_MODULE_PARAMS']['type']);
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('show_hint', $show_hint);
+$smarty->assign('onload', 'onLoad="document.loginForm.'.(!$cl_login?'login':'password').'.focus()"');
+$smarty->assign('title', $i18n->getKey('title.login'));
+$smarty->assign('content_page_name', 'login.tpl');
+$smarty->assign('about_text', $i18n->getKey('form.login.about'));
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/logout.php b/logout.php
new file mode 100644 (file)
index 0000000..adbcd54
--- /dev/null
@@ -0,0 +1,34 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+$auth->doLogout();
+session_unset();
+
+header('Location: login.php');
+?>
\ No newline at end of file
diff --git a/mysql.sql b/mysql.sql
new file mode 100644 (file)
index 0000000..3e28325
--- /dev/null
+++ b/mysql.sql
@@ -0,0 +1,339 @@
+# Usage: 
+# 1) Create a database using the "CREATE DATABASE" mysql command.
+# 2) Then, execute this script from command prompt with a command like this:
+# mysql -h host -u user -p -D db_name < mysql.sql
+
+# create database timetracker character set = 'utf8';
+
+# use timetracker;
+
+
+#
+# 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
+  `custom_logo` tinyint(4) default '0',                  # whether to use a custom logo or not
+  `status` tinyint(4) default '1',                       # team status
+  PRIMARY KEY (`id`)
+);
+
+
+#
+# Structure for table tt_users. This table is used to store user properties.
+#
+CREATE TABLE `tt_users` (
+  `id` int(11) NOT NULL auto_increment,          # user id
+  `timestamp` timestamp NOT NULL,                # modification timestamp
+  `login` varchar(50) COLLATE utf8_bin NOT NULL, # user login
+  `password` varchar(50) default NULL,           # password hash
+  `name` varchar(100) default NULL,              # user name
+  `team_id` int(11) NOT NULL,                    # team id
+  `role` int(11) default '4',                    # user role ("manager", "co-manager", "client", or "user")
+  `client_id` int(11) default NULL,              # client id for "client" user role
+  `rate` float(6,2) NOT NULL default '0.00',     # default hourly rate
+  `email` varchar(100) default NULL,             # user email
+  `status` tinyint(4) default '1',               # user status
+  PRIMARY KEY (`id`)
+);
+
+# Create an index that guarantees unique active and inactive logins.
+create unique index login_idx on tt_users(login, status);
+
+# Create admin account with password 'secret'. Admin is a superuser, who can create teams.
+DELETE from `tt_users` WHERE login = 'admin';
+INSERT INTO `tt_users` (`login`, `password`, `name`, `team_id`, `role`) VALUES ('admin', md5('secret'), 'Admin', '0', '1024');
+
+
+#
+# Structure for table tt_projects.
+#
+CREATE TABLE `tt_projects` (
+  `id` int(11) NOT NULL auto_increment,         # project id
+  `team_id` int(11) NOT NULL,                   # team id
+  `name` varchar(80) COLLATE utf8_bin NOT NULL, # project name
+  `description` varchar(255) default NULL,      # project description
+  `tasks` text default NULL,                    # comma-separated list of task ids associated with this project
+  `status` tinyint(4) default '1',                             # project status
+  PRIMARY KEY (`id`)
+);
+
+# Create an index that guarantees unique active and inactive projects per team.
+create unique index project_idx on tt_projects(team_id, name, status);
+
+
+#
+# Structure for table tt_tasks.
+#
+CREATE TABLE `tt_tasks` (
+  `id` int(11) NOT NULL auto_increment,         # task id
+  `team_id` int(11) NOT NULL,                   # team id
+  `name` varchar(80) COLLATE utf8_bin NOT NULL, # task name
+  `description` varchar(255) default NULL,      # task description
+  `status` tinyint(4) default '1',              # task status
+  PRIMARY KEY (`id`)
+);
+
+# Create an index that guarantees unique active and inactive tasks per team.
+create unique index task_idx on tt_tasks(team_id, name, status);
+
+
+#
+# Structure for table tt_user_project_binds. This table maps users to assigned projects.
+#
+CREATE TABLE `tt_user_project_binds` (
+  `id` int(11) NOT NULL auto_increment, # bind id
+  `user_id` int(11) NOT NULL,           # user id
+  `project_id` int(11) NOT NULL,        # project id
+  `rate` float(6,2) default '0.00',     # rate for this user when working on this project
+  `status` tinyint(4) default '1',      # bind status
+  PRIMARY KEY (`id`)
+);
+
+# Create an index that guarantees unique user to project binds.
+create unique index bind_idx on tt_user_project_binds(user_id, project_id);
+
+
+#
+# Structure for table tt_project_task_binds. This table maps projects to assigned tasks.
+#
+CREATE TABLE `tt_project_task_binds` (
+  `project_id` int(11) NOT NULL, # project id    
+  `task_id` int(11) NOT NULL     # task id
+);
+
+# Indexes for tt_project_task_binds.
+create index project_idx on tt_project_task_binds(project_id);
+create index task_idx on tt_project_task_binds(task_id);
+
+
+#
+# Structure for table tt_log. This is the table where time entries for users are stored.
+# If you use custom fields, additional info for each record may exist in tt_custom_field_log.
+#
+CREATE TABLE `tt_log` (
+  `id` bigint NOT NULL auto_increment, # time record id
+  `timestamp` timestamp NOT NULL,      # modification timestamp
+  `user_id` int(11) NOT NULL,          # user id
+  `date` date NOT NULL,                # date the record is for
+  `start` time default NULL,           # record start time (for example, 09:00)
+  `duration` time default NULL,        # record duration (for example, 1 hour)
+  `client_id` int(11) default NULL,    # client id
+  `project_id` int(11) default NULL,   # project id
+  `task_id` int(11) default NULL,      # task id
+  `invoice_id` int(11) default NULL,   # invoice id
+  `comment` blob,                      # user provided comment for time record
+  `billable` tinyint(4) default '0',   # whether the record is billable or not
+  `status` tinyint(4) default '1',     # time record status
+  PRIMARY KEY (`id`)
+);
+
+# Create indexes on tt_log for performance.
+create index date_idx on tt_log(date);
+create index user_idx on tt_log(user_id);
+create index client_idx on tt_log(client_id);
+create index invoice_idx on tt_log(invoice_id);
+create index project_idx on tt_log(project_id);
+create index task_idx on tt_log(task_id);
+
+
+#
+# Structure for table tt_invoices. Invoices are issued to clients for billable work.
+#
+CREATE TABLE `tt_invoices` (
+  `id` int(11) NOT NULL auto_increment,         # invoice id
+  `team_id` int(11) NOT NULL,                   # team id
+  `name` varchar(80) COLLATE utf8_bin NOT NULL, # invoice name
+  `date` date NOT NULL,                         # invoice date
+  `client_id` int(11) NOT NULL,                 # client id
+  `status` tinyint(4) default '1',              # invoice status
+  PRIMARY KEY (`id`)
+);
+
+# Create an index that guarantees unique invoice names per team.
+create unique index name_idx on tt_invoices(team_id, name, status);
+
+
+#
+# Structure for table tt_tmp_refs. Used for reset password mechanism.
+#
+CREATE TABLE `tt_tmp_refs` (
+  `timestamp` timestamp NOT NULL,     # creation timestamp
+  `ref` char(32) NOT NULL default '', # unique reference for user, used in urls
+  `user_id` int(11) NOT NULL          # user id
+);
+
+
+#
+# Structure for table tt_fav_reports. Favorite reports are pre-configured report configurations.
+#
+CREATE TABLE `tt_fav_reports` (
+  `id` int(11) NOT NULL auto_increment,                  # favorite report id
+  `name` varchar(200) NOT NULL,                          # favorite report name
+  `user_id` int(11) NOT NULL,                            # user id favorite report belongs to
+  `client_id` int(11) default NULL,                      # client id (if selected)
+  `cf_1_option_id` int(11) default NULL,                 # custom field 1 option id (if selected)
+  `project_id` int(11) default NULL,                     # project id (if selected)
+  `task_id` int(11) default NULL,                        # task id (if selected)
+  `billable` tinyint(4) default NULL,                    # whether to include billable, not billable, or all records
+  `invoice` tinyint(4) default NULL,                     # whether to include invoiced, not invoiced, or all records
+  `users` text default NULL,                             # Comma-separated list of user ids. Nothing here means "all" users. 
+  `period` tinyint(4) default NULL,                      # selected period type for report
+  `period_start` date default NULL,                      # period start
+  `period_end` date default NULL,                        # period end
+  `show_client` tinyint(4) NOT NULL default '0',         # whether to show client column
+  `show_invoice` tinyint(4) NOT NULL default '0',        # whether to show invoice column  
+  `show_project` tinyint(4) NOT NULL default '0',        # whether to show project column
+  `show_start` tinyint(4) NOT NULL default '0',          # whether to show start field
+  `show_duration` tinyint(4) NOT NULL default '0',       # whether to show duration field
+  `show_cost` tinyint(4) NOT NULL default '0',           # whether to show cost field  
+  `show_task` tinyint(4) NOT NULL default '0',           # whether to show task column
+  `show_end` tinyint(4) NOT NULL default '0',            # whether to show end field
+  `show_note` tinyint(4) NOT NULL default '0',           # whether to show note column
+  `show_custom_field_1` tinyint(4) NOT NULL default '0', # whether to show custom field 1
+  `show_totals_only` tinyint(4) NOT NULL default '0',    # whether to show totals only
+  `group_by` varchar(20) default NULL,                   # group by field
+  PRIMARY KEY (`id`)
+);
+
+
+#
+# Structure for table tt_cron. It is used to email favorite reports on schedule.
+#
+CREATE TABLE `tt_cron` (
+  `id` int(11) NOT NULL auto_increment,         # entry id
+  `team_id` int(11) NOT NULL,                   # team id
+  `cron_spec` varchar(255) NOT NULL,            # cron specification, "0 1 * * *" for "daily at 01:00"
+  `last` int(11) default NULL,                  # UNIX timestamp of when job was last run
+  `next` int(11) default NULL,                  # UNIX timestamp of when to run next job
+  `report_id` int(11) default NULL,             # report id from tt_fav_reports, a report to mail on schedule
+  `email` varchar(100) default NULL,            # email to send results to
+  `status` tinyint(4) default '1',              # entry status
+  PRIMARY KEY (`id`)
+);
+
+
+#
+# Structure for table tt_clients. A client is an entity for whom work is performed and who may be invoiced.
+#
+CREATE TABLE `tt_clients` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,         # client id
+  `team_id` int(11) NOT NULL,                   # team id
+  `name` varchar(80) COLLATE utf8_bin NOT NULL, # client name
+  `address` varchar(255) default NULL,          # client address
+  `tax` float(6,2) default '0.00',              # applicable tax for this client
+  `projects` text default NULL,                 # comma-separated list of project ids assigned to this client
+  `status` tinyint(4) default '1',              # client status
+  PRIMARY KEY (`id`)
+);
+
+# Create an index that guarantees unique active and inactive clients per team.
+create unique index client_name_idx on tt_clients(team_id, name, status);
+
+
+#
+# Structure for table tt_client_project_binds. This table maps clients to assigned projects.
+#
+CREATE TABLE `tt_client_project_binds` (
+  `client_id` int(11) NOT NULL, # client id
+  `project_id` int(11) NOT NULL # project id
+);
+
+# Indexes for tt_client_project_binds.
+create index client_idx on tt_client_project_binds(client_id);
+create index project_idx on tt_client_project_binds(project_id);
+
+
+#
+# Structure for table tt_config. This table is used to store configuration info for users.
+# For example, last_report_email parameter stores an email for user last report was emailed to.
+#
+CREATE TABLE `tt_config` (
+  `user_id` int(11) NOT NULL,            # user id
+  `param_name` varchar(32) NOT NULL,     # parameter name
+  `param_value` varchar(80) default NULL # parameter value
+);
+
+# Create an index that guarantees unique parameter names per user.
+create unique index param_idx on tt_config(user_id, param_name);
+
+
+# Below are the tables used by CustomFields plugin.
+
+#
+# Structure for table tt_custom_fields. This table contains definitions of custom fields.
+#
+CREATE TABLE `tt_custom_fields` (
+  `id` int(11) NOT NULL auto_increment,    # custom field id
+  `team_id` int(11) NOT NULL,              # team id
+  `type` tinyint(4) NOT NULL default '0',  # custom field type (text or dropdown)
+  `label` varchar(32) NOT NULL default '', # custom field label
+  `required` tinyint(4) default '0',       # whether this custom field is mandatory for time records
+  `status` tinyint(4) default '1',         # custom field status
+  PRIMARY KEY  (`id`)
+);
+
+
+#
+# Structure for table tt_custom_field_options. This table defines options for dropdown custom fields.
+#
+CREATE TABLE `tt_custom_field_options` (
+  `id` int(11) NOT NULL auto_increment,    # option id
+  `field_id` int(11) NOT NULL,             # custom field id              
+  `value` varchar(32) NOT NULL default '', # option value
+  PRIMARY KEY  (`id`)
+);
+
+
+#
+# Structure for table tt_custom_field_log.
+# This table supplements tt_log and contains custom field values for records.
+#
+CREATE TABLE `tt_custom_field_log` (
+  `id` bigint NOT NULL auto_increment, # cutom field log id
+  `log_id` bigint NOT NULL,            # id of a record in tt_log this record corresponds to
+  `field_id` int(11) NOT NULL,         # custom field id
+  `option_id` int(11) default NULL,    # Option id. Used for dropdown custom fields.
+  `value` varchar(255) default NULL,   # Text value. Used for text custom fields.
+  `status` tinyint(4) default '1',     # custom field log entry status
+  PRIMARY KEY  (`id`)
+);
+
+
+#
+# Structure for table tt_expense_items.
+# This table lists expense items.
+#
+CREATE TABLE `tt_expense_items` (
+  `id` bigint NOT NULL auto_increment, # expense item id
+  `date` date NOT NULL,                # date the record is for
+  `user_id` int(11) NOT NULL,          # user id the expense item is reported by
+  `client_id` int(11) default NULL,    # client id
+  `project_id` int(11) default NULL,   # project id
+  `name` varchar(255) NOT NULL,        # expense item name (what is an expense for)
+  `cost` decimal(10,2) default '0.00', # item cost (including taxes, etc.)
+  `invoice_id` int(11) default NULL,   # invoice id
+  `status` tinyint(4) default '1',     # item status
+  PRIMARY KEY  (`id`)
+);
+
+# Create indexes on tt_expense_items for performance.
+create index date_idx on tt_expense_items(date);
+create index user_idx on tt_expense_items(user_id);
+create index client_idx on tt_expense_items(client_id);
+create index project_idx on tt_expense_items(project_id);
+create index invoice_idx on tt_expense_items(invoice_id);
diff --git a/notification_add.php b/notification_add.php
new file mode 100644 (file)
index 0000000..4d1b457
--- /dev/null
@@ -0,0 +1,94 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+require_once(LIBRARY_DIR.'/tdcron/class.tdcron.php');
+require_once(LIBRARY_DIR.'/tdcron/class.tdcron.entry.php');
+import('form.Form');
+import('ttFavReportHelper');
+import('ttNotificationHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$fav_reports = ttFavReportHelper::getReports($user->id);
+
+if ($request->getMethod() == 'POST') {
+  $cl_fav_report = trim($request->getParameter('fav_report'));
+  $cl_cron_spec = trim($request->getParameter('cron_spec'));
+  $cl_email = trim($request->getParameter('email'));
+} else {
+  $cl_cron_spec = '0 4 * * 1'; // Default schedule - weekly on Mondays at 04:00 (server time).
+}
+
+$form = new Form('notificationForm');
+$form->addInput(array('type'=>'combobox',
+  'name'=>'fav_report',
+  'style'=>'width: 250px;',
+  'value'=>$cl_fav_report,
+  'data'=>$fav_reports,
+  'datakeys'=>array('id','name'),
+  'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'cron_spec','style'=>'width: 250px;','value'=>$cl_cron_spec));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'email','style'=>'width: 250px;','value'=>$cl_email));
+$form->addInput(array('type'=>'submit','name'=>'btn_add','value'=>$i18n->getKey('button.add')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!$cl_fav_report) $errors->add($i18n->getKey('error.report')); 
+  if (!ttValidCronSpec($cl_cron_spec)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.cron_schedule'));
+  if (!ttValidEmail($cl_email)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.email'));
+
+  if ($errors->isEmpty()) {
+       // Calculate next execution time.       
+       $next = tdCron::getNextOccurrence($cl_cron_spec, mktime()); 
+  
+    if (ttNotificationHelper::insert(array(
+        'team_id' => $user->team_id,
+        'cron_spec' => $cl_cron_spec,
+        'next' => $next,
+        'report_id' => $cl_fav_report,
+        'email' => $cl_email,
+        'status' => ACTIVE))) {
+        header('Location: notifications.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));       
+  }
+} // post
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+// $smarty->assign('onload', 'onLoad="document.clientForm.name.focus()"');
+$smarty->assign('title', $i18n->getKey('title.add_notification'));
+$smarty->assign('content_page_name', 'notification_add.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/notification_delete.php b/notification_delete.php
new file mode 100644 (file)
index 0000000..62f11ef
--- /dev/null
@@ -0,0 +1,70 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttNotificationHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$cl_notification_id = (int)$request->getParameter('id');
+$notification = ttNotificationHelper::get($cl_notification_id);
+$notification_to_delete = $notification['name'];
+
+$form = new Form('notificationDeleteForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_notification_id));
+$form->addInput(array('type'=>'submit','name'=>'btn_delete','value'=>$i18n->getKey('label.delete')));
+$form->addInput(array('type'=>'submit','name'=>'btn_cancel','value'=>$i18n->getKey('button.cancel')));
+
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('btn_delete')) {
+       if(ttNotificationHelper::get($cl_notification_id)) {
+      if (ttNotificationHelper::delete($cl_notification_id)) {
+        header('Location: notifications.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    } else
+      $errors->add($i18n->getKey('error.db'));
+  } else if ($request->getParameter('btn_cancel')) {
+       header('Location: notifications.php');
+       exit();
+  }
+} // post
+       
+$smarty->assign('notification_to_delete', $notification_to_delete);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.notificationDeleteForm.btn_cancel.focus()"');
+$smarty->assign('title', $i18n->getKey('title.delete_notification'));
+$smarty->assign('content_page_name', 'notification_delete.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/notification_edit.php b/notification_edit.php
new file mode 100644 (file)
index 0000000..e029c6c
--- /dev/null
@@ -0,0 +1,100 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+require_once(LIBRARY_DIR.'/tdcron/class.tdcron.php');
+require_once(LIBRARY_DIR.'/tdcron/class.tdcron.entry.php');
+import('form.Form');
+import('ttFavReportHelper');
+import('ttNotificationHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$notification_id = (int) $request->getParameter('id');
+$fav_reports = ttFavReportHelper::getReports($user->id);
+
+if ($request->getMethod() == 'POST') {
+  $cl_fav_report = trim($request->getParameter('fav_report'));
+  $cl_cron_spec = trim($request->getParameter('cron_spec'));
+  $cl_email = trim($request->getParameter('email'));
+} else {
+  $notification = ttNotificationHelper::get($notification_id);
+  $cl_fav_report = $notification['report_id'];
+  $cl_cron_spec = $notification['cron_spec'];
+  $cl_email = $notification['email'];
+}
+
+$form = new Form('notificationForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$notification_id));
+$form->addInput(array('type'=>'combobox',
+  'name'=>'fav_report',
+  'style'=>'width: 250px;',
+  'value'=>$cl_fav_report,
+  'data'=>$fav_reports,
+  'datakeys'=>array('id','name'),
+  'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'cron_spec','style'=>'width: 250px;','value'=>$cl_cron_spec));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'email','style'=>'width: 250px;','value'=>$cl_email));
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.save')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!$cl_fav_report) $errors->add($i18n->getKey('error.report')); 
+  if (!ttValidCronSpec($cl_cron_spec)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.cron_schedule'));
+  if (!ttValidEmail($cl_email)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.email'));
+
+  if ($errors->isEmpty()) {
+       // Calculate next execution time.       
+       $next = tdCron::getNextOccurrence($cl_cron_spec, mktime()); 
+         
+    if (ttNotificationHelper::update(array(
+        'id' => $notification_id,
+        'team_id' => $user->team_id,
+        'cron_spec' => $cl_cron_spec,
+        'next' => $next,
+        'report_id' => $cl_fav_report,
+        'email' => $cl_email,
+        'status' => ACTIVE))) {
+        header('Location: notifications.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));       
+  }
+} // post
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+// $smarty->assign('onload', 'onLoad="document.clientForm.name.focus()"');
+$smarty->assign('title', $i18n->getKey('title.add_notification'));
+$smarty->assign('content_page_name', 'notification_edit.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/notifications.php b/notifications.php
new file mode 100644 (file)
index 0000000..5f2f6d2
--- /dev/null
@@ -0,0 +1,57 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$form = new Form('notificationsForm');
+
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('btn_add')) {
+       // The Add button clicked. Redirect to notification_add.php page.
+       header('Location: notification_add.php');
+       exit();
+  }
+} else {
+  $form->addInput(array('type'=>'submit','name'=>'btn_add','value'=>$i18n->getKey('button.add')));
+  $notifications = ttTeamHelper::getNotifications($user->team_id);
+}
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('notifications', $notifications);
+$smarty->assign('title', $i18n->getKey('title.notifications'));
+$smarty->assign('content_page_name', 'notifications.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/password_change.php b/password_change.php
new file mode 100644 (file)
index 0000000..82ba11a
--- /dev/null
@@ -0,0 +1,97 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttUserHelper');
+import('ttUser');
+
+$auth->doLogout();
+
+$cl_ref = $request->getParameter('ref');
+if (!$cl_ref || $auth->isPasswordExternal()) {
+  header('Location: login.php');
+  exit();
+}
+
+// Get user ID.
+$user_id = ttUserHelper::getUserIdByTmpRef($cl_ref);
+if ($user_id) {
+  $user = new ttUser(null, $user_id); // Note: reusing $user from initialize.php.
+  // In case user language is different - reload $i18n.
+  if ($i18n->lang != $user->lang) {
+    $i18n->load($user->lang);
+    $smarty->assign('i18n', $i18n->keys);
+  }
+  if ($user->custom_logo) {
+    $smarty->assign('custom_logo', 'images/'.$user->team_id.'.png');
+    $smarty->assign('mobile_custom_logo', '../images/'.$user->team_id.'.png');
+  }
+  $smarty->assign('user', $user);
+}
+
+$cl_password1 = $request->getParameter('password1');
+$cl_password2 = $request->getParameter('password2');
+
+$form = new Form('newPasswordForm');
+$form->addInput(array('type'=>'text','maxlength'=>'120','name'=>'password1','aspassword'=>true,'value'=>$cl_password1));
+$form->addInput(array('type'=>'text','maxlength'=>'120','name'=>'password2','aspassword'=>true,'value'=>$cl_password2));
+$form->addInput(array('type'=>'hidden','name'=>'ref','value'=>$cl_ref));
+$form->addInput(array('type'=>'submit','name'=>'btn_save','value'=>$i18n->getKey('button.save')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_password1)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.password'));
+  if (!ttValidString($cl_password2)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.confirm_password'));
+  if ($cl_password1 !== $cl_password2)
+    $errors->add($i18n->getKey('error.not_equal'), $i18n->getKey('label.password'), $i18n->getKey('label.confirm_password'));
+
+  if ($errors->isEmpty()) {
+       // Use the "limit" plugin if we have one. Ignore include errors.
+    // The "limit" plugin is not required for normal operation of Time Tracker.
+    $cl_login = $user->login; // $cl_login is used in access_check.cpp.
+    @include('plugins/limit/access_check.php');
+       
+       ttUserHelper::setPassword($user_id, $cl_password1);
+
+    if ($auth->doLogin($user->login, $cl_password1)) {
+
+      setcookie('tt_login', $user->login, time() + COOKIE_EXPIRE, '/');
+      header('Location: time.php');
+      exit();
+    } else {
+      $errors->add($i18n->getKey('error.auth'));
+    }
+  }
+}
+
+$smarty->assign('forms', array($form->getName() => $form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.change_password'));
+$smarty->assign('content_page_name', 'password_change.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/password_reset.php b/password_reset.php
new file mode 100644 (file)
index 0000000..ccb1960
--- /dev/null
@@ -0,0 +1,120 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttUser');
+import('ttUserHelper');
+
+if ($auth->isPasswordExternal()) {
+  header('Location: login.php');
+  exit();
+}
+
+$form = new Form('resetPasswordForm');
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'login','style'=>'width: 300px;'));
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.reset_password')));
+
+if ($request->getMethod() == 'POST') {
+  $cl_login = $request->getParameter('login');
+  
+  // Validate user input.
+  if (!ttValidString($cl_login)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.login'));
+
+  if ($errors->IsEmpty()) {
+       if (!ttUserHelper::getUserByLogin($cl_login)) {
+         // User with a specified login was not found.
+         // In this case, if login looks like email, try finding user by email.
+         if (ttValidEmail($cl_login)) {
+        $login = ttUserHelper::getUserByEmail($cl_login);
+        if ($login)
+          $cl_login = $login;
+        else
+          $errors->add($i18n->getKey('error.no_login'));
+      } else
+        $errors->add($i18n->getKey('error.no_login'));
+       }
+  }
+  
+  if ($errors->IsEmpty()) {  
+    $user = new ttUser($cl_login); // Note: reusing $user from initialize.php here.
+    
+    // Prepare and save a temporary reference for user.
+    $temp_ref = md5(uniqid());
+    ttUserHelper::saveTmpRef($temp_ref, $user->id);
+
+    $user_i18n = null;
+    if ($user->lang != $i18n->lang) {
+      $user_i18n = new I18n();
+      $user_i18n->load($user->lang);   
+    } else
+      $user_i18n = &$i18n;
+      
+    // Where do we email to?
+    $receiver = null;
+    if ($user->email)
+      $receiver = $user->email;
+    else {
+      if (ttValidEmail($cl_login))
+        $receiver = $cl_login;
+      else
+        $errors->add($i18n->getKey('error.no_email')); 
+    }
+    
+    if ($receiver) {
+      import('mail.Mailer');
+      $sender = new Mailer();
+         $sender->setCharSet(CHARSET);
+      $sender->setSender(SENDER);
+      $sender->setReceiver("$receiver");
+      if ((!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] !== 'off')) || ($_SERVER['SERVER_PORT'] == 443))
+        $secure_connection = true;
+      if($secure_connection)
+        $http = 'https';
+      else
+        $http = 'http';
+
+      $cl_subject = $user_i18n->getKey('form.reset_password.email_subject');
+      if (APP_NAME)
+       $pass_edit_url = $http.'://'.$_SERVER['HTTP_HOST'].'/'.APP_NAME.'/password_change.php?ref='.$temp_ref;
+      else
+       $pass_edit_url = $http.'://'.$_SERVER['HTTP_HOST'].'/password_change.php?ref='.$temp_ref;
+      
+      $sender->setSendType(MAIL_MODE);
+      $res = $sender->send($cl_subject, sprintf($user_i18n->getKey('form.reset_password.email_body'), $pass_edit_url));
+         $smarty->assign('result_message', $res ? $i18n->getKey('form.reset_password.message') : $i18n->getKey('error.mail_send'));
+       }
+  }
+}
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.resetPasswordForm.login.focus()"');
+$smarty->assign('title', $i18n->getKey('title.reset_password'));
+$smarty->assign('content_page_name', 'password_reset.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/phpinfo.php b/phpinfo.php
new file mode 100644 (file)
index 0000000..c1b42ed
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+phpinfo();
+?>
+
diff --git a/profile_edit.php b/profile_edit.php
new file mode 100644 (file)
index 0000000..ebee59d
--- /dev/null
@@ -0,0 +1,271 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttUserHelper');
+
+// Access check.
+if (!ttAccessCheck(right_data_entry|right_view_reports)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+if (!defined('CURRENCY_DEFAULT')) define('CURRENCY_DEFAULT', '$');
+$can_change_login = $user->canManageTeam();
+
+if ($request->getMethod() == 'POST') {
+  $cl_name = trim($request->getParameter('name'));
+  $cl_login = trim($request->getParameter('login'));
+  if (!$auth->isPasswordExternal()) {
+    $cl_password1 = $request->getParameter('password1');
+    $cl_password2 = $request->getParameter('password2');
+  }
+  $cl_email = trim($request->getParameter('email'));
+  
+  if ($user->canManageTeam()) {
+       $cl_team = trim($request->getParameter('team_name'));
+    $cl_address = trim($request->getParameter('address'));
+    $cl_currency = trim($request->getParameter('currency'));
+    if (!$cl_currency) $cl_currency = CURRENCY_DEFAULT;
+    $cl_lock_interval = $request->getParameter('lock_interval');
+    $cl_lang = $request->getParameter('lang');
+    $cl_decimal_mark = $request->getParameter('decimal_mark');
+    $cl_custom_format_date = $request->getParameter('format_date');
+    $cl_custom_format_time = $request->getParameter('format_time');
+    $cl_start_week = $request->getParameter('start_week');
+    $cl_tracking_mode = $request->getParameter('tracking_mode');
+    $cl_record_type = $request->getParameter('record_type');
+    $cl_charts = $request->getParameter('charts');
+    $cl_clients = $request->getParameter('clients');
+    $cl_client_required = $request->getParameter('client_required');
+    $cl_invoices = $request->getParameter('invoices');
+    $cl_custom_fields = $request->getParameter('custom_fields');
+    $cl_expenses = $request->getParameter('expenses');
+    $cl_tax_expenses = $request->getParameter('tax_expenses');
+    $cl_notifications = $request->getParameter('notifications');
+  }
+} else {
+  $cl_name = $user->name;
+  $cl_login = $user->login;
+  $cl_email = $user->email;
+  if ($user->canManageTeam()) {
+    $cl_team = $user->team;
+    $cl_address = $user->address;        
+    $cl_currency = ($user->currency == ''? CURRENCY_DEFAULT : $user->currency);
+    $cl_lock_interval = $user->lock_interval;
+    $cl_lang = $user->lang;
+    $cl_decimal_mark = $user->decimal_mark;
+       $cl_custom_format_date = $user->date_format;
+    $cl_custom_format_time = $user->time_format;
+    $cl_start_week = $user->week_start;
+    $cl_tracking_mode = $user->tracking_mode;
+    $cl_record_type = $user->record_type;
+
+    // Which plugins do we have enabled?
+    $plugins = explode(',', $user->plugins);
+    $cl_charts = in_array('ch', $plugins);
+    $cl_clients = in_array('cl', $plugins);
+    $cl_client_required = in_array('cm', $plugins);
+    $cl_invoices = in_array('iv', $plugins);
+    $cl_custom_fields = in_array('cf', $plugins);
+    $cl_expenses = in_array('ex', $plugins);
+    $cl_tax_expenses = in_array('et', $plugins);
+    $cl_notifications = in_array('no', $plugins);
+  }
+}
+
+$form = new Form('profileForm');
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'name','value'=>$cl_name));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'login','value'=>$cl_login,'enable'=>$can_change_login));
+if (!$auth->isPasswordExternal()) {
+  $form->addInput(array('type'=>'text','maxlength'=>'30','name'=>'password1','aspassword'=>true,'value'=>$cl_password1));
+  $form->addInput(array('type'=>'text','maxlength'=>'30','name'=>'password2','aspassword'=>true,'value'=>$cl_password2));
+}
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'email','value'=>$cl_email));
+if ($user->canManageTeam()) {
+  $form->addInput(array('type'=>'text','maxlength'=>'200','name'=>'team_name','value'=>$cl_team));
+  $form->addInput(array('type'=>'textarea','name'=>'address','maxlength'=>'255','style'=>'width: 350px;','cols'=>'55','rows'=>'4','value'=>$cl_address));
+  $form->addInput(array('type'=>'text','maxlength'=>'7','name'=>'currency','value'=>$cl_currency));
+  $DECIMAL_MARK_OPTIONS = array(
+    array('id'=>'.','name'=>'.'),
+    array('id'=>',','name'=>','));
+  $form->addInput(array('type'=>'combobox','name'=>'decimal_mark','style'=>'width: 150px','data'=>$DECIMAL_MARK_OPTIONS,'datakeys'=>array('id','name'),'value'=>$cl_decimal_mark,
+    'onchange'=>'adjustDecimalPreview()'));
+  $form->addInput(array('type'=>'text','maxlength'=>'10','name'=>'lock_interval','value'=>$cl_lock_interval));
+  // Prepare an array of available languages.   
+  $lang_files = I18n::getLangFileList();
+  foreach ($lang_files as $lfile) {
+    $content = file(RESOURCE_DIR."/".$lfile);
+    $lname = '';
+    foreach ($content as $line) {
+      if (strstr($line, 'i18n_language')) {
+        $a = explode('=', $line);
+        $lname = trim(str_replace(';','',str_replace("'","",$a[1])));
+        break;
+      }
+    }
+    unset($content);
+    $longname_lang[] = array('id'=>I18n::getLangFromFilename($lfile),'name'=>$lname);
+  }
+  $longname_lang = mu_sort($longname_lang, 'name');
+  $form->addInput(array('type'=>'combobox','name'=>'lang','style'=>'width: 150px','data'=>$longname_lang,'datakeys'=>array('id','name'),'value'=>$cl_lang));
+  $DATE_FORMAT_OPTIONS = array(
+    array('id'=>'%Y-%m-%d','name'=>'Y-m-d'),
+    array('id'=>'%m/%d/%Y','name'=>'m/d/Y'),
+    array('id'=>'%d.%m.%Y','name'=>'d.m.Y'),
+    array('id'=>'%d.%m.%Y %a','name'=>'d.m.Y a'));
+  $form->addInput(array('type'=>'combobox','name'=>'format_date','style'=>'width: 150px;','data'=>$DATE_FORMAT_OPTIONS,'datakeys'=>array('id','name'),'value'=>$cl_custom_format_date,
+    'onchange'=>'MakeFormatPreview(&quot;date_format_preview&quot;, this);'));
+  $TIME_FORMAT_OPTIONS = array(
+    array('id'=>'%H:%M','name'=>$i18n->getKey('form.profile.24_hours')),
+    array('id'=>'%I:%M %p','name'=>$i18n->getKey('form.profile.12_hours')));
+  $form->addInput(array('type'=>'combobox','name'=>'format_time','style'=>'width: 150px;','data'=>$TIME_FORMAT_OPTIONS,'datakeys'=>array('id','name'),'value'=>$cl_custom_format_time,
+    'onchange'=>'MakeFormatPreview(&quot;time_format_preview&quot;, this);'));
+
+  // Prepare week start choices.
+  $week_start_options = array();
+  foreach ($i18n->weekdayNames as $id => $week_dn) {
+    $week_start_options[] = array('id' => $id, 'name' => $week_dn);
+  }
+  $form->addInput(array('type'=>'combobox','name'=>'start_week','style'=>'width: 150px;','data'=>$week_start_options,'datakeys'=>array('id','name'),'value'=>$cl_start_week));
+
+  // Prepare tracking mode choices.
+  $tracking_mode_options = array();
+  $tracking_mode_options[MODE_TIME] = $i18n->getKey('form.profile.mode_time');
+  $tracking_mode_options[MODE_PROJECTS] = $i18n->getKey('form.profile.mode_projects');
+  $tracking_mode_options[MODE_PROJECTS_AND_TASKS] = $i18n->getKey('form.profile.mode_projects_and_tasks');
+  $form->addInput(array('type'=>'combobox','name'=>'tracking_mode','style'=>'width: 150px;','data'=>$tracking_mode_options,'value'=>$cl_tracking_mode));
+  
+  // Prepare record type choices.
+  $record_type_options = array();
+  $record_type_options[TYPE_ALL] = $i18n->getKey('form.profile.type_all');
+  $record_type_options[TYPE_START_FINISH] = $i18n->getKey('form.profile.type_start_finish');
+  $record_type_options[TYPE_DURATION] = $i18n->getKey('form.profile.type_duration');
+  $form->addInput(array('type'=>'combobox','name'=>'record_type','style'=>'width: 150px;','data'=>$record_type_options,'value'=>$cl_record_type));
+    
+  $form->addInput(array('type'=>'checkbox','name'=>'charts','data'=>1,'value'=>$cl_charts));
+  $form->addInput(array('type'=>'checkbox','name'=>'clients','data'=>1,'value'=>$cl_clients,'onchange'=>'handlePluginCheckboxes()'));
+  $form->addInput(array('type'=>'checkbox','name'=>'client_required','data'=>1,'value'=>$cl_client_required));
+  
+  $form->addInput(array('type'=>'checkbox','name'=>'invoices','data'=>1,'value'=>$cl_invoices));
+  $form->addInput(array('type'=>'checkbox','name'=>'custom_fields','data'=>1,'value'=>$cl_custom_fields,'onchange'=>'handlePluginCheckboxes()'));
+  $form->addInput(array('type'=>'checkbox','name'=>'expenses','data'=>1,'value'=>$cl_expenses,'onchange'=>'handlePluginCheckboxes()'));
+  $form->addInput(array('type'=>'checkbox','name'=>'tax_expenses','data'=>1,'value'=>$cl_tax_expenses));
+  $form->addInput(array('type'=>'checkbox','name'=>'notifications','data'=>1,'value'=>$cl_notifications,'onchange'=>'handlePluginCheckboxes()'));
+}
+$form->addInput(array('type'=>'submit','name'=>'btn_save','value'=>$i18n->getKey('button.save')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.person_name'));
+  if ($can_change_login) {
+       if (!ttValidString($cl_login)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.login'));
+    
+    // New login must be unique.
+    if ($cl_login != $user->login && ttUserHelper::getUserByLogin($cl_login))
+      $errors->add($i18n->getKey('error.user_exists'));
+  }
+  if (!$auth->isPasswordExternal() && ($cl_password1 || $cl_password2)) {
+    if (!ttValidString($cl_password1)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.password'));
+    if (!ttValidString($cl_password2)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.confirm_password'));
+    if ($cl_password1 !== $cl_password2)
+      $errors->add($i18n->getKey('error.not_equal'), $i18n->getKey('label.password'), $i18n->getKey('label.confirm_password'));
+  }
+  if (!ttValidEmail($cl_email, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.email'));
+  if ($user->canManageTeam()) {
+       if (!ttValidString($cl_team, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.team_name'));
+       if (!ttValidString($cl_address, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.address'));
+       if (!ttValidString($cl_currency, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.currency'));
+       if (!ttValidInteger($cl_lock_interval, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.lock_interval'));
+  }
+  // Finished validating user input.
+  
+  if ($errors->isEmpty()) {
+    if ($cl_lock_interval == null || trim($cl_lock_interval) == '')
+      $cl_lock_interval = 0;
+
+    $update_result = true;
+    if ($user->canManageTeam()) {
+
+      // Prepare plugins string.
+      if ($cl_charts)
+        $plugins .= ',ch';
+      if ($cl_clients)
+        $plugins .= ',cl';
+      if ($cl_client_required)
+        $plugins .= ',cm';
+      if ($cl_invoices)
+        $plugins .= ',iv';     
+      if ($cl_custom_fields)
+        $plugins .= ',cf';
+      if ($cl_expenses)
+        $plugins .= ',ex';
+      if ($cl_tax_expenses)
+        $plugins .= ',et';
+      if ($cl_notifications)
+        $plugins .= ',no';
+      $plugins = trim($plugins, ',');
+       
+      $update_result = ttTeamHelper::update($user->team_id, array(
+        'name' => $cl_team,
+        'address' => $cl_address,
+        'currency' => $cl_currency,
+        'locktime' => $cl_lock_interval,
+        'lang' => $cl_lang,
+        'decimal_mark' => $cl_decimal_mark,
+        'date_format' => $cl_custom_format_date,
+        'time_format' => $cl_custom_format_time,
+        'week_start' => $cl_start_week,
+        'tracking_mode' => $cl_tracking_mode,
+        'record_type' => $cl_record_type,
+        'plugins' => $plugins));
+    }
+    if ($update_result) {
+      $update_result = ttUserHelper::update($user->id, array(
+        'name' => $cl_name,
+        'login' => $cl_login,
+        'password' => $cl_password1,
+        'email' => $cl_email,
+        'status' => ACTIVE));
+    }
+    if ($update_result) {
+      header('Location: time.php');
+      exit();
+    } else
+      $errors->add($i18n->getKey('error.db'));
+  }
+} // POST
+
+$smarty->assign('auth_external', $auth->isPasswordExternal());
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="handlePluginCheckboxes()"');
+$smarty->assign('title', $i18n->getKey('title.profile'));
+$smarty->assign('content_page_name', 'profile_edit.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/project_add.php b/project_add.php
new file mode 100644 (file)
index 0000000..2bdb77b
--- /dev/null
@@ -0,0 +1,96 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttProjectHelper');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$users = ttTeamHelper::getActiveUsers();
+foreach ($users as $user_item)
+  $all_users[$user_item['id']] = $user_item['name'];
+
+$tasks = ttTeamHelper::getActiveTasks($user->team_id);
+foreach ($tasks as $task_item)
+  $all_tasks[$task_item['id']] = $task_item['name'];
+
+if ($request->getMethod() == 'POST') {
+  $cl_name = trim($request->getParameter('project_name'));
+  $cl_description = trim($request->getParameter('description'));
+  $cl_users = $request->getParameter('users', array());
+  $cl_tasks = $request->getParameter('tasks', array());
+} else {
+  foreach ($users as $user_item)
+    $cl_users[] = $user_item['id'];
+  foreach ($tasks as $task_item)
+    $cl_tasks[] = $task_item['id'];
+}
+
+$form = new Form('projectForm');
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'project_name','style'=>'width: 250px;','value'=>$cl_name));
+$form->addInput(array('type'=>'textarea','name'=>'description','style'=>'width: 250px; height: 40px;','value'=>$cl_description));
+$form->addInput(array('type'=>'checkboxgroup','name'=>'users','data'=>$all_users,'layout'=>'H','value'=>$cl_users));
+if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode)
+  $form->addInput(array('type'=>'checkboxgroup','name'=>'tasks','data'=>$all_tasks,'layout'=>'H','value'=>$cl_tasks));
+$form->addInput(array('type'=>'submit','name'=>'btn_add','value'=>$i18n->getKey('button.add')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.thing_name'));
+  if (!ttValidString($cl_description, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.description'));
+       
+  if ($errors->isEmpty()) {
+       if (!ttProjectHelper::getProjectByName($cl_name)) {
+      if (ttProjectHelper::insert(array(
+        'team_id' => $user->team_id,
+        'name' => $cl_name,
+        'description' => $cl_description,
+        'users' => $cl_users,
+        'tasks' => $cl_tasks,
+        'status' => ACTIVE))) {
+          header('Location: projects.php');
+          exit();
+        } else
+          $errors->add($i18n->getKey('error.db'));
+    } else
+      $errors->add($i18n->getKey('error.project_exists'));
+  }
+} // post
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.projectForm.project_name.focus()"');
+$smarty->assign('title', $i18n->getKey('title.add_project'));
+$smarty->assign('content_page_name', 'project_add.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/project_delete.php b/project_delete.php
new file mode 100644 (file)
index 0000000..c49841b
--- /dev/null
@@ -0,0 +1,70 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttProjectHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$cl_project_id = (int)$request->getParameter('id');
+$project = ttProjectHelper::get($cl_project_id);
+$project_to_delete = $project['name'];
+
+$form = new Form('projectDeleteForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_project_id));
+$form->addInput(array('type'=>'submit','name'=>'btn_delete','value'=>$i18n->getKey('label.delete')));
+$form->addInput(array('type'=>'submit','name'=>'btn_cancel','value'=>$i18n->getKey('button.cancel')));
+
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('btn_delete')) {
+       if(ttProjectHelper::get($cl_project_id)) {
+      if (ttProjectHelper::delete($cl_project_id)) {
+        header('Location: projects.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    } else
+      $errors->add($i18n->getKey('error.db'));
+  } else if ($request->getParameter('btn_cancel')) {
+       header('Location: projects.php');
+       exit();
+  }
+} // post
+       
+$smarty->assign('project_to_delete', $project_to_delete);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.projectDeleteForm.btn_cancel.focus()"');
+$smarty->assign('title', $i18n->getKey('title.delete_project'));
+$smarty->assign('content_page_name', 'project_delete.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/project_edit.php b/project_edit.php
new file mode 100644 (file)
index 0000000..2726005
--- /dev/null
@@ -0,0 +1,134 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttProjectHelper');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$cl_project_id = (int)$request->getParameter('id');
+
+$users = ttTeamHelper::getActiveUsers();
+foreach ($users as $user_item)
+  $all_users[$user_item['id']] = $user_item['name'];
+
+$tasks = ttTeamHelper::getActiveTasks($user->team_id);
+foreach ($tasks as $task_item)
+  $all_tasks[$task_item['id']] = $task_item['name'];
+
+if ($request->getMethod() == 'POST') {
+  $cl_name = trim($request->getParameter('project_name'));
+  $cl_description = trim($request->getParameter('description'));
+  $cl_status = $request->getParameter('status');
+  $cl_users = $request->getParameter('users', array());
+  $cl_tasks = $request->getParameter('tasks', array());
+} else {
+  $project = ttProjectHelper::get($cl_project_id);
+  $cl_name = $project['name'];
+  $cl_description = $project['description'];
+  $cl_status = $project['status'];
+  
+  $mdb2 = getConnection();
+  $sql = "select user_id from tt_user_project_binds where status = 1 and project_id = $cl_project_id";
+  $res = $mdb2->query($sql);
+  if (is_a($res, 'PEAR_Error'))
+    die($res->getMessage());
+  while ($row = $res->fetchRow())
+    $cl_users[] = $row['user_id'];
+    
+  $cl_tasks = explode(',', $project['tasks']);
+}
+
+$form = new Form('projectForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_project_id));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'project_name','style'=>'width: 250px;','value'=>$cl_name));
+$form->addInput(array('type'=>'textarea','name'=>'description','style'=>'width: 250px; height: 40px;','value'=>$cl_description));
+$form->addInput(array('type'=>'combobox','name'=>'status','value'=>$cl_status,
+  'data'=>array(ACTIVE=>$i18n->getKey('dropdown.status_active'),INACTIVE=>$i18n->getKey('dropdown.status_inactive'))));
+$form->addInput(array('type'=>'checkboxgroup','name'=>'users','data'=>$all_users,'layout'=>'H','value'=>$cl_users));
+if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode)
+  $form->addInput(array('type'=>'checkboxgroup','name'=>'tasks','data'=>$all_tasks,'layout'=>'H','value'=>$cl_tasks));
+$form->addInput(array('type'=>'submit','name'=>'btn_save','value'=>$i18n->getKey('button.save')));
+$form->addInput(array('type'=>'submit','name'=>'btn_copy','value'=>$i18n->getKey('button.copy')));
+  
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.thing_name'));
+  if (!ttValidString($cl_description, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.description'));
+  
+  if ($errors->isEmpty()) {
+    if ($request->getParameter('btn_save')) {
+      $existing_project = ttProjectHelper::getProjectByName($cl_name);
+      if (!$existing_project || ($cl_project_id == $existing_project['id'])) {
+         // Update project information.
+         if (ttProjectHelper::update(array(
+           'id' => $cl_project_id,
+           'name' => $cl_name,
+           'description' => $cl_description,
+           'status' => $cl_status,
+           'users' => $cl_users,
+           'tasks' => $cl_tasks))) {
+           header('Location: projects.php');
+           exit();
+        } else
+           $errors->add($i18n->getKey('error.db'));
+      } else
+        $errors->add($i18n->getKey('error.project_exists'));
+    }
+
+    if ($request->getParameter('btn_copy')) {
+      if (!ttProjectHelper::getProjectByName($cl_name)) {
+        if (ttProjectHelper::insert(array(
+          'team_id' => $user->team_id,
+          'name' => $cl_name,
+          'description' => $cl_description,
+          'users' => $cl_users,
+          'tasks' => $cl_tasks,
+          'status' => ACTIVE))) {
+          header('Location: projects.php');
+          exit();
+        } else
+          $errors->add($i18n->getKey('error.db'));
+      } else
+        $errors->add($i18n->getKey('error.project_exists'));
+    }
+  }
+} // post
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.projectForm.name.focus()"');
+$smarty->assign('title', $i18n->getKey('title.edit_project'));
+$smarty->assign('content_page_name', 'project_edit.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/projects.php b/projects.php
new file mode 100644 (file)
index 0000000..8f1ce5c
--- /dev/null
@@ -0,0 +1,50 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_data_entry)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+if($user->canManageTeam()) {
+  $active_projects = ttTeamHelper::getActiveProjects($user->team_id);
+  $inactive_projects = ttTeamHelper::getInactiveProjects($user->team_id);
+} else
+  $active_projects = $user->getAssignedProjects();
+
+$smarty->assign('active_projects', $active_projects);
+$smarty->assign('inactive_projects', $inactive_projects);
+$smarty->assign('title', $i18n->getKey('title.projects'));
+$smarty->assign('content_page_name', 'projects.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/readme.txt b/readme.txt
new file mode 100644 (file)
index 0000000..437d4c3
--- /dev/null
@@ -0,0 +1,63 @@
+Anuko Time Tracker.
+Copyright (c) Anuko (https://www.anuko.com).
+
+Project home page: https://www.anuko.com/time_tracker/index.htm
+Free hosting of Time Tracker for individuals and small teams is available at https://timetracker.anuko.com
+
+Each file in this archive is protected by the LIBERAL FREEWARE LICENSE. 
+Read the file license.txt for details.
+
+
+INSTALLATION INSTRUCTIONS
+
+Detailed documentation is available at https://www.anuko.com/time_tracker/install_guide/index.htm
+
+The general installation procedure looks like this:
+
+- Install a web server and make sure it can serve HTML documents.
+- Install PHP, configure your server to work with PHP scripts, and make sure it can work with PHP files. 
+- Install the following PHP extensions: MySQL and GD. The GD extension is needed for pie-charts only.
+- Install a database server such as MySQL and make sure it is working properly. 
+- Install, configure, and test Anuko Time Tracker like so: 
+
+1) Unpack distribution files into a selected directory for Apache web server.
+2) Create a database using the mysql.sql file in the distribution.
+3) If you are upgrading from earlier versions run dbinstall.php from your browser and do the required "Update database structure" steps.
+4) Create user name and password to access the time tracker database. 
+5) Change $dsn value in /WEB-INF/config.php file to reflect your database connection parameters (user name and password).
+6) For UNIX systems set full access rights for catalog WEB-INF/templates_c/ (chmod 777 templates_c).
+7) If you install time tracker into a sub-directory of your site reflect this in the APP_NAME parameter in /WEB-INF/config.php file. For example, for http://localhost/timetracker/ set APP_NAME = "timetracker".
+8) Login to your time tracker site as admin with password "secret" without quotes and create at least one team.
+9) Change admin password (on the admin "options" page). You can also use the following SQL console command: 
+  update tt_users set password = md5('new_password_here') where login='admin'
+  or by using the "Change password of administrator account" option in http://your_time_tracker_site/dbinstall.php
+10) Test if everything is working.
+11) Remove dbinstall.php file from your installation directory.
+
+
+UPGRADE FROM EARLIER VERSIONS
+
+See https://www.anuko.com/time_tracker/upgrade.htm
+
+
+BLANK PAGES IN TIME TRACKER
+
+If you see a blank page in when trying to access Anuko Time Tracker it may mean many things, among others, such as:
+
+    * MySQL extension for PHP not installed or not working.
+    * Time tracker database not created.
+    * Access (login / password) to the database is not configured properly in config.php.
+    * MySQL service is down. 
+    * On UNIX systems - no full access rights for catalog WEB-INF/templates_c/ (chmod 777 templates_c).
+
+You need to thoroughly test each and every component to make sure they work together nicely.
+
+
+INSTALLATION / UPGRADES / DATA MIGRATION HELP
+
+Support is available on per-incident basis - see https://www.anuko.com/support.htm
+
+
+CHANGE LOG
+
+Change log is available at https://www.anuko.com/time_tracker/change_log/index.htm
\ No newline at end of file
diff --git a/register.php b/register.php
new file mode 100644 (file)
index 0000000..67f7d10
--- /dev/null
@@ -0,0 +1,110 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttUserHelper');
+
+if (!isTrue(MULTITEAM_MODE) || $auth->isPasswordExternal()) {
+  header('Location: login.php');
+  exit();
+}
+
+$auth->doLogout();
+
+if (!defined('CURRENCY_DEFAULT')) define('CURRENCY_DEFAULT', '$');
+
+if ($request->getMethod() == 'POST') {
+  $cl_team_name = trim($request->getParameter('team_name'));
+  $cl_currency = trim($request->getParameter('currency'));
+  if (!$cl_currency) $cl_currency = CURRENCY_DEFAULT;
+  $cl_manager_name = trim($request->getParameter('manager_name'));
+  $cl_manager_login = trim($request->getParameter('manager_login'));
+  $cl_password1 = $request->getParameter('password1');
+  $cl_password2 = $request->getParameter('password2');
+  $cl_manager_email = trim($request->getParameter('manager_email'));
+} else  
+  $cl_currency = CURRENCY_DEFAULT;
+
+$form = new Form('profileForm');
+$form->addInput(array('type'=>'text','maxlength'=>'200','name'=>'team_name','value'=>$cl_team_name));
+$form->addInput(array('type'=>'text','maxlength'=>'7','name'=>'currency','value'=>$cl_currency));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'manager_name','value'=>$cl_manager_name));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'manager_login','value'=>$cl_manager_login));
+$form->addInput(array('type'=>'text','maxlength'=>'30','name'=>'password1','aspassword'=>true,'value'=>$cl_password1));
+$form->addInput(array('type'=>'text','maxlength'=>'30','name'=>'password2','aspassword'=>true,'value'=>$cl_password2));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'manager_email','value'=>$cl_manager_email));
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.submit')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_team_name, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.team_name'));
+  if (!ttValidString($cl_currency, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.currency'));
+  if (!ttValidString($cl_manager_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.manager_name'));
+  if (!ttValidString($cl_manager_login)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.manager_login'));
+  if (!ttValidString($cl_password1)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.password'));
+  if (!ttValidString($cl_password2)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.confirm_password'));
+  if ($cl_password1 !== $cl_password2)
+    $errors->add($i18n->getKey('error.not_equal'), $i18n->getKey('label.password'), $i18n->getKey('label.confirm_password'));
+  if (!ttValidEmail($cl_manager_email, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.email'));
+
+  if ($errors->isEmpty()) {
+    if (!ttUserHelper::getUserByLogin($cl_manager_login)) {
+      // Create a new team.
+      $team_id = ttTeamHelper::insert(array('name'=>$cl_team_name,'currency'=>$cl_currency));
+      if ($team_id) {
+       // Team created, now create a team manager.
+       $user_id = ttUserHelper::insert(array(
+         'team_id' => $team_id,
+         'role' => ROLE_MANAGER,
+          'name' => $cl_manager_name,
+          'login' => $cl_manager_login,
+          'password' => $cl_password1,
+         'email' => $cl_manager_email));
+      }
+      if ($team_id && $user_id) {
+        if ($auth->doLogin($cl_manager_login, $cl_password1)) {
+          setcookie('tt_login', $cl_manager_login, time() + COOKIE_EXPIRE, '/');
+          header('Location: time.php');
+        } else {
+          header('Location: login.php');
+        }
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    } else
+      $errors->add($i18n->getKey('error.user_exists'));
+  }
+}
+
+$smarty->assign('title', $i18n->getKey('title.create_team'));
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.profileForm.team.focus()"');
+$smarty->assign('content_page_name', 'register.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/report.php b/report.php
new file mode 100644 (file)
index 0000000..5ed7108
--- /dev/null
@@ -0,0 +1,123 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('form.ActionForm');
+import('ttReportHelper');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_view_reports)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Use custom fields plugin if it is enabled.
+if (in_array('cf', explode(',', $user->plugins))) {
+  require_once('plugins/CustomFields.class.php');
+  $custom_fields = new CustomFields($user->team_id);
+  $smarty->assign('custom_fields', $custom_fields);
+}
+
+$form = new Form('reportForm');
+
+// Report settings are stored in session bean before we get here from reports.php.
+$bean = new ActionForm('reportBean', $form, $request);
+$client_id = $bean->getAttribute('client');
+if ($client_id && $bean->getAttribute('chinvoice') && ('no_grouping' == $bean->getAttribute('group_by')) && !$user->isClient()) {
+  // Client is selected and we are displaying the invoice column.
+  $recent_invoices = ttTeamHelper::getRecentInvoices($user->team_id, $client_id);
+  if ($recent_invoices) {
+    $form->addInput(array('type'=>'combobox',
+      'name'=>'recent_invoice',
+      'data'=>$recent_invoices,
+      'datakeys'=>array('id','name'),
+      'empty'=>array(''=>$i18n->getKey('dropdown.select_invoice'))));
+    $form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.submit')));
+    $smarty->assign('use_checkboxes', true);
+  }
+}
+
+if ($request->getMethod() == 'POST') {
+  foreach($_POST as $key => $val) {
+       if ('log_id_' == substr($key, 0, 7))
+         $time_log_ids[] = substr($key, 7);
+    if ('item_id_' == substr($key, 0, 8))
+         $expense_item_ids[] = substr($key, 8);
+       if ('recent_invoice' == $key)
+         $invoice_id = $val;
+  }
+  if ($time_log_ids || $expense_item_ids) {
+       // Some records are checked for invoice editing... Adjust their invoice accordingly.
+       ttReportHelper::assignToInvoice($invoice_id, $time_log_ids, $expense_item_ids);
+  }
+  // Re-display this form.
+  header('Location: report.php');
+  exit();
+} // post
+
+$group_by = $bean->getAttribute('group_by');
+
+$report_items = ttReportHelper::getItems($bean);
+if ('no_grouping' != $group_by)
+  $subtotals = ttReportHelper::getSubtotals($bean);
+$totals = ttReportHelper::getTotals($bean);
+
+// Assign variables that are used to print subtotals.
+if ($report_items && 'no_grouping' != $group_by) {
+  $smarty->assign('print_subtotals', true);
+  $smarty->assign('first_pass', true);
+  $smarty->assign('group_by', $group_by);
+  $smarty->assign('prev_grouped_by', '');
+  $smarty->assign('cur_grouped_by', '');
+}
+// Determine group by header.
+if ('no_grouping' != $group_by) {
+  if ('cf_1' == $group_by)
+    $smarty->assign('group_by_header', $custom_fields->fields[0]['label']);
+  else {
+    $key = 'label.'.$group_by;
+    $smarty->assign('group_by_header', $i18n->getKey($key));
+  }
+}
+// Assign variables that are used to alternate color of rows for different dates.
+$smarty->assign('prev_date', '');
+$smarty->assign('cur_date', '');
+$smarty->assign('report_row_class', 'rowReportItem');
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+
+$smarty->assign('report_items', $report_items);
+$smarty->assign('subtotals', $subtotals);
+$smarty->assign('totals', $totals);
+$smarty->assign('bean', $bean);
+$smarty->assign('title', $i18n->getKey('title.report').": ".$totals['start_date']." - ".$totals['end_date']);
+$smarty->assign('content_page_name', 'report.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/report_send.php b/report_send.php
new file mode 100644 (file)
index 0000000..18e731e
--- /dev/null
@@ -0,0 +1,99 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('form.ActionForm');
+import('ttSysConfig');
+import('ttReportHelper');
+
+// Access check.
+if (!ttAccessCheck(right_view_reports)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$sc = new ttSysConfig($user->id);
+
+if ($request->getMethod() == 'POST') {
+  $cl_receiver = trim($request->getParameter('receiver'));
+  $cl_cc = trim($request->getParameter('cc'));
+  $cl_subject = trim($request->getParameter('subject'));
+  $cl_comment = trim($request->getParameter('comment'));
+} else {
+  $cl_receiver = $sc->getValue(SYSC_LAST_REPORT_EMAIL);
+  $cl_cc = $sc->getValue(SYSC_LAST_REPORT_CC);
+  $cl_subject = $i18n->getKey('form.mail.report_subject');
+}
+
+$form = new Form('mailForm');
+$form->addInput(array('type'=>'text','name'=>'receiver','style'=>'width: 300px;','value'=>$cl_receiver));
+$form->addInput(array('type'=>'text','name'=>'cc','style'=>'width: 300px;','value'=>$cl_cc));
+$form->addInput(array('type'=>'text','name'=>'subject','style'=>'width: 300px;','value'=>$cl_subject));
+$form->addInput(array('type'=>'textarea','name'=>'comment','maxlength'=>'250','style'=>'width: 300px; height: 60px;'));
+$form->addInput(array('type'=>'submit','name'=>'btn_send','value'=>$i18n->getKey('button.send')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidEmailList($cl_receiver)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.mail.to'));
+  if (!ttValidEmailList($cl_cc, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.mail.cc'));
+  if (!ttValidString($cl_subject)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.mail.subject'));
+  if (!ttValidString($cl_comment, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.comment'));
+  
+  if ($errors->isEmpty()) {
+    // Save last report emails for future use.
+    $sc->setValue(SYSC_LAST_REPORT_EMAIL, $cl_receiver);
+       $sc->setValue(SYSC_LAST_REPORT_CC, $cl_cc);
+       
+       // Obtain session bean with report attributes.
+       $bean = new ActionForm('reportBean', new Form('reportForm'));
+       // Prepare report body.
+       $body = ttReportHelper::prepareReportBody($bean, $cl_comment);
+               
+    import('mail.Mailer');
+    $mailer = new Mailer();
+    $mailer->setCharSet(CHARSET);
+    $mailer->setContentType('text/html');
+    $mailer->setSender(SENDER);
+    $mailer->setReceiver($cl_receiver);
+    if (isset($cl_cc))
+      $mailer->setReceiverCC($cl_cc);
+    $mailer->setSendType(MAIL_MODE);
+    if ($mailer->send($cl_subject, $body))
+      $messages->add($i18n->getKey('form.mail.report_sent'));
+    else
+      $errors->add($i18n->getKey('error.mail_send'));
+  }
+}
+
+$smarty->assign('title', $i18n->getKey('title.send_report'));
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.mailForm.'.($cl_receiver?'comment':'receiver').'.focus()"');
+$smarty->assign('content_page_name', 'mail.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/reports.php b/reports.php
new file mode 100644 (file)
index 0000000..9e18a0b
--- /dev/null
@@ -0,0 +1,334 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('form.ActionForm');
+import('DateAndTime');
+import('ttTeamHelper');
+import('Period');
+import('ttProjectHelper');
+import('ttFavReportHelper');
+import('ttClientHelper');
+
+// Access check.
+if (!ttAccessCheck(right_view_reports)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Use custom fields plugin if it is enabled.
+if (in_array('cf', explode(',', $user->plugins))) {
+  require_once('plugins/CustomFields.class.php');
+  $custom_fields = new CustomFields($user->team_id);
+  $smarty->assign('custom_fields', $custom_fields);
+}
+
+$form = new Form('reportForm');
+
+// Get saved favorite reports for user.
+$report_list = ttFavReportHelper::getReports($user->id);
+$form->addInput(array('type'=>'combobox',
+  'name'=>'favorite_report',
+  'onchange'=>'document.reportForm.fav_report_changed.value=1;document.reportForm.submit();',
+  'style'=>'width: 250px;',
+  'data'=>$report_list,
+  'datakeys'=>array('id','name'),
+  'empty'=>array('-1'=>$i18n->getKey('dropdown.no'))
+));
+$form->addInput(array('type'=>'hidden','name'=>'fav_report_changed'));
+// Generate and Delete buttons.
+$form->addInput(array('type'=>'submit','name'=>'btn_generate','value'=>$i18n->getKey('button.generate')));
+$form->addInput(array('type'=>'submit','name'=>'btn_delete','value'=>$i18n->getKey('label.delete'),'onclick'=>"return confirm('".$i18n->getKey('form.reports.confirm_delete')."')"));
+
+// Dropdown for clients if the clients plugin is enabled.
+if (in_array('cl', explode(',', $user->plugins)) && !($user->isClient() && $user->client_id)) {
+  if ($user->canManageTeam() || ($user->isClient() && !$user->client_id))
+    $client_list = ttClientHelper::getClients($user->team_id);
+  else
+    $client_list = ttClientHelper::getClientsForUser();
+  $form->addInput(array('type'=>'combobox',
+    'name'=>'client',
+    'style'=>'width: 250px;',
+    'data'=>$client_list,
+    'datakeys'=>array('id', 'name'),
+    'empty'=>array(''=>$i18n->getKey('dropdown.all'))
+  ));
+}
+
+// If we have a TYPE_DROPDOWN custom field - add control to select an option.
+if ($custom_fields && $custom_fields->fields[0] && $custom_fields->fields[0]['type'] == CustomFields::TYPE_DROPDOWN) {
+    $form->addInput(array('type'=>'combobox','name'=>'option',
+      'style'=>'width: 250px;',
+      'value'=>$cl_cf_1,
+      'data'=>$custom_fields->options,
+      'empty'=>array(''=>$i18n->getKey('dropdown.all'))
+    ));
+}
+
+// Add controls for projects and tasks.
+if ($user->canManageTeam()) {
+  $project_list = ttProjectHelper::getProjects(); // Manager and co-managers can run reports on all active and inactive projects.
+} else if ($user->isClient()) {
+  $project_list = ttProjectHelper::getProjectsForClient();
+} else {
+  $project_list = ttProjectHelper::getAssignedProjects($user->id);     
+}
+$form->addInput(array('type'=>'combobox',
+  'onchange'=>'fillTaskDropdown(this.value);selectAssignedUsers(this.value);',
+  'name'=>'project',
+  'style'=>'width: 250px;',
+  'data'=>$project_list,
+  'datakeys'=>array('id','name'),
+  'empty'=>array(''=>$i18n->getKey('dropdown.all'))
+));
+if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+  $task_list = ttTeamHelper::getActiveTasks($user->team_id);
+  $form->addInput(array('type'=>'combobox',
+    'name'=>'task',
+    'style'=>'width: 250px;',
+    'data'=>$task_list,
+    'datakeys'=>array('id','name'),
+    'empty'=>array(''=>$i18n->getKey('dropdown.all'))
+  ));
+}
+
+// Add include records control.  
+$include_options = array('1'=>$i18n->getKey('form.reports.include_billable'),
+  '2'=>$i18n->getKey('form.reports.include_not_billable'));
+$form->addInput(array('type'=>'combobox',
+  'name'=>'include_records',
+  'style'=>'width: 250px;',
+  'data'=>$include_options,
+  'empty'=>array(''=>$i18n->getKey('dropdown.all'))
+));
+
+// Add invoiced / not invoiced selector.  
+$invoice_options = array('1'=>$i18n->getKey('form.reports.include_invoiced'),
+  '2'=>$i18n->getKey('form.reports.include_not_invoiced'));
+$form->addInput(array('type'=>'combobox',
+  'name'=>'invoice',
+  'style'=>'width: 250px;',
+  'data'=>$invoice_options,
+  'empty'=>array(''=>$i18n->getKey('dropdown.all'))
+));
+
+$user_list = array();
+if ($user->canManageTeam() || $user->isClient()) {
+  // Prepare user and assigned projects arrays.
+  if ($user->canManageTeam())
+    $users = ttTeamHelper::getUsers(); // Active and inactive users for managers.
+  else if ($user->isClient())
+    $users = ttTeamHelper::getUsersForClient(); // Active and inactive users for clients.
+
+  foreach ($users as $single_user) {
+    $user_list[$single_user['id']] = $single_user['name'];
+    $projects = ttProjectHelper::getAssignedProjects($single_user['id']);
+    if ($projects) {
+      foreach ($projects as $single_project) {
+        $assigned_projects[$single_user['id']][] = $single_project['id'];
+      }
+    }
+  }
+  $row_count = ceil(count($user_list)/3);
+  $form->addInput(array('type'=>'checkboxgroup',
+    'name'=>'users',
+    'data'=>$user_list,
+    'layout'=>'V',
+    'groupin'=>$row_count,
+    'style'=>'width: 100%;'
+  ));
+}
+
+// Add control for time period.
+$form->addInput(array('type'=>'combobox',
+  'name'=>'period',
+  'style'=>'width: 250px;',
+  'data'=>array(INTERVAL_THIS_MONTH=>$i18n->getKey('dropdown.this_month'),
+    INTERVAL_LAST_MONTH=>$i18n->getKey('dropdown.last_month'),
+    INTERVAL_THIS_WEEK=>$i18n->getKey('dropdown.this_week'),
+    INTERVAL_LAST_WEEK=>$i18n->getKey('dropdown.last_week')),
+  'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+));
+// Add controls for start and end dates.
+$form->addInput(array('type'=>'datefield','maxlength'=>'20','name'=>'start_date'));
+$form->addInput(array('type'=>'datefield','maxlength'=>'20','name'=>'end_date'));
+
+// Add checkboxes for fields.
+if (in_array('cl', explode(',', $user->plugins)))
+  $form->addInput(array('type'=>'checkbox','name'=>'chclient','data'=>1));     
+if (($user->canManageTeam() || $user->isClient()) && in_array('iv', explode(',', $user->plugins)))
+  $form->addInput(array('type'=>'checkbox','name'=>'chinvoice','data'=>1));    
+if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode)
+  $form->addInput(array('type'=>'checkbox','name'=>'chproject','data'=>1));
+if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode)
+  $form->addInput(array('type'=>'checkbox','name'=>'chtask','data'=>1));
+if ((TYPE_START_FINISH == $user->record_type) || (TYPE_ALL == $user->record_type)) {
+  $form->addInput(array('type'=>'checkbox','name'=>'chstart','data'=>1));
+  $form->addInput(array('type'=>'checkbox','name'=>'chfinish','data'=>1));
+}
+$form->addInput(array('type'=>'checkbox','name'=>'chduration','data'=>1));
+$form->addInput(array('type'=>'checkbox','name'=>'chnote','data'=>1));
+if (defined('COST_ON_REPORTS') && isTrue(COST_ON_REPORTS))
+  $form->addInput(array('type'=>'checkbox','name'=>'chcost','data'=>1));
+// If we have a custom field - add a checkbox for it.
+if ($custom_fields && $custom_fields->fields[0])
+  $form->addInput(array('type'=>'checkbox','name'=>'chcf_1','data'=>1));
+
+// Add group by control.
+$group_by_options['no_grouping'] = $i18n->getKey('form.reports.group_by_no');
+$group_by_options['date'] = $i18n->getKey('form.reports.group_by_date');
+if ($user->canManageTeam() || $user->isClient())
+  $group_by_options['user'] = $i18n->getKey('form.reports.group_by_user');
+if (in_array('cl', explode(',', $user->plugins)) && !($user->isClient() && $user->client_id))
+  $group_by_options['client'] = $i18n->getKey('form.reports.group_by_client'); 
+if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode)
+  $group_by_options['project'] = $i18n->getKey('form.reports.group_by_project');
+if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode)
+  $group_by_options['task'] = $i18n->getKey('form.reports.group_by_task');
+if ($custom_fields && $custom_fields->fields[0] && $custom_fields->fields[0]['type'] == CustomFields::TYPE_DROPDOWN) {
+  $group_by_options['cf_1'] = $custom_fields->fields[0]['label'];
+}
+$form->addInput(array('type'=>'combobox','onchange'=>'handleCheckboxes();','name'=>'group_by','data'=>$group_by_options));
+$form->addInput(array('type'=>'checkbox','name'=>'chtotalsonly','data'=>1));
+
+// Add text field for a new favorite report name.
+$form->addInput(array('type'=>'text','name'=>'new_fav_report','maxlength'=>'30','style'=>'width: 250px;'));
+// Save button.
+$form->addInput(array('type'=>'submit','name'=>'btn_save','value'=>$i18n->getKey('button.save')));
+  
+$form->addInput(array('type'=>'submit','name'=>'btn_generate','value'=>$i18n->getKey('button.generate')));
+
+// Create a bean (which is a mechanism to remember form values in session).
+$bean = new ActionForm('reportBean', $form, $request);
+// At this point form values are obtained from session if they are there...
+
+if (($request->getMethod() == 'GET') && !$bean->isSaved()) {
+  // No previous form data were found in session. Use the following default values.
+  $form->setValueByElement('users', array_keys($user_list));
+  $period = new Period(INTERVAL_THIS_MONTH, new DateAndTime($user->date_format));
+  $form->setValueByElement('start_date', $period->getBeginDate());
+  $form->setValueByElement('end_date', $period->getEndDate());
+  $form->setValueByElement('chclient', 1);
+  $form->setValueByElement('chinvoice', 0);
+  $form->setValueByElement('chproject', 1);
+  $form->setValueByElement('chstart', 1);  
+  $form->setValueByElement('chduration', 1);
+  $form->setValueByElement('chcost', 0);  
+  $form->setValueByElement('chtask', 1);
+  $form->setValueByElement('chfinish', 1);
+  $form->setValueByElement('chnote', 1);
+  $form->setValueByElement('chcf_1', 0);
+  $form->setValueByElement('chtotalsonly', 0);
+}
+
+$form->setValueByElement('fav_report_changed','');
+
+// Disable the Delete button when no favorite report is selected.
+if (!$bean->getAttribute('favorite_report') || ($bean->getAttribute('favorite_report') == -1))
+  $form->getElement('btn_delete')->setEnable(false);
+
+if ($request->getMethod() == 'POST') {
+  if((!$bean->getAttribute('btn_generate') && ($request->getParameter('fav_report_changed')))) {
+    // User changed favorite report. We need to load new values into the form.
+    if ($bean->getAttribute('favorite_report')) {
+      // This loads new favorite report options into the bean (into our form).
+      ttFavReportHelper::loadReport($user->id, $bean);
+      
+      // If user selected no favorite report - mark all user checkboxes (most probable scenario).
+      if ($bean->getAttribute('favorite_report') == -1)
+        $form->setValueByElement('users', array_keys($user_list));
+       
+      // Save form data in session for future use.
+      $bean->saveBean();
+      header('Location: reports.php');
+         exit();
+       }
+  } elseif ($bean->getAttribute('btn_save')) {
+       // User clicked the Save button. We need to save form options as new favorite report.
+       if (!ttValidString($bean->getAttribute('new_fav_report'))) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.reports.save_as_favorite'));
+       
+    if ($errors->isEmpty()) {
+      $id = ttFavReportHelper::saveReport($user->id, $bean);
+      if (!$id)
+        $errors->add($i18n->getKey('error.db'));
+      if ($errors->isEmpty()) {
+        $bean->setAttribute('favorite_report', $id);
+        $bean->saveBean();
+        header('Location: reports.php');
+        exit();
+      }
+    }
+  } elseif($bean->getAttribute('btn_delete')) {
+    // Delete button pressed. User wants to delete a favorite report.
+    if ($bean->getAttribute('favorite_report')) {
+      ttFavReportHelper::deleteReport($bean->getAttribute('favorite_report'));
+      // Load default report.
+      $bean->setAttribute('favorite_report','');
+      $bean->setAttribute('new_fav_report', $report_list[0]['name']);
+      ttFavReportHelper::loadReport($user->id, $bean);
+      $form->setValueByElement('users', array_keys($user_list));
+      $bean->saveBean();
+      header('Location: reports.php');
+         exit();
+       }
+  } else {
+    // Generate button pressed. Check some values.
+    if (!$bean->getAttribute('period')) {
+      $start_date = new DateAndTime($user->date_format, $bean->getAttribute('start_date'));
+      
+      if ($start_date->isError() || !$bean->getAttribute('start_date'))
+        $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.start_date'));
+        
+      $end_date = new DateAndTime($user->date_format, $bean->getAttribute('end_date'));
+      if ($end_date->isError() || !$bean->getAttribute('end_date'))
+        $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.end_date'));
+        
+      if ($start_date->compare($end_date) > 0)
+        $errors->add($i18n->getKey('error.interval'), $i18n->getKey('label.end_date'), $i18n->getKey('label.start_date'));
+    }
+    
+    $bean->saveBean();
+    
+    if ($errors->isEmpty()) {
+      // Now we can go ahead and create a report.
+      header('Location: report.php');
+      exit();
+    }
+  }
+}
+
+$smarty->assign('project_list', $project_list);
+$smarty->assign('task_list', $task_list);
+$smarty->assign('assigned_projects', $assigned_projects);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="handleCheckboxes()"');
+$smarty->assign('title', $i18n->getKey('title.reports'));
+$smarty->assign('content_page_name', 'reports.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/rtl.css b/rtl.css
new file mode 100644 (file)
index 0000000..ede511c
--- /dev/null
+++ b/rtl.css
@@ -0,0 +1,5 @@
+/* css definitions for rtl languages */
+
+html {
+  direction: rtl;
+}
\ No newline at end of file
diff --git a/task_add.php b/task_add.php
new file mode 100644 (file)
index 0000000..b37f106
--- /dev/null
@@ -0,0 +1,85 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('form.ActionForm');
+import('ttTeamHelper');
+import('ttTaskHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$projects = ttTeamHelper::getActiveProjects($user->team_id);
+
+if ($request->getMethod() == 'POST') {
+  $cl_name = trim($request->getParameter('name'));
+  $cl_description = trim($request->getParameter('description'));
+  $cl_projects = $request->getParameter('projects');
+} else {
+  foreach ($projects as $project_item)
+    $cl_projects[] = $project_item['id'];
+}
+
+$form = new Form('taskForm');
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'name','style'=>'width: 250px;','value'=>$cl_name));
+$form->addInput(array('type'=>'textarea','name'=>'description','style'=>'width: 250px; height: 40px;','value'=>$cl_description));
+$form->addInput(array('type'=>'checkboxgroup','name'=>'projects','layout'=>'H','data'=>$projects,'datakeys'=>array('id','name'),'value'=>$cl_projects));
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.add')));
+       
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.thing_name'));
+  if (!ttValidString($cl_description, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.description'));
+
+  if ($errors->isEmpty()) {
+       if (!ttTaskHelper::getTaskByName($cl_name)) {
+      if (ttTaskHelper::insert(array(
+        'team_id' => $user->team_id,
+        'name' => $cl_name,
+        'description' => $cl_description,
+        'status' => ACTIVE,
+        'projects' => $cl_projects))) {
+          header('Location: tasks.php');
+          exit();
+        } else
+          $errors->add($i18n->getKey('error.db'));
+    } else
+      $errors->add($i18n->getKey('error.task_exists'));
+  }                    
+} // post
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.taskForm.name.focus()"');
+$smarty->assign('title', $i18n->getKey('title.add_task'));
+$smarty->assign('content_page_name', 'task_add.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/task_delete.php b/task_delete.php
new file mode 100644 (file)
index 0000000..3329264
--- /dev/null
@@ -0,0 +1,70 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('ttTaskHelper');
+import('form.Form');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$cl_task_id = (int)$request->getParameter('id');
+$task = ttTaskHelper::getTask($cl_task_id);
+$task_to_delete = $task['name'];
+
+$form = new Form('taskDeleteForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_task_id));
+$form->addInput(array('type'=>'submit','name'=>'btn_delete','value'=>$i18n->getKey('label.delete')));
+$form->addInput(array('type'=>'submit','name'=>'btn_cancel','value'=>$i18n->getKey('button.cancel')));
+
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('btn_delete')) {
+    if(ttTaskHelper::getTask($cl_task_id)) {
+      if (ttTaskHelper::delete($cl_task_id)) {
+        header('Location: tasks.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    } else
+      $errors->add($i18n->getKey('error.db'));
+  } else if ($request->getParameter('btn_cancel')) {
+    header('Location: tasks.php');
+    exit();
+  }
+} // post
+
+$smarty->assign('task_to_delete', $task_to_delete);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.taskDeleteForm.btn_cancel.focus()"');
+$smarty->assign('title', $i18n->getKey('title.delete_task'));
+$smarty->assign('content_page_name', 'task_delete.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/task_edit.php b/task_edit.php
new file mode 100644 (file)
index 0000000..3a03d42
--- /dev/null
@@ -0,0 +1,115 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+import('ttTaskHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$cl_task_id = (int)$request->getParameter('id');
+$projects = ttTeamHelper::getActiveProjects($user->team_id);
+       
+if ($request->getMethod() == 'POST') {
+  $cl_name = trim($request->getParameter('name'));
+  $cl_description = trim($request->getParameter('description'));
+  $cl_status = $request->getParameter('status');
+  $cl_projects = $request->getParameter('projects');
+} else {
+  $task = ttTaskHelper::getTask($cl_task_id);
+  $cl_name = $task['name'];
+  $cl_description = $task['description'];
+  $cl_status = $task['status'];
+  
+  $assigned_projects = ttTaskHelper::getAssignedProjects($cl_task_id);
+  foreach ($assigned_projects as $project_item)
+    $cl_projects[] = $project_item['id'];
+}
+
+$form = new Form('taskForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_task_id));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'name','style'=>'width: 250px;','value'=>$cl_name));
+$form->addInput(array('type'=>'textarea','name'=>'description','style'=>'width: 250px; height: 40px;','value'=>$cl_description));
+$form->addInput(array('type'=>'combobox','name'=>'status','value'=>$cl_status,
+  'data'=>array(ACTIVE=>$i18n->getKey('dropdown.status_active'),INACTIVE=>$i18n->getKey('dropdown.status_inactive'))));
+$form->addInput(array('type'=>'checkboxgroup','name'=>'projects','layout'=>'H','data'=>$projects,'datakeys'=>array('id','name'),'value'=>$cl_projects));
+$form->addInput(array('type'=>'submit','name'=>'btn_save','value'=>$i18n->getKey('button.save')));
+$form->addInput(array('type'=>'submit','name'=>'btn_copy','value'=>$i18n->getKey('button.copy')));
+       
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.thing_name'));
+  if (!ttValidString($cl_description, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.description'));
+
+  if ($errors->isEmpty()) {
+       if ($request->getParameter('btn_save')) {
+      $existing_task = ttTaskHelper::getTaskByName($cl_name);
+      if (!$existing_task || ($cl_task_id == $existing_task['id'])) {
+        // Update task information.
+        if (ttTaskHelper::update(array(
+          'task_id' => $cl_task_id,
+          'name' => $cl_name,
+          'description' => $cl_description,
+          'status' => $cl_status,
+          'projects' => $cl_projects))) {
+          header('Location: tasks.php');
+          exit();
+        } else
+          $errors->add($i18n->getKey('error.db'));
+      } else
+        $errors->add($i18n->getKey('error.task_exists'));
+       }
+       
+    if ($request->getParameter('btn_copy')) {
+      if (!ttTaskHelper::getTaskByName($cl_name)) {
+        if (ttTaskHelper::insert(array(
+          'team_id' => $user->team_id,
+          'name' => $cl_name,
+          'description' => $cl_description,
+          'status' => $cl_status,
+          'projects' => $cl_projects))) {
+          header('Location: tasks.php');
+          exit();
+        } else
+          $errors->add($i18n->getKey('error.db'));
+      } else
+        $errors->add($i18n->getKey('error.task_exists'));
+    }
+  }                    
+} // post
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.edit_task'));
+$smarty->assign('content_page_name', 'task_edit.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/tasks.php b/tasks.php
new file mode 100644 (file)
index 0000000..6258831
--- /dev/null
+++ b/tasks.php
@@ -0,0 +1,44 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+$smarty->assign('active_tasks', ttTeamHelper::getActiveTasks($user->team_id));
+$smarty->assign('inactive_tasks', ttTeamHelper::getInactiveTasks($user->team_id));
+$smarty->assign('title', $i18n->getKey('title.tasks'));
+$smarty->assign('content_page_name', 'tasks.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/time.php b/time.php
new file mode 100644 (file)
index 0000000..a615112
--- /dev/null
+++ b/time.php
@@ -0,0 +1,374 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttUserHelper');
+import('ttTeamHelper');
+import('ttClientHelper');
+import('ttTimeHelper');
+import('DateAndTime');
+
+// This is a now removed check whether user browser supports cookies.
+// if (!isset($_COOKIE['tt_PHPSESSID'])) {
+  // This test gives a false-positive if user goes directly to this page
+  // as from a desktop shortcut (on first request only).
+  // die ("Your browser's cookie functionality is turned off. Please turn it on.");
+// }
+
+// Access check.
+if (!ttAccessCheck(right_data_entry)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Initialize and store date in session.
+$cl_date = $request->getParameter('date', @$_SESSION['date']);
+$selected_date = new DateAndTime(DB_DATEFORMAT, $cl_date);
+if($selected_date->isError())
+  $selected_date = new DateAndTime(DB_DATEFORMAT);
+if(!$cl_date)
+  $cl_date = $selected_date->toString(DB_DATEFORMAT);
+$_SESSION['date'] = $cl_date;
+
+// Use custom fields plugin if it is enabled.
+if (in_array('cf', explode(',', $user->plugins))) {
+  require_once('plugins/CustomFields.class.php');
+  $custom_fields = new CustomFields($user->team_id);
+  $smarty->assign('custom_fields', $custom_fields);
+}
+
+// Initialize variables.
+$cl_start = trim($request->getParameter('start'));
+$cl_finish = trim($request->getParameter('finish'));
+$cl_duration = trim($request->getParameter('duration'));
+$cl_note = trim($request->getParameter('note'));
+// Custom field.
+$cl_cf_1 = trim($request->getParameter(('cf_1'), ($request->getMethod()=='POST'? null : @$_SESSION['cf_1'])));
+$_SESSION['cf_1'] = $cl_cf_1;
+$cl_billable = 1;
+if (in_array('iv', explode(',', $user->plugins))) {
+  if ($request->getMethod() == 'POST') {
+    $cl_billable = $request->getParameter('billable');
+    $_SESSION['billable'] = (int) $cl_billable;
+  } else 
+    if (isset($_SESSION['billable']))
+      $cl_billable = $_SESSION['billable'];
+}
+$on_behalf_id = $request->getParameter('onBehalfUser', (isset($_SESSION['behalf_id'])? $_SESSION['behalf_id'] : $user->id));
+$cl_client = $request->getParameter('client', ($request->getMethod()=='POST'? null : @$_SESSION['client']));
+$_SESSION['client'] = $cl_client;
+$cl_project = $request->getParameter('project', ($request->getMethod()=='POST'? null : @$_SESSION['project']));
+$_SESSION['project'] = $cl_project;
+$cl_task = $request->getParameter('task', ($request->getMethod()=='POST'? null : @$_SESSION['task']));
+$_SESSION['task'] = $cl_task;
+
+// Elements of timeRecordForm.
+$form = new Form('timeRecordForm');
+
+if ($user->canManageTeam()) {
+  $user_list = ttTeamHelper::getActiveUsers(array('putSelfFirst'=>true));
+  if (count($user_list) > 1) {
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'this.form.submit();',
+      'name'=>'onBehalfUser',
+      'style'=>'width: 250px;',
+      'value'=>$on_behalf_id,
+      'data'=>$user_list,
+      'datakeys'=>array('id','name'),
+    ));
+    $smarty->assign('on_behalf_control', 1);
+  }
+}
+
+// Dropdown for clients in MODE_TIME. Use all active clients.
+if (MODE_TIME == $user->tracking_mode && in_array('cl', explode(',', $user->plugins))) {
+    $active_clients = ttTeamHelper::getActiveClients($user->team_id, true);
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'fillProjectDropdown(this.value);',
+      'name'=>'client',
+      'style'=>'width: 250px;',
+      'value'=>$cl_client,
+      'data'=>$active_clients,
+      'datakeys'=>array('id', 'name'),
+      'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+    ));
+  // Note: in other modes the client list is filtered to relevant clients only. See below.
+}
+
+if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+  // Dropdown for projects assigned to user.
+  $project_list = $user->getAssignedProjects();
+  $form->addInput(array('type'=>'combobox',
+    'onchange'=>'fillTaskDropdown(this.value);',
+    'name'=>'project',
+    'style'=>'width: 250px;',
+    'value'=>$cl_project,
+    'data'=>$project_list,
+    'datakeys'=>array('id','name'),
+    'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+  ));
+
+  // Dropdown for clients if the clients plugin is enabled.
+  if (in_array('cl', explode(',', $user->plugins))) {
+    $active_clients = ttTeamHelper::getActiveClients($user->team_id, true);
+    // We need an array of assigned project ids to do some trimming. 
+    foreach($project_list as $project)
+      $projects_assigned_to_user[] = $project['id'];
+
+    // Build a client list out of active clients. Use only clients that are relevant to user.
+    // Also trim their associated project list to only assigned projects (to user).
+    foreach($active_clients as $client) {
+      $projects_assigned_to_client = explode(',', $client['projects']);
+         $intersection = array_intersect($projects_assigned_to_client, $projects_assigned_to_user);
+         if ($intersection) {
+           $client['projects'] = implode(',', $intersection);
+           $client_list[] = $client;
+         }
+    }
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'fillProjectDropdown(this.value);',
+      'name'=>'client',
+      'style'=>'width: 250px;',
+      'value'=>$cl_client,
+      'data'=>$client_list,
+      'datakeys'=>array('id', 'name'),
+      'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+    ));
+  }
+}
+
+if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+  $task_list = ttTeamHelper::getActiveTasks($user->team_id);
+  $form->addInput(array('type'=>'combobox',
+    'name'=>'task',
+    'style'=>'width: 250px;',
+    'value'=>$cl_task,
+    'data'=>$task_list,
+    'datakeys'=>array('id','name'),
+    'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+  ));
+}
+
+// Add other controls.
+if ((TYPE_START_FINISH == $user->record_type) || (TYPE_ALL == $user->record_type)) {
+  $form->addInput(array('type'=>'text','name'=>'start','value'=>$cl_start,'onchange'=>"formDisable('start');"));
+  $form->addInput(array('type'=>'text','name'=>'finish','value'=>$cl_finish,'onchange'=>"formDisable('finish');"));
+}
+if (!$user->canManageTeam() && defined('READONLY_START_FINISH') && isTrue(READONLY_START_FINISH)) {
+  // Make the start and finish fields read-only.
+  $form->getElement('start')->setEnable(false);
+  $form->getElement('finish')->setEnable(false);       
+}
+if ((TYPE_DURATION == $user->record_type) || (TYPE_ALL == $user->record_type))
+  $form->addInput(array('type'=>'text','name'=>'duration','value'=>$cl_duration,'onchange'=>"formDisable('duration');"));
+$form->addInput(array('type'=>'textarea','name'=>'note','style'=>'width: 600px; height: 40px;','value'=>$cl_note));
+$form->addInput(array('type'=>'calendar','name'=>'date','value'=>$cl_date)); // calendar
+if (in_array('iv', explode(',', $user->plugins)))
+  $form->addInput(array('type'=>'checkbox','name'=>'billable','data'=>1,'value'=>$cl_billable));
+$form->addInput(array('type'=>'hidden','name'=>'browser_today','value'=>'')); // User current date, which gets filled in on btn_submit click.
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','onclick'=>'browser_today.value=get_date()','value'=>$i18n->getKey('button.submit')));
+  
+// If we have custom fields - add controls for them.
+if ($custom_fields && $custom_fields->fields[0]) {
+  // Only one custom field is supported at this time.
+  if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_TEXT) {
+       $form->addInput(array('type'=>'text','name'=>'cf_1','value'=>$cl_cf_1));
+  } else if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_DROPDOWN) {
+    $form->addInput(array('type'=>'combobox','name'=>'cf_1',
+      'style'=>'width: 250px;',
+      'value'=>$cl_cf_1,
+      'data'=>$custom_fields->options,
+      'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+    ));
+  }
+}
+
+// Determine lock date. Time entries earlier than lock date cannot be created or modified. 
+$lock_interval = $user->lock_interval;
+$lockdate = 0;
+if ($lock_interval > 0) {
+  $lockdate = new DateAndTime();
+  $lockdate->decDay($lock_interval);
+}
+
+// Submit.
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('btn_submit')) {
+       
+    // Validate user input.
+    if (in_array('cl', explode(',', $user->plugins)) && in_array('cm', explode(',', $user->plugins)) && !$cl_client)
+      $errors->add($i18n->getKey('error.client'));
+    if ($custom_fields) {
+      if (!ttValidString($cl_cf_1, !$custom_fields->fields[0]['required'])) $errors->add($i18n->getKey('error.field'), $custom_fields->fields[0]['label']);
+    }
+    if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+      if (!$cl_project) $errors->add($i18n->getKey('error.project'));          
+    }
+    if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+      if (!$cl_task) $errors->add($i18n->getKey('error.task'));
+    }      
+    if (!$cl_duration) {
+      if ('0' == $cl_duration)
+        $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.duration'));
+      else if ($cl_start || $cl_finish) {
+        if (!ttTimeHelper::isValidTime($cl_start))
+          $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.start'));
+        if ($cl_finish) {
+          if (!ttTimeHelper::isValidTime($cl_finish))
+            $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.finish'));
+          if (!ttTimeHelper::isValidInterval($cl_start, $cl_finish))
+            $errors->add($i18n->getKey('error.interval'), $i18n->getKey('label.finish'), $i18n->getKey('label.start'));
+        }
+      } else {
+       if ((TYPE_START_FINISH == $user->record_type) || (TYPE_ALL == $user->record_type)) {
+          $errors->add($i18n->getKey('error.empty'), $i18n->getKey('label.start'));
+          $errors->add($i18n->getKey('error.empty'), $i18n->getKey('label.finish'));
+       }
+        if ((TYPE_DURATION == $user->record_type) || (TYPE_ALL == $user->record_type))
+          $errors->add($i18n->getKey('error.empty'), $i18n->getKey('label.duration'));
+      }
+    } else {
+      if (!ttTimeHelper::isValidDuration($cl_duration))
+        $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.duration'));
+    }
+    if (!ttValidString($cl_note, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.note'));
+    // Finished validating user input.
+
+    // Prohibit creating entries in future.
+    if (defined('FUTURE_ENTRIES') && !isTrue(FUTURE_ENTRIES)) {
+      $browser_today = new DateAndTime(DB_DATEFORMAT, $request->getParameter('browser_today', null));
+      if ($selected_date->after($browser_today))
+        $errors->add($i18n->getKey('error.future_date'));
+    }
+    
+    // Prohibit creating time entries in locked interval.
+    if($lockdate && $selected_date->before($lockdate))
+      $errors->add($i18n->getKey('error.period_locked'));
+    
+    // Prohibit creating another uncompleted record.
+    if ($errors->isEmpty()) {
+      if (($not_completed_rec = ttTimeHelper::getUncompleted($user->getActiveUser())) && (($cl_finish == '') && ($cl_duration == '')))
+        $errors->add($i18n->getKey('error.uncompleted_exists')." <a href = 'time_edit.php?id=".$not_completed_rec['id']."'>".$i18n->getKey('error.goto_uncompleted')."</a>");
+    }
+    
+    // Prohibit creating an overlapping record.
+    if ($errors->isEmpty()) {
+      if (ttTimeHelper::overlaps($user->getActiveUser(), $cl_date, $cl_start, $cl_finish))
+        $errors->add($i18n->getKey('error.overlap'));
+    }  
+
+    // Insert record.
+    if ($errors->isEmpty()) {
+      $id = ttTimeHelper::insert(array(
+        'date' => $cl_date,
+        'user_id' => $user->getActiveUser(),
+        'client' => $cl_client,
+        'project' => $cl_project,
+        'task' => $cl_task,
+        'start' => $cl_start,
+        'finish' => $cl_finish,
+        'duration' => $cl_duration,
+        'note' => $cl_note,
+        'billable' => $cl_billable));
+               
+      // Insert a custom field if we have it.
+      $result = true;
+      if ($id && $custom_fields && $cl_cf_1) {
+        if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_TEXT)
+          $result = $custom_fields->insert($id, $custom_fields->fields[0]['id'], null, $cl_cf_1);
+        else if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_DROPDOWN)
+          $result = $custom_fields->insert($id, $custom_fields->fields[0]['id'], $cl_cf_1, null);
+      }
+      if ($id && $result) {
+        header('Location: time.php');
+        exit();
+      }
+      $errors->add($i18n->getKey('error.db'));
+    }
+  }
+  else if ($request->getParameter('btn_stop')) {
+       // Stop button pressed to finish an uncompleted record.
+       $record_id = $request->getParameter('record_id');
+       $record = ttTimeHelper::getRecord($record_id, $user->getActiveUser());
+       $browser_date = $request->getParameter('browser_date');
+       $browser_time = $request->getParameter('browser_time');
+       
+       // Can we complete this record?
+       if ($record['date'] == $browser_date                                // closing today's record
+         && ttTimeHelper::isValidInterval($record['start'], $browser_time) // finish time is greater than start time
+         && !ttTimeHelper::overlaps($user->getActiveUser(), $browser_date, $record['start'], $browser_time)) { // no overlap
+      $res = ttTimeHelper::update(array(
+          'id'=>$record['id'],  
+          'date'=>$record['date'],  
+          'user_id'=>$user->getActiveUser(),
+          'client'=>$record['client_id'],  
+          'project'=>$record['project_id'],  
+          'task'=>$record['task_id'],  
+          'start'=>$record['start'],  
+          'finish'=>$browser_time,
+          'note'=>$record['comment'],
+          'billable'=>$record['billable']));
+      if (!$res)
+        $errors->add($i18n->getKey('error.db'));
+       } else {
+      // Cannot complete, redirect for manual edit.
+      header('Location: time_edit.php?id='.$record_id);
+      exit();          
+       }
+  }
+  else if ($request->getParameter('onBehalfUser')) {
+    if($user->canManageTeam()) {
+      unset($_SESSION['behalf_id']);
+      unset($_SESSION['behalf_name']);
+       
+      if($on_behalf_id != $user->id) {
+        $_SESSION['behalf_id'] = $on_behalf_id;
+       $_SESSION['behalf_name'] = ttUserHelper::getUserName($on_behalf_id);                    
+      }
+      header('Location: time.php');
+      exit();
+    }
+  }
+}
+
+$week_total = ttTimeHelper::getTimeForWeek($user->getActiveUser(), $selected_date);
+
+$smarty->assign('week_total', $week_total);
+$smarty->assign('day_total', ttTimeHelper::getTimeForDay($user->getActiveUser(), $cl_date));
+$smarty->assign('time_records', ttTimeHelper::getRecords($user->getActiveUser(), $cl_date));
+$smarty->assign('client_list', $client_list);
+$smarty->assign('project_list', $project_list);
+$smarty->assign('task_list', $task_list);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="fillDropdowns()"');
+$smarty->assign('timestring', $selected_date->toString($user->date_format));
+$smarty->assign('title', $i18n->getKey('title.time'));
+$smarty->assign('content_page_name', 'time.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/time_delete.php b/time_delete.php
new file mode 100644 (file)
index 0000000..7125a57
--- /dev/null
@@ -0,0 +1,104 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttUserHelper');
+import('ttTimeHelper');
+import('DateAndTime');
+
+// Access check.
+if (!ttAccessCheck(right_data_entry)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Use Custom Fields plugin if we have one.
+// if (file_exists("plugins/CustomFields.class.php")) {
+//   require_once("plugins/CustomFields.class.php");
+//   $custom_fields = new CustomFields($user->team_id);
+// }
+
+$cl_id = $request->getParameter('id');
+$time_rec = ttTimeHelper::getRecord($cl_id, $user->getActiveUser());
+
+// Prohibit deleting invoiced records.
+if ($time_rec['invoice_id']) die($i18n->getKey('error.sys'));
+  
+// Escape comment for presentation.
+$time_rec['comment'] = htmlspecialchars($time_rec['comment']);
+        
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('delete_button'))  {  // Delete button pressed.
+  
+    // Determine if it's okay to delete the record.
+    $item_date = new DateAndTime(DB_DATEFORMAT, $time_rec['date']);
+    // Determine lock date.
+    $lock_interval = $user->lock_interval;
+    $lockdate = 0;
+    if ($lock_interval > 0) {
+      $lockdate = new DateAndTime();
+      $lockdate->decDay($lock_interval);
+    }
+    // Determine if the record is uncompleted.
+    $uncompleted = ($time_rec['duration'] == '0:00');
+       
+    if($lockdate && $item_date->before($lockdate) && !$uncompleted) {
+      $errors->add($i18n->getKey('error.period_locked'));
+    }
+           
+    if ($errors->isEmpty()) {
+       
+      // Delete the record.
+      $result = ttTimeHelper::delete($cl_id, $user->getActiveUser());
+
+      if ($result) {
+        header('Location: time.php');
+        exit();
+      } else {
+        $errors->add($i18n->getKey('error.db'));
+      }
+    }
+  }
+  if ($request->getParameter('cancel_button')) { // Cancel button pressed.
+       header('Location: time.php');
+       exit();
+  }
+}
+               
+$form = new Form('timeRecordForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_id));
+$form->addInput(array('type'=>'submit','name'=>'delete_button','value'=>$i18n->getKey('label.delete')));
+$form->addInput(array('type'=>'submit','name'=>'cancel_button','value'=>$i18n->getKey('button.cancel')));
+
+$smarty->assign('time_rec', $time_rec);
+$smarty->assign('forms', array($form->getName() => $form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.delete_time_record'));
+$smarty->assign('content_page_name', 'time_delete.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/time_edit.php b/time_edit.php
new file mode 100644 (file)
index 0000000..4c255db
--- /dev/null
@@ -0,0 +1,410 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttUserHelper');
+import('ttTeamHelper');
+import('ttClientHelper');
+import('ttTimeHelper');
+import('DateAndTime');
+
+// Access check.
+if (!ttAccessCheck(right_data_entry)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Use custom fields plugin if it is enabled.
+if (in_array('cf', explode(',', $user->plugins))) {
+  require_once('plugins/CustomFields.class.php');
+  $custom_fields = new CustomFields($user->team_id);
+  $smarty->assign('custom_fields', $custom_fields);
+}
+
+$cl_id = $request->getParameter('id');
+
+// Get the time record we are editing.
+$time_rec = ttTimeHelper::getRecord($cl_id, $user->getActiveUser());
+
+// Prohibit editing invoiced records.
+if ($time_rec['invoice_id']) die($i18n->getKey('error.sys'));
+
+$item_date = new DateAndTime(DB_DATEFORMAT, $time_rec['date']);
+  
+// Initialize variables.
+$cl_start = $cl_finish = $cl_duration = $cl_date = $cl_note = $cl_project = $cl_task = $cl_billable = null;
+if ($request->getMethod() == 'POST') {
+  $cl_start = trim($request->getParameter('start'));
+  $cl_finish = trim($request->getParameter('finish'));
+  $cl_duration = trim($request->getParameter('duration'));
+  $cl_date = $request->getParameter('date');
+  $cl_note = trim($request->getParameter('note'));
+  $cl_cf_1 = trim($request->getParameter('cf_1'));
+  $cl_client = $request->getParameter('client');
+  $cl_project = $request->getParameter('project');
+  $cl_task = $request->getParameter('task');
+  $cl_billable = 1;
+  if (in_array('iv', explode(',', $user->plugins)))
+    $cl_billable = $request->getParameter('billable');
+} else {
+  $cl_client = $time_rec['client_id'];
+  $cl_project = $time_rec['project_id'];
+  $cl_task = $time_rec['task_id'];
+  $cl_start = $time_rec['start'];
+  $cl_finish = $time_rec['finish'];
+  $cl_duration = $time_rec['duration'];
+  $cl_date     = $item_date->toString($user->date_format);
+  $cl_note = $time_rec['comment'];
+    
+  // If we have custom fields - obtain values for them.
+  if ($custom_fields) {
+    // Get custom field value for time record.
+       $fields = $custom_fields->get($time_rec['id']);
+       if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_TEXT)
+      $cl_cf_1 = $fields[0]['value'];
+    else if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_DROPDOWN)
+      $cl_cf_1 = $fields[0]['option_id'];
+  }
+  
+  $cl_billable = $time_rec['billable'];
+  
+  // Add an info message to the form if we are editing an uncompleted record.
+  if (($cl_start == $cl_finish) && ($cl_duration == '0:00')) {
+    $cl_finish = '';
+    $cl_duration = '';
+    $messages->add($i18n->getKey('form.time_edit.uncompleted'));
+  }
+}
+  
+// Initialize elements of 'timeRecordForm'.
+$form = new Form('timeRecordForm');
+
+// Dropdown for clients in MODE_TIME. Use all active clients.
+if (MODE_TIME == $user->tracking_mode && in_array('cl', explode(',', $user->plugins))) {
+    $active_clients = ttTeamHelper::getActiveClients($user->team_id, true);
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'fillProjectDropdown(this.value);',
+      'name'=>'client',
+      'style'=>'width: 250px;',
+      'value'=>$cl_client,
+      'data'=>$active_clients,
+      'datakeys'=>array('id', 'name'),
+      'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+    ));
+  // Note: in other modes the client list is filtered to relevant clients only. See below.
+}
+
+if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+  // Dropdown for projects assigned to user.
+  $project_list = $user->getAssignedProjects();
+  $form->addInput(array('type'=>'combobox',
+    'onchange'=>'fillTaskDropdown(this.value);',
+    'name'=>'project',
+    'style'=>'width: 250px;',
+    'value'=>$cl_project,
+    'data'=>$project_list,
+    'datakeys'=>array('id','name'),
+    'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+  ));
+
+  // Dropdown for clients if the clients plugin is enabled.
+  if (in_array('cl', explode(',', $user->plugins))) {
+    $active_clients = ttTeamHelper::getActiveClients($user->team_id, true);
+    // We need an array of assigned project ids to do some trimming. 
+    foreach($project_list as $project)
+      $projects_assigned_to_user[] = $project['id'];
+
+    // Build a client list out of active clients. Use only clients that are relevant to user.
+    // Also trim their associated project list to only assigned projects (to user).
+    foreach($active_clients as $client) {
+         $projects_assigned_to_client = explode(',', $client['projects']);
+         $intersection = array_intersect($projects_assigned_to_client, $projects_assigned_to_user);
+         if ($intersection) {
+        $client['projects'] = implode(',', $intersection);
+           $client_list[] = $client;
+         }
+    }
+    $form->addInput(array('type'=>'combobox',
+      'onchange'=>'fillProjectDropdown(this.value);',
+      'name'=>'client',
+      'style'=>'width: 250px;',
+      'value'=>$cl_client,
+      'data'=>$client_list,
+      'datakeys'=>array('id', 'name'),
+      'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+    ));
+  }
+}
+
+if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+  $task_list = ttTeamHelper::getActiveTasks($user->team_id);
+  $form->addInput(array('type'=>'combobox',
+    'name'=>'task',
+    'style'=>'width: 250px;',
+    'value'=>$cl_task,
+    'data'=>$task_list,
+    'datakeys'=>array('id','name'),
+    'empty'=>array(''=>$i18n->getKey('dropdown.select'))
+  ));
+}
+  
+// Add other controls.
+if ((TYPE_START_FINISH == $user->record_type) || (TYPE_ALL == $user->record_type)) {
+  $form->addInput(array('type'=>'text','name'=>'start','value'=>$cl_start,'onchange'=>"formDisable('start');"));
+  $form->addInput(array('type'=>'text','name'=>'finish','value'=>$cl_finish,'onchange'=>"formDisable('finish');"));
+}
+if (!$user->canManageTeam() && defined('READONLY_START_FINISH') && isTrue(READONLY_START_FINISH)) {
+  // Make the start and finish fields read-only.
+  $form->getElement('start')->setEnable(false);
+  $form->getElement('finish')->setEnable(false);       
+}
+if ((TYPE_DURATION == $user->record_type) || (TYPE_ALL == $user->record_type))
+  $form->addInput(array('type'=>'text','name'=>'duration','value'=>$cl_duration,'onchange'=>"formDisable('duration');"));
+$form->addInput(array('type'=>'datefield','name'=>'date','maxlength'=>'20','value'=>$cl_date));
+$form->addInput(array('type'=>'textarea','name'=>'note','style'=>'width: 250px; height: 200px;','value'=>$cl_note));
+// If we have custom fields - add controls for them.
+if ($custom_fields && $custom_fields->fields[0]) {
+  // Only one custom field is supported at this time.
+  if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_TEXT) {
+       $form->addInput(array('type'=>'text','name'=>'cf_1','value'=>$cl_cf_1));
+  } else if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_DROPDOWN) {
+    $form->addInput(array('type'=>'combobox',
+      'name'=>'cf_1',
+      'style'=>'width: 250px;',
+      'value'=>$cl_cf_1,
+      'data'=>$custom_fields->options,
+      'empty' => array('' => $i18n->getKey('dropdown.select'))
+    ));
+  }
+}
+// Hidden control for record id.
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$cl_id));
+if (in_array('iv', explode(',', $user->plugins)))
+  $form->addInput(array('type'=>'checkbox','name'=>'billable','data'=>1,'value'=>$cl_billable));
+$form->addInput(array('type'=>'hidden','name'=>'browser_today','value'=>'')); // User current date, which gets filled in on btn_save or btn_copy click.
+$form->addInput(array('type'=>'submit','name'=>'btn_save','onclick'=>'browser_today.value=get_date()','value'=>$i18n->getKey('button.save')));
+$form->addInput(array('type'=>'submit','name'=>'btn_copy','onclick'=>'browser_today.value=get_date()','value'=>$i18n->getKey('button.copy')));
+$form->addInput(array('type'=>'submit','name'=>'btn_delete','value'=>$i18n->getKey('label.delete')));
+
+if ($request->getMethod() == 'POST') {
+
+  // Validate user input.
+  if (in_array('cl', explode(',', $user->plugins)) && in_array('cm', explode(',', $user->plugins)) && !$cl_client)
+    $errors->add($i18n->getKey('error.client'));
+  if ($custom_fields) {
+    if (!ttValidString($cl_cf_1, !$custom_fields->fields[0]['required'])) $errors->add($i18n->getKey('error.field'), $custom_fields->fields[0]['label']);
+  }
+  if (MODE_PROJECTS == $user->tracking_mode || MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+    if (!$cl_project) $errors->add($i18n->getKey('error.project'));
+  }
+  if (MODE_PROJECTS_AND_TASKS == $user->tracking_mode) {
+    if (!$cl_task) $errors->add($i18n->getKey('error.task'));
+  }
+  if (!$cl_duration) {
+    if ('0' == $cl_duration)
+      $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.duration'));
+    else if ($cl_start || $cl_finish) {
+      if (!ttTimeHelper::isValidTime($cl_start))
+        $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.start'));
+      if ($cl_finish) {
+        if (!ttTimeHelper::isValidTime($cl_finish))
+          $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.finish'));
+        if (!ttTimeHelper::isValidInterval($cl_start, $cl_finish))
+          $errors->add($i18n->getKey('error.interval'), $i18n->getKey('label.finish'), $i18n->getKey('label.start'));
+      }
+    } else {
+      if ((TYPE_START_FINISH == $user->record_type) || (TYPE_ALL == $user->record_type)) {
+        $errors->add($i18n->getKey('error.empty'), $i18n->getKey('label.start'));
+        $errors->add($i18n->getKey('error.empty'), $i18n->getKey('label.finish'));
+      }
+      if ((TYPE_DURATION == $user->record_type) || (TYPE_ALL == $user->record_type))
+        $errors->add($i18n->getKey('error.empty'), $i18n->getKey('label.duration'));
+    }
+  } else {
+    if (!ttTimeHelper::isValidDuration($cl_duration))
+      $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.duration'));
+  }
+  if (!ttValidDate($cl_date)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.date'));
+  if (!ttValidString($cl_note, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.note'));
+  // Finished validating user input.
+      
+  // Determine lock date.
+  $lock_interval = $user->lock_interval;
+  $lockdate = 0;
+  if ($lock_interval > 0) {
+    $lockdate = new DateAndTime();
+    $lockdate->decDay($lock_interval);
+  }
+
+  // This is a new date for the time record.
+  $new_date = new DateAndTime($user->date_format, $cl_date);
+
+  // Prohibit creating entries in future.
+  if (defined('FUTURE_ENTRIES') && !isTrue(FUTURE_ENTRIES)) {
+    $browser_today = new DateAndTime(DB_DATEFORMAT, $request->getParameter('browser_today', null));
+    if ($new_date->after($browser_today))
+      $errors->add($i18n->getKey('error.future_date'));
+  }
+
+  // Save record.
+  if ($request->getParameter('btn_save')) {
+    // We need to:
+    // 1) Prohibit saving locked time entries in any form.
+    // 2) Prohibit saving completed unlocked entries into locked interval.
+    // 3) Prohibit saving uncompleted unlocked entries when another uncompleted entry exists.
+      
+    // Now, step by step.
+    if ($errors->isEmpty()) {
+      // 1) Prohibit saving locked time entries in any form.
+      if($lockdate && $item_date->before($lockdate))
+        $errors->add($i18n->getKey('error.period_locked'));        
+      // 2) Prohibit saving completed unlocked entries into locked interval.
+      if($errors->isEmpty() && $lockdate && $new_date->before($lockdate))
+        $errors->add($i18n->getKey('error.period_locked'));        
+      // 3) Prohibit saving uncompleted unlocked entries when another uncompleted entry exists.
+      $uncompleted = ($cl_finish == '' && $cl_duration == '');
+      if ($uncompleted) {
+        $not_completed_rec = ttTimeHelper::getUncompleted($user->getActiveUser());
+        if ($not_completed_rec && ($time_rec['id'] <> $not_completed_rec['id'])) {
+          // We have another not completed record.
+          $errors->add($i18n->getKey('error.uncompleted_exists')." <a href = 'time_edit.php?id=".$not_completed_rec['id']."'>".$i18n->getKey('error.goto_uncompleted')."</a>");
+        }
+      }
+    }
+    
+    // Prohibit creating an overlapping record.
+    if ($errors->isEmpty()) {
+      if (ttTimeHelper::overlaps($user->getActiveUser(), $new_date->toString(DB_DATEFORMAT), $cl_start, $cl_finish, $cl_id))
+        $errors->add($i18n->getKey('error.overlap'));
+    } 
+
+    // Now, an update.
+    if ($errors->isEmpty()) {
+      $res = ttTimeHelper::update(array(
+          'id'=>$cl_id,  
+          'date'=>$new_date->toString(DB_DATEFORMAT),
+          'user_id'=>$user->getActiveUser(),
+          'client'=>$cl_client,
+          'project'=>$cl_project,
+          'task'=>$cl_task,
+          'start'=>$cl_start,
+          'finish'=>$cl_finish,
+          'duration'=>$cl_duration,
+          'note'=>$cl_note,
+          'billable'=>$cl_billable));
+       
+      // If we have custom fields - update values.
+      if ($res && $custom_fields) {
+        if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_TEXT)
+          $res = $custom_fields->update($cl_id, $custom_fields->fields[0]['id'], null, $cl_cf_1);
+        else if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_DROPDOWN)
+          $res = $custom_fields->update($cl_id, $custom_fields->fields[0]['id'], $cl_cf_1, null);
+      }
+      if ($res)
+      {
+        header('Location: time.php?date='.$new_date->toString(DB_DATEFORMAT));
+        exit();
+      }
+    }
+  }
+      
+  // Save as new record.
+  if ($request->getParameter('btn_copy')) {
+    // We need to:
+    // 1) Prohibit saving into locked interval.
+    // 2) Prohibit saving uncompleted unlocked entries when another uncompleted entry exists.
+      
+    // Now, step by step.
+    if ($errors->isEmpty()) {
+      // 1) Prohibit saving into locked interval.
+      if($lockdate && $new_date->before($lockdate))
+        $errors->add($i18n->getKey('error.period_locked'));
+      // 2) Prohibit saving uncompleted unlocked entries when another uncompleted entry exists.
+      $uncompleted = ($cl_finish == '' && $cl_duration == '');
+      if ($uncompleted) {
+        $not_completed_rec = ttTimeHelper::getUncompleted($user->getActiveUser());
+        if ($not_completed_rec) {
+          // We have another not completed record.
+          $errors->add($i18n->getKey('error.uncompleted_exists')." <a href = 'time_edit.php?id=".$not_completed_rec['id']."'>".$i18n->getKey('error.goto_uncompleted')."</a>");
+        }
+      }
+    }
+    
+    // Prohibit creating an overlapping record.
+    if ($errors->isEmpty()) {
+      if (ttTimeHelper::overlaps($user->getActiveUser(), $new_date->toString(DB_DATEFORMAT), $cl_start, $cl_finish))
+        $errors->add($i18n->getKey('error.overlap'));
+    }
+
+    // Now, a new insert.
+    if ($errors->isEmpty()) {
+       
+      $id = ttTimeHelper::insert(array(
+        'date'=>$new_date->toString(DB_DATEFORMAT),
+        'user_id'=>$user->getActiveUser(),
+        'client'=>$cl_client,
+        'project'=>$cl_project,
+        'task'=>$cl_task,
+        'start'=>$cl_start,
+        'finish'=>$cl_finish,
+        'duration'=>$cl_duration,
+        'note'=>$cl_note,
+        'billable'=>$cl_billable));
+
+      // Insert a custom field if we have it.
+      $res = true;
+      if ($id && $custom_fields && $cl_cf_1) {
+        if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_TEXT)
+          $res = $custom_fields->insert($id, $custom_fields->fields[0]['id'], null, $cl_cf_1);
+        else if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_DROPDOWN)
+          $res = $custom_fields->insert($id, $custom_fields->fields[0]['id'], $cl_cf_1, null);
+      }
+      if ($id && $res) {
+        header('Location: time.php?date='.$new_date->toString(DB_DATEFORMAT));
+        exit();
+      }
+      $errors->add($i18n->getKey('error.db'));
+    }
+  }
+  
+  if ($request->getParameter('btn_delete')) {
+    header("Location: time_delete.php?id=$cl_id");
+    exit();
+  }
+} // End of if ($request->getMethod() == "POST")
+
+$smarty->assign('client_list', $client_list);
+$smarty->assign('project_list', $project_list);
+$smarty->assign('task_list', $task_list);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="fillDropdowns()"');
+$smarty->assign('title', $i18n->getKey('title.edit_time_record'));
+$smarty->assign('content_page_name', 'time_edit.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/tofile.php b/tofile.php
new file mode 100644 (file)
index 0000000..3d264e0
--- /dev/null
@@ -0,0 +1,228 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('form.ActionForm');
+import('ttReportHelper');
+
+// Access check.
+if (!ttAccessCheck(right_view_reports)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Use custom fields plugin if it is enabled.
+if (in_array('cf', explode(',', $user->plugins))) {
+  require_once('plugins/CustomFields.class.php');
+  $custom_fields = new CustomFields($user->team_id);
+}
+
+// Report settings are stored in session bean before we get here.
+$bean = new ActionForm('reportBean', new Form('reportForm'), $request);
+
+// At the moment, we distinguish 2 types of export to file:
+// 1) export to xml
+// 2) export to csv
+$type = $request->getParameter('type');
+
+// Also, there are 2 variations of report: totals only, or normal. Totals only means that the report
+// is grouped by (either date, user, client, project, task or cf_1) and user only needs to see subtotals by group.
+$totals_only = $bean->getAttribute('chtotalsonly');
+
+// Obtain items.
+if ($totals_only)
+  $subtotals = ttReportHelper::getSubtotals($bean);
+else
+  $items = ttReportHelper::getItems($bean);
+
+header('Pragma: public'); // This is needed for IE8 to download files over https.
+header('Content-Type: text/html; charset=utf-8');
+header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
+header('Cache-Control: no-store, no-cache, must-revalidate');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Cache-Control: private', false);
+       
+// Handle 2 cases of possible exports individually.
+
+// 1) entries exported to xml
+if ('xml' == $type) {
+  header('Content-Type: application/xml');
+  header('Content-Disposition: attachment; filename="timesheet.xml"');
+  
+  print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
+  print "<rows>\n";
+  
+  $group_by = $bean->getAttribute('group_by');
+  if ($totals_only) {
+    // Totals only report. Print subtotals.
+    foreach ($subtotals as $subtotal) {
+      print "<row>\n";
+      print "\t<".$group_by."><![CDATA[".$subtotal['name']."]]></".$group_by.">\n";
+      if ($bean->getAttribute('chduration')) {
+        $val = $subtotal['time'];
+        if($val && defined('EXPORT_DECIMAL_DURATION') && isTrue(EXPORT_DECIMAL_DURATION))
+          $val = time_to_decimal($val);
+        print "\t<duration><![CDATA[".$val."]]></duration>\n";
+      }
+      if ($bean->getAttribute('chcost')) {
+       print "\t<cost><![CDATA[";
+       if ($user->canManageTeam() || $user->isClient())
+         print $subtotal['cost'];
+       else
+         print $subtotal['expenses'];
+       print "]]></cost>\n";
+      }
+      print "</row>\n";
+    }
+  } else {
+    // Normal report.
+       foreach ($items as $item) {
+      print "<row>\n";
+
+      print "\t<date><![CDATA[".$item['date']."]]></date>\n";
+      if ($user->canManageTeam() || $user->isClient()) print "\t<user><![CDATA[".$item['user']."]]></user>\n"; 
+      if ($bean->getAttribute('chclient')) print "\t<client><![CDATA[".$item['client']."]]></client>\n";
+      if ($bean->getAttribute('chproject')) print "\t<project><![CDATA[".$item['project']."]]></project>\n";
+      if ($bean->getAttribute('chtask')) print "\t<task><![CDATA[".$item['task']."]]></task>\n";
+      if ($bean->getAttribute('chcf_1')) print "\t<cf_1><![CDATA[".$item['cf_1']."]]></cf_1>\n";
+      if ($bean->getAttribute('chstart')) print "\t<start><![CDATA[".$item['start']."]]></start>\n";
+      if ($bean->getAttribute('chfinish')) print "\t<finish><![CDATA[".$item['finish']."]]></finish>\n";
+      if ($bean->getAttribute('chduration')) {
+       $duration = $item['duration'];
+        if($duration && defined('EXPORT_DECIMAL_DURATION') && isTrue(EXPORT_DECIMAL_DURATION))
+          $duration = time_to_decimal($duration);
+          print "\t<duration><![CDATA[".$duration."]]></duration>\n";
+      }
+      if ($bean->getAttribute('chnote')) print "\t<note><![CDATA[".$item['note']."]]></note>\n";
+      if ($bean->getAttribute('chcost')) {
+       print "\t<cost><![CDATA[";
+       if ($user->canManageTeam() || $user->isClient())
+         print $item['cost'];
+       else
+         print $item['expense'];
+       print "]]></cost>\n";
+      }
+      if ($bean->getAttribute('chinvoice')) print "\t<invoice><![CDATA[".$item['invoice']."]]></invoice>\n";
+
+      print "</row>\n";
+       }
+  }
+  
+  print "</rows>";
+}    
+
+// 2) entries exported to csv
+if ('csv' == $type) {
+  header('Content-Type: application/csv');
+  header('Content-Disposition: attachment; filename="timesheet.csv"');
+    
+  // Print UTF8 BOM first to identify encoding.
+  $bom = chr(239).chr(187).chr(191); // 0xEF 0xBB 0xBF in the beginning of the file is UTF8 BOM.
+  print $bom; // Without this Excel does not display UTF8 characters properly.
+
+  $group_by = $bean->getAttribute('group_by');
+  if ($totals_only) {
+    // Totals only report.
+    
+       // Determine group_by header.
+    if ('cf_1' == $group_by)
+      $group_by_header = $custom_fields->fields[0]['label'];
+    else {
+      $key = 'label.'.$group_by;
+      $group_by_header = $i18n->getKey($key);
+    }
+
+    // Print headers.
+    print '"'.$group_by_header.'"';
+    if ($bean->getAttribute('chduration')) print ',"'.$i18n->getKey('label.duration').'"';
+    if ($bean->getAttribute('chcost')) print ',"'.$i18n->getKey('label.cost').'"';
+    print "\n";
+  
+    // Print subtotals.
+    foreach ($subtotals as $subtotal) {
+      print '"'.$subtotal['name'].'"';
+      if ($bean->getAttribute('chduration')) {
+        $val = $subtotal['time'];
+           if($val && defined('EXPORT_DECIMAL_DURATION') && isTrue(EXPORT_DECIMAL_DURATION))
+          $val = time_to_decimal($val);
+           print ',"'.$val.'"';
+      }
+      if ($bean->getAttribute('chcost')) {
+       if ($user->canManageTeam() || $user->isClient())
+          print ',"'.$subtotal['cost'].'"';
+        else
+          print ',"'.$subtotal['expenses'].'"';
+      }
+      print "\n";
+    }
+  } else {
+    // Normal report. Print headers.
+    print '"'.$i18n->getKey('label.date').'"';
+    if ($user->canManageTeam() || $user->isClient()) print ',"'.$i18n->getKey('label.user').'"';
+    if ($bean->getAttribute('chclient')) print ',"'.$i18n->getKey('label.client').'"';
+    if ($bean->getAttribute('chproject')) print ',"'.$i18n->getKey('label.project').'"';
+    if ($bean->getAttribute('chtask')) print ',"'.$i18n->getKey('label.task').'"';
+    if ($bean->getAttribute('chcf_1')) print ',"'.$custom_fields->fields[0]['label'].'"';
+    if ($bean->getAttribute('chstart')) print ',"'.$i18n->getKey('label.start').'"';
+    if ($bean->getAttribute('chfinish')) print ',"'.$i18n->getKey('label.finish').'"';
+    if ($bean->getAttribute('chduration')) print ',"'.$i18n->getKey('label.duration').'"';
+    if ($bean->getAttribute('chnote')) print ',"'.$i18n->getKey('label.note').'"';
+    if ($bean->getAttribute('chcost')) print ',"'.$i18n->getKey('label.cost').'"';
+    if ($bean->getAttribute('chinvoice')) print ',"'.$i18n->getKey('label.invoice').'"';
+    print "\n";
+
+    // Print items.
+    foreach ($items as $item) {
+      print '"'.$item['date'].'"';
+      if ($user->canManageTeam() || $user->isClient()) print ',"'.str_replace('"','""',$item['user']).'"';
+      if ($bean->getAttribute('chclient')) print ',"'.str_replace('"','""',$item['client']).'"';
+      if ($bean->getAttribute('chproject')) print ',"'.str_replace('"','""',$item['project']).'"';
+      if ($bean->getAttribute('chtask')) print ',"'.str_replace('"','""',$item['task']).'"';
+      if ($bean->getAttribute('chcf_1')) print ',"'.str_replace('"','""',$item['cf_1']).'"';
+      if ($bean->getAttribute('chstart')) print ',"'.$item['start'].'"';
+      if ($bean->getAttribute('chfinish')) print ',"'.$item['finish'].'"';
+      if ($bean->getAttribute('chduration')) {
+        $val = $item['duration'];
+        if($val && defined('EXPORT_DECIMAL_DURATION') && isTrue(EXPORT_DECIMAL_DURATION))
+          $val = time_to_decimal($val);
+        print ',"'.$val.'"';
+      }
+      if ($bean->getAttribute('chnote')) print ',"'.str_replace('"','""',$item['note']).'"';
+      if ($bean->getAttribute('chcost')) {
+       if ($user->canManageTeam() || $user->isClient())
+         print ',"'.$item['cost'].'"';
+       else
+         print ',"'.$item['expense'].'"';
+      }
+      if ($bean->getAttribute('chinvoice')) print ',"'.str_replace('"','""',$item['invoice']).'"';
+      print "\n";
+    }
+  }
+}
+?>
\ No newline at end of file
diff --git a/user_add.php b/user_add.php
new file mode 100644 (file)
index 0000000..cf2fe76
--- /dev/null
@@ -0,0 +1,175 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+import('ttUserHelper');
+import('form.Table');
+import('form.TableColumn');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Use the "limit" plugin if we have one. Ignore include errors.
+// The "limit" plugin is not required for normal operation of the Time Tracker.
+@include('plugins/limit/user_add.php');
+
+if (in_array('cl', explode(',', $user->plugins)))
+  $clients = ttTeamHelper::getActiveClients($user->team_id);
+
+$assigned_projects = array();
+if ($request->getMethod() == 'POST') {
+  $cl_name = trim($request->getParameter('name'));
+  $cl_login = trim($request->getParameter('login'));
+  if (!$auth->isPasswordExternal()) {
+    $cl_password1 = $request->getParameter('pas1');
+    $cl_password2 = $request->getParameter('pas2');
+  }
+  $cl_email = trim($request->getParameter('email'));
+  $cl_role = $request->getParameter('role');
+  if (!$cl_role) $cl_role = ROLE_USER;
+  $cl_client_id = $request->getParameter('client');
+  $cl_rate = $request->getParameter('rate');
+  $cl_projects = $request->getParameter('projects');
+  if (is_array($cl_projects)) {
+    foreach ($cl_projects as $p) {
+      if (ttValidFloat($request->getParameter('rate_'.$p), true)) {
+       $project_with_rate = array();
+        $project_with_rate['id'] = $p;
+        $project_with_rate['rate'] = $request->getParameter('rate_'.$p);
+        $assigned_projects[] = $project_with_rate;
+      } else
+        $errors->add($i18n->getKey('error.field'), 'rate_'.$p);
+    }
+  }
+}
+
+$form = new Form('userForm');
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'name','style'=>'width: 300px;','value'=>$cl_name));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'login','style'=>'width: 300px;','value'=>$cl_login));
+if (!$auth->isPasswordExternal()) {
+  $form->addInput(array('type'=>'text','maxlength'=>'30','name'=>'pas1','aspassword'=>true,'value'=>$cl_password1));
+  $form->addInput(array('type'=>'text','maxlength'=>'30','name'=>'pas2','aspassword'=>true,'value'=>$cl_password2));
+}
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'email','value'=>$cl_email));
+
+$roles[ROLE_USER] = $i18n->getKey('label.user');
+$roles[ROLE_COMANAGER] = $i18n->getKey('form.users.comanager');
+if (in_array('cl', explode(',', $user->plugins)))
+  $roles[ROLE_CLIENT] = $i18n->getKey('label.client');
+$form->addInput(array('type'=>'combobox','onchange'=>'handleClientControl()','name'=>'role','value'=>$cl_role,'data'=>$roles));
+if (in_array('cl', explode(',', $user->plugins)))
+  $form->addInput(array('type'=>'combobox','name'=>'client','value'=>$cl_client_id,'data'=>$clients,'datakeys'=>array('id', 'name'),'empty'=>array(''=>$i18n->getKey('dropdown.select'))));
+
+$form->addInput(array('type'=>'floatfield','maxlength'=>'10','name'=>'rate','format'=>'.2','value'=>$cl_rate));
+
+$projects = ttTeamHelper::getActiveProjects($user->team_id);
+
+// Define classes for the projects table.
+class NameCellRenderer extends DefaultCellRenderer {
+  function render(&$table, $value, $row, $column, $selected = false) {
+    $this->setOptions(array('width'=>200,'valign'=>'top'));
+    $this->setValue('<label for = "'.$table->getName().'_'.$row.'">'.htmlspecialchars($value).'</label>');
+    return $this->toString();
+  }
+}
+class RateCellRenderer extends DefaultCellRenderer {
+  function render(&$table, $value, $row, $column, $selected = false) {
+    global $assigned_projects;
+    $field = new FloatField('rate_'.$table->getValueAtName($row, 'id'), $table->getValueAtName($row, 'p_rate'));
+    $field->setFormName($table->getFormName());
+    $field->setLocalization($GLOBALS['I18N']);
+    $field->setSize(5);
+    $field->setFormat('.2');
+    foreach ($assigned_projects as $p) {
+      if ($p['id'] == $table->getValueAtName($row,'id')) $field->setValue($p['rate']);
+    }
+    $this->setValue($field->toStringControl());
+    return $this->toString();
+  }
+}
+// Create projects table.
+$table = new Table('projects');
+$table->setIAScript('setDefaultRate');
+$table->setTableOptions(array('width'=>'100%','cellspacing'=>'1','cellpadding'=>'3','border'=>'0'));
+$table->setRowOptions(array('valign'=>'top','class'=>'tableHeader'));
+$table->setData($projects);
+$table->setKeyField('id');
+$table->setValue($cl_projects);
+$table->addColumn(new TableColumn('name', $i18n->getKey('label.project'), new NameCellRenderer()));
+$table->addColumn(new TableColumn('p_rate', $i18n->getKey('form.users.rate'), new RateCellRenderer()));
+$form->addInputElement($table);
+
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.submit')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.person_name'));
+  if (!ttValidString($cl_login)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.login'));
+  if (!$auth->isPasswordExternal()) {
+       if (!ttValidString($cl_password1)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.password'));
+       if (!ttValidString($cl_password2)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.confirm_password'));
+    if ($cl_password1 !== $cl_password2)
+      $errors->add($i18n->getKey('error.not_equal'), $i18n->getKey('label.password'), $i18n->getKey('label.confirm_password'));
+  }    
+  if (!ttValidEmail($cl_email, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.email'));
+  if (!ttValidFloat($cl_rate, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.users.default_rate'));
+
+  if ($errors->isEmpty()) {
+    if (!ttUserHelper::getUserByLogin($cl_login)) {
+      $fields = array(
+        'name' => $cl_name,
+        'login' => $cl_login,
+        'password' => $cl_password1,
+        'rate' => $cl_rate,
+        'team_id' => $user->team_id,
+        'role' => $cl_role,
+        'client_id' => $cl_client_id,
+        'projects' => $assigned_projects,
+        'email' => $cl_email);
+      if (ttUserHelper::insert($fields)) {
+        header('Location: users.php');
+        exit();
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    } else
+      $errors->add($i18n->getKey('error.user_exists'));
+  }
+} // post
+
+$smarty->assign('auth_external', $auth->isPasswordExternal());
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.userForm.name.focus();handleClientControl();"');
+$smarty->assign('title', $i18n->getKey('title.add_user'));
+$smarty->assign('content_page_name', 'user_add.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/user_delete.php b/user_delete.php
new file mode 100644 (file)
index 0000000..aed2746
--- /dev/null
@@ -0,0 +1,100 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttUserHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Get user id we are deleting from the request.
+// A cast to int is for safety against manipulation of request parameter (sql injection). 
+$user_id = (int) $request->getParameter('id');
+
+// We need user name and login to display.
+$user_details = ttUserHelper::getUserDetails($user_id);
+
+// Security checks.
+$ok_to_go = $user->canManageTeam(); // Are we authorized for user deletes?
+if ($ok_to_go) $ok_to_go = $ok_to_go && $user_details; // Are we deleting a real user?
+if ($ok_to_go) $ok_to_go = $ok_to_go && ($user->team_id == $user_details['team_id']); // User belongs to our team?
+if ($ok_to_go && $user->isCoManager() && (ROLE_COMANAGER == $user_details['role']))
+  $ok_to_go = ($user->id == $user_details['id']); // Comanager is not allowed to delete other comanagers.
+if ($ok_to_go && $user->isCoManager() && (ROLE_MANAGER == $user_details['role']))
+  $ok_to_go = false; // Comanager is not allowed to delete a manager.  
+
+if (!$ok_to_go)
+  die ($i18n->getKey('error.sys'));
+else
+  $smarty->assign('user_to_delete', $user_details['name']." (".$user_details['login'].")");    
+
+// Create confirmation form.
+$form = new Form('userDeleteForm');
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$user_id));
+$form->addInput(array('type'=>'submit','name'=>'btn_delete','value'=>$i18n->getKey('label.delete')));
+$form->addInput(array('type'=>'submit','name'=>'btn_cancel','value'=>$i18n->getKey('button.cancel')));
+       
+if ($request->getMethod() == 'POST') {
+  if ($request->getParameter('btn_delete')) {
+    if (ttUserHelper::markDeleted($user_id)) {
+      // If we deleted the "on behalf" user reset its info in session.
+      if ($user_id == $user->behalf_id) {
+        unset($_SESSION['behalf_id']);
+        unset($_SESSION['behalf_name']);
+      }
+      // If we deleted our own account, do housekeeping and logout.
+      if ($user->id == $user_id) {
+       // Remove tt_login cookie that stores login name.
+       unset($_COOKIE['tt_login']);
+       setcookie('tt_login', NULL, -1);
+               
+        $auth->doLogout();
+        header('Location: login.php');
+      } else {
+       header('Location: users.php');
+      }
+      exit();
+    } else {
+      $errors->add($i18n->getKey('error.db'));
+    }
+  }
+  if ($request->getParameter('btn_cancel')) {
+    header('Location: users.php');
+    exit();
+  }
+}
+
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.delete_user'));
+$smarty->assign('content_page_name', 'user_delete.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/user_edit.php b/user_edit.php
new file mode 100644 (file)
index 0000000..e51142e
--- /dev/null
@@ -0,0 +1,238 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttProjectHelper');
+import('ttTeamHelper');
+import('ttUserHelper');
+import('form.Table');
+import('form.TableColumn');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Get user id we are editing from the request.
+$user_id = (int) $request->getParameter('id');
+
+// Get user details.
+$user_details = ttUserHelper::getUserDetails($user_id);
+
+// Security checks.
+$ok_to_go = $user->canManageTeam(); // Are we authorized for user management?
+if ($ok_to_go) $ok_to_go = $ok_to_go && $user_details; // Are we editing a real user?
+if ($ok_to_go) $ok_to_go = $ok_to_go && ($user->team_id == $user_details['team_id']); // User belongs to our team?
+if ($ok_to_go && $user->isCoManager() && (ROLE_COMANAGER == $user_details['role']))
+  $ok_to_go = ($user->id == $user_details['id']); // Comanager is not allowed to edit other comanagers.
+if ($ok_to_go && $user->isCoManager() && (ROLE_MANAGER == $user_details['role']))
+  $ok_to_go = false; // Comanager is not allowed to edit a manager.
+if (!$ok_to_go) {
+  die ($i18n->getKey('error.sys'));
+}
+
+if (in_array('cl', explode(',', $user->plugins)))
+  $clients = ttTeamHelper::getActiveClients($user->team_id);
+
+$projects = ttTeamHelper::getActiveProjects($user->team_id);
+$assigned_projects = array();
+
+if ($request->getMethod() == 'POST') {
+  $cl_name = trim($request->getParameter('name'));
+  $cl_login = trim($request->getParameter('login'));
+  if (!$auth->isPasswordExternal()) {
+    $cl_password1 = $request->getParameter('pas1');
+    $cl_password2 = $request->getParameter('pas2');
+  }
+  $cl_email = trim($request->getParameter('email'));
+  $cl_role = $request->getParameter('role');
+  $cl_client_id = $request->getParameter('client');
+  $cl_status = $request->getParameter('status');  
+  $cl_rate = $request->getParameter('rate');
+  $cl_projects = $request->getParameter('projects');
+  if (is_array($cl_projects)) {
+    foreach ($cl_projects as $p) {
+      if (ttValidFloat($request->getParameter('rate_'.$p), true)) {
+        $project_with_rate = array();
+        $project_with_rate['id'] = $p;
+        $project_with_rate['rate'] = $request->getParameter('rate_'.$p);
+        $assigned_projects[] = $project_with_rate;
+      } else
+        $errors->add($i18n->getKey('error.field'), 'rate_'.$p);
+    }
+  }
+} else {
+  $cl_name = $user_details['name'];
+  $cl_login = $user_details['login'];
+  $cl_email = $user_details['email'];
+  $cl_rate = str_replace('.', $user->decimal_mark, $user_details['rate']);
+  $cl_role = $user_details['role'];
+  $cl_client_id = $user_details['client_id'];
+  $cl_status = $user_details['status'];
+  $cl_projects = array();
+  $assigned_projects = ttProjectHelper::getAssignedProjects($user_id);
+  foreach($assigned_projects as $p) {
+    $cl_projects[] = $p['id'];
+  }
+}
+
+$form = new Form('userForm');
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'name','style'=>'width: 300px;','value'=>$cl_name));
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'login','style'=>'width: 300px;','value'=>$cl_login));
+if (!$auth->isPasswordExternal()) {
+  $form->addInput(array('type'=>'text','maxlength'=>'30','name'=>'pas1','aspassword'=>true,'value'=>$cl_password1));
+  $form->addInput(array('type'=>'text','maxlength'=>'30','name'=>'pas2','aspassword'=>true,'value'=>$cl_password2));
+}
+$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'email','style'=>'width: 300px;','value'=>$cl_email));
+
+$roles[ROLE_USER] = $i18n->getKey('label.user');
+$roles[ROLE_COMANAGER] = $i18n->getKey('form.users.comanager');
+if (in_array('cl', explode(',', $user->plugins)))
+  $roles[ROLE_CLIENT] = $i18n->getKey('label.client');
+$form->addInput(array('type'=>'combobox','onchange'=>'handleClientControl()','name'=>'role','value'=>$cl_role,'data'=>$roles));
+if (in_array('cl', explode(',', $user->plugins)))
+  $form->addInput(array('type'=>'combobox','name'=>'client','value'=>$cl_client_id,'data'=>$clients,'datakeys'=>array('id', 'name'),'empty'=>array(''=>$i18n->getKey('dropdown.select'))));
+
+$form->addInput(array('type'=>'combobox','name'=>'status','value'=>$cl_status,
+  'data'=>array(ACTIVE=>$i18n->getKey('dropdown.status_active'),INACTIVE=>$i18n->getKey('dropdown.status_inactive'))));
+$form->addInput(array('type'=>'floatfield','maxlength'=>'10','name'=>'rate','format'=>'.2','value'=>$cl_rate));
+
+// Define classes for the projects table.
+class NameCellRenderer extends DefaultCellRenderer {
+  function render(&$table, $value, $row, $column, $selected = false) {
+    $this->setOptions(array('width'=>200,'valign'=>'top'));
+    $this->setValue('<label for = "'.$table->getName().'_'.$row.'">'.htmlspecialchars($value).'</label>');
+    return $this->toString();
+  }
+}
+class RateCellRenderer extends DefaultCellRenderer {
+  function render(&$table, $value, $row, $column, $selected = false) {
+    global $assigned_projects;
+    $field = new FloatField('rate_'.$table->getValueAtName($row,'id'), $table->getValueAtName($row, 'p_rate'));
+    $field->setFormName($table->getFormName());
+    $field->setLocalization($GLOBALS['I18N']);
+    $field->setSize(5);
+    $field->setFormat('.2');
+    foreach ($assigned_projects as $p) {
+      if ($p['id'] == $table->getValueAtName($row,'id')) $field->setValue($p['rate']);
+    }
+    $this->setValue($field->toStringControl());
+    return $this->toString();
+  }
+}
+// Create projects table.
+$table = new Table('projects');
+$table->setIAScript('setRate');
+$table->setTableOptions(array('width'=>'100%','cellspacing'=>'1','cellpadding'=>'3','border'=>'0'));
+$table->setRowOptions(array('valign'=>'top','class'=>'tableHeader'));
+$table->setData($projects);
+$table->setKeyField('id');
+$table->setValue($cl_projects);
+$table->addColumn(new TableColumn('name', $i18n->getKey('label.project'), new NameCellRenderer()));
+$table->addColumn(new TableColumn('p_rate', $i18n->getKey('form.users.rate'), new RateCellRenderer()));
+$form->addInputElement($table);
+
+$form->addInput(array('type'=>'hidden','name'=>'id','value'=>$user_id));
+$form->addInput(array('type'=>'submit','name'=>'btn_submit','value'=>$i18n->getKey('button.save')));
+
+if ($request->getMethod() == 'POST') {
+  // Validate user input.
+  if (!ttValidString($cl_name)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.person_name'));
+  if (!ttValidString($cl_login)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.login'));
+  if (!$auth->isPasswordExternal() && ($cl_password1 || $cl_password2)) {
+       if (!ttValidString($cl_password1)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.password'));
+       if (!ttValidString($cl_password2)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.confirm_password'));
+    if ($cl_password1 !== $cl_password2)
+      $errors->add($i18n->getKey('error.not_equal'), $i18n->getKey('label.password'), $i18n->getKey('label.confirm_password'));
+  }    
+  if (!ttValidEmail($cl_email, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('label.email'));
+  if (!ttValidFloat($cl_rate, true)) $errors->add($i18n->getKey('error.field'), $i18n->getKey('form.users.default_rate'));
+
+  if ($errors->isEmpty()) {
+    $existing_user = ttUserHelper::getUserByLogin($cl_login);
+    if (!$existing_user || ($user_id == $existing_user['id'])) {
+
+      $fields = array(
+        'name' => $cl_name,
+        'login' => $cl_login,
+        'password' => $cl_password1,
+        'email' => $cl_email,
+        'status' => $cl_status,
+        'rate' => $cl_rate,
+        'projects' => $assigned_projects);
+      if (right_assign_roles & $user->rights) {
+       $fields['role'] = $cl_role;
+       $fields['client_id'] = $cl_client_id;
+      }
+
+      if (ttUserHelper::update($user_id, $fields)) {
+
+        // If our own login changed, set new one in cookie to remember it.
+        if (($user_id == $user->id) && ($user->login != $cl_login)) {
+          setcookie('tt_login', $cl_login, time() + COOKIE_EXPIRE, '/');
+        }
+      
+        // In case the name of the "on behalf" user has changed - set it in session.
+        if (($user->behalf_id == $user_id) && ($user->behalf_name != $cl_name)) {
+          $_SESSION['behalf_name'] = $cl_name;
+        }
+        
+        // If we deactivated our own account, do housekeeping and logout.
+        if ($user->id == $user_id && !is_null($cl_status) && $cl_status == INACTIVE) {
+         // Remove tt_login cookie that stores login name.
+         unset($_COOKIE['tt_login']);
+         setcookie('tt_login', NULL, -1);
+               
+          $auth->doLogout();
+          header('Location: login.php');
+          exit();
+        }
+        
+        header('Location: users.php');
+        exit();
+
+      } else
+        $errors->add($i18n->getKey('error.db'));
+    } else
+      $errors->add($i18n->getKey('error.user_exists'));
+  }
+} // post
+
+$rates = ttProjectHelper::getRates($user_id);
+$smarty->assign('rates', $rates);
+
+$smarty->assign('auth_external', $auth->isPasswordExternal());
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('onload', 'onLoad="document.userForm.name.focus();handleClientControl();"');
+$smarty->assign('user_id', $user_id);
+$smarty->assign('title', $i18n->getKey('title.edit_user'));
+$smarty->assign('content_page_name', 'user_edit.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file
diff --git a/users.php b/users.php
new file mode 100644 (file)
index 0000000..9de7223
--- /dev/null
+++ b/users.php
@@ -0,0 +1,52 @@
+<?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
+// +----------------------------------------------------------------------+
+
+require_once('initialize.php');
+import('form.Form');
+import('ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_data_entry)) {
+  header('Location: access_denied.php');
+  exit();
+}
+
+// Get users.
+$active_users = ttTeamHelper::getActiveUsers(array('getAllFields'=>true));
+if($user->canManageTeam()) {
+  $can_delete_manager = (1 == count($active_users));
+  $inactive_users = ttTeamHelper::getInactiveUsers($user->team_id, true);
+}
+
+$smarty->assign('active_users', $active_users);
+$smarty->assign('inactive_users', $inactive_users);
+$smarty->assign('can_delete_manager', $can_delete_manager);
+$smarty->assign('title', $i18n->getKey('title.users'));
+$smarty->assign('content_page_name', 'users.tpl');
+$smarty->display('index.tpl');
+?>
\ No newline at end of file