From 098a79f0819ebb89b7d48df4a6b154af4560f68e Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sun, 28 Feb 2016 19:39:25 -0800 Subject: [PATCH] Work in progress creating a repo Added files in root dir, subdirectories remain... --- charts.php | 230 ++++++++++++++ client_add.php | 89 ++++++ client_delete.php | 76 +++++ client_edit.php | 121 +++++++ clients.php | 44 +++ cron.php | 84 +++++ dbinstall.php | 675 ++++++++++++++++++++++++++++++++++++++++ default.css | 135 ++++++++ expense_delete.php | 90 ++++++ expense_edit.php | 218 +++++++++++++ expenses.php | 206 ++++++++++++ export.php | 84 +++++ favicon.ico | Bin 0 -> 894 bytes import.php | 56 ++++ index.php | 81 +++++ initialize.php | 251 +++++++++++++++ invoice_add.php | 99 ++++++ invoice_delete.php | 74 +++++ invoice_send.php | 102 ++++++ invoice_view.php | 90 ++++++ invoices.php | 45 +++ license.txt | 20 ++ login.php | 96 ++++++ logout.php | 34 ++ mysql.sql | 339 ++++++++++++++++++++ notification_add.php | 94 ++++++ notification_delete.php | 70 +++++ notification_edit.php | 100 ++++++ notifications.php | 57 ++++ password_change.php | 97 ++++++ password_reset.php | 120 +++++++ phpinfo.php | 4 + profile_edit.php | 271 ++++++++++++++++ project_add.php | 96 ++++++ project_delete.php | 70 +++++ project_edit.php | 134 ++++++++ projects.php | 50 +++ readme.txt | 63 ++++ register.php | 110 +++++++ report.php | 123 ++++++++ report_send.php | 99 ++++++ reports.php | 334 ++++++++++++++++++++ rtl.css | 5 + task_add.php | 85 +++++ task_delete.php | 70 +++++ task_edit.php | 115 +++++++ tasks.php | 44 +++ time.php | 374 ++++++++++++++++++++++ time_delete.php | 104 +++++++ time_edit.php | 410 ++++++++++++++++++++++++ tofile.php | 228 ++++++++++++++ user_add.php | 175 +++++++++++ user_delete.php | 100 ++++++ user_edit.php | 238 ++++++++++++++ users.php | 52 ++++ 55 files changed, 7231 insertions(+) create mode 100644 charts.php create mode 100644 client_add.php create mode 100644 client_delete.php create mode 100644 client_edit.php create mode 100644 clients.php create mode 100644 cron.php create mode 100644 dbinstall.php create mode 100644 default.css create mode 100644 expense_delete.php create mode 100644 expense_edit.php create mode 100644 expenses.php create mode 100644 export.php create mode 100644 favicon.ico create mode 100644 import.php create mode 100644 index.php create mode 100644 initialize.php create mode 100644 invoice_add.php create mode 100644 invoice_delete.php create mode 100644 invoice_send.php create mode 100644 invoice_view.php create mode 100644 invoices.php create mode 100644 license.txt create mode 100644 login.php create mode 100644 logout.php create mode 100644 mysql.sql create mode 100644 notification_add.php create mode 100644 notification_delete.php create mode 100644 notification_edit.php create mode 100644 notifications.php create mode 100644 password_change.php create mode 100644 password_reset.php create mode 100644 phpinfo.php create mode 100644 profile_edit.php create mode 100644 project_add.php create mode 100644 project_delete.php create mode 100644 project_edit.php create mode 100644 projects.php create mode 100644 readme.txt create mode 100644 register.php create mode 100644 report.php create mode 100644 report_send.php create mode 100644 reports.php create mode 100644 rtl.css create mode 100644 task_add.php create mode 100644 task_delete.php create mode 100644 task_edit.php create mode 100644 tasks.php create mode 100644 time.php create mode 100644 time_delete.php create mode 100644 time_edit.php create mode 100644 tofile.php create mode 100644 user_add.php create mode 100644 user_delete.php create mode 100644 user_edit.php create mode 100644 users.php diff --git a/charts.php b/charts.php new file mode 100644 index 00000000..5e26978e --- /dev/null +++ b/charts.php @@ -0,0 +1,230 @@ +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 index 00000000..503b9403 --- /dev/null +++ b/client_add.php @@ -0,0 +1,89 @@ +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 index 00000000..d03ef9c9 --- /dev/null +++ b/client_delete.php @@ -0,0 +1,76 @@ +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 index 00000000..400286d0 --- /dev/null +++ b/client_edit.php @@ -0,0 +1,121 @@ +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 index 00000000..61c66583 --- /dev/null +++ b/clients.php @@ -0,0 +1,44 @@ +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 index 00000000..22b4991d --- /dev/null +++ b/cron.php @@ -0,0 +1,84 @@ += 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']."
"; + else + echo "Error while emailing report...
"; + + // 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 index 00000000..c65358a8 --- /dev/null +++ b/dbinstall.php @@ -0,0 +1,675 @@ +\n"); +} +require_once('initialize.php'); +import('ttUserHelper'); +import('ttTaskHelper'); + +function setChange($sql) { + print "
".$sql."
"; + $mdb2 = getConnection(); + $affected = $mdb2->exec($sql); + if (is_a($affected, 'PEAR_Error')) { + print "error: ".$affected->getMessage()."
"; + } else { + print "successful update
\n"; + } +} + + + if ($_POST) { + print "Processing...
\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...
\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...
\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...
\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...
\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...
\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...
\n"; + } + + if ($_POST["cleanup"]) { + + $mdb2 = getConnection(); + $inactive_teams = ttTeamHelper::getInactiveTeams(); + + $count = count($inactive_teams); + print "$count inactive teams found...
\n"; + for ($i = 0; $i < $count; $i++) { + print " deleting team ".$inactive_teams[$i]."
\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.
\n"; +} +?> + + +
+
+

DB Install

+ + + + +
Create database structure (v1.9) +
(applies only to new installations, do not execute when updating)
+ +

Updates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Update database structure (v0.5 to v0.7)
Update database structure (v0.7 to v1.3.3)
Update database structure (v1.3.3 to v1.3.40)
Update database structure (v1.3.40 to v1.4.85)
Update database structure (v1.4.85 to v1.5.79)
Update database structure (v1.5.79 to v1.6)


Update database structure (v1.6 to v1.9)
+ +

DB Maintenance

+ + + + +
Clean up DB from inactive teams
+ +
+
+ + \ No newline at end of file diff --git a/default.css b/default.css new file mode 100644 index 00000000..da54b7e2 --- /dev/null +++ b/default.css @@ -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 index 00000000..e8828e0c --- /dev/null +++ b/expense_delete.php @@ -0,0 +1,90 @@ +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 index 00000000..f9c94dd6 --- /dev/null +++ b/expense_edit.php @@ -0,0 +1,218 @@ +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 index 00000000..9a08375b --- /dev/null +++ b/expenses.php @@ -0,0 +1,206 @@ +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 index 00000000..4bb9db3d --- /dev/null +++ b/export.php @@ -0,0 +1,84 @@ +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 index 0000000000000000000000000000000000000000..56e40077ef472a6c9a1d73153ab5aa3f36c2917e GIT binary patch literal 894 zcmbVL%}*0i5Pzmb11OM?7)fgQIQa)~6AqGKXyQL0F+xac^kCwNlUHLP9@GduP%BX& zslcTb4q%IXL>ek>sU@T>ZMUhk-4B6uxBKO`<7}4-ax%{Dyf<&&Z|2SW%`PDq@M&ry zI9?+UO@v$}gxo+y7imM){Z;8hL)WT6MM?m?40&EkFyc;{_QkTEU}AnPG_$y7pL4%? zH~I3_>$em3S?5OskE5&tE2tTckLHep6!SHf_e9dJKze#JHsKAA%&v^uKD-#SA<}Gy z!9hJvkbypyQj8EeV7C+WQXuPa{g~fMykFb3dH2RV(dW~j7Z#wi69|Ek5j{jyDj}?} zo8=xo;V)h)KYsdiZYw^y8XjBPc{b)qIP+JQe2Y3-YC4VG+*($VklrYaNi?|q~ zW_|mP)ySyJZ{Q0BXl{o7eqb0FABUUW<;z#}LvP=a*T2)=Naj;8>GbW}SHcEfRduk* z1l`@x)dkiEaQ9xhrUuqSg%Hver@i5{JCJhvVg_DO!0(6lb{H5i9MES0^YwD(uo%l7 zg(zk-p7(?)cYr$Cu1pRGw6uVx>2E^=+#dw4sH8bzFMAkBA;a0_oxkv<6129$%uH!x zLoa@L86*W5K}oUv?!n=9lG%vn&gMsZco=$mz-ooYM!-v9h9WjakSS#NfcuuD&*72M zjt;1=2aDxo#|U8sk-|P^*mHPX4h`wWOii8cXvQhLlw|l*Z`DFd>*@eIa#oN5SaddInput(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 index 00000000..d390ebfa --- /dev/null +++ b/index.php @@ -0,0 +1,81 @@ +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) { +?> + + + + + + + + + + + + + + + + + diff --git a/initialize.php b/initialize.php new file mode 100644 index 00000000..ce6acdfc --- /dev/null +++ b/initialize.php @@ -0,0 +1,251 @@ +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 index 00000000..4a25ddc1 --- /dev/null +++ b/invoice_add.php @@ -0,0 +1,99 @@ +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 index 00000000..9fb7c435 --- /dev/null +++ b/invoice_delete.php @@ -0,0 +1,74 @@ +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 index 00000000..1dd2323a --- /dev/null +++ b/invoice_send.php @@ -0,0 +1,102 @@ +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 index 00000000..a91ddf1b --- /dev/null +++ b/invoice_view.php @@ -0,0 +1,90 @@ +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 index 00000000..f495292e --- /dev/null +++ b/invoices.php @@ -0,0 +1,45 @@ +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 index 00000000..eaa424c6 --- /dev/null +++ b/license.txt @@ -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 index 00000000..c9ddc817 --- /dev/null +++ b/login.php @@ -0,0 +1,96 @@ +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 index 00000000..adbcd54d --- /dev/null +++ b/logout.php @@ -0,0 +1,34 @@ +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 index 00000000..3e283255 --- /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 index 00000000..4d1b457c --- /dev/null +++ b/notification_add.php @@ -0,0 +1,94 @@ +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 index 00000000..62f11ef8 --- /dev/null +++ b/notification_delete.php @@ -0,0 +1,70 @@ +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 index 00000000..e029c6ca --- /dev/null +++ b/notification_edit.php @@ -0,0 +1,100 @@ +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 index 00000000..5f2f6d2f --- /dev/null +++ b/notifications.php @@ -0,0 +1,57 @@ +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 index 00000000..82ba11a4 --- /dev/null +++ b/password_change.php @@ -0,0 +1,97 @@ +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 index 00000000..ccb19606 --- /dev/null +++ b/password_reset.php @@ -0,0 +1,120 @@ +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 index 00000000..c1b42ed6 --- /dev/null +++ b/phpinfo.php @@ -0,0 +1,4 @@ + + diff --git a/profile_edit.php b/profile_edit.php new file mode 100644 index 00000000..ebee59d5 --- /dev/null +++ b/profile_edit.php @@ -0,0 +1,271 @@ +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("date_format_preview", 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("time_format_preview", 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 index 00000000..2bdb77bb --- /dev/null +++ b/project_add.php @@ -0,0 +1,96 @@ +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 index 00000000..c49841b6 --- /dev/null +++ b/project_delete.php @@ -0,0 +1,70 @@ +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 index 00000000..2726005c --- /dev/null +++ b/project_edit.php @@ -0,0 +1,134 @@ +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 index 00000000..8f1ce5c6 --- /dev/null +++ b/projects.php @@ -0,0 +1,50 @@ +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 index 00000000..437d4c3f --- /dev/null +++ b/readme.txt @@ -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 index 00000000..67f7d108 --- /dev/null +++ b/register.php @@ -0,0 +1,110 @@ +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 index 00000000..5ed7108d --- /dev/null +++ b/report.php @@ -0,0 +1,123 @@ +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 index 00000000..18e731e8 --- /dev/null +++ b/report_send.php @@ -0,0 +1,99 @@ +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 index 00000000..9e18a0b0 --- /dev/null +++ b/reports.php @@ -0,0 +1,334 @@ +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 index 00000000..ede511c8 --- /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 index 00000000..b37f1065 --- /dev/null +++ b/task_add.php @@ -0,0 +1,85 @@ +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 index 00000000..3329264b --- /dev/null +++ b/task_delete.php @@ -0,0 +1,70 @@ +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 index 00000000..3a03d428 --- /dev/null +++ b/task_edit.php @@ -0,0 +1,115 @@ +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 index 00000000..6258831c --- /dev/null +++ b/tasks.php @@ -0,0 +1,44 @@ +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 index 00000000..a6151124 --- /dev/null +++ b/time.php @@ -0,0 +1,374 @@ +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')." ".$i18n->getKey('error.goto_uncompleted').""); + } + + // 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 index 00000000..7125a57e --- /dev/null +++ b/time_delete.php @@ -0,0 +1,104 @@ +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 index 00000000..4c255dbe --- /dev/null +++ b/time_edit.php @@ -0,0 +1,410 @@ +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')." ".$i18n->getKey('error.goto_uncompleted').""); + } + } + } + + // 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')." ".$i18n->getKey('error.goto_uncompleted').""); + } + } + } + + // 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 index 00000000..3d264e0d --- /dev/null +++ b/tofile.php @@ -0,0 +1,228 @@ +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 "\n"; + print "\n"; + + $group_by = $bean->getAttribute('group_by'); + if ($totals_only) { + // Totals only report. Print subtotals. + foreach ($subtotals as $subtotal) { + print "\n"; + print "\t<".$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\n"; + } + if ($bean->getAttribute('chcost')) { + print "\tcanManageTeam() || $user->isClient()) + print $subtotal['cost']; + else + print $subtotal['expenses']; + print "]]>\n"; + } + print "\n"; + } + } else { + // Normal report. + foreach ($items as $item) { + print "\n"; + + print "\t\n"; + if ($user->canManageTeam() || $user->isClient()) print "\t\n"; + if ($bean->getAttribute('chclient')) print "\t\n"; + if ($bean->getAttribute('chproject')) print "\t\n"; + if ($bean->getAttribute('chtask')) print "\t\n"; + if ($bean->getAttribute('chcf_1')) print "\t\n"; + if ($bean->getAttribute('chstart')) print "\t\n"; + if ($bean->getAttribute('chfinish')) print "\t\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\n"; + } + if ($bean->getAttribute('chnote')) print "\t\n"; + if ($bean->getAttribute('chcost')) { + print "\tcanManageTeam() || $user->isClient()) + print $item['cost']; + else + print $item['expense']; + print "]]>\n"; + } + if ($bean->getAttribute('chinvoice')) print "\t\n"; + + print "\n"; + } + } + + print ""; +} + +// 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 index 00000000..cf2fe76b --- /dev/null +++ b/user_add.php @@ -0,0 +1,175 @@ +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(''); + 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 index 00000000..aed2746a --- /dev/null +++ b/user_delete.php @@ -0,0 +1,100 @@ +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 index 00000000..e51142e1 --- /dev/null +++ b/user_edit.php @@ -0,0 +1,238 @@ +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(''); + 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 index 00000000..9de72230 --- /dev/null +++ b/users.php @@ -0,0 +1,52 @@ +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 -- 2.20.1