znt/module/program/tao.php

435 lines
18 KiB
PHP

<?php
declare(strict_types=1);
/**
* The tao file of program module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author chen.tao<chentao@easycorp.ltd>
* @package program
* @link https://www.zentao.net
*/
class programTao extends programModel
{
/**
*获取所有根项目集基本数据。
* Get base data of all root programs.
*
* @access protected
* @return array
*/
protected function getRootProgramList(): array
{
return $this->dao->select('id,name,PM,path,parent,type')
->from(TABLE_PROGRAM)
->where('parent')->eq('0')
->andWhere('deleted')->eq('0')
->andWhere('type')->eq('program')
->orderBy('order_asc')
->fetchAll();
}
/**
* 构建项目视图的项目集的操作数据。
* Build actions map for program.
*
* @param object $program
* @access protected
* @return array
*/
protected function buildProgramActionsMap(object $program): array
{
if($program->type == 'program' && (!$this->app->user->admin && !str_contains(",{$this->app->user->view->programs},", ",{$program->id},"))) return array();
if($program->type == 'project') $this->loadModel('project');
$modelClass = '';
if($program->type == 'project') $modelClass = get_class($this->project);
if($program->type == 'program') $modelClass = get_class($this);
if($this->config->edition != 'open')
{
$this->loadModel('flow');
$flowActions = $this->loadModel('workflowaction')->getList($program->type);
}
$actionsMap = array();
$canStartProgram = common::hasPriv($program->type, 'start', $program);
$canSuspendProgram = common::hasPriv($program->type, 'suspend', $program);
$canCloseProgram = common::hasPriv($program->type, 'close', $program);
$canActivateProgram = common::hasPriv($program->type, 'activate', $program);
$normalActions = array('start', 'close', 'activate');
foreach($normalActions as $action)
{
if($action == 'close' && (!$canCloseProgram || $program->status != 'doing')) continue;
if($action == 'activate' && (!$canActivateProgram || $program->status != 'closed')) continue;
if($action == 'start' && (!$canStartProgram || ($program->status != 'wait' && $program->status != 'suspended'))) continue;
if($this->config->edition != 'open')
{
foreach($flowActions as $flowAction)
{
if($flowAction->action == $action && $flowAction->extensionType != 'none' && $flowAction->status == 'enable' && !empty($flowAction->conditions))
{
if(!$this->flow->checkConditions($flowAction->conditions, $data)) continue 2;
}
}
}
$item = new stdclass();
$item->name = $action;
$item->url = helper::createLink($program->type, $action, "programID={$program->id}");
if($modelClass && !call_user_func_array(array($modelClass, 'isClickable'), array($program, $action))) $item->disabled = true;
$actionsMap[] = $item;
break;
}
if($canSuspendProgram || ($canCloseProgram && $program->status != 'doing') || ($canActivateProgram && $program->status != 'closed'))
{
$other = new stdclass();
$other->name = 'other';
$other->items = array();
$otherActions = array('suspend', 'close', 'activate');
foreach($otherActions as $action)
{
if(!common::hasPriv($program->type, $action, $program)) continue;
if($action == 'close' && $program->status == 'doing') continue;
if($this->config->edition != 'open')
{
foreach($flowActions as $flowAction)
{
if($flowAction->action == $action && $flowAction->extensionType != 'none' && $flowAction->status == 'enable' && !empty($flowAction->conditions))
{
if(!$this->flow->checkConditions($flowAction->conditions, $data)) continue 2;
}
}
}
$item = new stdclass();
$item->name = $action;
$item->url = helper::createLink($program->type, $action, "programID={$program->id}");
if($modelClass && !call_user_func_array(array($modelClass, 'isClickable'), array($program, $action))) $item->disabled = true;
if($action == 'close' && $program->status == 'closed') $item->hint = $this->lang->{$program->type}->tip->closed;
if($action == 'activate' && $program->status == 'doing') $item->hint = $this->lang->{$program->type}->tip->actived;
if($action == 'suspend' && $program->status == 'suspended') $item->hint = $this->lang->{$program->type}->tip->suspended;
if($action == 'suspend' && $program->status == 'closed') $item->hint = $this->lang->{$program->type}->tip->notSuspend;
$other->items[] = $item;
}
$actionsMap[] = $other;
}
return array_merge($actionsMap, $this->getNormalActions($program));
}
/**
* 获取基础操作的按钮数据。
* Get normal actions.
*
* @param object $program
* @access protected
* @return array
*/
protected function getNormalActions(object $program): array
{
$actionsMap = array();
$normalActions = $program->type == 'project' ? array('edit') : array('edit', 'create', 'delete');
foreach($normalActions as $action)
{
if(!common::hasPriv($program->type, $action)) continue;
$item = new stdclass();
$item->name = $action;
if($action != 'delete') $item->url = helper::createLink($program->type, $action, "programID={$program->id}");
if($action == 'delete') $item->url = "javascript:confirmDelete({$program->id}, 'program', '{$program->name}')";
if($action == 'create' && $program->status == 'closed')
{
$item->disabled = true;
$item->hint = $this->lang->program->tip->notCreate;
}
$actionsMap[] = $item;
}
return $actionsMap;
}
/**
* 构建项目视角中项目的操作数据。
* Build actions map for project.
*
* @param object $project
* @access protected
* @return array
*/
protected function buildProjectActionsMap(object $project): array
{
$this->loadModel('project');
$actionsMap = array();
if(common::hasPriv('project', 'team'))
{
$item = new stdclass();
$item->name = 'team';
$item->url = helper::createLink('project', 'team', "projectID={$project->id}");
$actionsMap[] = $item;
}
if(common::hasPriv('project', 'manageProducts') || common::hasPriv('project', 'whitelist') || common::hasPriv('project', 'delete'))
{
$more = new stdclass();
$more->name = 'more';
$more->items = array();
$moreActions = array('group', 'manageProducts', 'whitelist', 'delete');
foreach($moreActions as $action)
{
if(!common::hasPriv('project', $action)) continue;
$item = new stdclass();
$item->name = $action == 'manageProducts' ? 'link' : $action;
if($action != 'delete') $item->url = helper::createLink('project', $action, "projectID={$project->id}");
if($action == 'delete') $item->url = "javascript:confirmDelete({$project->id}, 'project', '{$project->name}')";
if($action == 'whitelist' and $project->acl == 'open')
{
$item->disabled = true;
$item->hint = $this->lang->project->tip->whitelist;
}
if($action == 'group' && $project->model == 'kanban')
{
$item->disabled = true;
$item->hint = $this->lang->project->tip->group;
}
if(!call_user_func_array(array($this->project, 'isClickable'), array($project, $item->name))) $item->disabled = true;
$more->items[] = $item;
}
$actionsMap[] = $more;
}
return $actionsMap;
}
/**
* 如果修改了父项目集,修改关联的产品。
* If change parent, then fix linked product.
*
* @param int $programID
* @param int $parent
* @param int $oldParent
* @param string $oldPath
* @access protected
* @return void
*/
protected function fixLinkedProduct(int $programID, int $parent, int $oldParent, string $oldPath): void
{
if($parent == $oldParent) return;
/* Move product to new top program. */
$oldTopProgram = $this->getTopByPath($oldPath);
$newTopProgram = $this->getTopByID($programID);
if($oldTopProgram == $newTopProgram) return;
if($oldParent == 0)
{
$this->dao->update(TABLE_PRODUCT)->set('program')->eq($newTopProgram)->where('program')->eq($oldTopProgram)->exec();
return;
}
/* Get the shadow products that produced by the program's no product projects. */
$shadowProducts = $this->dao->select('t1.id')->from(TABLE_PRODUCT)->alias('t1')
->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id=t2.product')
->leftJoin(TABLE_PROJECT)->alias('t3')->on('t2.project=t3.id')
->where('t3.path')->like("%,$programID,%")
->andWhere('t3.type')->eq('project')
->andWhere('t3.hasProduct')->eq('0')
->andWhere('t1.shadow')->eq('1')
->fetchPairs();
if($shadowProducts) $this->dao->update(TABLE_PRODUCT)->set('program')->eq($newTopProgram)->where('id')->in($shadowProducts)->exec();
}
/**
* 更新项目集的统计数据。
* Update stats of program.
*
* @param array $projectIdList
* @access protected
* @return bool
*/
protected function updateStats(array $projectIdList): bool
{
/* Get summary and members of executions to be refreshed. */
$stats = $this->getTaskStats($projectIdList);
$teamMembers = $this->dao->select('t1.root, COUNT(1) AS members')->from(TABLE_TEAM)->alias('t1')
->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account')
->where('t1.type')->eq('project')
->beginIF(!empty($projectIdList))->andWhere('t1.root')->in($projectIdList)->fi()
->andWhere('t2.deleted')->eq(0)
->groupBy('t1.root')
->fetchPairs('root');
foreach($teamMembers as $projectID => $teamCount)
{
if(!isset($stats[$projectID])) $stats[$projectID] = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'teamCount' => 0, 'totalConsumedNotDel' => 0, 'totalLeftNotDel' => 0);
$stats[$projectID]['teamCount'] = $teamCount;
}
foreach($stats as $projectID => $project)
{
$totalRealNotDel = $project['totalConsumedNotDel'] + $project['totalLeftNotDel'];
$progress = $totalRealNotDel ? floor($project['totalConsumedNotDel'] / $totalRealNotDel * 1000) / 1000 * 100 : 0;
$this->dao->update(TABLE_PROJECT)
->set('progress')->eq($progress)
->set('teamCount')->eq($project['teamCount'])
->set('estimate')->eq($project['totalEstimate'])
->set('consumed')->eq($project['totalConsumedNotDel'])
->set('left')->eq($project['totalLeftNotDel'])
->where('id')->eq($projectID)
->exec();
}
return !dao::isError();
}
/**
* 获取任务的统计数据。
* Get task stats.
*
* @param array $projectIdList
* @access protected
* @return array
*/
protected function getTaskStats(array $projectIdList): array
{
/* 1. Set execution has no tasks. */
$summary = $this->setNoTaskExecution($projectIdList);
/* 2. Get summary of executions to be refreshed. */
$tasks = $this->dao->select('t1.id, execution, t1.estimate, t1.consumed, t1.`left`, t1.status')->from(TABLE_TASK)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id')
->where('t1.deleted')->eq(0)
->andWhere('t1.isParent')->eq('0') // Ignore parent task.
->andWhere('t2.deleted')->eq(0)
->beginIF(!empty($projectIdList))->andWhere('t2.project')->in($projectIdList)->fi()
->fetchAll('id');
foreach($tasks as $task)
{
if(empty($task->execution)) continue;
if(!isset($summary[$task->execution]))
{
$summary[$task->execution] = new stdclass();
$summary[$task->execution]->totalEstimate = 0;
$summary[$task->execution]->totalConsumed = 0;
$summary[$task->execution]->totalLeft = 0;
}
$summary[$task->execution]->execution = $task->execution;
$summary[$task->execution]->totalEstimate += $task->estimate;
$summary[$task->execution]->totalConsumed += $task->consumed;
$summary[$task->execution]->totalLeft += ($task->status == 'closed' or $task->status == 'cancel') ? 0 : $task->left;
}
/* 3. Get all parents to be refreshed. */
$executions = array();
foreach($summary as $execution) $executions[$execution->execution] = $execution->execution;
$paths = $this->dao->select('id,path')->from(TABLE_PROJECT)->where('id')->in($executions)->fetchAll();
$executionPaths = array();
foreach($paths as $path) $executionPaths[$path->id] = explode(',', trim($path->path, ','));
/* 4. Compute stats of execution and parents. */
$stats = array();
$projectsPairs = $this->dao->select('id,deleted')->from(TABLE_PROJECT)->fetchPairs();
foreach($summary as $execution)
{
$executionID = $execution->execution;
if(!isset($executionPaths[$executionID])) continue;
foreach($executionPaths[$executionID] as $nodeID)
{
if(!isset($stats[$nodeID])) $stats[$nodeID] = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'teamCount' => 0, 'totalLeftNotDel' => 0, 'totalConsumedNotDel' => 0);
$stats[$nodeID]['totalEstimate'] += $execution->totalEstimate;
$stats[$nodeID]['totalConsumed'] += $execution->totalConsumed;
$stats[$nodeID]['totalLeft'] += $execution->totalLeft;
/* Check $execution->execution and $nodeID(path) is not deleted. */
if(empty($projectsPairs[$execution->execution]) && empty($projectsPairs[$nodeID]))
{
$stats[$nodeID]['totalConsumedNotDel'] += $execution->totalConsumed;
$stats[$nodeID]['totalLeftNotDel'] += $execution->totalLeft;
}
}
}
return $stats;
}
/**
* 设置没有任务的执行数据。
* Set execution stat when has no task.
*
* @param array $projectIdList
* @access protected
* @return array
*/
protected function setNoTaskExecution(array $projectIdList): array
{
$summary = array();
$executionGroup = $this->dao->select('id, project')->from(TABLE_PROJECT)->where('project')->in($projectIdList)->andWhere('deleted')->eq(0)->fetchGroup('project', 'id');
foreach($projectIdList as $projectID)
{
$executions = zget($executionGroup, $projectID, array());
foreach($executions as $executionID => $execution)
{
$taskCount = $this->dao->select('id')->from(TABLE_TASK)
->where('deleted')->eq(0)
->andWhere('execution')->eq($executionID)
->count();
if(empty($taskCount))
{
$summary[$executionID] = new stdclass();
$summary[$executionID]->totalEstimate = 0;
$summary[$executionID]->totalConsumed = 0;
$summary[$executionID]->totalLeft = 0;
$summary[$executionID]->execution = $executionID;
}
}
}
return $summary;
}
/**
* 更新项目集的进度。
* Update program progress.
*
* @access protected
* @return bool
*/
protected function updateProgress(): bool
{
$projectList = $this->dao->select('id,progress,path,consumed,`left`')->from(TABLE_PROJECT)
->where('type')->eq('project')
->andWhere('parent')->ne(0)
->andWhere('deleted')->eq(0)
->fetchAll('id');
$programProgress = array();
foreach($projectList as $projectID => $project)
{
$path = explode(',', trim($project->path, ','));
foreach($path as $programID)
{
if($programID == $projectID) continue;
if(!isset($programProgress[$programID])) $programProgress[$programID] = array('consumed' => 0, 'left' => 0);
$programProgress[$programID]['consumed'] += $project->consumed;
$programProgress[$programID]['left'] += $project->left;
}
}
foreach($programProgress as $programID => $hours)
{
$hours['progress'] = ($hours['consumed'] + $hours['left']) ? floor($hours['consumed'] / ($hours['consumed'] + $hours['left']) * 1000) / 1000 * 100 : 0;
$this->dao->update(TABLE_PROJECT)->data($hours)->where('id')->eq($programID)->exec();
}
return !dao::isError();
}
}