Add Gantt chart for projects
This commit is contained in:
114
app/Controller/Gantt.php
Normal file
114
app/Controller/Gantt.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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' => '*',
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -86,6 +86,38 @@ class TaskFilter extends Base
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
@@ -674,6 +706,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
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
49
app/Template/gantt/project.php
Normal file
49
app/Template/gantt/project.php
Normal file
@@ -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>
|
||||
65
app/Template/gantt/task_creation.php
Normal file
65
app/Template/gantt/task_creation.php
Normal file
@@ -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>
|
||||
@@ -14,6 +14,10 @@
|
||||
<i class="fa fa-line-chart fa-fw"></i>
|
||||
<?= $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'])) ?>
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user