diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/Controller/Gantt.php | 114 | ||||
-rw-r--r-- | app/Controller/Taskcreation.php | 3 | ||||
-rw-r--r-- | app/Model/Acl.php | 1 | ||||
-rw-r--r-- | app/Model/Task.php | 35 | ||||
-rw-r--r-- | app/Model/TaskCreation.php | 9 | ||||
-rw-r--r-- | app/Model/TaskFilter.php | 76 | ||||
-rw-r--r-- | app/Schema/Postgres.php | 2 | ||||
-rw-r--r-- | app/Template/gantt/project.php | 49 | ||||
-rw-r--r-- | app/Template/gantt/task_creation.php | 65 | ||||
-rw-r--r-- | app/Template/project/dropdown.php | 4 | ||||
-rw-r--r-- | app/common.php | 4 |
11 files changed, 353 insertions, 9 deletions
diff --git a/app/Controller/Gantt.php b/app/Controller/Gantt.php new file mode 100644 index 00000000..74e15d4f --- /dev/null +++ b/app/Controller/Gantt.php @@ -0,0 +1,114 @@ +<?php + +namespace Controller; + +use Model\Task; + +/** + * Gantt controller + * + * @package controller + * @author Frederic Guillot + */ +class Gantt extends Base +{ + /** + * Show Gantt chart for projects + */ + public function project() + { + $project = $this->getProject(); + $sorting = $this->request->getStringParam('sorting', 'board'); + $filter = $this->taskFilter->gantt()->filterByProject($project['id'])->filterByStatus(Task::STATUS_OPEN); + + if ($sorting === 'date') { + $filter->query->asc(Task::TABLE.'.date_started')->asc(Task::TABLE.'.date_creation'); + } + else { + $filter->query->asc('column_position')->asc(Task::TABLE.'.position'); + } + + $this->response->html($this->template->layout('gantt/project', array( + 'sorting' => $sorting, + 'tasks' => $filter->toGanttBars(), + 'project' => $project, + 'title' => t('Gantt chart for %s', $project['name']), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + ))); + } + + /** + * Save new task start date and due date + */ + public function saveDate() + { + $project = $this->getProject(); + $values = $this->request->getJson(); + + $result = $this->taskModification->update(array( + 'id' => $values['id'], + 'date_started' => strtotime($values['start']), + 'date_due' => strtotime($values['end']), + )); + + if (! $result) { + $this->response->json(array('message' => 'Unable to save task'), 400); + } + + $this->response->json(array('message' => 'OK'), 201); + } + + /** + * Simplified form to create a new task + * + * @access public + */ + public function task(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + $this->response->html($this->template->render('gantt/task_creation', array( + 'errors' => $errors, + 'values' => $values + array( + 'project_id' => $project['id'], + 'column_id' => $this->board->getFirstColumn($project['id']), + 'position' => 1 + ), + 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true), + 'colors_list' => $this->color->getList(), + 'categories_list' => $this->category->getList($project['id']), + 'swimlanes_list' => $this->swimlane->getList($project['id'], false, true), + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => $project['name'].' > '.t('New task') + ))); + } + + /** + * Validate and save a new task + * + * @access public + */ + public function saveTask() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateCreation($values); + + if ($valid) { + + $task_id = $this->taskCreation->create($values); + + if ($task_id !== false) { + $this->session->flash(t('Task created successfully.')); + $this->response->redirect($this->helper->url->to('gantt', 'project', array('project_id' => $project['id']))); + } + else { + $this->session->flashError(t('Unable to create your task.')); + } + } + + $this->task($values, $errors); + } +} diff --git a/app/Controller/Taskcreation.php b/app/Controller/Taskcreation.php index 7c841e10..ff25c5da 100644 --- a/app/Controller/Taskcreation.php +++ b/app/Controller/Taskcreation.php @@ -2,8 +2,6 @@ namespace Controller; -use Model\Project as ProjectModel; - /** * Task Creation controller * @@ -38,7 +36,6 @@ class Taskcreation extends Base 'ajax' => $this->request->isAjax(), 'errors' => $errors, 'values' => $values + array('project_id' => $project['id']), - 'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE), 'columns_list' => $this->board->getColumnsList($project['id']), 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true), 'colors_list' => $this->color->getList(), diff --git a/app/Model/Acl.php b/app/Model/Acl.php index 0840f44c..312ae7d4 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -65,6 +65,7 @@ class Acl extends Base 'project' => array('edit', 'update', 'share', 'integration', 'users', 'alloweverybody', 'allow', 'setowner', 'revoke', 'duplicate', 'disable', 'enable'), 'swimlane' => '*', 'budget' => '*', + 'gantt' => '*', ); /** diff --git a/app/Model/Task.php b/app/Model/Task.php index 71d973a4..ae8fc7a8 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -121,7 +121,7 @@ class Task extends Base */ public function getRecurrenceStatusList() { - return array ( + return array( Task::RECURRING_STATUS_NONE => t('No'), Task::RECURRING_STATUS_PENDING => t('Yes'), ); @@ -135,7 +135,7 @@ class Task extends Base */ public function getRecurrenceTriggerList() { - return array ( + return array( Task::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'), Task::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'), Task::RECURRING_TRIGGER_CLOSE => t('When task is closed'), @@ -150,7 +150,7 @@ class Task extends Base */ public function getRecurrenceBasedateList() { - return array ( + return array( Task::RECURRING_BASEDATE_DUEDATE => t('Existing due date'), Task::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'), ); @@ -164,10 +164,37 @@ class Task extends Base */ public function getRecurrenceTimeframeList() { - return array ( + return array( Task::RECURRING_TIMEFRAME_DAYS => t('Day(s)'), Task::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'), Task::RECURRING_TIMEFRAME_YEARS => t('Year(s)'), ); } + + /** + * Get task progress based on the column position + * + * @access public + * @param array $task + * @param array $columns + * @return integer + */ + public function getProgress(array $task, array $columns) + { + if ($task['is_active'] == self::STATUS_CLOSED) { + return 100; + } + + $position = 0; + + foreach ($columns as $column_id => $column_title) { + if ($column_id == $task['column_id']) { + break; + } + + $position++; + } + + return (int) ($position * 100) / count($columns); + } } diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php index e530da13..4373fa63 100644 --- a/app/Model/TaskCreation.php +++ b/app/Model/TaskCreation.php @@ -25,10 +25,17 @@ class TaskCreation extends Base return 0; } + $position = empty($values['position']) ? 0 : $values['position']; + $this->prepare($values); $task_id = $this->persist(Task::TABLE, $values); if ($task_id !== false) { + + if ($position > 0 && $values['position'] > 1) { + $this->taskPosition->movePosition($values['project_id'], $task_id, $values['column_id'], $position, $values['swimlane_id'], false); + } + $this->fireEvents($task_id, $values); } @@ -46,7 +53,7 @@ class TaskCreation extends Base $this->dateParser->convert($values, array('date_due')); $this->dateParser->convert($values, array('date_started'), true); $this->removeFields($values, array('another_task')); - $this->resetFields($values, array('creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); + $this->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); if (empty($values['column_id'])) { $values['column_id'] = $this->board->getFirstColumn($values['project_id']); diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php index 956ffbe8..8da4a214 100644 --- a/app/Model/TaskFilter.php +++ b/app/Model/TaskFilter.php @@ -87,6 +87,38 @@ class TaskFilter extends Base } /** + * Prepare filter for Gantt chart + * + * @access public + * @return TaskFilter + */ + public function gantt() + { + $this->query = $this->db->table(Task::TABLE); + $this->query->join(Board::TABLE, 'id', 'column_id', Task::TABLE); + $this->query->join(User::TABLE, 'id', 'owner_id', Task::TABLE); + + $this->query->columns( + Task::TABLE.'.id', + Task::TABLE.'.title', + Task::TABLE.'.project_id', + Task::TABLE.'.column_id', + Task::TABLE.'.color_id', + Task::TABLE.'.date_started', + Task::TABLE.'.date_due', + Task::TABLE.'.date_creation', + Task::TABLE.'.is_active', + Task::TABLE.'.position', + Board::TABLE.'.position AS column_position', + Board::TABLE.'.title AS column_title', + User::TABLE.'.name AS assignee_name', + User::TABLE.'.username AS assignee_username' + ); + + return $this; + } + + /** * Create a new query * * @access public @@ -675,6 +707,50 @@ class TaskFilter extends Base } /** + * Format tasks to be displayed in the Gantt chart + * + * @access public + * @return array + */ + public function toGanttBars() + { + $bars = array(); + $columns = array(); + + foreach ($this->query->findAll() as $task) { + if (! isset($column_count[$task['project_id']])) { + $columns[$task['project_id']] = $this->board->getColumnsList($task['project_id']); + } + + $start = $task['date_started'] ?: time(); + $end = $task['date_due'] ?: $start; + + $bars[] = array( + 'id' => $task['id'], + 'title' => $task['title'], + 'start' => array( + (int) date('Y', $start), + (int) date('n', $start), + (int) date('j', $start), + ), + 'end' => array( + (int) date('Y', $end), + (int) date('n', $end), + (int) date('j', $end), + ), + 'column_title' => $task['column_title'], + 'assignee' => $task['assignee_name'] ?: $task['assignee_username'], + 'progress' => $this->task->getProgress($task, $columns[$task['project_id']]).'%', + 'link' => $this->helper->url->href('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), + 'color' => $this->color->getColorProperties($task['color_id']), + 'not_defined' => empty($task['date_due']) || empty($task['date_started']), + ); + } + + return $bars; + } + + /** * Format the results to the ajax autocompletion * * @access public diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index e1ba07e3..143167e3 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -10,7 +10,7 @@ const VERSION = 63; function version_63($pdo) { - $pdo->exec("ALTER TABLE users ADD COLUMN is_project_admin INTEGER DEFAULT 0"); + $pdo->exec("ALTER TABLE users ADD COLUMN is_project_admin BOOLEAN DEFAULT '0'"); } function version_62($pdo) diff --git a/app/Template/gantt/project.php b/app/Template/gantt/project.php new file mode 100644 index 00000000..0c545be8 --- /dev/null +++ b/app/Template/gantt/project.php @@ -0,0 +1,49 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li> + <span class="dropdown"> + <span> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <?= $this->render('project/dropdown', array('project' => $project)) ?> + </ul> + </span> + </span> + </li> + <li> + <i class="fa fa-th fa-fw"></i> + <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> + </li> + </ul> + <ul class="views toolbar"> + <li <?= $sorting === 'board' ? 'class="active"' : '' ?>> + <i class="fa fa-sort-numeric-asc fa-fw"></i> + <?= $this->url->link(t('Sort by position'), 'gantt', 'project', array('project_id' => $project['id'], 'sorting' => 'board')) ?> + </li> + <li <?= $sorting === 'date' ? 'class="active"' : '' ?>> + <i class="fa fa-sort-amount-asc fa-fw"></i> + <?= $this->url->link(t('Sort by date'), 'gantt', 'project', array('project_id' => $project['id'], 'sorting' => 'date')) ?> + </li> + <li> + <i class="fa fa-plus fa-fw"></i> + <?= $this->url->link(t('Add task'), 'gantt', 'task', array('project_id' => $project['id']), false, 'popover') ?> + </li> + </ul> + </div> + + <?php if (! empty($tasks)): ?> + <div + id="gantt-chart" + data-tasks='<?= json_encode($tasks) ?>' + data-save-url="<?= $this->url->href('gantt', 'saveDate', array('project_id' => $project['id'])) ?>" + data-label-start-date="<?= t('Start date:') ?>" + data-label-end-date="<?= t('Due date:') ?>" + data-label-assignee="<?= t('Assignee:') ?>" + data-label-not-defined="<?= t('There is no start date or due date for this task.') ?>" + ></div> + <p class="alert alert-info"><?= t('Moving or resizing a task will change the start and due date of the task.') ?></p> + <?php else: ?> + <p class="alert"><?= t('There is no task in your project.') ?></p> + <?php endif ?> +</section>
\ No newline at end of file diff --git a/app/Template/gantt/task_creation.php b/app/Template/gantt/task_creation.php new file mode 100644 index 00000000..d0d14c1e --- /dev/null +++ b/app/Template/gantt/task_creation.php @@ -0,0 +1,65 @@ +<div class="page-header"> + <h2><?= t('New task') ?></h2> +</div> +<form method="post" action="<?= $this->url->href('gantt', 'saveTask', array('project_id' => $values['project_id'])) ?>" autocomplete="off"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('project_id', $values) ?> + <?= $this->form->hidden('column_id', $values) ?> + <?= $this->form->hidden('position', $values) ?> + + <div class="form-column"> + <?= $this->form->label(t('Title'), 'title') ?> + <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large') ?> + + <?= $this->form->label(t('Description'), 'description') ?> + + <div class="form-tabs"> + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"', 'tabindex="2"')) ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + </div> + </div> + + <div class="form-column"> + <?= $this->form->label(t('Assignee'), 'owner_id') ?> + <?= $this->form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?><br/> + + <?= $this->form->label(t('Category'), 'category_id') ?> + <?= $this->form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?><br/> + + <?php if (! (count($swimlanes_list) === 1 && key($swimlanes_list) === 0)): ?> + <?= $this->form->label(t('Swimlane'), 'swimlane_id') ?> + <?= $this->form->select('swimlane_id', $swimlanes_list, $values, $errors, array('tabindex="5"')) ?><br/> + <?php endif ?> + + <?= $this->form->label(t('Color'), 'color_id') ?> + <?= $this->form->select('color_id', $colors_list, $values, $errors, array('tabindex="7"')) ?><br/> + + <?= $this->form->label(t('Complexity'), 'score') ?> + <?= $this->form->number('score', $values, $errors, array('tabindex="8"')) ?><br/> + + <?= $this->form->label(t('Start Date'), 'date_started') ?> + <?= $this->form->text('date_started', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="9"'), 'form-date') ?> + + <?= $this->form->label(t('Due Date'), 'date_due') ?> + <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="10"'), 'form-date') ?><br/> + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" tabindex="11"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> + </div> +</form> diff --git a/app/Template/project/dropdown.php b/app/Template/project/dropdown.php index 0a53cc05..7b599d60 100644 --- a/app/Template/project/dropdown.php +++ b/app/Template/project/dropdown.php @@ -15,6 +15,10 @@ <?= $this->url->link(t('Analytics'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?> </li> <li> + <i class="fa fa-sliders fa-fw"></i> + <?= $this->url->link(t('Gantt chart'), 'gantt', 'project', array('project_id' => $project['id'])) ?> +</li> +<li> <i class="fa fa-pie-chart fa-fw"></i> <?= $this->url->link(t('Budget'), 'budget', 'index', array('project_id' => $project['id'])) ?> </li> diff --git a/app/common.php b/app/common.php index 7f66c05e..b8f5ccd2 100644 --- a/app/common.php +++ b/app/common.php @@ -126,6 +126,10 @@ if (ENABLE_URL_REWRITE) { $container['router']->addRoute('l/:project_id', 'listing', 'show', array('project_id')); $container['router']->addRoute('list/:project_id/:search', 'listing', 'show', array('project_id', 'search')); + // Gantt routes + $container['router']->addRoute('gantt/:project_id', 'gantt', 'project', array('project_id')); + $container['router']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project', array('project_id', 'sorting')); + // Subtask routes $container['router']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create', array('project_id', 'task_id')); $container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm', array('project_id', 'task_id', 'subtask_id')); |