summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--app/Controller/Analytic.php31
-rw-r--r--app/Model/ProjectAnalytic.php45
-rw-r--r--app/Model/TaskFinder.php1
-rw-r--r--app/Template/analytic/compare_hours.php57
-rw-r--r--app/Template/analytic/sidebar.php5
-rw-r--r--assets/js/src/CompareHoursColumnChart.js42
-rw-r--r--assets/js/src/Router.js1
8 files changed, 182 insertions, 2 deletions
diff --git a/Makefile b/Makefile
index 5aa0752e..54e65f2e 100644
--- a/Makefile
+++ b/Makefile
@@ -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..80ef3918 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,34 @@ class Analytic extends Base
'title' => t($title, $project['name']),
)));
}
+
+ /**
+ * Show comparison between actual and estimated hours chart
+ *
+ * @access public
+ */
+
+ 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,
+ 'title' => t('Compare hours for "%s"', $project['name']),
+ )));
+ }
}
diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php
index e77a0368..79277e79 100644
--- a/app/Model/ProjectAnalytic.php
+++ b/app/Model/ProjectAnalytic.php
@@ -179,4 +179,49 @@ class ProjectAnalytic extends Base
return $stats;
}
+
+ /**
+ * Get the time spent and estimated into each status
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getHoursByStatus($project_id)
+ {
+ $stats = array();
+
+ // Get the times related to each task
+ $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,
+ );
+
+
+ // Add times spent and estimated to each status
+ 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..d40809d2
--- /dev/null
+++ b/assets/js/src/CompareHoursColumnChart.js
@@ -0,0 +1,42 @@
+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);
+ }
+
+ 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();