diff --git a/app/Analytic/EstimatedActualColumnAnalytic.php b/app/Analytic/EstimatedActualColumnAnalytic.php new file mode 100644 index 000000000..a0bc1d12a --- /dev/null +++ b/app/Analytic/EstimatedActualColumnAnalytic.php @@ -0,0 +1,49 @@ +db->table(TaskModel::TABLE) + ->columns('SUM(time_estimated) AS hours_estimated', 'SUM(time_spent) AS hours_spent', 'column_id') + ->eq('project_id', $project_id) + ->groupBy('column_id') + ->findAll(); + + $columns = $this->columnModel->getList($project_id); + + $metrics = []; + foreach ($columns as $column_id => $column_title) { + $metrics[$column_id] = array( + 'hours_spent' => 0, + 'hours_estimated' => 0, + 'title' => $column_title, + ); + } + + foreach ($rows as $row) { + $metrics[$row['column_id']]['hours_spent'] = (float) $row['hours_spent']; + $metrics[$row['column_id']]['hours_estimated'] = (float) $row['hours_estimated']; + } + + return $metrics; + } +} diff --git a/app/Controller/AnalyticController.php b/app/Controller/AnalyticController.php index a30041b2c..27a7f25eb 100644 --- a/app/Controller/AnalyticController.php +++ b/app/Controller/AnalyticController.php @@ -130,6 +130,22 @@ class AnalyticController extends BaseController $this->commonAggregateMetrics('analytic/burndown', 'score', t('Burndown chart')); } + /** + * Estimated vs actual time per column + * + * @access public + */ + public function estimatedVsActualByColumn() + { + $project = $this->getProject(); + + $this->response->html($this->helper->layout->analytic('analytic/estimated_actual_column', array( + 'project' => $project, + 'metrics' => $this->estimatedActualColumnAnalytic->build($project['id']), + 'title' => t('Estimated vs actual time per column'), + ))); + } + /** * Common method for CFD and Burdown chart * diff --git a/app/Core/Base.php b/app/Core/Base.php index 3535a339d..55bcc0e77 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -15,6 +15,7 @@ use Pimple\Container; * @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic * @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic * @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic + * @property \Kanboard\Analytic\EstimatedActualColumnAnalytic $estimatedActualColumnAnalytic * @property \Kanboard\Core\Action\ActionManager $actionManager * @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager * @property \Kanboard\Core\ExternalTask\ExternalTaskManager $externalTaskManager diff --git a/app/Helper/DateHelper.php b/app/Helper/DateHelper.php index 3ccd10334..14287db52 100644 --- a/app/Helper/DateHelper.php +++ b/app/Helper/DateHelper.php @@ -77,6 +77,18 @@ class DateHelper extends Base return $dtF->diff($dtT)->format($format); } + /** + * Get duration in hours into human format + * + * @access public + * @param float $hours + * @return string + */ + public function durationHours($hours) + { + return sprintf('%0.2f %s', round($hours, 2), t('hours')); + } + /** * Get the age of an item in quasi human readable format. * It's in this format: <1h , NNh, NNd diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index b012d80b5..c1d823902 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface 'EstimatedTimeComparisonAnalytic', 'AverageLeadCycleTimeAnalytic', 'AverageTimeSpentColumnAnalytic', + 'EstimatedActualColumnAnalytic', ), 'Model' => array( 'ActionModel', diff --git a/app/Template/analytic/estimated_actual_column.php b/app/Template/analytic/estimated_actual_column.php new file mode 100644 index 000000000..c49944e41 --- /dev/null +++ b/app/Template/analytic/estimated_actual_column.php @@ -0,0 +1,30 @@ + + + + + +

+ + app->component('chart-project-estimated-actual-column', array( + 'metrics' => $metrics, + 'labelSpent' => t('Hours Spent'), + 'labelEstimated' => t('Hours Estimated'), + )) ?> + + + + + + + + + + + + + + +
text->e($column['title']) ?>dt->durationHours($column['hours_spent']) ?>dt->durationHours($column['hours_estimated']) ?>
+ \ No newline at end of file diff --git a/app/Template/analytic/sidebar.php b/app/Template/analytic/sidebar.php index d5ce88cb0..ccd5e593c 100644 --- a/app/Template/analytic/sidebar.php +++ b/app/Template/analytic/sidebar.php @@ -21,6 +21,9 @@
  • app->checkMenuSelection('AnalyticController', 'timeComparison') ?>> modal->replaceLink(t('Estimated vs actual time'), 'AnalyticController', 'timeComparison', array('project_id' => $project['id'])) ?>
  • +
  • app->checkMenuSelection('AnalyticController', 'estimatedVsActualByColumn') ?>> + modal->replaceLink(t('Estimated vs actual time per column'), 'AnalyticController', 'estimatedVsActualByColumn', array('project_id' => $project['id'])) ?> +
  • hook->render('template:analytic:sidebar', array('project' => $project)) ?> diff --git a/assets/js/app.min.js b/assets/js/app.min.js index 4b8d19bfe..0061aec30 100644 --- a/assets/js/app.min.js +++ b/assets/js/app.min.js @@ -84,7 +84,8 @@ KB.onClick('.task-board *',redirectToTaskView,!0);KB.onClick('.task-board-change KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:[plots],type:'bar'},bar:{width:{ratio:0.5}},axis:{x:{type:'category',categories:categories},y:{tick:{format:KB.utils.formatDuration}}},legend:{show:!1}})}});KB.component('chart-project-burndown',function(containerElement,options){this.render=function(){var metrics=options.metrics;var columns=[[options.labelTotal]];var categories=[];var inputFormat=d3.time.format("%Y-%m-%d");var outputFormat=d3.time.format(options.dateFormat);for(var i=0;i0){columns.push([currentValue])}}else{if(j>0){columns[j].push(currentValue);if(typeof columns[0][i]==='undefined'){columns[0].push(0)} columns[0][i]+=currentValue}else{categories.push(outputFormat(inputFormat.parse(currentValue)))}}}} KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:columns},axis:{x:{type:'category',categories:categories}}})}});KB.component('chart-project-cumulative-flow',function(containerElement,options){this.render=function(){var metrics=options.metrics;var columns=[];var groups=[];var categories=[];var inputFormat=d3.time.format("%Y-%m-%d");var outputFormat=d3.time.format(options.dateFormat);for(var i=0;i0){groups.push(currentValue);columns.push([currentValue])}}else{if(j>0){columns[j-1].push(currentValue)}else{categories.push(outputFormat(inputFormat.parse(currentValue)))}}}} -KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:columns.reverse(),type:'area-spline',groups:[groups],order:null},axis:{x:{type:'category',categories:categories}}})}});KB.component('chart-project-lead-cycle-time',function(containerElement,options){this.render=function(){var metrics=options.metrics;var cycle=[options.labelCycle];var lead=[options.labelLead];var categories=[];var types={};types[options.labelCycle]='area';types[options.labelLead]='area-spline';var colors={};colors[options.labelLead]='#afb42b';colors[options.labelCycle]='#4e342e';for(var i=0;i