diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | app/Controller/Analytic.php | 24 | ||||
-rw-r--r-- | app/Model/ProjectAnalytic.php | 40 | ||||
-rw-r--r-- | app/Model/TaskFinder.php | 1 | ||||
-rw-r--r-- | app/Template/analytic/compare_hours.php | 57 | ||||
-rw-r--r-- | app/Template/analytic/sidebar.php | 5 | ||||
-rw-r--r-- | assets/js/src/CompareHoursColumnChart.js | 43 | ||||
-rw-r--r-- | assets/js/src/Router.js | 1 |
8 files changed, 171 insertions, 2 deletions
@@ -4,7 +4,7 @@ CSS_APP = $(addprefix assets/css/src/, $(addsuffix .css, base links title table CSS_PRINT = $(addprefix assets/css/src/, $(addsuffix .css, print links table board task comment subtask markdown)) CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jquery-ui-timepicker-addon.min chosen.min fullcalendar.min font-awesome.min c3.min)) -JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Sidebar Search App Screenshot Calendar Board Swimlane Gantt Task Project TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart Router)) +JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Sidebar Search App Screenshot Calendar Board Swimlane Gantt Task Project TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart CompareHoursColumnChart Router)) JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min)) JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn)) diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php index e03d8cab..603ace01 100644 --- a/app/Controller/Analytic.php +++ b/app/Controller/Analytic.php @@ -1,6 +1,7 @@ <?php namespace Kanboard\Controller; +use Kanboard\Model\Task as TaskModel; /** * Project Analytic controller @@ -166,4 +167,27 @@ class Analytic extends Base 'title' => t($title, $project['name']), ))); } + + public function compareHours() + { + $project = $this->getProject(); + $params = $this->getProjectFilters('analytic', 'compareHours'); + $query = $this->taskFilter->search('status:all')->filterByProject($params['project']['id'])->getQuery(); + + + $paginator = $this->paginator + ->setUrl('analytics', 'compare_hours') + ->setMax(30) + ->setOrder(TaskModel::TABLE.'.id') + ->setQuery($query) + ->calculate(); + + $stats = $this->projectAnalytic->getHoursByStatus($project['id']); + + $this->response->html($this->layout('analytic/compare_hours', array( + 'project' => $project, + 'paginator' => $paginator, + 'metrics' => $stats, + ))); + } } diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php index e77a0368..8a982bd7 100644 --- a/app/Model/ProjectAnalytic.php +++ b/app/Model/ProjectAnalytic.php @@ -179,4 +179,44 @@ class ProjectAnalytic extends Base return $stats; } + + + public function getHoursByStatus($project_id) + { + $stats = array(); + $columns = $this->board->getColumnsList($project_id); + + // Get the time spent of the last move for each tasks + $tasks = $this->db + ->table(Task::TABLE) + ->columns('id', 'time_estimated', 'time_spent', 'is_active') + ->eq('project_id', $project_id) + ->desc('id') + ->limit(1000) + ->findAll(); + + // Init values + $stats['closed'] = array( + 'time_spent' => 0, + 'time_estimated' => 0, + ); + $stats['open'] = array( + 'time_spent' => 0, + 'time_estimated' => 0, + ); + + + // Get time spent foreach task/column and take into account the last move + foreach ($tasks as &$task) { + if ($task['is_active']) { + $stats['open']['time_estimated'] += $task['time_estimated']; + $stats['open']['time_spent'] += $task['time_spent']; + } else { + $stats['closed']['time_estimated'] += $task['time_estimated']; + $stats['closed']['time_spent'] += $task['time_spent']; + } + } + + return $stats; + } } diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index 9514fe4a..836fbe46 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -122,6 +122,7 @@ class TaskFinder extends Base 'tasks.recurrence_parent', 'tasks.recurrence_child', 'tasks.time_estimated', + 'tasks.time_spent', User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name', Category::TABLE.'.name AS category_name', diff --git a/app/Template/analytic/compare_hours.php b/app/Template/analytic/compare_hours.php new file mode 100644 index 00000000..c52023c8 --- /dev/null +++ b/app/Template/analytic/compare_hours.php @@ -0,0 +1,57 @@ +<div class="page-header"> + <h2><?= t('Compare Estimated Time vs Actual Time') ?></h2> +</div> + +<div class="listing"> + <ul> + <li><?= t('Estimated hours: ').'<strong>'.$this->e($metrics['open']['time_estimated']+$metrics['open']['time_estimated']) ?></strong></li> + <li><?= t('Actual hours: ').'<strong>'.$this->e($metrics['open']['time_spent']+$metrics['closed']['time_spent']) ?></strong></li> + </ul> +</div> + +<?php if (empty($metrics)): ?> + <p class="alert"><?= t('Not enough data to show the graph.') ?></p> +<?php else: ?> +<section id="analytic-compare-hours"> + <div id="chart" data-metrics='<?= json_encode($metrics, JSON_HEX_APOS)?>' data-label-spent="<?= t('Hours Spent') ?>" data-label-estimated="<?= t('Hours Estimated') ?>"></div> + + <?php if ($paginator->isEmpty()): ?> + <p class="alert"><?= t('No tasks found.') ?></p> + <?php elseif (! $paginator->isEmpty()): ?> + <table class="table-fixed table-small"> + <tr> + <th class="column-5"><?= $paginator->order(t('Id'), 'tasks.id') ?></th> + <th><?= $paginator->order(t('Title'), 'tasks.title') ?></th> + <th class="column-5"><?= $paginator->order(t('Status'), 'tasks.is_active') ?></th> + <th class="column-10"><?= $paginator->order(t('Estimated Time'), 'tasks.time_estimated') ?></th> + <th class="column-10"><?= $paginator->order(t('Actual Time'), 'tasks.time_spent') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $task): ?> + <tr> + <td class="task-table color-<?= $task['color_id'] ?>"> + <?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + </td> + <td> + <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + </td> + <td> + <?php if ($task['is_active'] == \Kanboard\Model\Task::STATUS_OPEN): ?> + <?= t('Open') ?> + <?php else: ?> + <?= t('Closed') ?> + <?php endif ?> + </td> + <td> + <?= $this->e($task['time_estimated']) ?> + </td> + <td> + <?= $this->e($task['time_spent']) ?> + </td> + </tr> + <?php endforeach ?> + </table> + + <?= $paginator ?> + <?php endif ?> +</section> +<?php endif ?> diff --git a/app/Template/analytic/sidebar.php b/app/Template/analytic/sidebar.php index c942f7ed..746fcebb 100644 --- a/app/Template/analytic/sidebar.php +++ b/app/Template/analytic/sidebar.php @@ -19,7 +19,10 @@ <li <?= $this->app->getRouterAction() === 'leadandcycletime' ? 'class="active"' : '' ?>> <?= $this->url->link(t('Lead and cycle time'), 'analytic', 'leadAndCycleTime', array('project_id' => $project['id'])) ?> </li> + <li <?= $this->app->getRouterAction() === 'comparehours' ? 'class="active"' : '' ?>> + <?= $this->url->link(t('Compare hours'), 'analytic', 'compareHours', array('project_id' => $project['id'])) ?> + </li> </ul> <div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div> <div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div> -</div>
\ No newline at end of file +</div> diff --git a/assets/js/src/CompareHoursColumnChart.js b/assets/js/src/CompareHoursColumnChart.js new file mode 100644 index 00000000..fca67689 --- /dev/null +++ b/assets/js/src/CompareHoursColumnChart.js @@ -0,0 +1,43 @@ +function CompareHoursColumnChart(app) { + this.app = app; +} + +CompareHoursColumnChart.prototype.execute = function() { + var metrics = $("#chart").data("metrics"); + var spent = [$("#chart").data("label-spent")]; + var estimated = [$("#chart").data("label-estimated")]; + var categories = []; + + for (var status in metrics) { + spent.push(parseInt(metrics[status].time_spent)); + estimated.push(parseInt(metrics[status].time_estimated)); + categories.push(status); + } + + console.log(spent); + c3.generate({ + data: { + columns: [spent, estimated], + type: 'bar' + }, + bar: { + width: { + ratio: 0.2 + } + }, + axis: { + x: { + type: 'category', + categories: categories + }, + y: { + tick: { + format: this.app.formatDuration + } + } + }, + legend: { + show: true + } + }); +}; diff --git a/assets/js/src/Router.js b/assets/js/src/Router.js index 0c96262c..ab23c0fd 100644 --- a/assets/js/src/Router.js +++ b/assets/js/src/Router.js @@ -30,6 +30,7 @@ jQuery(document).ready(function() { router.addRoute('analytic-avg-time-column', AvgTimeColumnChart); router.addRoute('analytic-task-time-column', TaskTimeColumnChart); router.addRoute('analytic-lead-cycle-time', LeadCycleTimeChart); + router.addRoute('analytic-compare-hours', CompareHoursColumnChart); router.addRoute('gantt-chart', Gantt); router.dispatch(app); app.listen(); |