* @link https://www.zentao.net */ class programplanTao extends programplanModel { /** * 更新项目阶段。 * update program plan. * * @param object $plan * @param array $conditions * @access protected * @return bool */ protected function updateRow(object $plan, array $conditions): bool { $requiredFields = $conditions['requiredFields'] ?? ''; $projectIDList = $conditions['relatedExecutionsID'] ?? ''; $project = $conditions['project'] ?? ''; $parentStage = $conditions['parentStage'] ?? ''; $parent = $conditions['parent'] ?? ''; $setCode = $conditions['setCode'] ?? ''; $getName = false; if($projectIDList && $project && $parentStage && $parent) $getName = true; $this->dao->update(TABLE_PROJECT)->data($plan) ->autoCheck() ->batchCheckIF($requiredFields, $requiredFields, 'notempty') ->checkIF($plan->end != '0000-00-00', 'end', 'ge', $plan->begin) ->checkIF(!empty($plan->percent), 'percent', 'float') ->checkIF(!empty($plan->name) && $getName, 'name', 'unique', "id in ({$projectIDList}) and type in ('sprint','stage') and `project` = {$project} and `deleted` = '0'" . ($parentStage ? " and `parent` = {$parent}" : '')) ->checkIF(!empty($plan->code) and $setCode, 'code', 'unique', "id != {$plan->id} and type in ('sprint','stage','kanban') and `deleted` = '0'") ->where('id')->eq($plan->id) ->exec(); return !dao::isError(); } /** * 获取父阶段键值对。 * Get parent stage list. * * @param int $executionID * @param int $planID * @param int $productID * @access protected * @return array|false */ protected function getParentStages(int $executionID, int $planID, int $productID): array|false { $parentStage = $this->dao->select('t2.id, t2.name')->from(TABLE_PROJECTPRODUCT) ->alias('t1')->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t1.product')->eq($productID) ->andWhere('t2.project')->eq($executionID) ->andWhere('t2.type')->eq('stage') ->andWhere('t2.deleted')->eq(0) ->andWhere('t2.path')->notlike("%,$planID,%") ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi() ->orderBy('t2.id desc') ->fetchPairs(); if(dao::isError()) return false; return $parentStage; } /** * 根据action获取excution。 * Get new parent and action. * * @param array $statusCount * @param object $parent * @param int $startTasks * @param string $action * @access protected * @return array */ protected function getNewParentAndAction(array $statusCount, object $parent, int $startTasks, string $action): array { $count = count($statusCount); $newParent = null; $parentAction = ''; $this->loadModel('execution'); if(isset($statusCount['wait']) && $count == 1 && helper::isZeroDate($parent->realBegan) && $startTasks == 0) { if($parent->status != 'wait') { $newParent = $this->execution->buildExecutionByStatus('wait'); $parentAction = 'waitbychild'; } } elseif(isset($statusCount['closed']) && $count == 1) { if($parent->status != 'closed') { $newParent = $this->execution->buildExecutionByStatus('closed'); $parentAction = 'closedbychild'; } } elseif(isset($statusCount['suspended']) && ($count == 1 || (isset($statusCount['closed']) && $count == 2))) { if($parent->status != 'suspended') { $newParent = $this->execution->buildExecutionByStatus('suspended'); $parentAction = 'suspendedbychild'; } } else { if($parent->status != 'doing') { $newParent = $this->execution->buildExecutionByStatus('doing'); $parentAction = $parent->status == 'wait' ? 'startbychildstart' : 'startbychild' . $action; } } return array('newParent' => $newParent, 'parentAction' => $parentAction); } /** * 校验提交数据是否必须。 * Check required items. * * @param object $oldPlan * @param object $plan * @param int $projectID * @access protected * @return bool */ protected function checkRequiredItems(object $oldPlan, object $plan, int $projectID): bool { /* 校验开始结束时间是否正确。 */ /* check begin and end date. */ if($plan->begin == '0000-00-00') dao::$errors['begin'][] = sprintf($this->lang->error->notempty, $this->lang->programplan->begin); if($plan->end == '0000-00-00') dao::$errors['end'][] = sprintf($this->lang->error->notempty, $this->lang->programplan->end); if(dao::isError()) return false; if($plan->parent) $parentStage = $this->getByID($plan->parent); if(isset($parentStage) && $plan->begin < $parentStage->begin) { dao::$errors['begin'] = sprintf($this->lang->programplan->error->letterParent, $parentStage->begin); return false; } if(isset($parentStage) && $plan->end > $parentStage->end) { dao::$errors['end'] = sprintf($this->lang->programplan->error->greaterParent, $parentStage->end); return false; } if($projectID) $this->loadModel('execution')->checkBeginAndEndDate($projectID, $plan->begin, $plan->end); if(dao::isError()) return false; $setCode = isset($this->config->setCode) && $this->config->setCode == 1; if($setCode && empty($plan->code)) { dao::$errors['code'][] = sprintf($this->lang->error->notempty, $this->lang->execution->code); return false; } /* 对于是否有子阶段进行判断处理。 */ /* check parent stage. */ $setPercent = isset($this->config->setPercent) && $this->config->setPercent == 1; if($plan->parent > 0) { $plan->attribute = $parentStage->attribute == 'mix' ? $plan->attribute : $parentStage->attribute; $plan->acl = $parentStage->acl; if($setPercent) { $childrenTotalPercent = $this->getTotalPercent($parentStage, true); $childrenTotalPercent = $plan->parent == $oldPlan->parent ? ($childrenTotalPercent - $oldPlan->percent + $plan->percent) : ($childrenTotalPercent + $plan->percent); if($childrenTotalPercent > 100) return dao::$errors['percent'][] = $this->lang->programplan->error->percentOver; } /* 如果子阶段有里程碑,那么父阶段的更新为0。 */ /* If child plan has milestone, update parent plan set milestone eq 0 . */ if($plan->milestone && $parentStage->milestone) $this->dao->update(TABLE_PROJECT)->set('milestone')->eq(0)->where('id')->eq($oldPlan->parent)->exec(); } else { /* Synchronously update sub-phase permissions. */ $childrenIDList = $this->dao->select('id')->from(TABLE_PROJECT)->where('parent')->eq($oldPlan->id)->fetchAll('id'); if(!empty($childrenIDList)) $this->dao->update(TABLE_PROJECT)->set('acl')->eq($plan->acl)->where('id')->in(array_keys($childrenIDList))->exec(); /* 相同父阶段的子阶段工作量占比之和不超过100%。 */ /* The workload of the parent plan cannot exceed 100%. */ $oldPlan->parent = $plan->parent; if($setPercent) { $totalPercent = $this->getTotalPercent($oldPlan); $totalPercent = $totalPercent + $plan->percent; if($totalPercent > 100) return dao::$errors['percent'][] = $this->lang->programplan->error->percentOver; } } return true; } /** * 组装里程碑数据。 * Format milestones use '/'. * * @param array $milestones * @param int $projectID * @access protected * @return array */ protected function formatMilestones(array $milestones, int $projectID): array { $allStages = $this->dao->select('id,name')->from(TABLE_EXECUTION) ->where('project')->eq($projectID) ->andWhere('type')->notin('program,project') ->fetchPairs(); foreach($milestones as $id => $path) { $paths = explode(',', trim($path, ',')); $stageName = ''; foreach($paths as $stage) { if(isset($allStages[$stage])) $stageName .= '/' . $allStages[$stage]; } $milestones[$id] = trim($stageName, '/'); } return $milestones; } /** 设置阶段基线数据。 * Set stage baseline data. * * @param array $oldPlans * @param array $plans * @access protected * @return array */ protected function setStageBaseline(array $oldPlans, array $plans): array { foreach($oldPlans as $id => $oldPlan) { if(!isset($plans[$id])) continue; $plans[$id]->version = $oldPlan->version; $plans[$id]->name = $oldPlan->name; $plans[$id]->milestone = $oldPlan->milestone; $plans[$id]->begin = $oldPlan->begin; $plans[$id]->end = $oldPlan->end; } return $plans; } /** * 设置任务基线数据。 * Set task baseline data. * * @param array $oldTasks * @param array $tasks * @access protected * @return array */ protected function setTaskBaseline(array $oldTasks, array $tasks): array { foreach($oldTasks as $id => $oldTask) { if(!isset($tasks[$id])) continue; $tasks[$id]->version = $oldTask->version; $tasks[$id]->name = $oldTask->name; $tasks[$id]->estStarted = $oldTask->estStarted; $tasks[$id]->deadline = $oldTask->deadline; } return $tasks; } /** * 设置甘特图阶段数据。 * Set the Gantt chart stage data. * * @param array $plans * @param array $datas * @param array $stageIndex * @param array $planIdList * @access protected * @return void */ protected function setPlan(array $plans, array &$datas, array &$stageIndex, array &$planIdList): void { $today = helper::today(); $isMilestone = " "; foreach($plans as $plan) { $planIdList[$plan->id] = $plan->id; $start = helper::isZeroDate($plan->begin) ? '' : $plan->begin; $end = helper::isZeroDate($plan->end) ? '' : $plan->end; $realBegan = helper::isZeroDate($plan->realBegan) ? '' : $plan->realBegan; $realEnd = helper::isZeroDate($plan->realEnd) ? '' : $plan->realEnd; $data = new stdclass(); $data->id = $plan->id; $data->type = 'plan'; $data->text = empty($plan->milestone) ? $plan->name : $plan->name . $isMilestone ; $data->name = $plan->name; if(isset($this->config->setPercent) and $this->config->setPercent == 1) $data->percent = $plan->percent; $data->attribute = zget($this->lang->stage->typeList, $plan->attribute); $data->milestone = zget($this->lang->programplan->milestoneList, $plan->milestone); $data->owner_id = $plan->PM; $data->status = $this->processStatus('execution', $plan); $data->begin = $start; $data->deadline = $end; $data->realBegan = $realBegan ? substr($realBegan, 0, 10) : ''; $data->realEnd = $realEnd ? substr($realEnd, 0, 10) : ''; $data->parent = $plan->grade == 1 ? 0 :$plan->parent; $data->open = true; $data->start_date = $realBegan ? $realBegan : $start; $data->endDate = $realEnd ? $realEnd : $end; $data->duration = 1; $data->color = $this->lang->execution->gantt->stage->color; $data->progressColor = $this->lang->execution->gantt->stage->progressColor; $data->textColor = $this->lang->execution->gantt->stage->textColor; $data->bar_height = $this->lang->execution->gantt->bar_height; /* Determines if the object is delay. */ $data->delay = $this->lang->programplan->delayList[0]; $data->delayDays = 0; if($today > $end) { $data->delay = $this->lang->programplan->delayList[1]; $data->delayDays = helper::diffDate($today, substr($end, 0, 10)); } if($data->endDate > $data->start_date) $data->duration = helper::diffDate(substr($data->endDate, 0, 10), substr($data->start_date, 0, 10)) + 1; if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date)); if($data->start_date == '' or $data->endDate == '') $data->duration = 1; $datas['data'][$plan->id] = $data; $stageIndex[$plan->id] = array('planID' => $plan->id, 'parent' => $plan->parent, 'progress' => array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalReal' => 0)); } } /** * 设置甘特图任务数据。 * Set the Gantt chart task data. * * @param array $tasks * @param array $plans * @param string $selectCustom * @param array $datas * @param array $stageIndex * @access protected * @return void */ protected function setTask(array $tasks, array $plans, string $selectCustom, array &$datas, array &$stageIndex): void { $taskPri = "%s "; $today = helper::today(); $taskTeams = $this->dao->select('task,account')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task', 'account'); $users = $this->loadModel('user')->getPairs('noletter'); foreach($tasks as $task) { $execution = zget($plans, $task->execution, array()); $priIcon = sprintf($taskPri, $task->pri, $task->pri, $task->pri); $estStart = helper::isZeroDate($task->estStarted) ? '' : $task->estStarted; $estEnd = helper::isZeroDate($task->deadline) ? '' : $task->deadline; $realBegan = helper::isZeroDate($task->realStarted) ? '' : $task->realStarted; $realEnd = (in_array($task->status, array('done', 'closed')) and !helper::isZeroDate($task->finishedDate)) ? $task->finishedDate : ''; $start = $realBegan ? $realBegan : $estStart; $end = $realEnd ? $realEnd : $estEnd; if(empty($start) and $execution) $start = $execution->begin; if(empty($end) and $execution) $end = $execution->end; if($start > $end) $end = $start; $data = new stdclass(); $data->id = $task->execution . '-' . $task->id; $data->type = 'task'; $data->text = $priIcon . $task->name; $data->percent = ''; $data->status = $this->processStatus('task', $task); $data->owner_id = $task->assignedTo; $data->attribute = ''; $data->milestone = ''; $data->begin = $start; $data->deadline = $end; $data->realBegan = $realBegan ? substr($realBegan, 0, 10) : ''; $data->realEnd = $realEnd ? substr($realEnd, 0, 10) : ''; $data->pri = $task->pri; $data->parent = $task->parent > 0 ? $task->execution . '-' . $task->parent : $task->execution; $data->open = true; $progress = $task->consumed ? round($task->consumed / ($task->left + $task->consumed), 3) : 0; $data->progress = $progress; $data->taskProgress = ($progress * 100) . '%'; $data->start_date = $start; $data->endDate = $end; $data->duration = 1; $data->estimate = $task->estimate; $data->consumed = $task->consumed; $data->color = zget($this->lang->execution->gantt->color, $task->pri, $this->lang->execution->gantt->defaultColor); $data->progressColor = zget($this->lang->execution->gantt->progressColor, $task->pri, $this->lang->execution->gantt->defaultProgressColor); $data->textColor = zget($this->lang->execution->gantt->textColor, $task->pri, $this->lang->execution->gantt->defaultTextColor); $data->bar_height = $this->lang->execution->gantt->bar_height; /* Determines if the object is delay. */ $data->delay = $this->lang->programplan->delayList[0]; $data->delayDays = 0; if($today > $end) { $data->delay = $this->lang->programplan->delayList[1]; $data->delayDays = helper::diffDate($today, substr($end, 0, 10)); } /* If multi task then show the teams. */ if($task->mode == 'multi' and !empty($taskTeams[$task->id])) { $teams = array_keys($taskTeams[$task->id]); $assigneds = array(); foreach($teams as $assignedTo) $assigneds[] = zget($users, $assignedTo); $data->owner_id = implode(',', $assigneds); } if($data->endDate > $data->start_date) $data->duration = helper::diffDate(substr($data->endDate, 0, 10), substr($data->start_date, 0, 10)) + 1; if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date)); if($data->start_date == '' or $data->endDate == '') $data->duration = 1; if(strpos($selectCustom, 'task') !== false) $datas['data'][$data->id] = $data; foreach($stageIndex as $index => $stage) { if($stage['planID'] == $task->execution) { $stageIndex[$index]['progress']['totalEstimate'] += $task->estimate; $stageIndex[$index]['progress']['totalConsumed'] += $task->parent == '-1' ? 0 : $task->consumed; $stageIndex[$index]['progress']['totalReal'] += ($task->left + $task->consumed); $parent = $stage['parent']; if(isset($stageIndex[$parent])) { $stageIndex[$parent]['progress']['totalEstimate'] += $task->estimate; $stageIndex[$parent]['progress']['totalConsumed'] += $task->parent == '-1' ? 0 : $task->consumed; $stageIndex[$parent]['progress']['totalReal'] += ($task->left + $task->consumed); } } } } } /** * 设置关联任务数据。 * Set relation task data. * * @param array $planIdList * @param array $datas * @access protected * @return array */ protected function setRelationTask(array $planIdList, array $datas): array { $datas['links'] = array(); if($this->config->edition == 'open') return $datas; $relations = $this->dao->select('*')->from(TABLE_RELATIONOFTASKS)->where('execution')->in($planIdList)->orderBy('task,pretask')->fetchAll(); foreach($relations as $relation) { $link['source'] = $relation->execution . '-' . $relation->pretask; $link['target'] = $relation->execution . '-' . $relation->task; $link['type'] = $this->config->execution->gantt->linkType[$relation->condition][$relation->action]; $datas['links'][] = $link; } return $datas; } /** * 获取阶段信息。 * Get stage list. * * @param int $executionID * @param int $productID * @param string $browseType * @param string $orderBy * @access protected * @return array */ protected function getStageList(int $executionID, int $productID, string $browseType, $orderBy = 'id_asc') { if(empty($executionID)) return array(); $projectModel = $this->dao->select('model')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch('model'); return $this->dao->select('t1.*')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.project') ->where('1 = 1') ->beginIF($projectModel != 'waterfallplus')->andWhere('t1.type')->eq('stage')->fi() ->beginIF($productID)->andWhere('t2.product')->eq($productID)->fi() ->beginIF($browseType == 'all')->andWhere('t1.project')->eq($executionID)->fi() ->beginIF($browseType == 'parent')->andWhere('t1.parent')->eq($executionID)->fi() ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi() ->andWhere('t1.deleted')->eq('0') ->orderBy($orderBy) ->fetchAll('id'); } /** * 获取子阶段数量。 * Get stage count. * * @param int $planID * @param string $mode * @access protected * @return int */ protected function getStageCount(int $planID, string $mode = ''): int { return $this->dao->select('COUNT(*) AS count')->from(TABLE_PROJECT) ->where('parent')->eq($planID) ->beginIF($mode == 'milestone')->andWhere('milestone')->eq(1)->fi() ->andWhere('deleted')->eq(0) ->fetch('count'); } /** * 计算阶段在数据库中的位置序号。 * Compute the new order for programplan. if orders is not set then begin with the order in zt_project table. * * @param array $orders * @param array $plans * @access protected * @return array */ protected function computeOrders(array $orders, array $plans):array { if(!isset($orders)) $orders = array(); asort($orders); if(count($orders) < count($plans)) { $orderIndex = empty($orders) ? 0 : count($orders); $lastID = $this->dao->select('id')->from(TABLE_EXECUTION)->orderBy('id_desc')->fetch('id'); for($i = $orderIndex; $i < count($plans); $i ++) { $lastID ++; $orders[$i] = $lastID * 5; } } return $orders; } }