From 62827e6cf470449c117624058fb36ad94804bcc0 Mon Sep 17 00:00:00 2001 From: emkael Date: Tue, 5 May 2020 14:25:42 +0200 Subject: Time tracking related plugins --- .../AllSubtaskTimeTrackingExportCommand.php | 29 ++ .../Console/SubtaskTimeTrackingExportCommand.php | 35 ++ .../Controller/SubtaskAjaxController.php | 45 +++ .../Controller/SubtaskStatusController.php | 49 +++ .../Controller/TimeTrackingEditorController.php | 377 +++++++++++++++++++++ .../Export/SubtaskTimeTrackingExport.php | 218 ++++++++++++ .../Timetrackingeditor/Filter/SubtaskIdFilter.php | 39 +++ .../Filter/SubtaskTasksFilter.php | 46 +++ .../Filter/SubtaskTitleFilter.php | 47 +++ .../Formatter/SubtaskAutoCompleteFormatter.php | 39 +++ plugins/Timetrackingeditor/Html.php | 182 ++++++++++ .../Model/SubtaskTimeTrackingCreationModel.php | 53 +++ .../Model/SubtaskTimeTrackingEditModel.php | 143 ++++++++ .../Model/SubtaskTimeTrackingModel.php | 133 ++++++++ plugins/Timetrackingeditor/Plugin.php | 91 +++++ plugins/Timetrackingeditor/README.md | 40 +++ plugins/Timetrackingeditor/Schema/Mysql.php | 16 + plugins/Timetrackingeditor/Schema/Postgres.php | 16 + plugins/Timetrackingeditor/Schema/Sqlite.php | 16 + plugins/Timetrackingeditor/Template/create.php | 41 +++ plugins/Timetrackingeditor/Template/edit.php | 41 +++ plugins/Timetrackingeditor/Template/menu.php | 23 ++ plugins/Timetrackingeditor/Template/remove.php | 20 ++ plugins/Timetrackingeditor/Template/start.php | 23 ++ plugins/Timetrackingeditor/Template/stop.php | 23 ++ .../Timetrackingeditor/Template/subtask/table.php | 89 +++++ .../Template/time_tracking_editor.php | 53 +++ plugins/Timetrackingeditor/Test/PluginTest.php | 17 + .../Test/TimetrackingeditorTest.php | 48 +++ .../Validator/SubtaskTimeTrackingValidator.php | 73 ++++ .../assets/css/timetrackingeditor.css | 3 + 31 files changed, 2068 insertions(+) create mode 100644 plugins/Timetrackingeditor/Console/AllSubtaskTimeTrackingExportCommand.php create mode 100644 plugins/Timetrackingeditor/Console/SubtaskTimeTrackingExportCommand.php create mode 100644 plugins/Timetrackingeditor/Controller/SubtaskAjaxController.php create mode 100644 plugins/Timetrackingeditor/Controller/SubtaskStatusController.php create mode 100644 plugins/Timetrackingeditor/Controller/TimeTrackingEditorController.php create mode 100644 plugins/Timetrackingeditor/Export/SubtaskTimeTrackingExport.php create mode 100644 plugins/Timetrackingeditor/Filter/SubtaskIdFilter.php create mode 100644 plugins/Timetrackingeditor/Filter/SubtaskTasksFilter.php create mode 100644 plugins/Timetrackingeditor/Filter/SubtaskTitleFilter.php create mode 100644 plugins/Timetrackingeditor/Formatter/SubtaskAutoCompleteFormatter.php create mode 100644 plugins/Timetrackingeditor/Html.php create mode 100644 plugins/Timetrackingeditor/Model/SubtaskTimeTrackingCreationModel.php create mode 100644 plugins/Timetrackingeditor/Model/SubtaskTimeTrackingEditModel.php create mode 100644 plugins/Timetrackingeditor/Model/SubtaskTimeTrackingModel.php create mode 100644 plugins/Timetrackingeditor/Plugin.php create mode 100644 plugins/Timetrackingeditor/README.md create mode 100644 plugins/Timetrackingeditor/Schema/Mysql.php create mode 100644 plugins/Timetrackingeditor/Schema/Postgres.php create mode 100644 plugins/Timetrackingeditor/Schema/Sqlite.php create mode 100644 plugins/Timetrackingeditor/Template/create.php create mode 100644 plugins/Timetrackingeditor/Template/edit.php create mode 100644 plugins/Timetrackingeditor/Template/menu.php create mode 100644 plugins/Timetrackingeditor/Template/remove.php create mode 100644 plugins/Timetrackingeditor/Template/start.php create mode 100644 plugins/Timetrackingeditor/Template/stop.php create mode 100644 plugins/Timetrackingeditor/Template/subtask/table.php create mode 100644 plugins/Timetrackingeditor/Template/time_tracking_editor.php create mode 100644 plugins/Timetrackingeditor/Test/PluginTest.php create mode 100644 plugins/Timetrackingeditor/Test/TimetrackingeditorTest.php create mode 100644 plugins/Timetrackingeditor/Validator/SubtaskTimeTrackingValidator.php create mode 100644 plugins/Timetrackingeditor/assets/css/timetrackingeditor.css (limited to 'plugins/Timetrackingeditor') diff --git a/plugins/Timetrackingeditor/Console/AllSubtaskTimeTrackingExportCommand.php b/plugins/Timetrackingeditor/Console/AllSubtaskTimeTrackingExportCommand.php new file mode 100644 index 00000000..6a56aa22 --- /dev/null +++ b/plugins/Timetrackingeditor/Console/AllSubtaskTimeTrackingExportCommand.php @@ -0,0 +1,29 @@ +setName('export:allsubtaskstimetracking') + ->setDescription('Subtasks Time Tracking CSV export for all events'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $data = $this->subtaskTimeTrackingExport->exportAll(); + + if (is_array($data)) { + Html::output($data); + } + } +} diff --git a/plugins/Timetrackingeditor/Console/SubtaskTimeTrackingExportCommand.php b/plugins/Timetrackingeditor/Console/SubtaskTimeTrackingExportCommand.php new file mode 100644 index 00000000..4f6e6384 --- /dev/null +++ b/plugins/Timetrackingeditor/Console/SubtaskTimeTrackingExportCommand.php @@ -0,0 +1,35 @@ +setName('export:subtaskstimetracking') + ->setDescription('Subtasks Time Tracking CSV export') + ->addArgument('project_id', InputArgument::REQUIRED, 'Project id') + ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)') + ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $data = $this->subtaskTimeTrackingExport->export( + $input->getArgument('project_id'), + $input->getArgument('start_date'), + $input->getArgument('end_date') + ); + + if (is_array($data)) { + Html::output($data); + } + } +} diff --git a/plugins/Timetrackingeditor/Controller/SubtaskAjaxController.php b/plugins/Timetrackingeditor/Controller/SubtaskAjaxController.php new file mode 100644 index 00000000..4871bfe0 --- /dev/null +++ b/plugins/Timetrackingeditor/Controller/SubtaskAjaxController.php @@ -0,0 +1,45 @@ +request->getStringParam('term'); + $task_id = $this->request->getIntegerParam('task_id'); + + $subtaskQuery = new QueryBuilder(); + $subtaskQuery->withQuery($this->db + ->table(SubtaskModel::TABLE) + ->eq('task_id', $task_id) + ->columns(SubtaskModel::TABLE.'*')); + + if (ctype_digit($search)) { + $subtaskQuery->withFilter(new SubtaskIdFilter($search)); + } else { + $subtaskQuery->withFilter(new SubtaskTitleFilter($search)); + } + + $this->response->json($subtaskQuery->format(new SubtaskAutoCompleteFormatter($this->container))); + } +} diff --git a/plugins/Timetrackingeditor/Controller/SubtaskStatusController.php b/plugins/Timetrackingeditor/Controller/SubtaskStatusController.php new file mode 100644 index 00000000..c27881a1 --- /dev/null +++ b/plugins/Timetrackingeditor/Controller/SubtaskStatusController.php @@ -0,0 +1,49 @@ + In Progress -> Done. + */ + public function change() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); + + if ($subtask['status'] == SubtaskModel::STATUS_DONE) { + $status = SubtaskModel::STATUS_TODO; + } else { + $status = SubtaskModel::STATUS_DONE; + } + $subtask['status'] = $status; + $this->subtaskModel->update($subtask); + + if ($this->request->getIntegerParam('refresh-table') === 0) { + + $html = $this->helper->subtask->toggleStatus($subtask, $task['project_id']); + } else { + $html = $this->renderTable($task); + } + + $this->response->html($html); + } + + protected function renderTable(array $task) + { + return $this->template->render('subtask/table', array( + 'task' => $task, + 'subtasks' => $this->subtaskModel->getAll($task['id']), + 'editable' => true, + )); + } +} +?> diff --git a/plugins/Timetrackingeditor/Controller/TimeTrackingEditorController.php b/plugins/Timetrackingeditor/Controller/TimeTrackingEditorController.php new file mode 100644 index 00000000..815074f2 --- /dev/null +++ b/plugins/Timetrackingeditor/Controller/TimeTrackingEditorController.php @@ -0,0 +1,377 @@ +getProject(); + + if (empty($values)) { + $values = array('project_id' => $project['id'], + 'task_id' => $this->request->getIntegerParam('task_id'), + 'subtask_id' => $this->request->getIntegerParam('subtask_id') + ); + } + + $values['subtask'] = $this->subtaskModel->getById($values['subtask_id']); + + $this->response->html($this->template->render('Timetrackingeditor:start', array( + 'values' => $values, + 'errors' => $errors, + 'project' => $project, + 'title' => t('Start a new timer') + ))); + } + + /** + * Show Form to stop the timer + * @access public + * @param array $values + * @param arry $errors + */ + + public function stop(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + if (empty($values)) { + $values = array('project_id' => $project['id'], + 'task_id' => $this->request->getIntegerParam('task_id'), + 'subtask_id' => $this->request->getIntegerParam('subtask_id') + ); + } + + $values['subtask'] = $this->subtaskModel->getById($values['subtask_id']); + + $timetracking = $this->subtaskTimeTrackingEditModel + ->getOpenTimer( + $this->userSession->getId(), + $values['subtask_id'] + ); + + $values['comment'] = $timetracking["comment"]; + $values['is_billable'] = $timetracking['is_billable']; + + $this->response->html($this->template->render('Timetrackingeditor:stop', array( + 'values' => $values, + 'errors' => $errors, + 'project' => $project, + 'title' => t('Stop a timer') + ))); + } + + +/** + * Start the timer and save comment and is_billable + * @access public + * + */ + + public function startsave() + { + $values = $this->request->getValues(); + $project = $this->getProject(); + $task = $this->getTask(); + + if (!$this->subtaskTimeTrackingModel->logStartTimeExtended( + $values['subtask_id'], + $this->userSession->getId(), + $values['comment'], + $values['is_billable'] ?: 0)) { + // TODO: Best way to display the errors? + $this->flash->failure("Another Timer is already running"); + return false; + } + + $this->subtaskStatusModel->toggleStatus($values['subtask_id']); + + return $this->response->redirect($this->helper->url->to('SubtaskStatusController', 'change', array( + 'refresh-table' => 1, + 'project_id' => $project['id'], + 'task_id' => $task['id'], + 'subtask_id' => $values['subtask_id'] + )), true); + } + + /** + * Stop the timer and save comment and is_billable + * + * @access public + */ + public function stopsave() + { + + $values = $this->request->getValues(); + $project = $this->getProject(); + $task = $this->getTask(); + + $this->subtaskTimeTrackingModel->logEndTimeExtended( + $values['subtask_id'], + $this->userSession->getId(), + $values['comment'], + $values['is_billable'] ?: 0); + + $this->subtaskStatusModel->toggleStatus($values['subtask_id']); + + return $this->response->redirect($this->helper->url->to('SubtaskStatusController', 'change', array( + 'refresh-table' => 1, + 'project_id' => $project['id'], + 'task_id' => $task['id'], + 'subtask_id' => $values['subtask_id'] + )), true); + } + /** + * Show Form to create new entry + * @access public + * @param array $values + * @param array $errors + */ + public function create(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + if (empty($values)) { + $values = array('project_id' => $project['id'], + 'task_id' => $this->request->getIntegerParam('task_id') + ); + } + + if ($this->request->getIntegerParam('subtask_id')) { + $values['opposite_subtask_id'] = $this->request->getIntegerParam('subtask_id'); + $subtask = $this->subtaskModel->getById($values['opposite_subtask_id']); + + $values['subtask'] = $subtask['title']; + $autofocus = "time_spent"; + } else { + $autofocus = "subtask"; + } + + + $this->response->html($this->template->render('Timetrackingeditor:create', array( + 'values' => $values, + 'errors' => $errors, + 'project' => $project, + 'autofocus' => $autofocus, + 'title' => t('Add new time tracking event') + ))); + } + + /** + * Edit an existing entry + * + * @access public + * @param array $values + * @param array $errors + */ + public function edit(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + if (empty($values)) { + $values = array('project_id' => $project['id'], + 'task_id' => $this->request->getIntegerParam('task_id'), + 'subtask_id' => $this->request->getIntegerParam('subtask_id'), + 'id' => $this->request->getIntegerParam('id') + ); + } + + $values = $this->subtaskTimeTrackingEditModel->getById($this->request->getIntegerParam('id')); + + $values = $this->dateParser->format($values, array('start'), $this->dateParser->getUserDateFormat()); + $values['subtask'] = $values['subtask_title']; + $values['opposite_subtask_id'] = $values['subtask_id']; + + $this->response->html($this->template->render('Timetrackingeditor:edit', array( + 'values' => $values, + 'errors' => $errors, + 'project' => $project, + 'title' => t('Edit a time tracking event') + ))); + } + + /** + * Update a time tracking entry + * + * @access public + * @param array $values + * @param array $errors + */ + public function update(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $values = $this->request->getValues(); + $oldtimetracking = $this->subtaskTimeTrackingModel->getById($values['id']); + + if (!isset($values['is_billable'])) { + $values["is_billable"] = 0; + } + + list($valid, $errors) = $this->subtaskTimeTrackingValidator->validateModification($values); + + if ($valid && $this->subtaskTimeTrackingEditModel->update($values)) { + $this->flash->success(t('Timetracking entry updated successfully.')); + $this->updateTimespent($values['task_id'], $oldtimetracking['subtask_id'], $oldtimetracking['time_spent'] * -1); + $this->updateTimespent($values['task_id'], $values['opposite_subtask_id'], $values['time_spent']); + + if ($oldtimetracking['is_billable'] == 1) { + $this->updateTimebillable($values['task_id'], $oldtimetracking['opposite_subtask_id'], $oldtimetracking['time_spent'] * -1); + } + if ($values['is_billable'] == 1) { + $this->updateTimebillable($values['task_id'], $values['opposite_subtask_id'], $values['time_spent']); + } + return $this->afterSave($project, $values); + } + + $this->flash->failure(t('Unable to update your time tracking entry.')); + return $this->edit($values, $errors); + + } + + + /** + * Save a newly created time tracking entry + * @access public + * @param array $values + * @param array $errors + */ + public function save(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->subtaskTimeTrackingValidator->validateCreation($values); + + if ($valid && $this->subtaskTimeTrackingCreationModel->create($values)) { + $this->updateTimespent($values['task_id'], $values['opposite_subtask_id'], $values['time_spent']); + if (isset($values['is_billable']) && $values['is_billable'] == 1) { + $this->updateTimebillable($values['task_id'], $values['opposite_subtask_id'], $values['time_spent']); + } + $this->flash->success(t('Timetracking entry added successfully.')); + + return $this->afterSave($project, $values); + } + + $this->flash->failure(t('Unable to create your time tracking entry.')); + return $this->create($values, $errors); + + } + + /** + * Confirmation dialog before removing an entry + * + * @access public + */ + public function confirm() + { + + $id = $this->request->getIntegerParam('id'); + + $timetracking = $this->subtaskTimeTrackingEditModel->getById($id); + + $this->response->html($this->template->render('timetrackingeditor:remove', array( + 'timetracking' => $timetracking, + ))); + } + + /** + * Remove an entry + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $id = $this->request->getIntegerParam('id'); + $timetracking = $this->subtaskTimeTrackingEditModel->getById($id); + + if ($this->subtaskTimeTrackingEditModel->remove($id)) { + $this->updateTimespent($timetracking['task_id'], $timetracking['subtask_id'], $timetracking['time_spent'] * -1); + if ($timetracking['is_billable'] == 1) { + $this->updateTimebillable($timetracking['task_id'], $timetracking['subtask_id'], $timetracking['time_spent'] * -1); + } + $this->flash->success(t('Entry removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this entry.')); + } + + $this->response->redirect($this->helper->url->to('TaskViewController', 'timetracking', array('project_id' => $timetracking['project_id'], 'task_id' => $timetracking['task_id'])), true); + } + + /** + * update time spent for the task + * + * @access private + * @param int $task_id + * @param int $subtask_id + * @return bool + */ + + private function updateTimespent($task_id, $subtask_id, $time_spent) + { + $this->subtaskTimeTrackingModel->updateSubtaskTimeSpent($subtask_id, $time_spent); + return $this->subtaskTimeTrackingModel->updateTaskTimeTracking($task_id); + + } + +/** + * update time billable for the task + * + * @access private + * @param int $task_id + * @param int $subtask_id + * @return bool + */ + private function updateTimebillable($task_id, $subtask_id, $time_billable) + { + $this->subtaskTimeTrackingModel->updateSubtaskTimeBillable($subtask_id, $time_billable); + return $this->subtaskTimeTrackingModel->updateTaskTimeTracking($task_id); + } + + + /** + * Present another, empty form if add_another is activated + * + * @access private + * @param array $project + * @param array $values + */ + private function afterSave(array $project, array &$values) + { + if (isset($values['add_another']) && $values['add_another'] == 1) { + return $this->create(array( + 'project_id' => $this->getProject()['id'], + 'subtask' => $values['subtask'], + 'opposite_subtask_id' => $values['opposite_subtask_id'], + 'task_id' => $values['task_id'], + 'start' => $values['start'], + 'is_billable' => $values['is_billable'], + 'add_another' => 1, + )); + } + + return $this->response->redirect($this->helper->url->to('TaskViewController', 'timetracking', array('project_id' => $project['id'], 'task_id' => $values['task_id'])), true); + } + +} diff --git a/plugins/Timetrackingeditor/Export/SubtaskTimeTrackingExport.php b/plugins/Timetrackingeditor/Export/SubtaskTimeTrackingExport.php new file mode 100644 index 00000000..c9fc1071 --- /dev/null +++ b/plugins/Timetrackingeditor/Export/SubtaskTimeTrackingExport.php @@ -0,0 +1,218 @@ +getSubtasksTimeTracking($project_id, $from, $to); + $results = array($this->getColumns()); + + foreach ($subtaskstt as $subtasktt) { + $results[] = $this->format($subtasktt); + } + + return $results; + } + + public function exportAll() + { + $subtaskstt = $this->getAllSubtasksTimeTracking(); + $results = array($this->getFormats()); + $results[] = $this->getColumns(); + + foreach ($subtaskstt as $subtasktt) { + $results[] = $this->format($subtasktt); + } + + return $results; + } + + /** + * Get column titles + * + * @access public + * @return string[] + */ + public function getColumns() + { + return array( + e('TimeTracking Id'), + e('User Id'), + e('Subtask Id'), + e('start'), + e('end'), + e('Time Spent'), + e('Is Billable?'), + e('Comment'), + e('Task Id'), + e('Task Title'), + e('Subtask Title'), + e('Project Id'), + e('Project Name'), + e('Color Id'), + e('Username'), + e('User Fullname'), + ); + } + + /** + * Get Format of the getColumns + * + * @access public + * @return string[] + */ + public function getFormats() + { + return array( + 'num', + 'num', + 'num', + 'date', + 'date', + 'dec', + 'bool', + 'text', + 'num', + 'text', + 'text', + 'num', + 'text', + 'num', + 'text', + 'text' + ); + } + + /** + * Format the output of a subtask array + * + * @access public + * @param array $subtask Subtask properties + * @return array + */ + public function format(array $subtasktt) + { + $values = array(); + $values[] = $subtasktt['id']; + $values[] = $subtasktt['user_id']; + $values[] = $subtasktt['subtask_id']; + $values[] = $this->helper->dt->date($subtasktt['start']); + $values[] = $this->helper->dt->date($subtasktt['end']); + $values[] = str_replace(".",",",$subtasktt['time_spent']); + $values[] = $subtasktt['is_billable']; + $values[] = $this->helper->text->markdown($subtasktt['comment']); + $values[] = $subtasktt['task_id']; + $values[] = $subtasktt['task_title']; + $values[] = $subtasktt['subtask_title']; + $values[] = $subtasktt['project_id']; + $values[] = $subtasktt['project_name']; + $values[] = $subtasktt['color_id']; + $values[] = $subtasktt['username']; + $values[] = $subtasktt['user_fullname']; + return $values; + } + + /** + * Get all time tracking events for a given project + * + * @access public + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function getSubtasksTimeTracking($project_id, $from, $to) + { + if (! is_numeric($from)) { + $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); + } + + if (! is_numeric($to)) { + $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); + } + + return $this->db->table(SubtaskTimeTrackingModel::TABLE) + ->eq('project_id', $project_id) + ->columns( + SubtaskTimeTrackingModel::TABLE.'.id', + SubtaskTimeTrackingModel::TABLE.'.user_id', + SubtaskTimeTrackingModel::TABLE.'.subtask_id', + SubtaskTimeTrackingModel::TABLE.'.start', + SubtaskTimeTrackingModel::TABLE.'.end', + SubtaskTimeTrackingModel::TABLE.'.time_spent', + SubtaskTimeTrackingModel::TABLE.'.is_billable', + SubtaskTimeTrackingModel::TABLE.'.comment', + SubtaskModel::TABLE.'.task_id', + SubtaskModel::TABLE.'.title AS subtask_title', + TaskModel::TABLE.'.project_id', + ProjectModel::TABLE.'.name AS project_name', + TaskModel::TABLE.'.title AS task_title', + TaskModel::TABLE.'.color_id', + UserModel::TABLE.'.username', + UserModel::TABLE.'.name AS user_fullname' + ) + ->join(SubtaskModel::TABLE, 'id', 'subtask_id') + ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) + ->join(UserModel::TABLE, 'id', 'user_id', SubtaskTimeTrackingModel::TABLE) + ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE) + ->gte(SubtaskTimeTrackingModel::TABLE.'.start', $from) + ->lte(SubtaskTimeTrackingModel::TABLE.'.start', $to) + ->eq(TaskModel::TABLE.'.project_id', $project_id) + ->findAll(); + } + + public function getAllSubtasksTimeTracking() + { + + return $this->db->table(SubtaskTimeTrackingModel::TABLE) + ->columns( + SubtaskTimeTrackingModel::TABLE.'.id', + SubtaskTimeTrackingModel::TABLE.'.user_id', + SubtaskTimeTrackingModel::TABLE.'.subtask_id', + SubtaskTimeTrackingModel::TABLE.'.start', + SubtaskTimeTrackingModel::TABLE.'.end', + SubtaskTimeTrackingModel::TABLE.'.time_spent', + SubtaskTimeTrackingModel::TABLE.'.is_billable', + SubtaskTimeTrackingModel::TABLE.'.comment', + SubtaskModel::TABLE.'.task_id', + SubtaskModel::TABLE.'.title AS subtask_title', + TaskModel::TABLE.'.project_id', + ProjectModel::TABLE.'.name AS project_name', + TaskModel::TABLE.'.title AS task_title', + TaskModel::TABLE.'.color_id', + UserModel::TABLE.'.username', + UserModel::TABLE.'.name AS user_fullname' + ) + ->join(SubtaskModel::TABLE, 'id', 'subtask_id') + ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) + ->join(UserModel::TABLE, 'id', 'user_id', SubtaskTimeTrackingModel::TABLE) + ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE) + ->findAll(); + + + } +} diff --git a/plugins/Timetrackingeditor/Filter/SubtaskIdFilter.php b/plugins/Timetrackingeditor/Filter/SubtaskIdFilter.php new file mode 100644 index 00000000..79779446 --- /dev/null +++ b/plugins/Timetrackingeditor/Filter/SubtaskIdFilter.php @@ -0,0 +1,39 @@ +query->eq(SubtaskModel::TABLE.'.id', $this->value); + return $this; + } +} diff --git a/plugins/Timetrackingeditor/Filter/SubtaskTasksFilter.php b/plugins/Timetrackingeditor/Filter/SubtaskTasksFilter.php new file mode 100644 index 00000000..d96a1a34 --- /dev/null +++ b/plugins/Timetrackingeditor/Filter/SubtaskTasksFilter.php @@ -0,0 +1,46 @@ +value) || ctype_digit($this->value)) { + $this->query->eq(SubtaskModel::TABLE.'.task_id', $this->value); + } else { + $this->query->ilike(TaskModel::TABLE.'.name', $this->value); + } + + return $this; + } +} diff --git a/plugins/Timetrackingeditor/Filter/SubtaskTitleFilter.php b/plugins/Timetrackingeditor/Filter/SubtaskTitleFilter.php new file mode 100644 index 00000000..5be456f1 --- /dev/null +++ b/plugins/Timetrackingeditor/Filter/SubtaskTitleFilter.php @@ -0,0 +1,47 @@ +value) || (strlen($this->value) > 1 && $this->value{0} === '#' && ctype_digit(substr($this->value, 1)))) { + $this->query->beginOr(); + $this->query->eq(SubtaskModel::TABLE.'.id', str_replace('#', '', $this->value)); + $this->query->ilike(SubtaskModel::TABLE.'.title', '%'.$this->value.'%'); + $this->query->closeOr(); + } else { + $this->query->ilike(SubtaskModel::TABLE.'.title', '%'.$this->value.'%'); + } + + return $this; + } +} diff --git a/plugins/Timetrackingeditor/Formatter/SubtaskAutoCompleteFormatter.php b/plugins/Timetrackingeditor/Formatter/SubtaskAutoCompleteFormatter.php new file mode 100644 index 00000000..724a146e --- /dev/null +++ b/plugins/Timetrackingeditor/Formatter/SubtaskAutoCompleteFormatter.php @@ -0,0 +1,39 @@ +query->columns( + SubtaskModel::TABLE.'.id', + SubtaskModel::TABLE.'.title' + )->asc(SubtaskModel::TABLE.'.id')->findAll(); + + foreach ($subtasks as &$subtask) { + $subtask['value'] = $subtask['title']; + $subtask['label'] = ' > #'.$subtask['id'].' '.$subtask['title']; + } + + return $subtasks; + } +} diff --git a/plugins/Timetrackingeditor/Html.php b/plugins/Timetrackingeditor/Html.php new file mode 100644 index 00000000..eb3e1fa5 --- /dev/null +++ b/plugins/Timetrackingeditor/Html.php @@ -0,0 +1,182 @@ +write('php://output', $rows); + } + + /** + * Define column mapping between CSV and SQL columns + * + * @access public + * @param array $columns + * @return Csv + */ + public function setColumnMapping(array $columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Write HTML file + * + * @access public + * @param string $filename + * @param array $rows + * @return Html + */ + public function write($filename, array $rows) + { + + $fp = fopen($filename, 'w'); + + if (is_resource($fp)) { + $types = array_shift($rows); + + $this->writeHeader($fp); + foreach ($rows as $row) { + $this->writeRow($fp, $types, $row); + } + $this->writeFooter($fp); + fclose($fp); + } + + return $this; + } + + /** + * write a HTML header + * + * @param $fp filepointer + */ + private function writeHeader($fp) + { + fwrite($fp,"\n"); + fwrite($fp,""); + + } + + /** + * write a single row + * + * @param fp filepointer + * @param $row row + */ + private function writeRow($fp, array $types, array $row) + { + fwrite($fp,""); + foreach ($row as $key => $value) { + fwrite($fp,""); + } + fwrite($fp,"\n"); + } + + /** + * write a HTML footer + */ + private function writeFooter($fp) + { + fwrite($fp,"
".$value."
"); + } + + /** + * Associate columns header with row values + * + * @access private + * @param array $row + * @return array + */ + private function associateColumns(array $row) + { + $line = array(); + $index = 0; + + foreach ($this->columns as $sql_name => $csv_name) { + if (isset($row[$index])) { + $line[$sql_name] = $row[$index]; + } else { + $line[$sql_name] = ''; + } + + $index++; + } + + return $line; + } + + /** + * Filter empty rows + * + * @access private + * @param array $row + * @return array + */ + private function filterRow(array $row) + { + return array_filter($row); + } +} diff --git a/plugins/Timetrackingeditor/Model/SubtaskTimeTrackingCreationModel.php b/plugins/Timetrackingeditor/Model/SubtaskTimeTrackingCreationModel.php new file mode 100644 index 00000000..806d9345 --- /dev/null +++ b/plugins/Timetrackingeditor/Model/SubtaskTimeTrackingCreationModel.php @@ -0,0 +1,53 @@ +prepare($values); + $subtrackingid = $this->db->table(SubtaskTimeTrackingModel::TABLE)->persist($values); + + return (int) $subtrackingid; + } + + /** + * Prepare data + * + * @access public + * @param array $values Form values + */ + public function prepare(array &$values) + { + if ($this->userSession->isLogged()) { + $values['user_id'] = $this->userSession->getId(); + } + + $values["subtask_id"] = $values["opposite_subtask_id"]; + + $this->helper->model->removeFields($values, array('project_id', 'task_id', 'opposite_subtask_id', 'subtask', 'add_another')); + + // Calculate end time + $values = $this->dateParser->convert($values, array('start'), true); + $values["end"] = $values["start"] + ($values['time_spent']*60*60); + } +} diff --git a/plugins/Timetrackingeditor/Model/SubtaskTimeTrackingEditModel.php b/plugins/Timetrackingeditor/Model/SubtaskTimeTrackingEditModel.php new file mode 100644 index 00000000..a6ee24dc --- /dev/null +++ b/plugins/Timetrackingeditor/Model/SubtaskTimeTrackingEditModel.php @@ -0,0 +1,143 @@ +db + ->table(SubtaskTimeTrackingModel::TABLE) + ->columns( + SubtaskTimeTrackingModel::TABLE.'.id', + SubtaskTimeTrackingModel::TABLE.'.subtask_id', + SubtaskTimeTrackingModel::TABLE.'.end', + SubtaskTimeTrackingModel::TABLE.'.start', + SubtaskTimeTrackingModel::TABLE.'.time_spent', + SubtaskTimeTrackingModel::TABLE.'.comment', + SubtaskTimeTrackingModel::TABLE.'.is_billable' + ) + ->eq(SubtaskTimeTrackingModel::TABLE.'.subtask_id', $subtask_id) + ->eq(SubtaskTimeTrackingModel::TABLE.'.user_id', $user_id) + ->eq(SubtaskTimeTrackingModel::TABLE.'.end', 0) + ->findOne(); + } + + /** + * Get by Id + * + * @access public + * @param int $id TimetrackingId + * @return \PicoDb\Table + */ + public function getById($id) + { + return $this->db + ->table(SubtaskTimeTrackingModel::TABLE) + ->columns( + SubtaskTimeTrackingModel::TABLE.'.id', + SubtaskTimeTrackingModel::TABLE.'.subtask_id', + SubtaskTimeTrackingModel::TABLE.'.end', + SubtaskTimeTrackingModel::TABLE.'.start', + SubtaskTimeTrackingModel::TABLE.'.time_spent', + SubtaskTimeTrackingModel::TABLE.'.comment', + SubtaskTimeTrackingModel::TABLE.'.is_billable', + SubtaskModel::TABLE.'.task_id', + SubtaskModel::TABLE.'.title AS subtask_title', + TaskModel::TABLE.'.title AS task_title', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.color_id' + ) + ->join(SubtaskModel::TABLE, 'id', 'subtask_id') + ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) + ->eq(SubtaskTimeTrackingModel::TABLE.'.id', $id) + ->findOne(); + + } + + /** + * Create a time tracking event + * + * @access public + * @param array $values Form values + * @return integer + */ + public function create(array $values) + { + + $this->prepare($values); + $subtrackingid = $this->db->table(SubtaskTimeTrackingModel::TABLE)->persist($values); + + return (int) $subtrackingid; + } + + /** + * Update a time tracking event + * + * @access public + * @param array $values + * @return boolean + */ + public function update(array $values) + { + $this->prepare($values); + + return $this->db->table(SubtaskTimeTrackingModel::TABLE)->eq('id', $values['id'])->update($values); + } + + + /** + * remove an entry + * + * @access public + * @param int $id + * @return boolran + */ + public function remove($id) + { + return $this->db->table(SubtaskTimeTrackingModel::TABLE)->eq('id', $id)->remove(); + + } + + /** + * Prepare data + * + * @access public + * @param array $values Form values + */ + public function prepare(array &$values) + { + if ($this->userSession->isLogged()) { + $values['user_id'] = $this->userSession->getId(); + } + + $values["subtask_id"] = $values["opposite_subtask_id"]; + + $this->helper->model->removeFields($values, array('project_id', 'task_id', 'opposite_subtask_id', 'subtask', 'add_another', 'old_time_spent', 'old_opposite_subtask_id')); + + // Calculate end time + $values = $this->dateParser->convert($values, array('start'), true); + $values["end"] = $values["start"] + ($values['time_spent']*60*60); + } +} diff --git a/plugins/Timetrackingeditor/Model/SubtaskTimeTrackingModel.php b/plugins/Timetrackingeditor/Model/SubtaskTimeTrackingModel.php new file mode 100644 index 00000000..1fcf8738 --- /dev/null +++ b/plugins/Timetrackingeditor/Model/SubtaskTimeTrackingModel.php @@ -0,0 +1,133 @@ +hasTimer($subtask_id, $user_id) && + $this->db + ->table(self::TABLE) + ->insert(array('subtask_id' => $subtask_id, + 'user_id' => $user_id, + 'comment' => $comment, + 'is_billable' => $is_billable, + 'start' => time(), + 'end' => 0)); + } + + /** + * Log end time + * + * @access public + * @param integer $subtask_id + * @param integer $user_id + * @return boolean + */ + public function logEndTimeExtended($subtask_id, $user_id, $comment, $is_billable) + { + $time_spent = $this->getTimeSpent($subtask_id, $user_id); + + if ($time_spent > 0) { + $this->updateSubtaskTimeSpent($subtask_id, $time_spent); + } + + return $this->db + ->table(self::TABLE) + ->eq('subtask_id', $subtask_id) + ->eq('user_id', $user_id) + ->eq('end', 0) + ->update(array( + 'end' => time(), + 'time_spent' => $time_spent, + 'comment' => $comment, + 'is_billable' => $is_billable + )); + } + + /** + * Get query for task timesheet (pagination) + * + * @access public + * @param integer $task_id Task id + * @return \PicoDb\Table + */ + public function getTaskQuery($task_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.subtask_id', + self::TABLE.'.end', + self::TABLE.'.start', + self::TABLE.'.time_spent', + self::TABLE.'.user_id', + self::TABLE.'.comment', + self::TABLE.'.is_billable', + SubtaskModel::TABLE.'.task_id', + SubtaskModel::TABLE.'.title AS subtask_title', + TaskModel::TABLE.'.project_id', + UserModel::TABLE.'.username', + UserModel::TABLE.'.name AS user_fullname' + ) + ->join(SubtaskModel::TABLE, 'id', 'subtask_id') + ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) + ->join(UserModel::TABLE, 'id', 'user_id', self::TABLE) + ->eq(TaskModel::TABLE.'.id', $task_id); + } + + /** + * Update subtask time billable + * + * @access public + * @param integer $subtask_id + * @param float $time_billable + * @return bool + */ + public function updateSubtaskTimeBillable($subtask_id, $time_billable) + { + $subtask = $this->subtaskModel->getById($subtask_id); + + return $this->subtaskModel->update(array( + 'id' => $subtask['id'], + 'time_billable' => $subtask['time_billable'] + $time_billable, + 'task_id' => $subtask['task_id'], + ), false); + } + + /** + * get a Subtasktimetracking entry by Id + * + * @access public + * @param $id the subtasktimetracking id + * @return array + */ + public function getById($id) + { + return $this->db + ->table(SubtaskTimeTrackingModel::TABLE) + ->eq('id', $id) + ->findOne(); + } + + +} diff --git a/plugins/Timetrackingeditor/Plugin.php b/plugins/Timetrackingeditor/Plugin.php new file mode 100644 index 00000000..f36403fb --- /dev/null +++ b/plugins/Timetrackingeditor/Plugin.php @@ -0,0 +1,91 @@ +hook->on("template:layout:css", array("template" => "plugins/Timetrackingeditor/assets/css/timetrackingeditor.css")); + $this->template->setTemplateOverride('task/time_tracking_details', 'timetrackingeditor:time_tracking_editor'); + $this->template->setTemplateOverride('subtask/table', 'timetrackingeditor:subtask/table'); + + # $this->helper->register("subtask", "Kanboard\Plugin\Timetrackingeditor\Helper\SubtaskHelper"); + + $this->container["cli"]->add(new AllSubtaskTimeTrackingExportCommand($this->container)); + } + + public function onStartup() + { + Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale'); + } + + public function getClasses() + { + return array( + 'Plugin\Timetrackingeditor\Model' => array( + 'SubtaskTimeTrackingCreationModel', + 'SubtaskTimeTrackingEditModel', + 'SubtaskTimeTrackingModel', + ), + 'Plugin\Timetrackingeditor\Filter' => array( + 'SubtaskFilter', + 'SubtaskTaskFilter', + 'SubtaskTitleFilter' + ), + 'Plugin\Timetrackingeditor\Console' => array( + 'AllSubtaskTimeTrackingExportCommand' + ), + 'Plugin\Timetrackingeditor\Controller' => array( + 'SubtaskStatusController', + 'SubtaskAjaxController', + 'TimeTrackingEditorController' + ), + 'Plugin\Timetrackingeditor\Export' => array( + 'SubtaskTimeTrackingExport' + ), + 'Plugin\Timetrackingeditor\Validator' => array( + 'SubtaskTimeTrackingValidator' + ), + 'Plugin\Timetrackingeditor\Formatter' => array( + 'SubtaskAutoCompleteFormatter' + ), + ); + } + + public function getPluginName() + { + return 'TimeTrackingEditor'; + } + + public function getPluginDescription() + { + return t('Allows Editing of TimeTracking Values'); + } + + public function getPluginAuthor() + { + return 'Thomas Stinner'; + } + + public function getPluginVersion() + { + return '1.0.21'; + } + + public function getPluginHomepage() + { + return 'https://github.com/stinnux/kanboard-timetrackingeditor'; + } + + public function getCompatibleVersion() + { + return '>=1.2.4'; + } +} diff --git a/plugins/Timetrackingeditor/README.md b/plugins/Timetrackingeditor/README.md new file mode 100644 index 00000000..68907fc0 --- /dev/null +++ b/plugins/Timetrackingeditor/README.md @@ -0,0 +1,40 @@ +Timetrackingeditor +================== + + +Allows manual adding and editon of Timetracking Entries + +Author +------ + +- Thomas Stinner + +Requirements +------------ + +- Kanboard >= 1.0.38 (not testet with older versions) + + +Installation +------------ + +You have the choice between 3 methods: + +1. Install the plugin from the Kanboard plugin manager in one click +2. Download the zip file and decompress everything under the directory `plugins/Timetrackingeditor` +3. Clone this repository into the folder `plugins/Timetrackingeditor` + +Note: Plugin folder is case-sensitive. + + +Documentation +------------- + +With this plugin you are able to edit, remove and manually create entries in the Time Tracking Table. + +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. + +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. + +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``` + diff --git a/plugins/Timetrackingeditor/Schema/Mysql.php b/plugins/Timetrackingeditor/Schema/Mysql.php new file mode 100644 index 00000000..8fda41c7 --- /dev/null +++ b/plugins/Timetrackingeditor/Schema/Mysql.php @@ -0,0 +1,16 @@ +exec("ALTER TABLE subtasks add time_billable INT default 0"); +} + +function version_1($pdo) +{ + $pdo->exec("ALTER TABLE subtask_time_tracking add comment TEXT"); + $pdo->exec("ALTER TABLE subtask_time_tracking add is_billable TINYINT"); +} diff --git a/plugins/Timetrackingeditor/Schema/Postgres.php b/plugins/Timetrackingeditor/Schema/Postgres.php new file mode 100644 index 00000000..b735e931 --- /dev/null +++ b/plugins/Timetrackingeditor/Schema/Postgres.php @@ -0,0 +1,16 @@ +exec("ALTER TABLE subtasks add time_billable INTEGER default 0"); +} + +function version_1($pdo) +{ + $pdo->exec("ALTER TABLE subtask_time_tracking add comment TEXT"); + $pdo->exec("ALTER TABLE subtask_time_tracking add is_billable BOOLEAN"); +} diff --git a/plugins/Timetrackingeditor/Schema/Sqlite.php b/plugins/Timetrackingeditor/Schema/Sqlite.php new file mode 100644 index 00000000..ffbeac85 --- /dev/null +++ b/plugins/Timetrackingeditor/Schema/Sqlite.php @@ -0,0 +1,16 @@ +exec("ALTER TABLE subtasks add time_billable NUMERIC default 0"); +} + +function version_1($pdo) +{ + $pdo->exec("ALTER TABLE subtask_time_tracking add comment TEXT"); + $pdo->exec("ALTER TABLE subtask_time_tracking add is_billable INTEGER"); +} diff --git a/plugins/Timetrackingeditor/Template/create.php b/plugins/Timetrackingeditor/Template/create.php new file mode 100644 index 00000000..362cfde4 --- /dev/null +++ b/plugins/Timetrackingeditor/Template/create.php @@ -0,0 +1,41 @@ + +
+ + form->csrf() ?> + + form->hidden('project_id', $values) ?> + form->hidden('task_id', $values) ?> + form->hidden('opposite_subtask_id', $values) ?> + + form->label(t('Subtask'), 'subtask') ?> + form->text( + 'subtask', + $values, + $errors, + array( + 'required', + (!isset($autofocus) || $autofocus == 'subtask' ? 'autofocus' : ''), + 'placeholder="'.t('Start to type subtask title...').'"', + 'title="'.t('Start to type subtask title...').'"', + 'data-dst-field="opposite_subtask_id"', + 'data-search-url="'.$this->url->href('SubtaskAjaxController', 'autocomplete', array('plugin' => 'timetrackingeditor', 'task_id' => $values['task_id'])).'"', + ), + 'autocomplete') ?> + + form->label(t('Time spent'), 'time_spent') ?> + form->numeric('time_spent', $values, $errors, array('maxlength="10"', 'required', (isset($autofocus) && $autofocus == "time_spent" ? 'autofocus' : '')), 'form-numeric') ?> hours + + form->label(t('Start Date'), 'start') ?> + form->text('start', $values, $errors, array('maxlength="10"', 'required'), 'form-date') ?> + + form->label(t('Comment'), 'comment') ?> + form->textarea('comment', $values, $errors, array(), 'markdown-editor') ?> + + form->checkbox('is_billable', t('Billable?'), 1, isset($values['is_billable']) && $values['is_billable'] == 1) ?> + form->checkbox('add_another', t('Add another event'), 1, isset($values['add_another']) && $values['add_another'] == 1) ?> + + modal->submitButtons(); ?> + +
diff --git a/plugins/Timetrackingeditor/Template/edit.php b/plugins/Timetrackingeditor/Template/edit.php new file mode 100644 index 00000000..28323812 --- /dev/null +++ b/plugins/Timetrackingeditor/Template/edit.php @@ -0,0 +1,41 @@ + +
+ + form->csrf() ?> + + form->hidden('project_id', $values) ?> + form->hidden('task_id', $values) ?> + form->hidden('opposite_subtask_id', $values) ?> + form->hidden('id', $values) ?> + + + form->label(t('Subtask'), 'subtask') ?> + form->text( + 'subtask', + $values, + $errors, + array( + 'required', + 'autofocus', + 'placeholder="'.t('Start to type subtask title...').'"', + 'title="'.t('Start to type subtask title...').'"', + 'data-dst-field="opposite_subtask_id"', + 'data-search-url="'.$this->url->href('SubtaskAjaxController', 'autocomplete', array('plugin' => 'timetrackingeditor', 'task_id' => $values['task_id'])).'"', + ), + 'autocomplete') ?> + + form->label(t('Start Date'), 'start') ?> + form->text('start', $values, $errors, array('maxlength="10"', 'required'), 'form-date') ?> + + form->label(t('Time spent'), 'time_spent') ?> + form->numeric('time_spent', $values, $errors, array('maxlength="10"', 'required'), 'form-numeric') ?> hours + + form->label(t('Comment'), 'comment') ?> + form->textarea('comment', $values, $errors, array(), 'markdown-editor') ?> + + form->checkbox('is_billable', t('Billable?'), 1, $values['is_billable'] == 1) ?> + + modal->submitButtons(); ?> +
diff --git a/plugins/Timetrackingeditor/Template/menu.php b/plugins/Timetrackingeditor/Template/menu.php new file mode 100644 index 00000000..fb715c4b --- /dev/null +++ b/plugins/Timetrackingeditor/Template/menu.php @@ -0,0 +1,23 @@ + diff --git a/plugins/Timetrackingeditor/Template/remove.php b/plugins/Timetrackingeditor/Template/remove.php new file mode 100644 index 00000000..f86ad424 --- /dev/null +++ b/plugins/Timetrackingeditor/Template/remove.php @@ -0,0 +1,20 @@ + + +
+
+ +
    +
  • + text->e($timetracking['subtask_title']) ?> +
  • +
+
+ +
+ 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') ?> + + url->link(t('cancel'), 'TimeTrackingEditorController', 'show', array('plugin' => 'timetrackingeditor','id' => $timetracking['id'], 'project_id' => $timetracking['project_id']), false, 'close-popover') ?> +
+
diff --git a/plugins/Timetrackingeditor/Template/start.php b/plugins/Timetrackingeditor/Template/start.php new file mode 100644 index 00000000..cc67febd --- /dev/null +++ b/plugins/Timetrackingeditor/Template/start.php @@ -0,0 +1,23 @@ + +
+ + form->csrf() ?> + + form->hidden('project_id', $values) ?> + form->hidden('task_id', $values) ?> + form->hidden('subtask_id', $values) ?> + + + + + form->label(t('Comment'), 'comment') ?> + form->textarea('comment', $values, $errors, array(), 'markdown-editor') ?> + + form->checkbox('is_billable', t('Billable?'), 1, isset($values['is_billable']) && $values['is_billable'] == 1) ?> + +
+ modal->submitButtons() ?> +
+
diff --git a/plugins/Timetrackingeditor/Template/stop.php b/plugins/Timetrackingeditor/Template/stop.php new file mode 100644 index 00000000..63d3905d --- /dev/null +++ b/plugins/Timetrackingeditor/Template/stop.php @@ -0,0 +1,23 @@ + +
+ + form->csrf() ?> + + form->hidden('project_id', $values) ?> + form->hidden('task_id', $values) ?> + form->hidden('subtask_id', $values) ?> + + + + + form->label(t('Comment'), 'comment') ?> + form->textarea('comment', $values, $errors, array(), 'markdown-editor') ?> + + form->checkbox('is_billable', t('Billable?'), 1, isset($values['is_billable']) && $values['is_billable'] == 1) ?> + +
+ modal->submitButtons() ?> +
+
diff --git a/plugins/Timetrackingeditor/Template/subtask/table.php b/plugins/Timetrackingeditor/Template/subtask/table.php new file mode 100644 index 00000000..fa7f62ff --- /dev/null +++ b/plugins/Timetrackingeditor/Template/subtask/table.php @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + subtask->renderToggleStatus($task, $subtask, "table") ?> + + subtask->getTitle($subtask) ?> + + + + text->e($subtask['name'] ?: $subtask['username']) ?> + + +
    +
  • + + text->e($subtask['time_spent']).'h' ?> + + + + text->e($subtask['time_estimated']).'h' ?> + + + text->e($subtask['time_billable']).'h' ?> + + +
  • + user->getId()): ?> +
  • + + modal->medium("pause",t('Stop timer'), 'TimeTrackingEditorController', 'stop', + array('plugin' => 'Timetrackingeditor', + 'project_id' => $task['project_id'], + 'task_id' => $subtask['task_id'], + 'subtask_id' => $subtask['id'])) ?> + (dt->age($subtask['timer_start_date']) ?>) + + modal->medium("play-circle-o",t('Start timer'), 'TimeTrackingEditorController', 'start', + array('plugin' => 'Timetrackingeditor', + 'project_id' => $task['project_id'], + 'task_id' => $subtask['task_id'], + 'subtask_id' => $subtask['id'])) ?> + +
  • + +
+
+ render('subtask/menu', array( + 'task' => $task, + 'subtask' => $subtask, + )) ?> + + modal->medium("clock-o", t('New'), 'TimeTrackingEditorController', + 'create', array( + 'plugin' => 'Timetrackingeditor', + 'task_id' => $task['id'], + 'project_id' => $task['project_id'], + 'subtask_id' => $subtask['id'])) ?> +
+ diff --git a/plugins/Timetrackingeditor/Template/time_tracking_editor.php b/plugins/Timetrackingeditor/Template/time_tracking_editor.php new file mode 100644 index 00000000..2457305b --- /dev/null +++ b/plugins/Timetrackingeditor/Template/time_tracking_editor.php @@ -0,0 +1,53 @@ +
+

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

+
+ +render('task/time_tracking_summary', array('task' => $task)) ?> + +

+ +modal->medium("plus",t('Add a new timetracking entry'), 'TimeTrackingEditorController', + 'create', array( + 'plugin' => 'timetrackingeditor', + 'task_id' => $task['id'], + 'project_id' => $task['project_id'])) ?> + +isEmpty()): ?> +

+ + + + + + + + + + + getCollection() as $record): ?> + + + + + + + + + +
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'])) ?> + + + + app->tooltipMarkdown($record['comment']) ?> + dt->datetime($record['start']) ?>dt->datetime($record['end']) ?> + user->isCurrentUser($record['user_id'])) { ?> + render('timetrackingeditor:menu', array( + 'task' => $task, + 'subtask_id' => $record['subtask_id'], + 'id' => $record['id'] + )) ?> + +
+ + + diff --git a/plugins/Timetrackingeditor/Test/PluginTest.php b/plugins/Timetrackingeditor/Test/PluginTest.php new file mode 100644 index 00000000..e563e9e4 --- /dev/null +++ b/plugins/Timetrackingeditor/Test/PluginTest.php @@ -0,0 +1,17 @@ +container); + $this->assertSame(null, $plugin->initialize()); + $this->assertSame(null, $plugin->onStartup()); + $this->assertNotEmpty($plugin->getPluginName()); + $this->assertNotEmpty($plugin->getPluginDescription()); + $this->assertNotEmpty($plugin->getPluginAuthor()); + $this->assertNotEmpty($plugin->getPluginVersion()); + $this->assertNotEmpty($plugin->getPluginHomepage()); + } +} diff --git a/plugins/Timetrackingeditor/Test/TimetrackingeditorTest.php b/plugins/Timetrackingeditor/Test/TimetrackingeditorTest.php new file mode 100644 index 00000000..551d9b10 --- /dev/null +++ b/plugins/Timetrackingeditor/Test/TimetrackingeditorTest.php @@ -0,0 +1,48 @@ + +container); + $plugin->scan(); + } + + public function testAll() + { + $this->assertCreateTeamProject(); + + $this->task_id = $this->app->createTask(array('project_id' => $this->projectId, 'title' => 'Task 1')); + $this->subtask_id = $this->app->createSubTask(array('project_id' => $this->projectId, 'task_id' => $this->task_id, 'title' => 'Subtask 1')); + + $this->assertNotFalse($this->task_id); + $this->assertNotFalse($this->subtask_id); + + $this->testCreateEntry(); + } + + public function testCreateEntry() + { + + + + } + + +} diff --git a/plugins/Timetrackingeditor/Validator/SubtaskTimeTrackingValidator.php b/plugins/Timetrackingeditor/Validator/SubtaskTimeTrackingValidator.php new file mode 100644 index 00000000..6bddc47a --- /dev/null +++ b/plugins/Timetrackingeditor/Validator/SubtaskTimeTrackingValidator.php @@ -0,0 +1,73 @@ +commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $rules = array ( + new Validators\Required('id', t('The Timetracking id is required')) + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Common validation rules, valid for creation and modification + * + * @access private + */ + private function commonValidationRules() + { + $rules = array( + new Validators\Required('task_id', t('The Task id is required')), + new Validators\Required('opposite_subtask_id', t('The subtask is required')), + new Validators\Required('start', t('The Start Date is required')), + new Validators\Required('time_spent', t('The Time spent is required')), + ); + return $rules; + } +} diff --git a/plugins/Timetrackingeditor/assets/css/timetrackingeditor.css b/plugins/Timetrackingeditor/assets/css/timetrackingeditor.css new file mode 100644 index 00000000..f2a9d586 --- /dev/null +++ b/plugins/Timetrackingeditor/assets/css/timetrackingeditor.css @@ -0,0 +1,3 @@ +.right { + text-align: right; +} -- cgit v1.2.3