├── assets └── css │ └── timetrackingeditor.css ├── Schema ├── Mysql.php ├── Postgres.php └── Sqlite.php ├── Test ├── PluginTest.php └── TimetrackingeditorTest.php ├── Filter ├── SubtaskIdFilter.php ├── SubtaskTasksFilter.php └── SubtaskTitleFilter.php ├── Console ├── AllSubtaskTimeTrackingExportCommand.php └── SubtaskTimeTrackingExportCommand.php ├── Template ├── menu.php ├── remove.php ├── stop.php ├── start.php ├── edit.php ├── create.php ├── time_tracking_editor.php └── subtask │ └── table.php ├── Formatter └── SubtaskAutoCompleteFormatter.php ├── Controller ├── SubtaskStatusController.php ├── SubtaskAjaxController.php └── TimeTrackingEditorController.php ├── Model ├── SubtaskTimeTrackingCreationModel.php ├── SubtaskTimeTrackingModel.php └── SubtaskTimeTrackingEditModel.php ├── README.md ├── Validator └── SubtaskTimeTrackingValidator.php ├── Plugin.php ├── Html.php └── Export └── SubtaskTimeTrackingExport.php /assets/css/timetrackingeditor.css: -------------------------------------------------------------------------------- 1 | .right { 2 | text-align: right; 3 | } 4 | -------------------------------------------------------------------------------- /Schema/Mysql.php: -------------------------------------------------------------------------------- 1 | exec("ALTER TABLE subtasks add time_billable INT default 0"); 10 | } 11 | 12 | function version_1($pdo) 13 | { 14 | $pdo->exec("ALTER TABLE subtask_time_tracking add comment TEXT"); 15 | $pdo->exec("ALTER TABLE subtask_time_tracking add is_billable TINYINT"); 16 | } 17 | -------------------------------------------------------------------------------- /Schema/Postgres.php: -------------------------------------------------------------------------------- 1 | exec("ALTER TABLE subtasks add time_billable INTEGER default 0"); 10 | } 11 | 12 | function version_1($pdo) 13 | { 14 | $pdo->exec("ALTER TABLE subtask_time_tracking add comment TEXT"); 15 | $pdo->exec("ALTER TABLE subtask_time_tracking add is_billable BOOLEAN"); 16 | } 17 | -------------------------------------------------------------------------------- /Schema/Sqlite.php: -------------------------------------------------------------------------------- 1 | exec("ALTER TABLE subtasks add time_billable NUMERIC default 0"); 10 | } 11 | 12 | function version_1($pdo) 13 | { 14 | $pdo->exec("ALTER TABLE subtask_time_tracking add comment TEXT"); 15 | $pdo->exec("ALTER TABLE subtask_time_tracking add is_billable INTEGER"); 16 | } 17 | -------------------------------------------------------------------------------- /Test/PluginTest.php: -------------------------------------------------------------------------------- 1 | container); 9 | $this->assertSame(null, $plugin->initialize()); 10 | $this->assertSame(null, $plugin->onStartup()); 11 | $this->assertNotEmpty($plugin->getPluginName()); 12 | $this->assertNotEmpty($plugin->getPluginDescription()); 13 | $this->assertNotEmpty($plugin->getPluginAuthor()); 14 | $this->assertNotEmpty($plugin->getPluginVersion()); 15 | $this->assertNotEmpty($plugin->getPluginHomepage()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Filter/SubtaskIdFilter.php: -------------------------------------------------------------------------------- 1 | query->eq(SubtaskModel::TABLE.'.id', $this->value); 37 | return $this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Console/AllSubtaskTimeTrackingExportCommand.php: -------------------------------------------------------------------------------- 1 | setName('export:allsubtaskstimetracking') 18 | ->setDescription('Subtasks Time Tracking CSV export for all events'); 19 | } 20 | 21 | protected function execute(InputInterface $input, OutputInterface $output) 22 | { 23 | $data = $this->subtaskTimeTrackingExport->exportAll(); 24 | 25 | if (is_array($data)) { 26 | Html::output($data); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Template/menu.php: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /Template/remove.php: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | 8 | 13 |
14 | 15 |
16 | url->link(t('Yes'), 'TimeTrackingEditorController', 'remove', array('plugin' => 'timetrackingeditor', 'id' => $timetracking['id'], 'project_id' => $timetracking['project_id'], 'subtask_id' => $timetracking['subtask_id']), true, 'btn btn-red') ?> 17 | 18 | url->link(t('cancel'), 'TimeTrackingEditorController', 'show', array('plugin' => 'timetrackingeditor','id' => $timetracking['id'], 'project_id' => $timetracking['project_id']), false, 'close-popover') ?> 19 |
20 |
21 | -------------------------------------------------------------------------------- /Template/stop.php: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 | form->csrf() ?> 7 | 8 | form->hidden('project_id', $values) ?> 9 | form->hidden('task_id', $values) ?> 10 | form->hidden('subtask_id', $values) ?> 11 | 12 | 13 | 14 | 15 | form->label(t('Comment'), 'comment') ?> 16 | form->textarea('comment', $values, $errors, array(), 'markdown-editor') ?> 17 | 18 | form->checkbox('is_billable', t('Billable?'), 1, isset($values['is_billable']) && $values['is_billable'] == 1) ?> 19 | 20 |
21 | modal->submitButtons() ?> 22 |
23 |
24 | -------------------------------------------------------------------------------- /Template/start.php: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 | form->csrf() ?> 7 | 8 | form->hidden('project_id', $values) ?> 9 | form->hidden('task_id', $values) ?> 10 | form->hidden('subtask_id', $values) ?> 11 | 12 | 13 | 14 | 15 | form->label(t('Comment'), 'comment') ?> 16 | form->textarea('comment', $values, $errors, array(), 'markdown-editor') ?> 17 | 18 | form->checkbox('is_billable', t('Billable?'), 1, isset($values['is_billable']) && $values['is_billable'] == 1) ?> 19 | 20 |
21 | modal->submitButtons() ?> 22 |
23 |
24 | -------------------------------------------------------------------------------- /Formatter/SubtaskAutoCompleteFormatter.php: -------------------------------------------------------------------------------- 1 | query->columns( 28 | SubtaskModel::TABLE.'.id', 29 | SubtaskModel::TABLE.'.title' 30 | )->asc(SubtaskModel::TABLE.'.id')->findAll(); 31 | 32 | foreach ($subtasks as &$subtask) { 33 | $subtask['value'] = $subtask['title']; 34 | $subtask['label'] = ' > #'.$subtask['id'].' '.$subtask['title']; 35 | } 36 | 37 | return $subtasks; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Filter/SubtaskTasksFilter.php: -------------------------------------------------------------------------------- 1 | value) || ctype_digit($this->value)) { 39 | $this->query->eq(SubtaskModel::TABLE.'.task_id', $this->value); 40 | } else { 41 | $this->query->ilike(TaskModel::TABLE.'.name', $this->value); 42 | } 43 | 44 | return $this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Test/TimetrackingeditorTest.php: -------------------------------------------------------------------------------- 1 | 2 | container); 24 | $plugin->scan(); 25 | } 26 | 27 | public function testAll() 28 | { 29 | $this->assertCreateTeamProject(); 30 | 31 | $this->task_id = $this->app->createTask(array('project_id' => $this->projectId, 'title' => 'Task 1')); 32 | $this->subtask_id = $this->app->createSubTask(array('project_id' => $this->projectId, 'task_id' => $this->task_id, 'title' => 'Subtask 1')); 33 | 34 | $this->assertNotFalse($this->task_id); 35 | $this->assertNotFalse($this->subtask_id); 36 | 37 | $this->testCreateEntry(); 38 | } 39 | 40 | public function testCreateEntry() 41 | { 42 | 43 | 44 | 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Console/SubtaskTimeTrackingExportCommand.php: -------------------------------------------------------------------------------- 1 | setName('export:subtaskstimetracking') 17 | ->setDescription('Subtasks Time Tracking CSV export') 18 | ->addArgument('project_id', InputArgument::REQUIRED, 'Project id') 19 | ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)') 20 | ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)'); 21 | } 22 | 23 | protected function execute(InputInterface $input, OutputInterface $output) 24 | { 25 | $data = $this->subtaskTimeTrackingExport->export( 26 | $input->getArgument('project_id'), 27 | $input->getArgument('start_date'), 28 | $input->getArgument('end_date') 29 | ); 30 | 31 | if (is_array($data)) { 32 | Html::output($data); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Filter/SubtaskTitleFilter.php: -------------------------------------------------------------------------------- 1 | value) || (strlen($this->value) > 1 && $this->value{0} === '#' && ctype_digit(substr($this->value, 1)))) { 37 | $this->query->beginOr(); 38 | $this->query->eq(SubtaskModel::TABLE.'.id', str_replace('#', '', $this->value)); 39 | $this->query->ilike(SubtaskModel::TABLE.'.title', '%'.$this->value.'%'); 40 | $this->query->closeOr(); 41 | } else { 42 | $this->query->ilike(SubtaskModel::TABLE.'.title', '%'.$this->value.'%'); 43 | } 44 | 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Controller/SubtaskStatusController.php: -------------------------------------------------------------------------------- 1 | In Progress -> Done. 16 | */ 17 | public function change() 18 | { 19 | $task = $this->getTask(); 20 | $subtask = $this->getSubtask(); 21 | 22 | if ($subtask['status'] == SubtaskModel::STATUS_DONE) { 23 | $status = SubtaskModel::STATUS_TODO; 24 | } else { 25 | $status = SubtaskModel::STATUS_DONE; 26 | } 27 | $subtask['status'] = $status; 28 | $this->subtaskModel->update($subtask); 29 | 30 | if ($this->request->getIntegerParam('refresh-table') === 0) { 31 | 32 | $html = $this->helper->subtask->toggleStatus($subtask, $task['project_id']); 33 | } else { 34 | $html = $this->renderTable($task); 35 | } 36 | 37 | $this->response->html($html); 38 | } 39 | 40 | protected function renderTable(array $task) 41 | { 42 | return $this->template->render('subtask/table', array( 43 | 'task' => $task, 44 | 'subtasks' => $this->subtaskModel->getAll($task['id']), 45 | 'editable' => true, 46 | )); 47 | } 48 | } 49 | ?> 50 | -------------------------------------------------------------------------------- /Controller/SubtaskAjaxController.php: -------------------------------------------------------------------------------- 1 | request->getStringParam('term'); 29 | $task_id = $this->request->getIntegerParam('task_id'); 30 | 31 | $subtaskQuery = new QueryBuilder(); 32 | $subtaskQuery->withQuery($this->db 33 | ->table(SubtaskModel::TABLE) 34 | ->eq('task_id', $task_id) 35 | ->columns(SubtaskModel::TABLE.'*')); 36 | 37 | if (ctype_digit($search)) { 38 | $subtaskQuery->withFilter(new SubtaskIdFilter($search)); 39 | } else { 40 | $subtaskQuery->withFilter(new SubtaskTitleFilter($search)); 41 | } 42 | 43 | $this->response->json($subtaskQuery->format(new SubtaskAutoCompleteFormatter($this->container))); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Model/SubtaskTimeTrackingCreationModel.php: -------------------------------------------------------------------------------- 1 | prepare($values); 28 | $subtrackingid = $this->db->table(SubtaskTimeTrackingModel::TABLE)->persist($values); 29 | 30 | return (int) $subtrackingid; 31 | } 32 | 33 | /** 34 | * Prepare data 35 | * 36 | * @access public 37 | * @param array $values Form values 38 | */ 39 | public function prepare(array &$values) 40 | { 41 | if ($this->userSession->isLogged()) { 42 | $values['user_id'] = $this->userSession->getId(); 43 | } 44 | 45 | $values["subtask_id"] = $values["opposite_subtask_id"]; 46 | 47 | $this->helper->model->removeFields($values, array('project_id', 'task_id', 'opposite_subtask_id', 'subtask', 'add_another')); 48 | 49 | // Calculate end time 50 | $values = $this->dateParser->convert($values, array('start'), true); 51 | $values["end"] = $values["start"] + ($values['time_spent']*60*60); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Timetrackingeditor 2 | ================== 3 | 4 | 5 | ## Important Notice 6 | 7 | I am no longer using Kanboard, therefore i will no longer maintain or support the Timetrackingeditor. I twould be great if someone is interested in volunteering to maintain the project in the future. 8 | 9 | Allows manual adding and editon of Timetracking Entries 10 | 11 | Author 12 | ------ 13 | 14 | - Thomas Stinner 15 | 16 | Requirements 17 | ------------ 18 | 19 | - Kanboard >= 1.0.38 (not testet with older versions) 20 | 21 | 22 | Installation 23 | ------------ 24 | 25 | You have the choice between 3 methods: 26 | 27 | 1. Install the plugin from the Kanboard plugin manager in one click 28 | 2. Download the zip file and decompress everything under the directory `plugins/Timetrackingeditor` 29 | 3. Clone this repository into the folder `plugins/Timetrackingeditor` 30 | 31 | Note: Plugin folder is case-sensitive. 32 | 33 | 34 | Documentation 35 | ------------- 36 | 37 | With this plugin you are able to edit, remove and manually create entries in the Time Tracking Table. 38 | 39 | Just go to a subtask, select "Time Tracking" (only visible if you have entered either an estimate and/or time spent value for the Task). Now you have the opportunity to add/remove/delete entries. You are only allowed to remove and edit your own entries. 40 | 41 | You can also add comments to every Time Tracking entry and select if the time is billable or not. Entries that have been selected as billable have a shopping cart symbol. 42 | 43 | Additionally you can export all time tracking entries as an HTML table (which makes it easy to import to excel) using the command ``` kanboard export:allsubtaskstimetracking``` 44 | 45 | -------------------------------------------------------------------------------- /Template/edit.php: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 | form->csrf() ?> 7 | 8 | form->hidden('project_id', $values) ?> 9 | form->hidden('task_id', $values) ?> 10 | form->hidden('opposite_subtask_id', $values) ?> 11 | form->hidden('id', $values) ?> 12 | 13 | 14 | form->label(t('Subtask'), 'subtask') ?> 15 | form->text( 16 | 'subtask', 17 | $values, 18 | $errors, 19 | array( 20 | 'required', 21 | 'autofocus', 22 | 'placeholder="'.t('Start to type subtask title...').'"', 23 | 'title="'.t('Start to type subtask title...').'"', 24 | 'data-dst-field="opposite_subtask_id"', 25 | 'data-search-url="'.$this->url->href('SubtaskAjaxController', 'autocomplete', array('plugin' => 'timetrackingeditor', 'task_id' => $values['task_id'])).'"', 26 | ), 27 | 'autocomplete') ?> 28 | 29 | form->label(t('Start Date'), 'start') ?> 30 | form->text('start', $values, $errors, array('maxlength="10"', 'required'), 'form-date') ?> 31 | 32 | form->label(t('Time spent'), 'time_spent') ?> 33 | form->numeric('time_spent', $values, $errors, array('maxlength="10"', 'required'), 'form-numeric') ?> hours 34 | 35 | form->label(t('Comment'), 'comment') ?> 36 | form->textarea('comment', $values, $errors, array(), 'markdown-editor') ?> 37 | 38 | form->checkbox('is_billable', t('Billable?'), 1, $values['is_billable'] == 1) ?> 39 | 40 | modal->submitButtons(); ?> 41 |
42 | -------------------------------------------------------------------------------- /Template/create.php: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 | form->csrf() ?> 7 | 8 | form->hidden('project_id', $values) ?> 9 | form->hidden('task_id', $values) ?> 10 | form->hidden('opposite_subtask_id', $values) ?> 11 | 12 | form->label(t('Subtask'), 'subtask') ?> 13 | form->text( 14 | 'subtask', 15 | $values, 16 | $errors, 17 | array( 18 | 'required', 19 | (!isset($autofocus) || $autofocus == 'subtask' ? 'autofocus' : ''), 20 | 'placeholder="'.t('Start to type subtask title...').'"', 21 | 'title="'.t('Start to type subtask title...').'"', 22 | 'data-dst-field="opposite_subtask_id"', 23 | 'data-search-url="'.$this->url->href('SubtaskAjaxController', 'autocomplete', array('plugin' => 'timetrackingeditor', 'task_id' => $values['task_id'])).'"', 24 | ), 25 | 'autocomplete') ?> 26 | 27 | form->label(t('Time spent'), 'time_spent') ?> 28 | form->numeric('time_spent', $values, $errors, array('maxlength="10"', 'required', (isset($autofocus) && $autofocus == "time_spent" ? 'autofocus' : '')), 'form-numeric') ?> hours 29 | 30 | form->label(t('Start Date'), 'start') ?> 31 | form->text('start', $values, $errors, array('maxlength="10"', 'required'), 'form-date') ?> 32 | 33 | form->label(t('Comment'), 'comment') ?> 34 | form->textarea('comment', $values, $errors, array(), 'markdown-editor') ?> 35 | 36 | form->checkbox('is_billable', t('Billable?'), 1, isset($values['is_billable']) && $values['is_billable'] == 1) ?> 37 | form->checkbox('add_another', t('Add another event'), 1, isset($values['add_another']) && $values['add_another'] == 1) ?> 38 | 39 | modal->submitButtons(); ?> 40 | 41 |
42 | -------------------------------------------------------------------------------- /Validator/SubtaskTimeTrackingValidator.php: -------------------------------------------------------------------------------- 1 | commonValidationRules())); 30 | 31 | return array( 32 | $v->execute(), 33 | $v->getErrors() 34 | ); 35 | } 36 | 37 | /** 38 | * Validate modification 39 | * 40 | * @access public 41 | * @param array $values Form values 42 | * @return array $valid, $errors [0] = Success or not, [1] = List of errors 43 | */ 44 | public function validateModification(array $values) 45 | { 46 | $rules = array ( 47 | new Validators\Required('id', t('The Timetracking id is required')) 48 | ); 49 | 50 | $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); 51 | 52 | return array( 53 | $v->execute(), 54 | $v->getErrors() 55 | ); 56 | } 57 | 58 | /** 59 | * Common validation rules, valid for creation and modification 60 | * 61 | * @access private 62 | */ 63 | private function commonValidationRules() 64 | { 65 | $rules = array( 66 | new Validators\Required('task_id', t('The Task id is required')), 67 | new Validators\Required('opposite_subtask_id', t('The subtask is required')), 68 | new Validators\Required('start', t('The Start Date is required')), 69 | new Validators\Required('time_spent', t('The Time spent is required')), 70 | ); 71 | return $rules; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Template/time_tracking_editor.php: -------------------------------------------------------------------------------- 1 |
2 |

text->e($task['title']) ?>

3 |
4 | 5 | render('task/time_tracking_summary', array('task' => $task)) ?> 6 | 7 |

8 | 9 | modal->medium("plus",t('Add a new timetracking entry'), 'TimeTrackingEditorController', 10 | 'create', array( 11 | 'plugin' => 'timetrackingeditor', 12 | 'task_id' => $task['id'], 13 | 'project_id' => $task['project_id'])) ?> 14 | 15 | isEmpty()): ?> 16 |

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | getCollection() as $record): ?> 28 | 29 | 35 | 36 | 37 | 38 | 39 | 48 | 49 | 50 |
order(t('User'), 'username') ?>order(t('Subtask'), 'subtask_title') ?>order(t('Start'), 'start') ?>order(t('End'), 'end') ?>order(t('Time spent'), \Kanboard\Model\SubtaskTimeTrackingModel::TABLE.'.time_spent') ?>
url->link($this->text->e($record['user_fullname'] ?: $record['username']), 'UserViewController', 'show', array('user_id' => $record['user_id'])) ?> 30 | 31 | 32 | 33 | app->tooltipMarkdown($record['comment']) ?> 34 | dt->datetime($record['start']) ?>dt->datetime($record['end']) ?> 40 | user->isCurrentUser($record['user_id'])) { ?> 41 | render('timetrackingeditor:menu', array( 42 | 'task' => $task, 43 | 'subtask_id' => $record['subtask_id'], 44 | 'id' => $record['id'] 45 | )) ?> 46 | 47 |
51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | hook->on("template:layout:css", array("template" => "plugins/Timetrackingeditor/assets/css/timetrackingeditor.css")); 16 | $this->template->setTemplateOverride('task/time_tracking_details', 'timetrackingeditor:time_tracking_editor'); 17 | $this->template->setTemplateOverride('subtask/table', 'timetrackingeditor:subtask/table'); 18 | 19 | # $this->helper->register("subtask", "Kanboard\Plugin\Timetrackingeditor\Helper\SubtaskHelper"); 20 | 21 | $this->container["cli"]->add(new AllSubtaskTimeTrackingExportCommand($this->container)); 22 | } 23 | 24 | public function onStartup() 25 | { 26 | Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale'); 27 | } 28 | 29 | public function getClasses() 30 | { 31 | return array( 32 | 'Plugin\Timetrackingeditor\Model' => array( 33 | 'SubtaskTimeTrackingCreationModel', 34 | 'SubtaskTimeTrackingEditModel', 35 | 'SubtaskTimeTrackingModel', 36 | ), 37 | 'Plugin\Timetrackingeditor\Filter' => array( 38 | 'SubtaskFilter', 39 | 'SubtaskTaskFilter', 40 | 'SubtaskTitleFilter' 41 | ), 42 | 'Plugin\Timetrackingeditor\Console' => array( 43 | 'AllSubtaskTimeTrackingExportCommand' 44 | ), 45 | 'Plugin\Timetrackingeditor\Controller' => array( 46 | 'SubtaskStatusController', 47 | 'SubtaskAjaxController', 48 | 'TimeTrackingEditorController' 49 | ), 50 | 'Plugin\Timetrackingeditor\Export' => array( 51 | 'SubtaskTimeTrackingExport' 52 | ), 53 | 'Plugin\Timetrackingeditor\Validator' => array( 54 | 'SubtaskTimeTrackingValidator' 55 | ), 56 | 'Plugin\Timetrackingeditor\Formatter' => array( 57 | 'SubtaskAutoCompleteFormatter' 58 | ), 59 | ); 60 | } 61 | 62 | public function getPluginName() 63 | { 64 | return 'TimeTrackingEditor'; 65 | } 66 | 67 | public function getPluginDescription() 68 | { 69 | return t('Allows Editing of TimeTracking Values'); 70 | } 71 | 72 | public function getPluginAuthor() 73 | { 74 | return 'Thomas Stinner'; 75 | } 76 | 77 | public function getPluginVersion() 78 | { 79 | return '1.0.21'; 80 | } 81 | 82 | public function getPluginHomepage() 83 | { 84 | return 'https://github.com/stinnux/kanboard-timetrackingeditor'; 85 | } 86 | 87 | public function getCompatibleVersion() 88 | { 89 | return '>=1.2.4'; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Template/subtask/table.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 28 | 33 | 68 | 69 | 75 | 83 | 84 | 85 | 86 | 87 | 88 |
21 | 22 | 23 | subtask->renderToggleStatus($task, $subtask, "table") ?> 24 | 25 | subtask->getTitle($subtask) ?> 26 | 27 | 29 | 30 | text->e($subtask['name'] ?: $subtask['username']) ?> 31 | 32 | 34 |
    35 |
  • 36 | 37 | text->e($subtask['time_spent']).'h' ?> 38 | 39 | 40 | 41 | text->e($subtask['time_estimated']).'h' ?> 42 | 43 | 44 | text->e($subtask['time_billable']).'h' ?> 45 | 46 | 47 |
  • 48 | user->getId()): ?> 49 |
  • 50 | 51 | modal->medium("pause",t('Stop timer'), 'TimeTrackingEditorController', 'stop', 52 | array('plugin' => 'Timetrackingeditor', 53 | 'project_id' => $task['project_id'], 54 | 'task_id' => $subtask['task_id'], 55 | 'subtask_id' => $subtask['id'])) ?> 56 | (dt->age($subtask['timer_start_date']) ?>) 57 | 58 | modal->medium("play-circle-o",t('Start timer'), 'TimeTrackingEditorController', 'start', 59 | array('plugin' => 'Timetrackingeditor', 60 | 'project_id' => $task['project_id'], 61 | 'task_id' => $subtask['task_id'], 62 | 'subtask_id' => $subtask['id'])) ?> 63 | 64 |
  • 65 | 66 |
67 |
70 | render('subtask/menu', array( 71 | 'task' => $task, 72 | 'subtask' => $subtask, 73 | )) ?> 74 | 76 | modal->medium("clock-o", t('New'), 'TimeTrackingEditorController', 77 | 'create', array( 78 | 'plugin' => 'Timetrackingeditor', 79 | 'task_id' => $task['id'], 80 | 'project_id' => $task['project_id'], 81 | 'subtask_id' => $subtask['id'])) ?> 82 |
89 | 90 | -------------------------------------------------------------------------------- /Model/SubtaskTimeTrackingModel.php: -------------------------------------------------------------------------------- 1 | hasTimer($subtask_id, $user_id) && 27 | $this->db 28 | ->table(self::TABLE) 29 | ->insert(array('subtask_id' => $subtask_id, 30 | 'user_id' => $user_id, 31 | 'comment' => $comment, 32 | 'is_billable' => $is_billable, 33 | 'start' => time(), 34 | 'end' => 0)); 35 | } 36 | 37 | /** 38 | * Log end time 39 | * 40 | * @access public 41 | * @param integer $subtask_id 42 | * @param integer $user_id 43 | * @return boolean 44 | */ 45 | public function logEndTimeExtended($subtask_id, $user_id, $comment, $is_billable) 46 | { 47 | $time_spent = $this->getTimeSpent($subtask_id, $user_id); 48 | 49 | if ($time_spent > 0) { 50 | $this->updateSubtaskTimeSpent($subtask_id, $time_spent); 51 | } 52 | 53 | return $this->db 54 | ->table(self::TABLE) 55 | ->eq('subtask_id', $subtask_id) 56 | ->eq('user_id', $user_id) 57 | ->eq('end', 0) 58 | ->update(array( 59 | 'end' => time(), 60 | 'time_spent' => $time_spent, 61 | 'comment' => $comment, 62 | 'is_billable' => $is_billable 63 | )); 64 | } 65 | 66 | /** 67 | * Get query for task timesheet (pagination) 68 | * 69 | * @access public 70 | * @param integer $task_id Task id 71 | * @return \PicoDb\Table 72 | */ 73 | public function getTaskQuery($task_id) 74 | { 75 | return $this->db 76 | ->table(self::TABLE) 77 | ->columns( 78 | self::TABLE.'.id', 79 | self::TABLE.'.subtask_id', 80 | self::TABLE.'.end', 81 | self::TABLE.'.start', 82 | self::TABLE.'.time_spent', 83 | self::TABLE.'.user_id', 84 | self::TABLE.'.comment', 85 | self::TABLE.'.is_billable', 86 | SubtaskModel::TABLE.'.task_id', 87 | SubtaskModel::TABLE.'.title AS subtask_title', 88 | TaskModel::TABLE.'.project_id', 89 | UserModel::TABLE.'.username', 90 | UserModel::TABLE.'.name AS user_fullname' 91 | ) 92 | ->join(SubtaskModel::TABLE, 'id', 'subtask_id') 93 | ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) 94 | ->join(UserModel::TABLE, 'id', 'user_id', self::TABLE) 95 | ->eq(TaskModel::TABLE.'.id', $task_id); 96 | } 97 | 98 | /** 99 | * Update subtask time billable 100 | * 101 | * @access public 102 | * @param integer $subtask_id 103 | * @param float $time_billable 104 | * @return bool 105 | */ 106 | public function updateSubtaskTimeBillable($subtask_id, $time_billable) 107 | { 108 | $subtask = $this->subtaskModel->getById($subtask_id); 109 | 110 | return $this->subtaskModel->update(array( 111 | 'id' => $subtask['id'], 112 | 'time_billable' => $subtask['time_billable'] + $time_billable, 113 | 'task_id' => $subtask['task_id'], 114 | ), false); 115 | } 116 | 117 | /** 118 | * get a Subtasktimetracking entry by Id 119 | * 120 | * @access public 121 | * @param $id the subtasktimetracking id 122 | * @return array 123 | */ 124 | public function getById($id) 125 | { 126 | return $this->db 127 | ->table(SubtaskTimeTrackingModel::TABLE) 128 | ->eq('id', $id) 129 | ->findOne(); 130 | } 131 | 132 | 133 | } 134 | -------------------------------------------------------------------------------- /Html.php: -------------------------------------------------------------------------------- 1 | write('php://output', $rows); 63 | } 64 | 65 | /** 66 | * Define column mapping between CSV and SQL columns 67 | * 68 | * @access public 69 | * @param array $columns 70 | * @return Csv 71 | */ 72 | public function setColumnMapping(array $columns) 73 | { 74 | $this->columns = $columns; 75 | return $this; 76 | } 77 | 78 | /** 79 | * Write HTML file 80 | * 81 | * @access public 82 | * @param string $filename 83 | * @param array $rows 84 | * @return Html 85 | */ 86 | public function write($filename, array $rows) 87 | { 88 | 89 | $fp = fopen($filename, 'w'); 90 | 91 | if (is_resource($fp)) { 92 | $types = array_shift($rows); 93 | 94 | $this->writeHeader($fp); 95 | foreach ($rows as $row) { 96 | $this->writeRow($fp, $types, $row); 97 | } 98 | $this->writeFooter($fp); 99 | fclose($fp); 100 | } 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * write a HTML header 107 | * 108 | * @param $fp filepointer 109 | */ 110 | private function writeHeader($fp) 111 | { 112 | fwrite($fp,"\n"); 119 | fwrite($fp,""); 120 | 121 | } 122 | 123 | /** 124 | * write a single row 125 | * 126 | * @param fp filepointer 127 | * @param $row row 128 | */ 129 | private function writeRow($fp, array $types, array $row) 130 | { 131 | fwrite($fp,""); 132 | foreach ($row as $key => $value) { 133 | fwrite($fp,""); 134 | } 135 | fwrite($fp,"\n"); 136 | } 137 | 138 | /** 139 | * write a HTML footer 140 | */ 141 | private function writeFooter($fp) 142 | { 143 | fwrite($fp,"
".$value."
"); 144 | } 145 | 146 | /** 147 | * Associate columns header with row values 148 | * 149 | * @access private 150 | * @param array $row 151 | * @return array 152 | */ 153 | private function associateColumns(array $row) 154 | { 155 | $line = array(); 156 | $index = 0; 157 | 158 | foreach ($this->columns as $sql_name => $csv_name) { 159 | if (isset($row[$index])) { 160 | $line[$sql_name] = $row[$index]; 161 | } else { 162 | $line[$sql_name] = ''; 163 | } 164 | 165 | $index++; 166 | } 167 | 168 | return $line; 169 | } 170 | 171 | /** 172 | * Filter empty rows 173 | * 174 | * @access private 175 | * @param array $row 176 | * @return array 177 | */ 178 | private function filterRow(array $row) 179 | { 180 | return array_filter($row); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Model/SubtaskTimeTrackingEditModel.php: -------------------------------------------------------------------------------- 1 | db 31 | ->table(SubtaskTimeTrackingModel::TABLE) 32 | ->columns( 33 | SubtaskTimeTrackingModel::TABLE.'.id', 34 | SubtaskTimeTrackingModel::TABLE.'.subtask_id', 35 | SubtaskTimeTrackingModel::TABLE.'.end', 36 | SubtaskTimeTrackingModel::TABLE.'.start', 37 | SubtaskTimeTrackingModel::TABLE.'.time_spent', 38 | SubtaskTimeTrackingModel::TABLE.'.comment', 39 | SubtaskTimeTrackingModel::TABLE.'.is_billable' 40 | ) 41 | ->eq(SubtaskTimeTrackingModel::TABLE.'.subtask_id', $subtask_id) 42 | ->eq(SubtaskTimeTrackingModel::TABLE.'.user_id', $user_id) 43 | ->eq(SubtaskTimeTrackingModel::TABLE.'.end', 0) 44 | ->findOne(); 45 | } 46 | 47 | /** 48 | * Get by Id 49 | * 50 | * @access public 51 | * @param int $id TimetrackingId 52 | * @return \PicoDb\Table 53 | */ 54 | public function getById($id) 55 | { 56 | return $this->db 57 | ->table(SubtaskTimeTrackingModel::TABLE) 58 | ->columns( 59 | SubtaskTimeTrackingModel::TABLE.'.id', 60 | SubtaskTimeTrackingModel::TABLE.'.subtask_id', 61 | SubtaskTimeTrackingModel::TABLE.'.end', 62 | SubtaskTimeTrackingModel::TABLE.'.start', 63 | SubtaskTimeTrackingModel::TABLE.'.time_spent', 64 | SubtaskTimeTrackingModel::TABLE.'.comment', 65 | SubtaskTimeTrackingModel::TABLE.'.is_billable', 66 | SubtaskModel::TABLE.'.task_id', 67 | SubtaskModel::TABLE.'.title AS subtask_title', 68 | TaskModel::TABLE.'.title AS task_title', 69 | TaskModel::TABLE.'.project_id', 70 | TaskModel::TABLE.'.color_id' 71 | ) 72 | ->join(SubtaskModel::TABLE, 'id', 'subtask_id') 73 | ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) 74 | ->eq(SubtaskTimeTrackingModel::TABLE.'.id', $id) 75 | ->findOne(); 76 | 77 | } 78 | 79 | /** 80 | * Create a time tracking event 81 | * 82 | * @access public 83 | * @param array $values Form values 84 | * @return integer 85 | */ 86 | public function create(array $values) 87 | { 88 | 89 | $this->prepare($values); 90 | $subtrackingid = $this->db->table(SubtaskTimeTrackingModel::TABLE)->persist($values); 91 | 92 | return (int) $subtrackingid; 93 | } 94 | 95 | /** 96 | * Update a time tracking event 97 | * 98 | * @access public 99 | * @param array $values 100 | * @return boolean 101 | */ 102 | public function update(array $values) 103 | { 104 | $this->prepare($values); 105 | 106 | return $this->db->table(SubtaskTimeTrackingModel::TABLE)->eq('id', $values['id'])->update($values); 107 | } 108 | 109 | 110 | /** 111 | * remove an entry 112 | * 113 | * @access public 114 | * @param int $id 115 | * @return boolran 116 | */ 117 | public function remove($id) 118 | { 119 | return $this->db->table(SubtaskTimeTrackingModel::TABLE)->eq('id', $id)->remove(); 120 | 121 | } 122 | 123 | /** 124 | * Prepare data 125 | * 126 | * @access public 127 | * @param array $values Form values 128 | */ 129 | public function prepare(array &$values) 130 | { 131 | if ($this->userSession->isLogged()) { 132 | $values['user_id'] = $this->userSession->getId(); 133 | } 134 | 135 | $values["subtask_id"] = $values["opposite_subtask_id"]; 136 | 137 | $this->helper->model->removeFields($values, array('project_id', 'task_id', 'opposite_subtask_id', 'subtask', 'add_another', 'old_time_spent', 'old_opposite_subtask_id')); 138 | 139 | // Calculate end time 140 | $values = $this->dateParser->convert($values, array('start'), true); 141 | $values["end"] = $values["start"] + ($values['time_spent']*60*60); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Export/SubtaskTimeTrackingExport.php: -------------------------------------------------------------------------------- 1 | getSubtasksTimeTracking($project_id, $from, $to); 32 | $results = array($this->getColumns()); 33 | 34 | foreach ($subtaskstt as $subtasktt) { 35 | $results[] = $this->format($subtasktt); 36 | } 37 | 38 | return $results; 39 | } 40 | 41 | public function exportAll() 42 | { 43 | $subtaskstt = $this->getAllSubtasksTimeTracking(); 44 | $results = array($this->getFormats()); 45 | $results[] = $this->getColumns(); 46 | 47 | foreach ($subtaskstt as $subtasktt) { 48 | $results[] = $this->format($subtasktt); 49 | } 50 | 51 | return $results; 52 | } 53 | 54 | /** 55 | * Get column titles 56 | * 57 | * @access public 58 | * @return string[] 59 | */ 60 | public function getColumns() 61 | { 62 | return array( 63 | e('TimeTracking Id'), 64 | e('User Id'), 65 | e('Subtask Id'), 66 | e('start'), 67 | e('end'), 68 | e('Time Spent'), 69 | e('Is Billable?'), 70 | e('Comment'), 71 | e('Task Id'), 72 | e('Task Title'), 73 | e('Subtask Title'), 74 | e('Project Id'), 75 | e('Project Name'), 76 | e('Color Id'), 77 | e('Username'), 78 | e('User Fullname'), 79 | ); 80 | } 81 | 82 | /** 83 | * Get Format of the getColumns 84 | * 85 | * @access public 86 | * @return string[] 87 | */ 88 | public function getFormats() 89 | { 90 | return array( 91 | 'num', 92 | 'num', 93 | 'num', 94 | 'date', 95 | 'date', 96 | 'dec', 97 | 'bool', 98 | 'text', 99 | 'num', 100 | 'text', 101 | 'text', 102 | 'num', 103 | 'text', 104 | 'num', 105 | 'text', 106 | 'text' 107 | ); 108 | } 109 | 110 | /** 111 | * Format the output of a subtask array 112 | * 113 | * @access public 114 | * @param array $subtask Subtask properties 115 | * @return array 116 | */ 117 | public function format(array $subtasktt) 118 | { 119 | $values = array(); 120 | $values[] = $subtasktt['id']; 121 | $values[] = $subtasktt['user_id']; 122 | $values[] = $subtasktt['subtask_id']; 123 | $values[] = $this->helper->dt->date($subtasktt['start']); 124 | $values[] = $this->helper->dt->date($subtasktt['end']); 125 | $values[] = str_replace(".",",",$subtasktt['time_spent']); 126 | $values[] = $subtasktt['is_billable']; 127 | $values[] = $this->helper->text->markdown($subtasktt['comment']); 128 | $values[] = $subtasktt['task_id']; 129 | $values[] = $subtasktt['task_title']; 130 | $values[] = $subtasktt['subtask_title']; 131 | $values[] = $subtasktt['project_id']; 132 | $values[] = $subtasktt['project_name']; 133 | $values[] = $subtasktt['color_id']; 134 | $values[] = $subtasktt['username']; 135 | $values[] = $subtasktt['user_fullname']; 136 | return $values; 137 | } 138 | 139 | /** 140 | * Get all time tracking events for a given project 141 | * 142 | * @access public 143 | * @param integer $project_id Project id 144 | * @param mixed $from Start date (timestamp or user formatted date) 145 | * @param mixed $to End date (timestamp or user formatted date) 146 | * @return array 147 | */ 148 | public function getSubtasksTimeTracking($project_id, $from, $to) 149 | { 150 | if (! is_numeric($from)) { 151 | $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); 152 | } 153 | 154 | if (! is_numeric($to)) { 155 | $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); 156 | } 157 | 158 | return $this->db->table(SubtaskTimeTrackingModel::TABLE) 159 | ->eq('project_id', $project_id) 160 | ->columns( 161 | SubtaskTimeTrackingModel::TABLE.'.id', 162 | SubtaskTimeTrackingModel::TABLE.'.user_id', 163 | SubtaskTimeTrackingModel::TABLE.'.subtask_id', 164 | SubtaskTimeTrackingModel::TABLE.'.start', 165 | SubtaskTimeTrackingModel::TABLE.'.end', 166 | SubtaskTimeTrackingModel::TABLE.'.time_spent', 167 | SubtaskTimeTrackingModel::TABLE.'.is_billable', 168 | SubtaskTimeTrackingModel::TABLE.'.comment', 169 | SubtaskModel::TABLE.'.task_id', 170 | SubtaskModel::TABLE.'.title AS subtask_title', 171 | TaskModel::TABLE.'.project_id', 172 | ProjectModel::TABLE.'.name AS project_name', 173 | TaskModel::TABLE.'.title AS task_title', 174 | TaskModel::TABLE.'.color_id', 175 | UserModel::TABLE.'.username', 176 | UserModel::TABLE.'.name AS user_fullname' 177 | ) 178 | ->join(SubtaskModel::TABLE, 'id', 'subtask_id') 179 | ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) 180 | ->join(UserModel::TABLE, 'id', 'user_id', SubtaskTimeTrackingModel::TABLE) 181 | ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE) 182 | ->gte(SubtaskTimeTrackingModel::TABLE.'.start', $from) 183 | ->lte(SubtaskTimeTrackingModel::TABLE.'.start', $to) 184 | ->eq(TaskModel::TABLE.'.project_id', $project_id) 185 | ->findAll(); 186 | } 187 | 188 | public function getAllSubtasksTimeTracking() 189 | { 190 | 191 | return $this->db->table(SubtaskTimeTrackingModel::TABLE) 192 | ->columns( 193 | SubtaskTimeTrackingModel::TABLE.'.id', 194 | SubtaskTimeTrackingModel::TABLE.'.user_id', 195 | SubtaskTimeTrackingModel::TABLE.'.subtask_id', 196 | SubtaskTimeTrackingModel::TABLE.'.start', 197 | SubtaskTimeTrackingModel::TABLE.'.end', 198 | SubtaskTimeTrackingModel::TABLE.'.time_spent', 199 | SubtaskTimeTrackingModel::TABLE.'.is_billable', 200 | SubtaskTimeTrackingModel::TABLE.'.comment', 201 | SubtaskModel::TABLE.'.task_id', 202 | SubtaskModel::TABLE.'.title AS subtask_title', 203 | TaskModel::TABLE.'.project_id', 204 | ProjectModel::TABLE.'.name AS project_name', 205 | TaskModel::TABLE.'.title AS task_title', 206 | TaskModel::TABLE.'.color_id', 207 | UserModel::TABLE.'.username', 208 | UserModel::TABLE.'.name AS user_fullname' 209 | ) 210 | ->join(SubtaskModel::TABLE, 'id', 'subtask_id') 211 | ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) 212 | ->join(UserModel::TABLE, 'id', 'user_id', SubtaskTimeTrackingModel::TABLE) 213 | ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE) 214 | ->findAll(); 215 | 216 | 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Controller/TimeTrackingEditorController.php: -------------------------------------------------------------------------------- 1 | getProject(); 30 | 31 | if (empty($values)) { 32 | $values = array('project_id' => $project['id'], 33 | 'task_id' => $this->request->getIntegerParam('task_id'), 34 | 'subtask_id' => $this->request->getIntegerParam('subtask_id') 35 | ); 36 | } 37 | 38 | $values['subtask'] = $this->subtaskModel->getById($values['subtask_id']); 39 | 40 | $this->response->html($this->template->render('Timetrackingeditor:start', array( 41 | 'values' => $values, 42 | 'errors' => $errors, 43 | 'project' => $project, 44 | 'title' => t('Start a new timer') 45 | ))); 46 | } 47 | 48 | /** 49 | * Show Form to stop the timer 50 | * @access public 51 | * @param array $values 52 | * @param arry $errors 53 | */ 54 | 55 | public function stop(array $values = array(), array $errors = array()) 56 | { 57 | $project = $this->getProject(); 58 | 59 | if (empty($values)) { 60 | $values = array('project_id' => $project['id'], 61 | 'task_id' => $this->request->getIntegerParam('task_id'), 62 | 'subtask_id' => $this->request->getIntegerParam('subtask_id') 63 | ); 64 | } 65 | 66 | $values['subtask'] = $this->subtaskModel->getById($values['subtask_id']); 67 | 68 | $timetracking = $this->subtaskTimeTrackingEditModel 69 | ->getOpenTimer( 70 | $this->userSession->getId(), 71 | $values['subtask_id'] 72 | ); 73 | 74 | $values['comment'] = $timetracking["comment"]; 75 | $values['is_billable'] = $timetracking['is_billable']; 76 | 77 | $this->response->html($this->template->render('Timetrackingeditor:stop', array( 78 | 'values' => $values, 79 | 'errors' => $errors, 80 | 'project' => $project, 81 | 'title' => t('Stop a timer') 82 | ))); 83 | } 84 | 85 | 86 | /** 87 | * Start the timer and save comment and is_billable 88 | * @access public 89 | * 90 | */ 91 | 92 | public function startsave() 93 | { 94 | $values = $this->request->getValues(); 95 | $project = $this->getProject(); 96 | $task = $this->getTask(); 97 | 98 | if (!$this->subtaskTimeTrackingModel->logStartTimeExtended( 99 | $values['subtask_id'], 100 | $this->userSession->getId(), 101 | $values['comment'], 102 | $values['is_billable'] ?: 0)) { 103 | // TODO: Best way to display the errors? 104 | $this->flash->failure("Another Timer is already running"); 105 | return false; 106 | } 107 | 108 | $this->subtaskStatusModel->toggleStatus($values['subtask_id']); 109 | 110 | return $this->response->redirect($this->helper->url->to('SubtaskStatusController', 'change', array( 111 | 'refresh-table' => 1, 112 | 'project_id' => $project['id'], 113 | 'task_id' => $task['id'], 114 | 'subtask_id' => $values['subtask_id'] 115 | )), true); 116 | } 117 | 118 | /** 119 | * Stop the timer and save comment and is_billable 120 | * 121 | * @access public 122 | */ 123 | public function stopsave() 124 | { 125 | 126 | $values = $this->request->getValues(); 127 | $project = $this->getProject(); 128 | $task = $this->getTask(); 129 | 130 | $this->subtaskTimeTrackingModel->logEndTimeExtended( 131 | $values['subtask_id'], 132 | $this->userSession->getId(), 133 | $values['comment'], 134 | $values['is_billable'] ?: 0); 135 | 136 | $this->subtaskStatusModel->toggleStatus($values['subtask_id']); 137 | 138 | return $this->response->redirect($this->helper->url->to('SubtaskStatusController', 'change', array( 139 | 'refresh-table' => 1, 140 | 'project_id' => $project['id'], 141 | 'task_id' => $task['id'], 142 | 'subtask_id' => $values['subtask_id'] 143 | )), true); 144 | } 145 | /** 146 | * Show Form to create new entry 147 | * @access public 148 | * @param array $values 149 | * @param array $errors 150 | */ 151 | public function create(array $values = array(), array $errors = array()) 152 | { 153 | $project = $this->getProject(); 154 | 155 | if (empty($values)) { 156 | $values = array('project_id' => $project['id'], 157 | 'task_id' => $this->request->getIntegerParam('task_id') 158 | ); 159 | } 160 | 161 | if ($this->request->getIntegerParam('subtask_id')) { 162 | $values['opposite_subtask_id'] = $this->request->getIntegerParam('subtask_id'); 163 | $subtask = $this->subtaskModel->getById($values['opposite_subtask_id']); 164 | 165 | $values['subtask'] = $subtask['title']; 166 | $autofocus = "time_spent"; 167 | } else { 168 | $autofocus = "subtask"; 169 | } 170 | 171 | 172 | $this->response->html($this->template->render('Timetrackingeditor:create', array( 173 | 'values' => $values, 174 | 'errors' => $errors, 175 | 'project' => $project, 176 | 'autofocus' => $autofocus, 177 | 'title' => t('Add new time tracking event') 178 | ))); 179 | } 180 | 181 | /** 182 | * Edit an existing entry 183 | * 184 | * @access public 185 | * @param array $values 186 | * @param array $errors 187 | */ 188 | public function edit(array $values = array(), array $errors = array()) 189 | { 190 | $project = $this->getProject(); 191 | 192 | if (empty($values)) { 193 | $values = array('project_id' => $project['id'], 194 | 'task_id' => $this->request->getIntegerParam('task_id'), 195 | 'subtask_id' => $this->request->getIntegerParam('subtask_id'), 196 | 'id' => $this->request->getIntegerParam('id') 197 | ); 198 | } 199 | 200 | $values = $this->subtaskTimeTrackingEditModel->getById($this->request->getIntegerParam('id')); 201 | 202 | $values = $this->dateParser->format($values, array('start'), $this->dateParser->getUserDateFormat()); 203 | $values['subtask'] = $values['subtask_title']; 204 | $values['opposite_subtask_id'] = $values['subtask_id']; 205 | 206 | $this->response->html($this->template->render('Timetrackingeditor:edit', array( 207 | 'values' => $values, 208 | 'errors' => $errors, 209 | 'project' => $project, 210 | 'title' => t('Edit a time tracking event') 211 | ))); 212 | } 213 | 214 | /** 215 | * Update a time tracking entry 216 | * 217 | * @access public 218 | * @param array $values 219 | * @param array $errors 220 | */ 221 | public function update(array $values = array(), array $errors = array()) 222 | { 223 | $project = $this->getProject(); 224 | $values = $this->request->getValues(); 225 | $oldtimetracking = $this->subtaskTimeTrackingModel->getById($values['id']); 226 | 227 | if (!isset($values['is_billable'])) { 228 | $values["is_billable"] = 0; 229 | } 230 | 231 | list($valid, $errors) = $this->subtaskTimeTrackingValidator->validateModification($values); 232 | 233 | if ($valid && $this->subtaskTimeTrackingEditModel->update($values)) { 234 | $this->flash->success(t('Timetracking entry updated successfully.')); 235 | $this->updateTimespent($values['task_id'], $oldtimetracking['subtask_id'], $oldtimetracking['time_spent'] * -1); 236 | $this->updateTimespent($values['task_id'], $values['opposite_subtask_id'], $values['time_spent']); 237 | 238 | if ($oldtimetracking['is_billable'] == 1) { 239 | $this->updateTimebillable($values['task_id'], $oldtimetracking['opposite_subtask_id'], $oldtimetracking['time_spent'] * -1); 240 | } 241 | if ($values['is_billable'] == 1) { 242 | $this->updateTimebillable($values['task_id'], $values['opposite_subtask_id'], $values['time_spent']); 243 | } 244 | return $this->afterSave($project, $values); 245 | } 246 | 247 | $this->flash->failure(t('Unable to update your time tracking entry.')); 248 | return $this->edit($values, $errors); 249 | 250 | } 251 | 252 | 253 | /** 254 | * Save a newly created time tracking entry 255 | * @access public 256 | * @param array $values 257 | * @param array $errors 258 | */ 259 | public function save(array $values = array(), array $errors = array()) 260 | { 261 | $project = $this->getProject(); 262 | $values = $this->request->getValues(); 263 | 264 | list($valid, $errors) = $this->subtaskTimeTrackingValidator->validateCreation($values); 265 | 266 | if ($valid && $this->subtaskTimeTrackingCreationModel->create($values)) { 267 | $this->updateTimespent($values['task_id'], $values['opposite_subtask_id'], $values['time_spent']); 268 | if (isset($values['is_billable']) && $values['is_billable'] == 1) { 269 | $this->updateTimebillable($values['task_id'], $values['opposite_subtask_id'], $values['time_spent']); 270 | } 271 | $this->flash->success(t('Timetracking entry added successfully.')); 272 | 273 | return $this->afterSave($project, $values); 274 | } 275 | 276 | $this->flash->failure(t('Unable to create your time tracking entry.')); 277 | return $this->create($values, $errors); 278 | 279 | } 280 | 281 | /** 282 | * Confirmation dialog before removing an entry 283 | * 284 | * @access public 285 | */ 286 | public function confirm() 287 | { 288 | 289 | $id = $this->request->getIntegerParam('id'); 290 | 291 | $timetracking = $this->subtaskTimeTrackingEditModel->getById($id); 292 | 293 | $this->response->html($this->template->render('timetrackingeditor:remove', array( 294 | 'timetracking' => $timetracking, 295 | ))); 296 | } 297 | 298 | /** 299 | * Remove an entry 300 | * 301 | * @access public 302 | */ 303 | public function remove() 304 | { 305 | $this->checkCSRFParam(); 306 | $id = $this->request->getIntegerParam('id'); 307 | $timetracking = $this->subtaskTimeTrackingEditModel->getById($id); 308 | 309 | if ($this->subtaskTimeTrackingEditModel->remove($id)) { 310 | $this->updateTimespent($timetracking['task_id'], $timetracking['subtask_id'], $timetracking['time_spent'] * -1); 311 | if ($timetracking['is_billable'] == 1) { 312 | $this->updateTimebillable($timetracking['task_id'], $timetracking['subtask_id'], $timetracking['time_spent'] * -1); 313 | } 314 | $this->flash->success(t('Entry removed successfully.')); 315 | } else { 316 | $this->flash->failure(t('Unable to remove this entry.')); 317 | } 318 | 319 | $this->response->redirect($this->helper->url->to('TaskViewController', 'timetracking', array('project_id' => $timetracking['project_id'], 'task_id' => $timetracking['task_id'])), true); 320 | } 321 | 322 | /** 323 | * update time spent for the task 324 | * 325 | * @access private 326 | * @param int $task_id 327 | * @param int $subtask_id 328 | * @return bool 329 | */ 330 | 331 | private function updateTimespent($task_id, $subtask_id, $time_spent) 332 | { 333 | $this->subtaskTimeTrackingModel->updateSubtaskTimeSpent($subtask_id, $time_spent); 334 | return $this->subtaskTimeTrackingModel->updateTaskTimeTracking($task_id); 335 | 336 | } 337 | 338 | /** 339 | * update time billable for the task 340 | * 341 | * @access private 342 | * @param int $task_id 343 | * @param int $subtask_id 344 | * @return bool 345 | */ 346 | private function updateTimebillable($task_id, $subtask_id, $time_billable) 347 | { 348 | $this->subtaskTimeTrackingModel->updateSubtaskTimeBillable($subtask_id, $time_billable); 349 | return $this->subtaskTimeTrackingModel->updateTaskTimeTracking($task_id); 350 | } 351 | 352 | 353 | /** 354 | * Present another, empty form if add_another is activated 355 | * 356 | * @access private 357 | * @param array $project 358 | * @param array $values 359 | */ 360 | private function afterSave(array $project, array &$values) 361 | { 362 | if (isset($values['add_another']) && $values['add_another'] == 1) { 363 | return $this->create(array( 364 | 'project_id' => $this->getProject()['id'], 365 | 'subtask' => $values['subtask'], 366 | 'opposite_subtask_id' => $values['opposite_subtask_id'], 367 | 'task_id' => $values['task_id'], 368 | 'start' => $values['start'], 369 | 'is_billable' => $values['is_billable'], 370 | 'add_another' => 1, 371 | )); 372 | } 373 | 374 | return $this->response->redirect($this->helper->url->to('TaskViewController', 'timetracking', array('project_id' => $project['id'], 'task_id' => $values['task_id'])), true); 375 | } 376 | 377 | } 378 | --------------------------------------------------------------------------------