summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-07-05 16:07:21 -0400
committerFrederic Guillot <fred@kanboard.net>2015-07-05 16:07:21 -0400
commitbb8b4c0e36afc05ff5b0cb3ac6465351a696b001 (patch)
treee7499d04cfe32ed429413c2bbe0c81c33700d36a
parent67b9a56469b406b44cd1baad4445ddb6d707794f (diff)
Add task analytics
-rw-r--r--app/Controller/Activity.php16
-rw-r--r--app/Controller/Task.php11
-rw-r--r--app/Core/Helper.php2
-rw-r--r--app/Helper/Dt.php (renamed from app/Helper/Datetime.php)18
-rw-r--r--app/Model/TaskAnalytic.php74
-rw-r--r--app/Model/Transition.php16
-rw-r--r--app/ServiceProvider/ClassProvider.php1
-rw-r--r--app/Template/activity/task.php (renamed from app/Template/task/activity.php)0
-rw-r--r--app/Template/board/task_private.php4
-rw-r--r--app/Template/subtask/show.php2
-rw-r--r--app/Template/task/analytics.php32
-rw-r--r--app/Template/task/sidebar.php5
-rw-r--r--app/Template/task/transitions.php2
-rw-r--r--app/Template/timetable_day/index.php4
-rw-r--r--app/Template/timetable_extra/index.php4
-rw-r--r--app/Template/timetable_off/index.php4
-rw-r--r--app/Template/timetable_week/index.php8
-rw-r--r--tests/units/DatetimeHelperTest.php10
18 files changed, 186 insertions, 27 deletions
diff --git a/app/Controller/Activity.php b/app/Controller/Activity.php
index 2276b3b8..234e4be4 100644
--- a/app/Controller/Activity.php
+++ b/app/Controller/Activity.php
@@ -26,4 +26,20 @@ class Activity extends Base
'title' => t('%s\'s activity', $project['name'])
)));
}
+
+ /**
+ * Display task activities
+ *
+ * @access public
+ */
+ public function task()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->taskLayout('activity/task', array(
+ 'title' => $task['title'],
+ 'task' => $task,
+ 'events' => $this->projectActivity->getTask($task['id']),
+ )));
+ }
}
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index dc83f7b1..b6e4845f 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -88,19 +88,20 @@ class Task extends Base
}
/**
- * Display task activities
+ * Display task analytics
*
* @access public
*/
- public function activites()
+ public function analytics()
{
$task = $this->getTask();
- $this->response->html($this->taskLayout('task/activity', array(
+ $this->response->html($this->taskLayout('task/analytics', array(
'title' => $task['title'],
'task' => $task,
- 'ajax' => $this->request->isAjax(),
- 'events' => $this->projectActivity->getTask($task['id']),
+ 'lead_time' => $this->taskAnalytic->getLeadTime($task),
+ 'cycle_time' => $this->taskAnalytic->getCycleTime($task),
+ 'column_averages' => $this->taskAnalytic->getAverageTimeByColumn($task),
)));
}
diff --git a/app/Core/Helper.php b/app/Core/Helper.php
index e4f225b0..64eaed23 100644
--- a/app/Core/Helper.php
+++ b/app/Core/Helper.php
@@ -12,7 +12,7 @@ use Pimple\Container;
*
* @property \Helper\App $app
* @property \Helper\Asset $asset
- * @property \Helper\Datetime $datetime
+ * @property \Helper\Dt $dt
* @property \Helper\File $file
* @property \Helper\Form $form
* @property \Helper\Subtask $subtask
diff --git a/app/Helper/Datetime.php b/app/Helper/Dt.php
index 74ea9bdd..be595605 100644
--- a/app/Helper/Datetime.php
+++ b/app/Helper/Dt.php
@@ -2,15 +2,31 @@
namespace Helper;
+use DateTime;
+
/**
* DateTime helpers
*
* @package helper
* @author Frederic Guillot
*/
-class Datetime extends \Core\Base
+class Dt extends \Core\Base
{
/**
+ * Get duration in seconds into human format
+ *
+ * @access public
+ * @param integer $seconds
+ * @return string
+ */
+ public function duration($seconds)
+ {
+ $dtF = new DateTime("@0");
+ $dtT = new DateTime("@$seconds");
+ return $dtF->diff($dtT)->format('%a days, %h hours, %i minutes and %s seconds');
+ }
+
+ /**
* Get the age of an item in quasi human readable format.
* It's in this format: <1h , NNh, NNd
*
diff --git a/app/Model/TaskAnalytic.php b/app/Model/TaskAnalytic.php
new file mode 100644
index 00000000..41579c7d
--- /dev/null
+++ b/app/Model/TaskAnalytic.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Model;
+
+/**
+ * Task Analytic
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskAnalytic extends Base
+{
+ /**
+ * Get the time between date_creation and date_completed or now if empty
+ *
+ * @access public
+ * @param array $task
+ * @return integer
+ */
+ public function getLeadTime(array $task)
+ {
+ return ($task['date_completed'] ?: time()) - $task['date_creation'];
+ }
+
+ /**
+ * Get the time between date_started and date_completed or now if empty
+ *
+ * @access public
+ * @param array $task
+ * @return integer
+ */
+ public function getCycleTime(array $task)
+ {
+ if (empty($task['date_started'])) {
+ return 0;
+ }
+
+ return ($task['date_completed'] ?: time()) - $task['date_started'];
+ }
+
+ /**
+ * Get the average time spent in each column
+ *
+ * @access public
+ * @param array $task
+ * @return array
+ */
+ public function getAverageTimeByColumn(array $task)
+ {
+ $result = array();
+ $columns = $this->board->getColumnsList($task['project_id']);
+ $averages = $this->transition->getAverageTimeSpentByTask($task['id']);
+
+ foreach ($columns as $column_id => $column_title) {
+
+ $time_spent = 0;
+
+ if (empty($averages) && $task['column_id'] == $column_id) {
+ $time_spent = time() - $task['date_creation'];
+ }
+ else {
+ $time_spent = isset($averages[$column_id]) ? $averages[$column_id] : 0;
+ }
+
+ $result[] = array(
+ 'id' => $column_id,
+ 'title' => $column_title,
+ 'time_spent' => $time_spent,
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Model/Transition.php b/app/Model/Transition.php
index cb759e4a..959b6aca 100644
--- a/app/Model/Transition.php
+++ b/app/Model/Transition.php
@@ -39,6 +39,22 @@ class Transition extends Base
}
/**
+ * Get average time spent by task for each column
+ *
+ * @access public
+ * @param integer $task_id
+ * @return array
+ */
+ public function getAverageTimeSpentByTask($task_id)
+ {
+ return $this->db
+ ->hashtable(self::TABLE)
+ ->groupBy('src_column_id')
+ ->eq('task_id', $task_id)
+ ->getAll('src_column_id', 'SUM(time_spent) AS time_spent');
+ }
+
+ /**
* Get all transitions by task
*
* @access public
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index 1fa0d0ef..609f5824 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -42,6 +42,7 @@ class ClassProvider implements ServiceProviderInterface
'SubtaskTimeTracking',
'Swimlane',
'Task',
+ 'TaskAnalytic',
'TaskCreation',
'TaskDuplication',
'TaskExport',
diff --git a/app/Template/task/activity.php b/app/Template/activity/task.php
index cc4aad03..cc4aad03 100644
--- a/app/Template/task/activity.php
+++ b/app/Template/activity/task.php
diff --git a/app/Template/board/task_private.php b/app/Template/board/task_private.php
index 87121f2c..3f4010ea 100644
--- a/app/Template/board/task_private.php
+++ b/app/Template/board/task_private.php
@@ -43,8 +43,8 @@
<?php if ($task['is_active'] == 1): ?>
<div class="task-board-days">
- <span title="<?= t('Task age in days')?>" class="task-days-age"><?= $this->datetime->age($task['date_creation']) ?></span>
- <span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= $this->datetime->age($task['date_moved']) ?></span>
+ <span title="<?= t('Task age in days')?>" class="task-days-age"><?= $this->dt->age($task['date_creation']) ?></span>
+ <span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= $this->dt->age($task['date_moved']) ?></span>
</div>
<?php else: ?>
<div class="task-board-closed"><i class="fa fa-ban fa-fw"></i><?= t('Closed') ?></div>
diff --git a/app/Template/subtask/show.php b/app/Template/subtask/show.php
index b91e830f..cc82a74e 100644
--- a/app/Template/subtask/show.php
+++ b/app/Template/subtask/show.php
@@ -48,7 +48,7 @@
<?php if ($subtask['is_timer_started']): ?>
<i class="fa fa-pause"></i>
<?= $this->url->link(t('Stop timer'), 'timer', 'subtask', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
- (<?= $this->datetime->age($subtask['timer_start_date']) ?>)
+ (<?= $this->dt->age($subtask['timer_start_date']) ?>)
<?php else: ?>
<i class="fa fa-play-circle-o"></i>
<?= $this->url->link(t('Start timer'), 'timer', 'subtask', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
diff --git a/app/Template/task/analytics.php b/app/Template/task/analytics.php
new file mode 100644
index 00000000..dbee0e9c
--- /dev/null
+++ b/app/Template/task/analytics.php
@@ -0,0 +1,32 @@
+<div class="page-header">
+ <h2><?= t('Analytics') ?></h2>
+</div>
+
+<div class="listing">
+ <ul>
+ <li><?= t('Lead time: ').'<strong>'.$this->dt->duration($lead_time) ?></strong></li>
+ <li><?= t('Cycle time: ').'<strong>'.$this->dt->duration($cycle_time) ?></strong></li>
+ </ul>
+</div>
+
+<h3><?= t('Average time spent for each column') ?></h3>
+<table class="table-stripped">
+ <tr>
+ <th><?= t('Column') ?></th>
+ <th><?= t('Average time spent') ?></th>
+ </tr>
+ <?php foreach ($column_averages as $column): ?>
+ <tr>
+ <td><?= $this->e($column['title']) ?></td>
+ <td><?= $this->dt->duration($column['time_spent']) ?></td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<div class="alert alert-info">
+ <ul>
+ <li><?= t('The lead time is the time between the task creation and the completion.') ?></li>
+ <li><?= t('The cycle time is the time between the start date and the completion.') ?></li>
+ <li><?= t('If the task is not closed the current time is used.') ?></li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index bb137ac9..8b0f3c6e 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -5,11 +5,14 @@
<?= $this->url->link(t('Summary'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<li>
- <?= $this->url->link(t('Activity stream'), 'task', 'activites', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->url->link(t('Activity stream'), 'activity', 'task', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<li>
<?= $this->url->link(t('Transitions'), 'task', 'transitions', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
+ <li>
+ <?= $this->url->link(t('Analytics'), 'task', 'analytics', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
<?php if ($task['time_estimated'] > 0 || $task['time_spent'] > 0): ?>
<li>
<?= $this->url->link(t('Time tracking'), 'task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
diff --git a/app/Template/task/transitions.php b/app/Template/task/transitions.php
index 6455fd66..2ca2387f 100644
--- a/app/Template/task/transitions.php
+++ b/app/Template/task/transitions.php
@@ -19,7 +19,7 @@
<td><?= $this->e($transition['src_column']) ?></td>
<td><?= $this->e($transition['dst_column']) ?></td>
<td><?= $this->url->link($this->e($transition['name'] ?: $transition['username']), 'user', 'show', array('user_id' => $transition['user_id'])) ?></td>
- <td><?= n(round($transition['time_spent'] / 3600, 2)).' '.t('hours') ?></td>
+ <td><?= $this->dt->duration($transition['time_spent']) ?></td>
</tr>
<?php endforeach ?>
</table>
diff --git a/app/Template/timetable_day/index.php b/app/Template/timetable_day/index.php
index d2877816..386ceec2 100644
--- a/app/Template/timetable_day/index.php
+++ b/app/Template/timetable_day/index.php
@@ -30,10 +30,10 @@
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Start time'), 'start') ?>
- <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
+ <?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('End time'), 'end') ?>
- <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
+ <?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
diff --git a/app/Template/timetable_extra/index.php b/app/Template/timetable_extra/index.php
index d3224ae6..e9982335 100644
--- a/app/Template/timetable_extra/index.php
+++ b/app/Template/timetable_extra/index.php
@@ -42,10 +42,10 @@
<?= $this->form->checkbox('all_day', t('All day'), 1) ?>
<?= $this->form->label(t('Start time'), 'start') ?>
- <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
+ <?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('End time'), 'end') ?>
- <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
+ <?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('Comment'), 'comment') ?>
<?= $this->form->text('comment', $values, $errors) ?>
diff --git a/app/Template/timetable_off/index.php b/app/Template/timetable_off/index.php
index 75e02dbd..615c2b8d 100644
--- a/app/Template/timetable_off/index.php
+++ b/app/Template/timetable_off/index.php
@@ -42,10 +42,10 @@
<?= $this->form->checkbox('all_day', t('All day'), 1) ?>
<?= $this->form->label(t('Start time'), 'start') ?>
- <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
+ <?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('End time'), 'end') ?>
- <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
+ <?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('Comment'), 'comment') ?>
<?= $this->form->text('comment', $values, $errors) ?>
diff --git a/app/Template/timetable_week/index.php b/app/Template/timetable_week/index.php
index 552e9302..d58c6cfb 100644
--- a/app/Template/timetable_week/index.php
+++ b/app/Template/timetable_week/index.php
@@ -13,7 +13,7 @@
</tr>
<?php foreach ($timetable as $slot): ?>
<tr>
- <td><?= $this->datetime->getWeekDay($slot['day']) ?></td>
+ <td><?= $this->dt->getWeekDay($slot['day']) ?></td>
<td><?= $slot['start'] ?></td>
<td><?= $slot['end'] ?></td>
<td>
@@ -32,13 +32,13 @@
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Day'), 'day') ?>
- <?= $this->form->select('day', $this->datetime->getWeekDays(), $values, $errors) ?>
+ <?= $this->form->select('day', $this->dt->getWeekDays(), $values, $errors) ?>
<?= $this->form->label(t('Start time'), 'start') ?>
- <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
+ <?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('End time'), 'end') ?>
- <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
+ <?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
diff --git a/tests/units/DatetimeHelperTest.php b/tests/units/DatetimeHelperTest.php
index 216cf34c..21d452dd 100644
--- a/tests/units/DatetimeHelperTest.php
+++ b/tests/units/DatetimeHelperTest.php
@@ -2,13 +2,13 @@
require_once __DIR__.'/Base.php';
-use Helper\Datetime;
+use Helper\Dt;
class DatetimeHelperTest extends Base
{
public function testAge()
{
- $h = new Datetime($this->container);
+ $h = new Dt($this->container);
$this->assertEquals('&lt;15m', $h->age(0, 30));
$this->assertEquals('&lt;30m', $h->age(0, 1000));
@@ -20,7 +20,7 @@ class DatetimeHelperTest extends Base
public function testGetDayHours()
{
- $h = new Datetime($this->container);
+ $h = new Dt($this->container);
$slots = $h->getDayHours();
@@ -36,7 +36,7 @@ class DatetimeHelperTest extends Base
public function testGetWeekDays()
{
- $h = new Datetime($this->container);
+ $h = new Dt($this->container);
$slots = $h->getWeekDays();
@@ -48,7 +48,7 @@ class DatetimeHelperTest extends Base
public function testGetWeekDay()
{
- $h = new Datetime($this->container);
+ $h = new Dt($this->container);
$this->assertEquals('Monday', $h->getWeekDay(1));
$this->assertEquals('Sunday', $h->getWeekDay(7));