From cf06d1f24f272b85d194994e81ebd56fa4d97f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E5=B9=BF=E6=98=8E?= Date: Wed, 25 Aug 2021 15:54:01 +0800 Subject: [PATCH] * Code for task #41775. --- module/program/control.php | 6 + module/program/css/kanban.css | 14 ++ module/program/lang/de.php | 3 + module/program/lang/en.php | 3 + module/program/lang/fr.php | 3 + module/program/lang/vi.php | 3 + module/program/lang/zh-cn.php | 13 ++ module/program/model.php | 268 +++++++++++++++++++++++++++- module/program/view/kanban.html.php | 129 +++++++++++++ module/project/model.php | 4 +- 10 files changed, 442 insertions(+), 4 deletions(-) create mode 100644 module/program/css/kanban.css create mode 100644 module/program/view/kanban.html.php diff --git a/module/program/control.php b/module/program/control.php index 0efabdc13c..7a4c5f17ce 100644 --- a/module/program/control.php +++ b/module/program/control.php @@ -67,6 +67,12 @@ class program extends control $this->display(); } + public function kanban() + { + $this->view->kanbanGroup = $this->program->getKanbanGroup(); + $this->display(); + } + /** * Program products list. * diff --git a/module/program/css/kanban.css b/module/program/css/kanban.css new file mode 100644 index 0000000000..6b01db9cf4 --- /dev/null +++ b/module/program/css/kanban.css @@ -0,0 +1,14 @@ +.main-table .table {cursor: default;} +.main-table .table td {background: #f5f5f5;} + +.board-item {border: 1px solid #EBEBEB; padding: 5px 10px; cursor: default; border-radius: 2px; background-color: #fff;} +.board-item:hover {border-color: #ccc;} +.board-item + .board-item {margin-top: 10px;} + +#kanban {overflow-y: auto;} +#kanban tbody > tr > td {line-height: 24px;} +#kanban tbody > tr > td{border-right: 3px solid #fff; border-bottom: 3px solid #fff;} +#kanban thead > tr:first-child > th:not(:first-child) {border-right: 3px solid #fff; border-bottom: 3px solid #fff;} +#kanban thead > tr:last-child > th {border: 3px solid #fff;} + +.table-grouped tbody > tr > td:first-child, .table-grouped thead > tr > th:first-child {padding-left: 3px;} diff --git a/module/program/lang/de.php b/module/program/lang/de.php index fe55012bc3..b2ac9aeb41 100644 --- a/module/program/lang/de.php +++ b/module/program/lang/de.php @@ -62,6 +62,9 @@ $lang->program->readjustTime = 'Change the program begin&end date.'; $lang->program->stakeholderTypeList['inside'] = 'Inside'; $lang->program->stakeholderTypeList['outside'] = 'Outside'; +$lang->program->typeList['my'] = 'My Programs'; +$lang->program->typeList['others'] = 'Others'; + $lang->program->noProgram = 'No program.'; $lang->program->showClosed = 'Closed programs.'; $lang->program->tips = 'If a parent item set is selected, products under that parent item set can be associated. If no item set is selected, a product with the same name as the item is created by default and associated with that item.'; diff --git a/module/program/lang/en.php b/module/program/lang/en.php index 117203a571..60f2ac64a3 100644 --- a/module/program/lang/en.php +++ b/module/program/lang/en.php @@ -74,6 +74,9 @@ $lang->program->isStakeholderKey = 'Key stakeholder'; $lang->program->stakeholderTypeList['inside'] = 'Inside'; $lang->program->stakeholderTypeList['outside'] = 'Outside'; +$lang->program->typeList['my'] = 'My Programs'; +$lang->program->typeList['others'] = 'Others'; + $lang->program->noProgram = 'No program.'; $lang->program->showClosed = 'Closed programs.'; $lang->program->tips = 'If a parent item set is selected, products under that parent item set can be associated. If no item set is selected, a product with the same name as the item is created by default and associated with that item.'; diff --git a/module/program/lang/fr.php b/module/program/lang/fr.php index fe55012bc3..b2ac9aeb41 100644 --- a/module/program/lang/fr.php +++ b/module/program/lang/fr.php @@ -62,6 +62,9 @@ $lang->program->readjustTime = 'Change the program begin&end date.'; $lang->program->stakeholderTypeList['inside'] = 'Inside'; $lang->program->stakeholderTypeList['outside'] = 'Outside'; +$lang->program->typeList['my'] = 'My Programs'; +$lang->program->typeList['others'] = 'Others'; + $lang->program->noProgram = 'No program.'; $lang->program->showClosed = 'Closed programs.'; $lang->program->tips = 'If a parent item set is selected, products under that parent item set can be associated. If no item set is selected, a product with the same name as the item is created by default and associated with that item.'; diff --git a/module/program/lang/vi.php b/module/program/lang/vi.php index 85f3aba0d7..2b3a3d4cf7 100644 --- a/module/program/lang/vi.php +++ b/module/program/lang/vi.php @@ -62,6 +62,9 @@ $lang->program->readjustTime = 'Change the program begin&end date.'; $lang->program->stakeholderTypeList['inside'] = 'Inside'; $lang->program->stakeholderTypeList['outside'] = 'Outside'; +$lang->program->typeList['my'] = 'My Programs'; +$lang->program->typeList['others'] = 'Others'; + $lang->program->noProgram = 'No program.'; $lang->program->showClosed = 'Closed programs.'; $lang->program->tips = 'If a parent item set is selected, products under that parent item set can be associated. If no item set is selected, a product with the same name as the item is created by default and associated with that item.'; diff --git a/module/program/lang/zh-cn.php b/module/program/lang/zh-cn.php index e707dcc8dc..5ad72f36e0 100644 --- a/module/program/lang/zh-cn.php +++ b/module/program/lang/zh-cn.php @@ -115,3 +115,16 @@ $lang->program->statusList['suspended'] = '已挂起'; $lang->program->statusList['closed'] = '已关闭'; $lang->program->featureBar['all'] = '所有'; + +$lang->program->kanban = new stdclass(); +$lang->program->kanban->typeList['my'] = '我参与的项目集'; +$lang->program->kanban->typeList['others'] = '其他项目集'; + +$lang->program->kanban->activeProducts = '未关闭的产品'; +$lang->program->kanban->activePlans = '未过期的计划'; +$lang->program->kanban->waitProjects = '未开始的项目'; +$lang->program->kanban->doingProjects = '进行中的项目'; +$lang->program->kanban->doingExecutions = '进行中的执行'; +$lang->program->kanban->normalReleases = '正常的发布'; + +$lang->program->kanban->laneColorList = array('#32C5FF', '#006AF1', '#9D28B2', '#FF8F26', '#FFC20E', '#00A78E', '#7FBB00', '#424BAC', '#C0E9FF', '#EC2761'); diff --git a/module/program/model.php b/module/program/model.php index 9a257bd36f..8a9893cff4 100644 --- a/module/program/model.php +++ b/module/program/model.php @@ -96,7 +96,7 @@ class programModel extends model */ public function getByID($programID = 0) { - $program = $this->dao->select('*')->from(TABLE_PROGRAM)->where('id')->eq($programID)->andWhere('`type`')->eq('program')->fetch(); + $program = $this->dao->select('*')->from(TABLE_PROGRAM)->where('id')->eq($programID)->fetch(); $program = $this->loadModel('file')->replaceImgURL($program, 'desc'); return $program; } @@ -143,7 +143,271 @@ class programModel extends model ->fetchAll('id'); } - /** + /** + * Get kanban group data. + * + * @access public + * @return void + */ + public function getKanbanGroup() + { + $kanbanGroup = array(); + $kanbanGroup['my'] = array(); + $kanbanGroup['others'] = array(); + + /* Get all prived programs. */ + $programs = $this->dao->select('id, name')->from(TABLE_PROGRAM) + ->where('type')->eq('program') + ->andWhere('deleted')->eq(0) + ->andWhere('grade')->eq(1) + ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->programs)->fi() + ->andWhere('status')->ne('closed') + ->fetchAll('id'); + + $involvedPrograms = $this->getInvolvedPrograms($this->app->user->account); + + /* Get all products under programs. */ + $productGroup = $this->dao->select('id, program, name')->from(TABLE_PRODUCT) + ->where('deleted')->eq(0) + ->andWhere('program')->in(array_keys($programs)) + ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->products)->fi() + ->andWhere('status')->ne('closed') + ->fetchGroup('program', 'id'); + + $productPairs = array(); + foreach($productGroup as $programID => $products) + { + foreach($products as $productID => $product) $productPairs[$productID] = $productID; + } + + /* Get all plans under products. */ + $plans = $this->dao->select('id, product, title')->from(TABLE_PRODUCTPLAN) + ->where('deleted')->eq(0) + ->andWhere('product')->in($productPairs) + ->andWhere('end')->gt(helper::today()) + ->fetchGroup('product'); + + /* Get all products linked projects and executions. */ + $projectGroup = $this->dao->select('t1.product, t2.id, t2.name, t2.status')->from(TABLE_PROJECTPRODUCT)->alias('t1') + ->leftJoin(TABLE_PROJECT)->alias('t2') + ->on('t1.project = t2.id') + ->where('t2.deleted')->eq(0) + ->andWhere('t1.product')->in($productPairs) + ->andWhere('t2.status')->ne('closed') + ->andWhere('t2.type')->eq('project') + ->fetchGroup('product'); + + $projectPairs = array(); + foreach($projectGroup as $projects) + { + foreach($projects as $project) $projectPairs[$project->id] = $project->id; + } + + $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution') + ->from(TABLE_TASK) + ->where('project')->in($projectPairs) + ->andWhere('parent')->lt(1) + ->andWhere('deleted')->eq(0) + ->fetchGroup('project', 'id'); + + $projectHours = $this->computeProgress($tasks); + + $releases = $this->dao->select('product, id, name')->from(TABLE_RELEASE) + ->where('product')->in($productPairs) + ->andWhere('deleted')->eq(0) + ->andWhere('status')->eq('normal') + ->fetchGroup('product'); + + $doingExecutions = $this->dao->select('id, project, name')->from(TABLE_EXECUTION) + ->where('type')->in('sprint,stage') + ->andWhere('status')->eq('doing') + ->andWhere('deleted')->eq(0) + ->orderBy('id_asc') + ->fetchAll('project'); + + $executionPairs = array(); + foreach($doingExecutions as $execution) $executionPairs[$execution->id] = $execution->id; + + $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution') + ->from(TABLE_TASK) + ->where('execution')->in($executionPairs) + ->andWhere('parent')->lt(1) + ->andWhere('deleted')->eq(0) + ->fetchGroup('execution', 'id'); + + $executionHours = $this->computeProgress($tasks); + + foreach($productGroup as $programID => $products) + { + foreach($products as $productID => $product) + { + $product->plans = zget($plans, $productID, array()); + $product->releases = zget($releases, $productID, array()); + $projects = zget($projectGroup, $productID, array()); + foreach($projects as $project) + { + $status = $project->status == 'wait' ? 'wait' : 'doing'; + $execution = zget($doingExecutions, $project->id, array()); + if(!empty($execution)) $execution->hours = zget($executionHours, $execution->id, array()); + + $project->execution = $execution; + $project->hours = zget($projectHours, $project->id, array()); + $product->projects[$status][] = $project; + } + } + } + + //区分出其他项目集 + foreach($programs as $programID => $program) + { + $program->products = zget($productGroup, $programID, array()); + + if(in_array($programID, $involvedPrograms)) + { + $kanbanGroup['my'][$programID] = $program; + } + else + { + $kanbanGroup['others'][$programID] = $program; + } + } + + return $kanbanGroup; + } + + /** + * Get involved programs by user. + * + * @param string $account + * @access public + * @return void + */ + public function getInvolvedPrograms($account) + { + $involvedPrograms = array(); + + /* All involves in program table. */ + $objects = $this->dao->select('id, type, project')->from(TABLE_PROGRAM) + ->where('deleted')->eq(0) + ->andWhere("(openedBy = '$account' or PM = '$account')") + ->fetchAll('id'); + + foreach($objects as $id => $object) + { + if($object->type == 'program') $involvedPrograms[$id] = $id; + if($object->type == 'project') + { + $programID = $this->getTopByID($id); + $involvedPrograms[$programID] = $programID; + } + if($object->type == 'sprint' || $object->type == 'stage') + { + $programID = $this->getTopByID($object->project); + $involvedPrograms[$programID] = $programID; + } + } + + /* All involves in stakeholder table. */ + $stakeholders = $this->dao->select('t1.objectID, t2.type')->from(TABLE_STAKEHOLDER)->alias('t1') + ->leftJoin(TABLE_PROJECT)->alias('t2') + ->on('t1.objectID = t2.id') + ->where('t1.objectType')->in("program,project") + ->andWhere('t1.user')->eq($account) + ->fetchAll('objectID'); + + foreach($stakeholders as $objectID => $object) + { + if($object->type == 'program') + { + $involvedPrograms[$objectID] = $objectID; + } + if($object->type == 'project') + { + $programID = $this->getTopByID($objectID); + $involvedPrograms[$programID] = $programID; + } + } + + /* All involves in team table. */ + $teams = $this->dao->select('t1.root, t2.project, t2.type')->from(TABLE_TEAM)->alias('t1') + ->leftJoin(TABLE_PROJECT)->alias('t2') + ->on('t1.root = t2.id') + ->where('t1.account')->eq($account) + ->andWhere('t1.type')->in('project,execution') + ->fetchAll('root'); + + foreach($teams as $objectID => $object) + { + if($object->type == 'project') + { + $programID = $this->getTopByID($objectID); + $involvedPrograms[$programID] = $programID; + } + if($object->type == 'execution') + { + $programID = $this->getTopByID($object->project); + $involvedPrograms[$programID] = $programID; + } + } + + /* All involves in products table. */ + $products = $this->dao->select('id, program, createdBy, PO, QD, RD')->from(TABLE_PRODUCT) + ->where('deleted')->eq(0) + ->andWhere("(createdBy = '$account' or PO = '$account' or QD = '$account' or RD = '$account')") + ->fetchAll('id'); + + foreach($products as $id => $product) $involvedPrograms[$product->program] = $product->program; + + /* Check priv. */ + $involvedPrograms = $this->dao->select('id')->from(TABLE_PROGRAM) + ->where('deleted')->eq(0) + ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->programs)->fi() + ->andWhere('id')->in($involvedPrograms) + ->andWhere('grade')->eq(1) + ->fetchPairs(); + + return $involvedPrograms; + } + + /** + * Compute progress for project or execution. + * + * @param array $tasks + * @access public + * @return void + */ + public function computeProgress($tasks) + { + $hours = array(); + foreach($tasks as $projectID => $projectTasks) + { + $hour = new stdclass(); + $hour->totalConsumed = 0; + $hour->totalEstimate = 0; + $hour->totalLeft = 0; + + foreach($projectTasks as $task) + { + $hour->totalConsumed += $task->consumed; + $hour->totalEstimate += $task->estimate; + if($task->status != 'cancel' and $task->status != 'closed') $hour->totalLeft += $task->left; + } + $hours[$projectID] = $hour; + } + + 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, 2) * 100 : 0; + } + + return $hours; + } + + /** * Get project list data. * * @param int $programID diff --git a/module/program/view/kanban.html.php b/module/program/view/kanban.html.php new file mode 100644 index 0000000000..ab1d0349a3 --- /dev/null +++ b/module/program/view/kanban.html.php @@ -0,0 +1,129 @@ + + * @package ZenTaoPMS + * @version $Id + */ +?> + +
+ $programGroup):?> + +
+
+
program->kanban->typeList[$type];?>
+
+ + + + + + + + + + + + + + + + + $program):?> + + + + products)):?> + products as $productID => $product):?> + ';?> + + + + + + + ';?> + + + + + + + + + + + + + 9) $colorIndex = 0;?> + + +
program->kanban->activeProducts;?>program->kanban->activePlans;?>program->kanban->waitProjects;?>program->statusList['doing'];?>program->kanban->normalReleases;?>
program->kanban->doingProjects;?>program->kanban->doingExecutions;?>
name;?>name;?> + plans as $planID => $plan):?> +
+
+
+ createLink('productplan', 'view', "planID=$plan->id"), $plan->title);?> +
+
+
+ +
+ projects['wait'])):?> + projects['wait'] as $projectID => $project):?> +
+
+
+ createLink('project', 'view', "projectID=$projectID"), $project->name);?> +
+
+
+ + +
+ projects['doing'])):?> + projects['doing'] as $projectID => $project):?> +
+
+
+ createLink('project', 'view', "projectID=$projectID"), $project->name);?> +
+
+
+ + +
+ projects['doing'])):?> + projects['doing'] as $projectID => $project):?> +
+
+
+ execution)):?> + createLink('execution', 'view', "executionID={$project->execution->id}"), $project->execution->name);?> + +
+
+
+ + +
+ releases as $releaseID => $release):?> +
+
+
+ createLink('release', 'view', "releaseID=$release->id"), $release->name);?> +
+
+
+ +
+
+
+
+ +
+ diff --git a/module/project/model.php b/module/project/model.php index 4db84eb125..319570cb95 100644 --- a/module/project/model.php +++ b/module/project/model.php @@ -760,7 +760,7 @@ class projectModel extends model $this->dao->insert(TABLE_PROJECT)->data($project) ->autoCheck() ->batchcheck($requiredFields, 'notempty') - ->check('name', 'unique', "type='project'") + ->check('name', 'unique', "type='project' AND deleted='0'") ->exec(); /* Add the creater to the team. */ @@ -951,7 +951,7 @@ class projectModel extends model ->checkIF($project->begin != '', 'begin', 'date') ->checkIF($project->end != '', 'end', 'date') ->checkIF($project->end != '', 'end', 'gt', $project->begin) - ->check('name', 'unique', "id != $projectID AND type='project'") + ->check('name', 'unique', "id != $projectID AND type='project' AND deleted='0'") ->where('id')->eq($projectID) ->exec();