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 + */ +?> + +
+ +
+

noData;?>

+
+ + $programs):?> +
+ + + + + + + + + + + + + + + + + + $programProduct):?> + + + + + + + + + + + + + + + + + +
product->myProduct : $lang->product->otherProduct;?>
product->unclosedProduct;?>product->unexpiredPlan;?>product->doing;?>product->normalRelease;?>
product->doingProject;?>product->doingExecution;;?>
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}");?> +
+
+
+
+
hours->progress);?>
+
+
+
+
+
+
+ + +
+ + $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}");?> + +
+
+ +
+ +
+
progress);?>
+
+
+ +
+
+
+
+ + +
+ + $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. *