diff --git a/module/product/control.php b/module/product/control.php
index 794991a4f1..8903dc5fbc 100644
--- a/module/product/control.php
+++ b/module/product/control.php
@@ -1153,6 +1153,39 @@ class product extends control
*/
public function kanban()
{
+ $kanbanGroup = $this->product->getStats4Kanban();
+ extract($kanbanGroup);
+
+ $products = array();
+ $myProducts = array();
+ $otherProducts = array();
+ foreach($productList as $productID => $product)
+ {
+ if($product->status == 'normal' and $product->PO == $this->app->user->account)
+ {
+ $myProducts[$product->program][$productID] = $productID;
+ }
+ elseif($product->status == 'normal' and !($product->PO == $this->app->user->account))
+ {
+ $otherProducts[$product->program][$productID] = $productID;
+ }
+ }
+
+ $products['myProducts'] = $myProducts;
+ $products['otherProducts'] = $otherProducts;
+
+ $this->view->title = $this->lang->product->kanban;
+ $this->view->products = $products;
+ $this->view->programList = array(0 => $this->lang->product->emptyProgram) + $programList;
+ $this->view->productList = $productList;
+ $this->view->planList = $planList;
+ $this->view->projectList = $projectList;
+ $this->view->projectProduct = $projectProduct;
+ $this->view->latestExecutions = $projectLatestExecutions;
+ $this->view->hourList = $hourList;
+ $this->view->emptyHour = (object) array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0);
+ $this->view->releaseList = $releaseList;
+
$this->display();
}
diff --git a/module/product/css/kanban.css b/module/product/css/kanban.css
new file mode 100644
index 0000000000..1292c30cfc
--- /dev/null
+++ b/module/product/css/kanban.css
@@ -0,0 +1,20 @@
+#kanban {padding: 10px;}
+.main-table tbody>tr>td {padding: 10px;}
+.table caption {background: #fff; border-top-left-radius: 2px; border-top-right-radius: 2px; margin-bottom: 0; font-weight: 500;}
+.table .program {writing-mode: vertical-lr; padding-left: 4px !important; color: #fff;}
+
+.table th, .table td {border-top: 2px solid #fff; border-bottom: 2px solid #fff; border-left: 3px solid #fff; border-bottom: 3px solid #fff;}
+.table tbody tr:last-child td {border-bottom: unset;}
+td.project, td.execution {padding: 10px 0 !important;}
+
+.board-item {border: 1px solid #ebebeb; padding: 3px 10px; cursor: default; border-radius: 2px; background-color: #fff; text-align: left;}
+.board-item:hover {border-color: #ccc;}
+.board {padding: 0 10px;}
+.board + .board {margin-top: 5px;}
+.board-item + .board-item {margin-top: 10px;}
+.project .board, .execution .board {padding-top: 5px; border-top: 2px solid #fff;}
+.project .board:first-child, .execution .board:first-child {padding-top: 0; border-top: unset;}
+.release .board-item {display: flex; flex-flow: row nowrap; justify-content: flex-start; align-items: center;}
+.emptyBoard {padding: 3px 10px; cursor: default;}
+
+.project .table-row > .table-col:last-child, .execution .table-row > .table-col:last-child {width: 24px; padding-top: 2px;}
diff --git a/module/product/lang/zh-cn.php b/module/product/lang/zh-cn.php
index dbdc830838..741a675de8 100644
--- a/module/product/lang/zh-cn.php
+++ b/module/product/lang/zh-cn.php
@@ -131,6 +131,19 @@ $lang->product->unclosed = '未关闭';
$lang->product->unplan = "未计划";
$lang->product->viewByUser = '按用户查看';
+/* Product Kanban. */
+$lang->product->myProduct = '我负责的产品';
+$lang->product->otherProduct = '其他产品';
+$lang->product->unclosedProduct = '未关闭的产品';
+$lang->product->unexpiredPlan = '未过期的计划';
+$lang->product->doing = '进行中';
+$lang->product->doingProject = '进行中的项目';
+$lang->product->doingExecution = '进行中的执行';
+$lang->product->normalRelease = '正常的发布';
+$lang->product->emptyProgram = '无项目集归属产品';
+
+$lang->product->kanbanColorList = array('#32C5FF', '#006AF1', '#9D28B2', '#FF8F26', '#FFC20E', '#00A78E', '#7FBB00', '#424BAC', '#C0E9FF', '#EC2761');
+
$lang->product->allStory = '所有';
$lang->product->allProduct = '全部' . $lang->productCommon;
$lang->product->allProductsOfProject = '全部关联' . $lang->productCommon;
diff --git a/module/product/model.php b/module/product/model.php
index 2791c39417..95769a7bf6 100644
--- a/module/product/model.php
+++ b/module/product/model.php
@@ -1520,6 +1520,62 @@ class productModel extends model
return $stats;
}
+ /**
+ * Get stats for product kanban.
+ *
+ * @access public
+ * @return void
+ */
+ public function getStats4Kanban()
+ {
+ $date = date('Y-m-d');
+ $this->loadModel('program');
+
+ $productList = $this->getList();
+ $programList = $this->program->getTopPairs('', '', true);
+ $projectList = $this->program->getProjectStats(0, 'doing');
+
+ $projectProduct = $this->dao->select('t1.product,t1.project')->from(TABLE_PROJECTPRODUCT)->alias('t1')
+ ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
+ ->where('t1.product')->in(array_keys($productList))
+ ->andWhere('t2.type')->eq('project')
+ ->andWhere('t2.status')->eq('doing')
+ ->andWhere('t2.deleted')->eq('0')
+ ->fetchGroup('product', 'project');
+
+ $planList = $this->dao->select('id,product,title,parent,begin,end')->from(TABLE_PRODUCTPLAN)
+ ->where('product')->in(array_keys($productList))
+ ->andWhere('deleted')->eq(0)
+ ->andWhere('end')->ge($date)
+ ->andWhere('parent')->ne(-1)
+ ->orderBy('begin desc')
+ ->fetchGroup('product', 'id');
+
+ $executionList = $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1')
+ ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project=t2.id')
+ ->where('type')->in('stage,sprint')
+ ->andWhere('t2.project')->in(array_keys($projectList))
+ ->andWhere('t1.product')->in(array_keys($productList))
+ ->andWhere('status')->eq('doing')
+ ->andWhere('deleted')->eq('0')
+ ->orderBy('id_desc')
+ ->fetchGroup('project', 'id');
+
+ $projectLatestExecutions = array();
+ $latestExecutionList = array();
+ foreach($executionList as $projectID => $executions)
+ {
+ $latestExecutionList[key($executions)] = current($executions);
+ $projectLatestExecutions[$projectID] = current($executions);
+ }
+
+ $hourList = $this->loadModel('project')->computerProgress($latestExecutionList);
+
+ $releaseList = $this->dao->select('id,product,name,marker')->from(TABLE_RELEASE)->where('product')->in(array_keys($productList))->fetchGroup('product', 'id');
+
+ return array('programList' => $programList, 'productList' => $productList, 'planList' => $planList, 'projectList' => $projectList, 'projectProduct' => $projectProduct, 'projectLatestExecutions' => $projectLatestExecutions, 'hourList' => $hourList, 'releaseList' => $releaseList);
+ }
+
/**
* Get product line pairs.
*
diff --git a/module/product/view/kanban.html.php b/module/product/view/kanban.html.php
new file mode 100644
index 0000000000..e4faf15789
--- /dev/null
+++ b/module/product/view/kanban.html.php
@@ -0,0 +1,149 @@
+
+ * @package ZenTaoPMS
+ * @version $Id
+ */
+?>
+
+
+
+
+
+ $programs):?>
+
+
+ product->myProduct : $lang->product->otherProduct;?>
+
+
+ |
+ product->unclosedProduct;?> |
+ product->unexpiredPlan;?> |
+ product->doing;?> |
+ product->normalRelease;?> |
+
+
+ | product->doingProject;?> |
+ product->doingExecution;;?> |
+
+
+
+
+ $programProduct):?>
+
+
+
+
+ |
+
+ createLink('product', 'browse', "productID=$productID"), $productList[$productID]->name, '', "title={$productList[$productID]->name}");?> |
+
+
+ $plan):?>
+ createLink('productplan', 'view', "planID=$planID"), $plan->title, '', "title={$plan->title}");?>
+
+
+ |
+
+
+ $project):?>
+
+ delay) ? 'border-left: 3px solid red' : 'border-left: 3px solid #0bd986';?>
+
+
+
+ createLink('project', 'index', "projectID=$projectID"), $projectList[$projectID]->name, '', "title={$projectList[$projectID]->name}");?>
+
+
+
+
+
+
+
+ |
+
+
+ $project):?>
+
+ end);
+ $borderStyle = $delay > 0 ? 'border-left: 3px solid red' : 'border-left: 3px solid #0bd986';
+ }
+ ?>
+
+ id : 0;?>
+ name : ''?>
+
+
+
+
+ createLink('execution', 'task', "executionID=$executionID"), $executionName, '', "title={$executionName}");?>
+
+
+
+
+
+
+
+
+ |
+
+
+ $release):?>
+
+ createLink('release', 'view', "releaseID=$releaseID"), $release->name, '', "title={$release->name} class='text-ellipsis'");?>
+ marker) echo " ";?>
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/module/program/model.php b/module/program/model.php
index 8a9893cff4..be4ee5baa6 100644
--- a/module/program/model.php
+++ b/module/program/model.php
@@ -837,16 +837,17 @@ class programModel extends model
*
* @param string $model
* @param string $mode
+ * @param bool $isQueryAll
* @access public
* @return array
*/
- public function getTopPairs($model = '', $mode = '')
+ public function getTopPairs($model = '', $mode = '', $isQueryAll = false)
{
return $this->dao->select('id,name')->from(TABLE_PROGRAM)
->where('type')->eq('program')
->andWhere('grade')->eq(1)
->beginIF(strpos($mode, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi()
- ->andWhere('id')->in($this->app->user->view->programs)
+ ->beginIF(!$isQueryAll)->andWhere('id')->in($this->app->user->view->programs)->fi()
->andWhere('deleted')->eq(0)
->beginIF($model)->andWhere('model')->eq($model)->fi()
->orderBy('`order`')
diff --git a/module/project/model.php b/module/project/model.php
index bf5c04778b..30bbd39103 100644
--- a/module/project/model.php
+++ b/module/project/model.php
@@ -1688,40 +1688,9 @@ class projectModel extends model
->fetchAll('id');
}
- $hours = array();
+ $hours = $this->computerProgress($executions);
$emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0);
- /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */
- $tasks = $this->dao->select('id, execution, estimate, consumed, `left`, status, closedReason')
- ->from(TABLE_TASK)
- ->where('execution')->in(array_keys($executions))
- ->andWhere('parent')->lt(1)
- ->andWhere('deleted')->eq(0)
- ->fetchGroup('execution', 'id');
-
- /* Compute totalEstimate, totalConsumed, totalLeft. */
- foreach($tasks as $executionID => $executionTasks)
- {
- $hour = (object)$emptyHour;
- foreach($executionTasks as $task)
- {
- $hour->totalEstimate += $task->estimate;
- $hour->totalConsumed += $task->consumed;
- if($task->status != 'cancel' and $task->status != 'closed') $hour->totalLeft += $task->left;
- }
- $hours[$executionID] = $hour;
- }
-
- /* Compute totalReal and progress. */
- foreach($hours as $hour)
- {
- $hour->totalEstimate = round($hour->totalEstimate, 1) ;
- $hour->totalConsumed = round($hour->totalConsumed, 1);
- $hour->totalLeft = round($hour->totalLeft, 1);
- $hour->totalReal = $hour->totalConsumed + $hour->totalLeft;
- $hour->progress = $hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 3) * 100 : 0;
- }
-
/* Get burndown charts datas. */
$burns = $this->dao->select('execution, date AS name, `left` AS value')
->from(TABLE_BURN)
@@ -1838,6 +1807,52 @@ class projectModel extends model
return array('kanbanGroup' => array('my' => $myProjects, 'other' => $otherProjects), 'latestExecutions' => $latestExecutions);
}
+ /**
+ * Computer execution progress.
+ *
+ * @param array $executions
+ * @access public
+ * @return array
+ */
+ public function computerProgress($executions)
+ {
+ $hours = array();
+ $emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0);
+
+ /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */
+ $tasks = $this->dao->select('id, execution, estimate, consumed, `left`, status, closedReason')
+ ->from(TABLE_TASK)
+ ->where('execution')->in(array_keys($executions))
+ ->andWhere('parent')->lt(1)
+ ->andWhere('deleted')->eq(0)
+ ->fetchGroup('execution', 'id');
+
+ /* Compute totalEstimate, totalConsumed, totalLeft. */
+ foreach($tasks as $executionID => $executionTasks)
+ {
+ $hour = (object)$emptyHour;
+ foreach($executionTasks as $task)
+ {
+ $hour->totalEstimate += $task->estimate;
+ $hour->totalConsumed += $task->consumed;
+ if($task->status != 'cancel' and $task->status != 'closed') $hour->totalLeft += $task->left;
+ }
+ $hours[$executionID] = $hour;
+ }
+
+ /* Compute totalReal and progress. */
+ foreach($hours as $hour)
+ {
+ $hour->totalEstimate = round($hour->totalEstimate, 1) ;
+ $hour->totalConsumed = round($hour->totalConsumed, 1);
+ $hour->totalLeft = round($hour->totalLeft, 1);
+ $hour->totalReal = $hour->totalConsumed + $hour->totalLeft;
+ $hour->progress = $hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 3) * 100 : 0;
+ }
+
+ return $hours;
+ }
+
/**
* Set menu of project module.
*