<?php
/**
 * The control file of execution module of ZenTaoPMS.
 *
 * @copyright   Copyright 2009-2023 禅道软件（青岛）有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
 * @author      Chunsheng Wang <chunsheng@cnezsoft.com>
 * @package     execution
 * @version     $Id: control.php 5094 2013-07-10 08:46:15Z chencongzhi520@gmail.com $
 * @link        https://www.zentao.net
 */
class execution extends control
{
    /**
     * The global $executions.
     *
     * @var array
     * @access public
     */
    public $executions;

    /**
     * The global $objectType.
     *
     * @var string
     * @access public
     */
    public $objectType = 'execution';

    /**
     * Construct function, Set executions.
     *
     * @param  string $moduleName
     * @param  string $methodName
     * @access public
     * @return void
     */
    public function __construct($moduleName = '', $methodName = '')
    {
        parent::__construct($moduleName, $methodName);

        $this->loadModel('project');
        if($this->app->upgrading || !isset($this->app->user)) return false;

        $mode = $this->app->tab == 'execution' ? 'multiple' : '';
        if((defined('RUN_MODE') and RUN_MODE == 'api') or $this->viewType == 'json') $mode = '';

        $this->executions = $this->execution->getPairs(0, 'all', "nocode,noprefix,{$mode}");
        $skipCreateStep   = array('computeburn', 'ajaxgetdropmenu', 'executionkanban', 'ajaxgetteammembers', 'all', 'ajaxgetcopyprojectexecutions');
        if(in_array($this->methodName, $skipCreateStep)) return false;
        if($this->executions || $this->methodName == 'index' || $this->methodName == 'create' || $this->app->getViewType() == 'mhtml') return false;
        if($this->app->tab == 'project' && in_array($this->app->rawMethod, array('linkstory', 'importplanstories', 'unlinkstory', 'export'))) return false;

        $this->locate($this->createLink('execution', 'create'));
    }

    /**
     * 执行任务列表页。
     * Browse task list for a execution.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function browse($executionID = 0)
    {
        $this->locate($this->createLink($this->moduleName, 'task', "executionID=$executionID"));
    }

    /**
     * 设置页面公共数据。
     * Common actions.
     *
     * @param  int          $executionID
     * @param  string       $extra
     * @access public
     * @return object|false
     */
    public function commonAction($executionID = 0, $extra = '')
    {
        /* Get executions and products info. */
        $executionID     = $this->execution->checkAccess($executionID, $this->executions);
        $execution       = $this->execution->getById($executionID);
        $products        = $this->loadModel('product')->getProducts($executionID);
        $childExecutions = $this->execution->getChildExecutions($executionID);
        $teamMembers     = $this->execution->getTeamMembers($executionID);
        $actions         = $this->loadModel('action')->getList($this->objectType, $executionID);
        $project         = $this->loadModel('project')->getByID($execution->project);

        /* Set menu. */
        $this->execution->setMenu($executionID, 0, $extra);

        /* Assign view data. */
        $this->view->projectID       = $execution->project;
        $this->view->hidden          = !empty($project->hasProduct) ? '' : 'hide';
        $this->view->executions      = $this->executions;
        $this->view->execution       = $execution;
        $this->view->executionID     = $executionID;
        $this->view->childExecutions = $childExecutions;
        $this->view->products        = $products;
        $this->view->teamMembers     = $teamMembers;
        $this->view->actions         = $actions;

        return $execution;
    }

    /**
     * 执行下任务列表页。
     * Tasks of a execution.
     *
     * @param  int    $executionID
     * @param  string $status
     * @param  string $param
     * @param  string $orderBy
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function task($executionID = 0, $status = 'unclosed', $param = 0, $orderBy = '', $recTotal = 0, $recPerPage = 100, $pageID = 1)
    {
        if(!isset($_SESSION['limitedExecutions'])) $this->execution->getLimitedExecution();

        /* Save to session. */
        $this->session->set('taskList', $this->app->getURI(true), 'execution');

        /* Set browse type. */
        $browseType  = strtolower($status);
        $execution   = $this->commonAction($executionID, $status);
        $executionID = $execution->id;
        if($execution->type == 'kanban' && $this->config->vision != 'lite' && $this->app->getViewType() != 'json') $this->locate($this->createLink('execution', 'kanban', "executionID=$executionID"));

        /* Save the recently five executions visited in the cookie. */
        $this->executionZen->setRecentExecutions($executionID);

        /* Append id for second sort and process the order by field. */
        if(!$orderBy) $orderBy = $this->cookie->executionTaskOrder ? $this->cookie->executionTaskOrder : 'status,id_desc';
        $sort = common::appendOrder($orderBy);
        $this->executionZen->setTaskPageStorage($executionID, $sort, $browseType, (int)$param);

        /* Set queryID, moduleID and productID. */
        $queryID = $moduleID = $productID = 0;
        if($browseType == 'bysearch')  $queryID   = (int)$param;
        if($browseType == 'bymodule')  $moduleID  = (int)$param;
        if($browseType == 'byproduct') $productID = (int)$param;
        if(!in_array($browseType, array('bysearch', 'bymodule', 'byproduct')))
        {
            $moduleID  = $this->cookie->moduleBrowseParam  ? $this->cookie->moduleBrowseParam  : 0;
            $productID = $this->cookie->productBrowseParam ? $this->cookie->productBrowseParam : 0;
        }

        /* Load pager and get tasks. */
        $this->app->loadClass('pager', true);
        if($this->app->getViewType() == 'mhtml' || $this->app->getViewType() == 'xhtml') $recPerPage = 10;
        $pager = new pager($recTotal, $recPerPage, $pageID);
        $tasks = $this->execution->getTasks((int)$productID, $executionID, $this->executions, $browseType, $queryID, (int)$moduleID, $sort, $pager);

        /* team member pairs. */
        $memberPairs = array();
        foreach($this->view->teamMembers as $key => $member) $memberPairs[$key] = $member->realname;
        $memberPairs = $this->loadModel('user')->setCurrentUserFirst($memberPairs);

        /* Append branches to task. */
        $this->loadModel('task');
        $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($this->view->products));
        foreach($tasks as $task)
        {
            if($task->mode == 'multi' && strpos('done,closed', $task->status) === false)
            {
                $task->assignedTo = '';

                $taskTeam = $this->task->getTeamByTask($task->id);
                foreach($taskTeam as $teamMember)
                {
                    if($this->app->user->account == $teamMember->account && $teamMember->status != 'done')
                    {
                        $task->assignedTo = $this->app->user->account;
                        break;
                    }
                }
            }
            if(isset($branchGroups[$task->product][$task->branch])) $task->branch = $branchGroups[$task->product][$task->branch];
            if($task->needConfirm) $task->status = 'changed';
        }

        $showAllModule = empty($this->config->execution->task->allModule) ? '' : 'allModule';
        $showModule    = !empty($this->config->execution->task->showModule) ? $this->config->execution->task->showModule : '';

        /* Build the search form. */
        $modules   = $this->loadModel('tree')->getTaskOptionMenu($executionID, 0, $showModule);
        $actionURL = $this->createLink('execution', 'task', "executionID=$executionID&status=bySearch&param=myQueryID");
        $this->config->execution->search['onMenuBar'] = 'yes';
        if(!$execution->multiple) unset($this->config->execution->search['fields']['execution']);
        $this->execution->buildTaskSearchForm($executionID, $this->executions, $queryID, $actionURL, $modules);

        $this->view->title       = $execution->name . $this->lang->hyphen . $this->lang->execution->task;
        $this->view->tasks       = $tasks;
        $this->view->pager       = $pager;
        $this->view->orderBy     = $orderBy;
        $this->view->browseType  = $browseType;
        $this->view->status      = $status;
        $this->view->param       = $param;
        $this->view->moduleID    = $moduleID;
        $this->view->modules     = $modules;
        $this->view->modulePairs = $showModule ? $this->tree->getModulePairs($executionID, 'task', $showModule) : array();
        $this->view->moduleTree  = $this->tree->getTaskTreeMenu($executionID, (int)$productID, 0, array('treeModel', 'createTaskLink'), $showAllModule);
        $this->view->memberPairs = $memberPairs;
        $this->view->execution   = $execution;
        $this->view->productID   = $productID;
        $this->view->users       = $this->loadModel('user')->getPairs('noletter');
        $this->view->features    = $this->execution->getExecutionFeatures($execution);

        $this->display();
    }

    /**
     * 任务分组视图。
     * Browse tasks in group.
     *
     * @param  int    $executionID
     * @param  string $groupBy     story|status|pri|assignedTo|finishedBy|closedBy|type
     * @param  string $filter
     * @access public
     * @return void
     */
    public function grouptask($executionID = 0, $groupBy = 'story', $filter = '')
    {
        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;

        /* Execution of the ops type don't show group by story. */
        if(empty($groupBy))$groupBy = 'story';
        if($groupBy == 'story' && $execution->lifetime == 'ops')
        {
            $groupBy = 'status';
            unset($this->lang->execution->groups['story']);
        }

        $sort        = common::appendOrder($groupBy);
        $tasks       = $this->loadModel('task')->getExecutionTasks($executionID, 0, 'all', array(), $sort);
        $groupBy     = str_replace('`', '', $groupBy);
        $groupTasks  = array();
        $allCount    = 0;
        foreach($tasks as $task)
        {
            if($task->mode == 'multi') $task->assignedToRealName = $this->lang->task->team;

            $groupTasks[] = $task;
            $allCount ++;
            if(isset($task->children))
            {
                foreach($task->children as $child)
                {
                    $groupTasks[] = $child;
                    $allCount ++;
                }
                $task->children = true;
                unset($task->children);
            }
        }

        /* Get users and build task group data. */
        $users = $this->loadModel('user')->getPairs('noletter');
        $tasks = $groupTasks;
        list($groupTasks, $groupByList) = $this->executionZen->buildGroupTasks($groupBy, $groupTasks, array('' => '') + $users);

        /* Remove task by filter and group. */
        $filter = (empty($filter) && isset($this->lang->execution->groupFilter[$groupBy])) ? key($this->lang->execution->groupFilter[$groupBy]) : $filter;
        list($groupTasks, $allCount) = $this->executionZen->filterGroupTasks($groupTasks, $groupBy, $filter, $allCount, $tasks);

        /* Assign. */
        $this->view->title       = $execution->name . $this->lang->hyphen . $this->lang->execution->task;
        $this->view->members     = $this->execution->getTeamMembers($executionID);
        $this->view->tasks       = $groupTasks;
        $this->view->groupByList = $groupByList;
        $this->view->browseType  = 'group';
        $this->view->groupBy     = $groupBy;
        $this->view->orderBy     = $groupBy;
        $this->view->executionID = $executionID;
        $this->view->users       = $users;
        $this->view->features    = $this->execution->getExecutionFeatures($execution);
        $this->view->filter      = $filter;
        $this->view->allCount    = $allCount;
        $this->view->execution   = $execution;
        $this->display();
    }

    /**
     * Import tasks undoned from other executions.
     *
     * @param  int    $executionID
     * @param  int    $fromExecution
     * @param  string $orderBy
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function importTask($toExecution, $fromExecution = 0, $orderBy = 'id_desc', $recPerPage = 20, $pageID = 1)
    {
        if(!empty($_POST))
        {
            $taskIdList = form::data($this->config->execution->form->importTask)->get('taskIdList');
            $dateExceed = $this->execution->importTask($toExecution, $taskIdList);

            $result = array('load' => true);
            if($dateExceed) $result['message'] = sprintf($this->lang->task->error->dateExceed, implode(',', $dateExceed));
            return $this->sendSuccess($result);
        }

        $execution   = $this->commonAction($toExecution);
        $toExecution = $execution->id;
        $project     = $this->loadModel('project')->getByID($execution->project);
        $branches    = $this->execution->getBranches($toExecution);
        $tasks       = $this->execution->getTasks2Imported($toExecution, $branches, $orderBy);
        $executions  = $this->execution->getToImport(array_keys($tasks), $execution->type, $project->model);
        unset($executions[$toExecution]);

        $tasks2Imported = array();
        if($fromExecution == 0)
        {
            foreach($executions as $executionID => $executionName) $tasks2Imported = array_merge($tasks2Imported, $tasks[$executionID]);
        }
        else
        {
            $tasks2Imported = zget($tasks, $fromExecution, array());
        }

        /* Pager. */
        $this->app->loadClass('pager', true);
        $pager = new pager(count($tasks2Imported), $recPerPage, $pageID);

        $tasks2ImportedList = array_chunk($tasks2Imported, $pager->recPerPage, true);
        $tasks2ImportedList = empty($tasks2ImportedList) ? $tasks2ImportedList : $tasks2ImportedList[$pageID - 1];
        $tasks2ImportedList = $this->loadModel('task')->processTasks($tasks2ImportedList);

        $this->view->title          = $execution->name . $this->lang->hyphen . $this->lang->execution->importTask;
        $this->view->pager          = $pager;
        $this->view->orderBy        = $orderBy;
        $this->view->executions     = $executions;
        $this->view->fromExecution  = $fromExecution;
        $this->view->tasks2Imported = $tasks2ImportedList;
        $this->view->memberPairs    = $this->loadModel('user')->getPairs('noletter|pofirst');
        $this->display();
    }

    /**
     * 导入Bug。
     * Import from Bug.
     *
     * @param  int    $executionID
     * @param  string $browseType
     * @param  int    $param
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function importBug($executionID = 0, $browseType = 'all', $param = 0, $recTotal = 0, $recPerPage = 30, $pageID = 1)
    {
        $this->session->set('bugList', $this->app->getURI(true), 'execution');
        $execution = $this->execution->getByID($executionID);
        if(!empty($_POST))
        {
            $postData = form::batchData($this->config->execution->form->importBug)->get();
            $tasks    = $this->executionZen->buildTasksForImportBug($execution, $postData);
            if(dao::isError()) return $this->sendError(dao::getError());

            $this->execution->importBug($tasks);
            if(dao::isError()) return $this->sendError(dao::getError());

            return $this->sendSuccess(array('load' => true, 'closeModal' => true));
        }

        $this->execution->setMenu($executionID);

        /* Get users, products, executions, project and projects.*/
        $users      = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted');
        $products   = $this->loadModel('product')->getProductPairsByProject($executionID);
        $executions = !empty($products) ? $this->execution->getPairsByProduct(array_keys($products)) : array($executionID => $execution->name);
        $project    = $this->loadModel('project')->getByID($execution->project);
        !empty($products) ? $projects = $this->product->getProjectPairsByProductIDList(array_keys($products)) : $projects[$project->id] = $project->name;

        /* Set browseType, productID, moduleID and queryID. */
        $browseType = strtolower($browseType);
        $queryID    = ($browseType == 'bysearch') ? (int)$param : 0;

        /* Load pager. */
        $this->app->loadClass('pager', true);
        $pager = new pager($recTotal, $recPerPage, $pageID);

        /* Get bugs.*/
        $bugs = $this->executionZen->getImportBugs($executionID, array_keys($products), $browseType, $queryID, $pager);

        /* Build the search form. */
        $this->executionZen->buildImportBugSearchForm($execution, $queryID, $products, $executions, $projects);

        if($this->app->tab == 'project') $this->view->projectID = $execution->project;

        /* Assign. */
        $this->view->title       = $executions[$executionID] . $this->lang->hyphen . $this->lang->execution->importBug;
        $this->view->pager       = $pager;
        $this->view->bugs        = $bugs;
        $this->view->browseType  = $browseType;
        $this->view->param       = $param;
        $this->view->users       = $users;
        $this->view->execution   = $execution;
        $this->view->executionID = $executionID;
        $this->display();
    }

    /**
     * 需求列表。
     * Browse stories of a execution.
     *
     * @param  int    $executionID
     * @param  string $storyType   story|requirement
     * @param  string $orderBy
     * @param  string $type        all|byModule|byProduct|byBranch|bySearch
     * @param  int    $param
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function story($executionID = 0, $storyType = 'story', $orderBy = 'order_desc', $type = 'all', $param = 0, $recTotal = 0, $recPerPage = 50, $pageID = 1)
    {
        /* Load these models. */
        $this->loadModel('story');
        $this->loadModel('requirement');
        $this->loadModel('epic');
        $this->loadModel('product');

        /* Change for requirement story title. */
        $this->lang->story->linkStory = str_replace($this->lang->URCommon, $this->lang->SRCommon, $this->lang->story->linkStory);
        if($storyType == 'requirement')
        {
            $this->story->replaceURLang($storyType);
            $this->config->product->search['fields']['title'] = $this->lang->story->title;
            unset($this->config->product->search['fields']['plan']);
            unset($this->config->product->search['params']['plan']);
            unset($this->config->product->search['fields']['stage']);
            unset($this->config->product->search['params']['stage']);
        }

        /* Process the order by field. */
        if(!$orderBy) $orderBy = $this->cookie->executionStoryOrder ? $this->cookie->executionStoryOrder : 'pri';

        $param     = (int)$param;
        $type      = strtolower($type);
        $productID = $this->executionZen->setStorageForStory((string)$executionID, $type, (string)$param, $orderBy);

        /* Append id for second sort. */
        $sort = common::appendOrder($orderBy);
        if(strpos($sort, 'pri_') !== false) $sort = str_replace('pri_', 'priOrder_', $sort);

        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;
        $project     = $this->loadModel('project')->getByID($execution->project);

        /* Build the search form. */
        $products  = $this->product->getProducts($executionID);
        $actionURL = $this->createLink('execution', 'story', "executionID=$executionID&storyType=$storyType&orderBy=$orderBy&type=bySearch&queryID=myQueryID");
        $this->executionZen->buildStorySearchForm($execution, $productID, $products, $type == 'bysearch' ? $param : 0, $actionURL);

        /* Load pager. */
        $this->app->loadClass('pager', true);
        if($this->app->getViewType() == 'xhtml') $recPerPage = 10;
        $pager = new pager($recTotal, $recPerPage, $pageID);

        $stories = $this->story->getExecutionStories($executionID, 0, $sort, $type, (string)$param, $project->storyType ?? 'story', '', $pager);
        $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false);

        if(!empty($stories)) $stories = $this->story->mergeReviewer($stories);
        if($this->config->edition == 'ipd') $stories = $this->loadModel('story')->getAffectObject($stories, 'story');

        $this->executionZen->assignCountForStory($executionID, $stories, $storyType);
        $this->executionZen->assignRelationForStory($execution, $products, $productID, $type, $storyType, $param, $orderBy, $pager);

        $this->view->productID          = $productID;
        $this->view->project            = $project;
        $this->view->linkedProductCount = count($products);
        $this->display();
    }

    /**
     * 查看需求详情。
     * View a story.
     *
     * @param  int    $storyID
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function storyView($storyID, $executionID = 0)
    {
        $this->session->set('productList', $this->app->getURI(true), 'product');

        $story = $this->loadModel('story')->getByID($storyID);
        echo $this->fetch('story', 'view', "storyID=$storyID&version=$story->version&param=" . ($executionID ? $executionID : $this->session->execution) . "&storyType={$story->type}");
    }

    /**
     * Bug列表。
     * Browse bugs of a execution.
     *
     * @param  int    $executionID
     * @param  int    $productID
     * @param  int    $branchID
     * @param  string $orderBy
     * @param  string $build
     * @param  string $type
     * @param  int    $param
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function bug($executionID = 0, $productID = 0, $branch = 'all', $orderBy = 'status,id_desc', $build = '', $type = 'all', $param = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        /* Save session and load model. */
        $this->loadModel('bug');
        $this->session->set('bugList', $this->app->getURI(true), 'execution');
        $this->session->set('buildList', $this->app->getURI(true), 'execution');

        $type        = strtolower($type);
        $execution   = $this->commonAction($executionID);
        $project     = $this->loadModel('project')->getByID($execution->project);
        $executionID = $execution->id;
        $products    = $this->product->getProducts($execution->id);
        $param       = (int)$param;

        if(count($products) === 1) $productID = current($products)->id;

        /* Set the product drop-down and search fields. */
        $productOption = array();
        $branchOption  = array();
        $showBranch    = $this->loadModel('branch')->showBranch($productID);
        if($execution->hasProduct)
        {
            list($productOption, $branchOption) = $this->executionZen->buildProductSwitcher($executionID, $productID, $products);
            unset($this->config->bug->search['fields']['product']);
            unset($this->config->bug->search['params']['product']);
            if($project->model != 'scrum')
            {
                unset($this->config->bug->search['fields']['plan']);
                unset($this->config->bug->search['params']['plan']);
            }
        }

        /* Load pager and get bugs, user. */
        $this->app->loadClass('pager', true);
        if($this->app->getViewType() == 'xhtml') $recPerPage = 10;
        $pager = new pager($recTotal, $recPerPage, $pageID);
        $sort  = common::appendOrder($orderBy);
        $bugs  = $this->bug->getExecutionBugs($executionID, $productID, $branch, $build, $type, $param, $sort, '', $pager);
        $bugs  = $this->bug->batchAppendDelayedDays($bugs);

        /* Build the search form. */
        $actionURL = $this->createLink('execution', 'bug', "executionID=$executionID&productID=$productID&branch=$branch&orderBy=$orderBy&build=$build&type=bysearch&queryID=myQueryID");
        $this->execution->buildBugSearchForm($products, $param, $actionURL);

        $bugs = $this->bug->processBuildForBugs($bugs);
        $this->executionZen->assignBugVars($execution, $project, $productID, $branch, $products, $orderBy, $type, $param, $build, $bugs, $pager);

        $storyIdList = $taskIdList = array();
        foreach($bugs as $bug)
        {
            if($bug->story)  $storyIdList[$bug->story] = $bug->story;
            if($bug->task)   $taskIdList[$bug->task]   = $bug->task;
            if($bug->toTask) $taskIdList[$bug->toTask] = $bug->toTask;
        }

        $this->view->showBranch     = $showBranch;
        $this->view->productOption  = $productOption;
        $this->view->branchOption   = $branchOption;
        $this->view->plans          = $this->loadModel('productplan')->getForProducts(array_keys($products));
        $this->view->tasks          = $this->loadModel('task')->getPairsByIdList($taskIdList);
        $this->view->stories        = $this->loadModel('story')->getPairsByList($storyIdList);
        $this->view->switcherParams = "executionID={$executionID}&productID={$productID}&currentMethod=bug";
        $this->view->switcherText   = isset($products[$productID]) ? $products[$productID]->name : $this->lang->product->all;
        if(empty($project->hasProduct)) $this->config->excludeSwitcherList[] = 'execution-bug';
        $this->display();
    }

    /**
     * 用例列表。
     * Execution case list.
     *
     * @param  int    $executionID
     * @param  int    $productID
     * @param  string $branchID
     * @param  string $type
     * @param  int    $moduleID
     * @param  string $orderBy
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function testcase($executionID = 0, $productID = 0, $branchID = 'all', $type = 'all', $param = 0, $moduleID = 0, $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        $this->commonAction($executionID);
        $uri = $this->app->getURI(true);
        $this->session->set('caseList', $uri, 'execution');
        $this->session->set('bugList',  $uri, 'execution');

        $products = $this->product->getProducts($executionID);
        if(count($products) == 1) $productID = key($products);

        $execution     = $this->execution->getByID($executionID);
        $productOption = array();
        $branchOption  = array();
        if($execution->hasProduct)
        {
            list($productOption, $branchOption) = $this->executionZen->buildProductSwitcher($executionID, $productID, $products);
        }

        /* Load pager. */
        $this->app->loadClass('pager', true);
        $pager = pager::init($recTotal, $recPerPage, $pageID);

        unset($this->config->testcase->dtable->fieldList['title']['nestedToggle']);
        if($productID && $products[$productID]->type == 'normal') unset($this->config->testcase->dtable->fieldList['branch']);

        /* Build the search form. */
        $actionURL = $this->createLink('execution', 'testcase', "executionID=$executionID&productID=$productID&branchID=$branchID&type=bysearch&queryID=myQueryID&moduleID=0&orderBy=$orderBy");
        $this->execution->buildCaseSearchForm($products, $param, $actionURL, $executionID);

        $this->executionZen->assignTestcaseVars($executionID, $productID, $branchID, $moduleID, $param, $orderBy, $type, $pager);

        $this->view->execution        = $execution;
        $this->view->productOption    = $productOption;
        $this->view->branchOption     = $branchOption;
        $this->view->switcherParams   = "executionID={$executionID}&productID={$productID}&currentMethod=testcase";
        $this->view->switcherText     = isset($products[$productID]) ? $products[$productID]->name : $this->lang->product->all;
        $this->view->switcherObjectID = $productID;
        if(empty($execution->hasProduct)) $this->config->excludeSwitcherList[] = 'execution-testcase';
        $this->display();
    }

    /**
     * 测试报告列表。
     * List of test reports for the execution.
     *
     * @param  int    $executionID
     * @param  string $objectType   project|execution|product
     * @param  string $extra
     * @param  string $orderBy
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function testreport($executionID = 0, $objectType = 'execution', $extra = '', $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        echo $this->fetch('testreport', 'browse', "objectID=$executionID&objectType=$objectType&extra=$extra&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID");
    }

    /**
     * 版本列表。
     * Browse builds of a execution.
     *
     * @param  int    $executionID
     * @param  string $type        all|product|bysearch
     * @param  int    $param
     * @param  string $orderBy
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function build($executionID = 0, $type = 'all', $param = 0, $orderBy = 't1.date_desc,t1.id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        $execution   = $this->commonAction($executionID);
        $executionID = (int)$execution->id;

        /* Get products' list. */
        $products = $this->product->getProducts($executionID, 'all', '', false);

        /* Build the search form. */
        $type = strtolower($type);
        $this->project->buildProjectBuildSearchForm($products, $type == 'bysearch' ? (int)$param : 0, $executionID, $param, 'execution');

        /* Load pager. */
        $this->app->loadClass('pager', true);
        $pager = pager::init($recTotal, $recPerPage, $pageID);

        /* Get builds. */
        $this->loadModel('build');
        if($type == 'bysearch')
        {
            $builds = $this->loadModel('build')->getExecutionBuildsBySearch($executionID, 0, $pager);
        }
        else
        {
            $builds = $this->loadModel('build')->getExecutionBuilds($executionID, $type, (string)$param, $orderBy, $pager);
        }

        /* Set view data. */
        $this->view->title    = $execution->name . $this->lang->hyphen . $this->lang->execution->build;
        $this->view->users    = $this->loadModel('user')->getPairs('noletter');
        $this->view->builds   = $this->executionZen->processBuildListData($builds, $executionID);
        $this->view->product  = $type == 'product' ? $param : '';
        $this->view->products = $products;
        $this->view->type     = $type;
        $this->view->param    = $param;
        $this->view->orderBy  = $orderBy;
        $this->view->pager    = $pager;
        $this->display();
    }

    /**
     * 测试单列表页面。
     * Browse test tasks of execution.
     *
     * @param  int    $executionID
     * @param  string $orderBy
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function testtask($executionID = 0, $orderBy = 'product_asc,id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        $this->loadModel('testtask');
        $this->app->loadLang('testreport');

        /* Save session. */
        $this->session->set('testtaskList', $this->app->getURI(true), 'execution');
        $this->session->set('buildList', $this->app->getURI(true), 'execution');

        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;

        /* Load pager. */
        $this->app->loadClass('pager', true);
        $pager = pager::init($recTotal, $recPerPage, $pageID);
        $tasks = $this->testtask->getExecutionTasks($executionID, 'execution', $orderBy, $pager);

        $this->executionZen->assignTesttaskVars($tasks);

        $this->view->title         = $this->executions[$executionID] . $this->lang->hyphen . $this->lang->testtask->common;
        $this->view->execution     = $execution;
        $this->view->project       = $this->loadModel('project')->getByID($execution->project);
        $this->view->executionID   = $executionID;
        $this->view->executionName = $this->executions[$executionID];
        $this->view->pager         = $pager;
        $this->view->orderBy       = $orderBy;
        $this->view->users         = $this->loadModel('user')->getPairs('noclosed|noletter');
        $this->view->products      = $this->loadModel('product')->getPairs('', 0);
        $this->view->canBeChanged  = common::canModify('execution', $execution); // Determines whether an object is editable.

        $this->display();
    }

    /**
     * 燃尽图。
     * Browse burndown chart of a execution.
     *
     * @param  int    $executionID
     * @param  string $type        noweekend|withweekend
     * @param  string $interval
     * @param  string $burnBy      left|estimate|storyPoint
     * @access public
     * @return void
     */
    public function burn($executionID = 0, $type = 'noweekend', $interval = '', $burnBy = 'left')
    {
        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;

        /* Determine whether to display delay data. */
        if(((strpos('closed,suspended', $execution->status) === false && helper::today() > $execution->end)
            || ($execution->status == 'closed'    && substr($execution->closedDate, 0, 10) > $execution->end)
            || ($execution->status == 'suspended' && $execution->suspendedDate > $execution->end))
            && strpos($type, 'delay') === false)
        $type .= ',withdelay';

        /* Get burn date list and interval. */
        $deadline = $execution->status == 'closed' ? substr($execution->closedDate, 0, 10) : $execution->suspendedDate;
        $deadline = strpos('closed,suspended', $execution->status) === false ? helper::today() : $deadline;
        $endDate  = strpos($type, 'withdelay') !== false ? $deadline : $execution->end;
        list($dateList, $interval) = $this->execution->getDateList($execution->begin, $endDate, $type, $interval, 'Y-m-d', $execution->end);

        $executionEnd = strpos($type, 'withdelay') !== false ? $execution->end : '';
        $burnBy       = $this->cookie->burnBy ? $this->cookie->burnBy : $burnBy;
        $chartData    = $this->execution->buildBurnData($executionID, $dateList, $burnBy, $executionEnd);

        $allDateList = date::getDateList($execution->begin, $endDate, 'Y-m-d', $type, $this->config->execution->weekend);
        $dayList     = array_fill(1, (int)floor(count($allDateList) / $this->config->execution->maxBurnDay) + 5, '');
        foreach($dayList as $key => $val) $dayList[$key] = $this->lang->execution->interval . ($key + 1) . $this->lang->day;

        unset($this->lang->TRActions);

        /* Shows the variables needed to burn page. */
        $this->view->title         = $execution->name . $this->lang->hyphen . $this->lang->execution->burn;
        $this->view->tabID         = 'burn';
        $this->view->burnBy        = $burnBy;
        $this->view->executionID   = $executionID;
        $this->view->executionName = $execution->name;
        $this->view->type          = $type;
        $this->view->interval      = $interval;
        $this->view->chartData     = $chartData;
        $this->view->dayList       = array('full' => $this->lang->execution->interval . '1' . $this->lang->day) + $dayList;

        $this->display();
    }

    /**
     * 更新燃尽图数据。
     * Compute burndown data.
     *
     * @param  string $reload
     * @access public
     * @return void
     */
    public function computeBurn($reload = 'no')
    {
        $burns = $this->execution->computeBurn();

        if($reload == 'yes') return $this->send(array('load' => true, 'result' => 'success'));
        return print(json_encode($burns));

    }

    /**
     * 累计流图。
     * Cumulative flow diagram.
     *
     * @param  int    $executionID
     * @param  string $type        story|bug|task
     * @param  string $withWeekend
     * @param  string $begin
     * @param  string $end
     * @access public
     * @return void
     */
    public function cfd($executionID = 0, $type = 'story', $withWeekend = 'false', $begin = '', $end = '')
    {
        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;

        $this->app->loadClass('date');
        $minDate = !helper::isZeroDate($execution->openedDate) ? date('Y-m-d', strtotime($execution->openedDate)) : date('Y-m-d', strtotime($execution->begin));
        $maxDate = !helper::isZeroDate($execution->closedDate) ? date('Y-m-d', strtotime($execution->closedDate)) : helper::today();
        if($execution->lifetime == 'ops' or in_array($execution->attribute, array('request', 'review'))) $type = 'task';

        if(!empty($_POST))
        {
            $begin = htmlspecialchars($this->post->begin, ENT_QUOTES);
            $end   = htmlspecialchars($this->post->end, ENT_QUOTES);

            $this->executionZen->checkCFDDate($begin, $end, $minDate, $maxDate);
            if(dao::isError()) return $this->sendError(dao::getError());

            $this->execution->computeCFD($executionID);
            $this->execution->updateCFDData($executionID, $begin);
            return $this->send(array('result' => 'success', 'locate' => $this->createLink('execution', 'cfd', "executionID=$executionID&type=$type&withWeekend=$withWeekend&begin=" . helper::safe64Encode(urlencode($begin)) . "&end=" . helper::safe64Encode(urlencode($end)))));
        }

        if($begin && $end)
        {
            $begin = urldecode(helper::safe64Decode($begin));
            $end   = urldecode(helper::safe64Decode($end));
        }
        else
        {
            list($begin, $end) = $this->execution->getBeginEnd4CFD($execution);
        }
        $dateList  = date::getDateList($begin, $end, 'Y-m-d', $withWeekend == 'false'? 'noweekend' : '');
        $chartData = $this->execution->buildCFDData($executionID, $dateList, $type);
        if(isset($chartData['line'])) $chartData['line'] = array_reverse($chartData['line']);

        $this->view->title         = $this->lang->execution->CFD;
        $this->view->type          = $type;
        $this->view->execution     = $execution;
        $this->view->withWeekend   = $withWeekend;
        $this->view->executionName = $execution->name;
        $this->view->executionID   = $executionID;
        $this->view->chartData     = $chartData;
        $this->view->features      = $this->execution->getExecutionFeatures($execution);
        $this->view->begin         = $begin;
        $this->view->end           = $end;
        $this->view->minDate       = $minDate;
        $this->view->maxDate       = $maxDate;
        $this->display();
    }

    /**
     * 计算累计流图数据。
     * Compute cfd data.
     *
     * @param  string $reload
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function computeCFD($reload = 'no', $executionID = 0)
    {
        $this->execution->computeCFD($executionID);
        if($reload == 'yes') return $this->sendSuccess(array('load' => true));
    }

    /**
     * 修改燃尽图的首天工时
     * Fix burn for first date.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function fixFirst($executionID)
    {
        $execution = $this->execution->getByID($executionID);

        if($_POST)
        {
            $burn     = $this->execution->getBurnByExecution($executionID, $execution->begin, 0);
            $withLeft = $this->post->withLeft ? $this->post->withLeft : 0;
            $burnData = form::data($this->config->execution->form->fixfirst)
                ->add('execution', $executionID)
                ->add('date', $execution->begin)
                ->add('left', $withLeft ? $this->post->estimate : $burn->left)
                ->add('consumed', empty($burn) ? 0 : $burn->consumed)
                ->get();

            if(!$burnData->left) $burnData->left = 0;

            if(is_numeric($burnData->estimate)) $this->execution->fixFirst($burnData);
            return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true));
        }

        $this->view->firstBurn = $this->execution->getBurnByExecution($executionID, $execution->begin);
        $this->view->execution = $execution;
        $this->display();
    }

    /**
     * 执行团队列表。
     * Browse team of a execution.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function team($executionID = 0)
    {
        $execution   = $this->commonAction($executionID);
        $deptID      = $this->app->user->admin ? 0 : $this->app->user->dept;
        $teamMembers = array();
        foreach($this->view->teamMembers as $member)
        {
            $member->days    = $member->days . $this->lang->execution->day;
            $member->hours   = $member->hours . $this->lang->execution->workHour;
            $member->total   = $member->totalHours . $this->lang->execution->workHour;
            $member->actions = array();
            if(common::hasPriv('execution', 'unlinkMember', $member) && common::canModify('execution', $execution)) $member->actions = array('unlink');

            $teamMembers[] = $member;
        }

        $this->view->title        = $execution->name . $this->lang->hyphen . $this->lang->execution->team;
        $this->view->deptUsers    = $this->loadModel('dept')->getDeptUserPairs($deptID, 'id');
        $this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable.
        $this->view->recTotal     = count($this->view->teamMembers);
        $this->view->teamMembers  = $teamMembers;

        $this->display();
    }

    /**
     * Create a execution.
     *
     * @param int    $projectID
     * @param int    $executionID
     * @param int    $copyExecutionID
     * @param int    $planID
     * @param string $confirm
     * @param int    $productID
     * @param string $extra
     *
     * @access public
     * @return void
     */
    public function create($projectID = 0, $executionID = 0, $copyExecutionID = 0, $planID = 0, $confirm = 'no', $productID = 0, $extra = '')
    {
        if($this->app->tab == 'doc')     unset($this->lang->doc->menu->execution['subMenu']);
        if($this->app->tab == 'project') $this->project->setMenu($projectID);

        $extra = str_replace(array(',', ' '), array('&', ''), $extra);
        parse_str($extra, $output);

        $execution = $this->executionZen->initFieldsForCreate($projectID, $output);
        if($copyExecutionID) $this->executionZen->setFieldsByCopyExecution($execution, $copyExecutionID);
        $projectID = isset($execution->project) ? $execution->project : $projectID;

        if($executionID) return $this->executionZen->displayAfterCreated($projectID, $executionID, $planID, $confirm);

        $allProjects = $this->project->getPairsByModel('all', 'noclosed,multiple');
        $this->loadModel('project')->checkAccess($projectID, $allProjects);
        if(empty($projectID)) $projectID = key($allProjects) ? key($allProjects) : 0;

        $project = empty($projectID) ? null : $this->loadModel('project')->fetchByID($projectID);
        $project ? $this->executionZen->correctExecutionCommonLang($project, $execution->type) : $project = null;
        $products = $this->executionZen->getLinkedProducts($copyExecutionID, $planID, $project);
        $this->executionZen->setLinkedBranches($products, $copyExecutionID, $planID, $project);

        if(!empty($_POST))
        {
            /* Filter empty plans. */
            if(isset($_POST['attribute']) and $_POST['attribute'] == 'review') unset($_POST['plans']);
            if(!empty($_POST['plans']))
            {
                foreach($_POST['plans'] as $key => $planItem) $_POST['plans'][$key] = array_filter($_POST['plans'][$key]);
                $_POST['plans'] = array_filter($_POST['plans']);
            }

            $execution = $this->executionZen->buildExecutionForCreate();
            if(!$execution) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $executionID = $this->execution->create($execution, isset($_POST['teamMembers']) ? $_POST['teamMembers'] : array());
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->loadModel('action')->create($this->objectType, $executionID, 'opened', '', $project->hasProduct ? implode(',', $_POST['products']) : '');
            if(!empty($projectID) and strpos(',kanban,agileplus,waterfallplus,ipd,', ",$project->model,") !== false and $execution->type == 'kanban')
            {
                $execution = $this->execution->fetchByID($executionID);
                $this->loadModel('kanban')->createRDKanban($execution);
            }

            $message = $this->executeHooks($executionID);
            if(empty($message)) $message = $this->lang->saveSuccess;

            if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $message, 'id' => $executionID));

            $location = $this->executionZen->getAfterCreateLocation($projectID, $executionID, $project->model);
            return $this->send(array('result' => 'success', 'message' => $message, 'load' => $location));
        }

        list($this->view->pmUsers, $this->view->poUsers, $this->view->qdUsers, $this->view->rdUsers) = $this->executionZen->setUserMoreLink();
        $this->executionZen->setCopyProjects($project);

        $this->view->title               = $this->app->tab == 'execution' ? $this->lang->execution->createExec : $this->lang->execution->create;
        $this->view->gobackLink          = (isset($output['from']) and $output['from'] == 'global') ? $this->createLink('execution', 'all') : '';
        $this->view->allProducts         = array_filter($this->executionZen->getAllProductsForCreate($project));
        $this->view->allProjects         = $allProjects;
        $this->view->multiBranchProducts = $this->loadModel('product')->getMultiBranchPairs();
        $this->view->products            = $products;
        $this->view->teams               = $this->execution->getCanCopyObjects((int)$projectID);
        $this->view->users               = $this->loadModel('user')->getPairs('nodeleted|noclosed');
        $this->view->copyExecutionID     = $copyExecutionID;
        $this->view->productID           = $productID;
        $this->view->projectID           = $projectID;
        $this->view->from                = $this->app->tab;
        $this->view->isStage             = isset($output['type']) && $output['type'] == 'stage';
        $this->view->isKanban            = isset($output['type']) && $output['type'] == 'kanban';
        $this->view->project             = $project;
        $this->view->planID              = $planID;

        $this->display();
    }

    /**
     * 编辑一个执行。
     * Edit a execution.
     *
     * @param  int    $executionID
     * @param  string $action
     * @param  string $extra
     * @param  string $newPlans
     * @param  string $confirm
     * @access public
     * @return void
     */
    public function edit($executionID, $action = 'edit', $extra = '', $newPlans = '', $confirm = 'no')
    {
        /* Update linked plans if confirmed and new plans added. */
        $this->executionZen->updateLinkedPlans($executionID, $newPlans, $confirm);

        $this->loadModel('product');
        $this->loadModel('programplan');
        $this->loadModel('productplan');
        $this->app->loadLang('program');
        $this->app->loadLang('stage');

        $execution           = $this->execution->getById($executionID);
        $project             = $this->project->getById($execution->project);
        $branches            = $this->project->getBranchesByProject($executionID);
        $linkedProductIdList = empty($branches) ? '' : array_keys($branches);

        /* Set menu. */
        $this->execution->setMenu($executionID);

        if(!empty($_POST))
        {
            $oldProducts  = $this->product->getProducts($executionID, 'all', '', true, $linkedProductIdList);
            $oldPlans     = $this->dao->select('plan')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($executionID)->andWhere('plan')->ne(0)->fetchPairs('plan');
            $oldExecution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch();

            /* Get the data from the post. */
            $formData = form::data(null, $executionID)
                ->add('id', $executionID)
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->setDefault('lastEditedDate', helper::now())
                ->setIF($this->post->heightType == 'auto', 'displayCards', 0)
                ->setIF(helper::isZeroDate($this->post->begin), 'begin', '')
                ->setIF(helper::isZeroDate($this->post->end), 'end', '')
                ->setIF($this->post->status == 'closed' && $oldExecution->status != 'closed', 'closedDate', helper::now())
                ->setIF($this->post->status == 'suspended' && $oldExecution->status != 'suspended', 'suspendedDate', helper::today())
                ->setIF($oldExecution->type == 'stage', 'project', $oldExecution->project)
                ->setDefault('project', $oldExecution->project)
                ->setDefault('team', $this->post->name)
                ->setDefault('branch', $this->post->branch)
                ->setDefault('attribute', $oldExecution->attribute)
                ->get();

            $changes = $this->execution->update($executionID, $formData);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->execution->updateProducts($executionID, $formData);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->loadModel('action');
            if($action == 'undelete')
            {
                $this->dao->update(TABLE_EXECUTION)->set('deleted')->eq('0')->where('id')->eq($executionID)->exec();
                $this->dao->update(TABLE_ACTION)->set('extra')->eq(actionModel::BE_UNDELETED)->where('id')->eq($extra)->exec();
                $this->action->create($this->objectType, $executionID, 'undeleted');
            }

            $oldProducts  = array_keys($oldProducts);
            $newProducts  = $this->product->getProducts($executionID, 'all', '', true, $linkedProductIdList);
            $newProducts  = array_keys($newProducts);
            $diffProducts = array_merge(array_diff($oldProducts, $newProducts), array_diff($newProducts, $oldProducts));
            $products     = $diffProducts ? implode(',', $newProducts) : '';

            if($changes or $diffProducts)
            {
                $actionID = $this->action->create($this->objectType, $executionID, 'edited', '', $products);
                $this->action->logHistory($actionID, $changes);
            }

            if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->programplan->computeProgress($executionID, 'edit');

            /* Redirect to confirm page if the execution can link plan stories. */
            $this->executionZen->checkLinkPlan($executionID, $oldPlans);

            $message = $this->executeHooks($executionID);
            if($message) $this->lang->saveSuccess = $message;

            if($formData->status == 'doing') $this->loadModel('common')->syncPPEStatus($executionID);

            /* If link from no head then reload. */
            if(isInModal())
            {
                $kanbanLoad = array('selector' => '#main>*');
                return $this->sendSuccess(array('closeModal' => true, 'callback' => $execution->type == 'kanban' ? 'loadCurrentPage(' . json_encode($kanbanLoad) . ');' : 'loadCurrentPage()'));
            }

            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => inlink('view', "executionID=$executionID")));
        }

        $executions = $this->executions;
        unset($executions[$executionID]); /* Remove current execution from the executions. */

        /* Get linked objects of this execution. */
        $linkedObjects = $this->executionZen->getLinkedObjects($execution);

        list($pmUsers, $poUsers, $qdUsers, $rdUsers) = $this->executionZen->setUserMoreLink($execution);

        if(!$project->hasProduct) $this->lang->execution->PO = $this->lang->common->story . $this->lang->execution->owner;

        if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd')))
        {
            $parentStage = $this->project->getByID($execution->parent, 'stage');
            $this->view->enableOptionalAttr = (empty($parentStage) or (!empty($parentStage) and $parentStage->attribute == 'mix'));
        }
        if($this->app->tab == 'project' and $project->model == 'waterfallplus')
        {
            $productID       = $this->product->getProductIDByProject($executionID);
            $parentStageList = $this->programplan->getParentStageList($execution->project, $executionID, $productID);
            unset($parentStageList[0]);
        }

        $this->view->title                = $this->lang->execution->edit . $this->lang->hyphen . $execution->name;
        $this->view->executions           = $executions;
        $this->view->execution            = $execution;
        $this->view->project              = $project;
        $this->view->poUsers              = $poUsers;
        $this->view->pmUsers              = $pmUsers;
        $this->view->qdUsers              = $qdUsers;
        $this->view->rdUsers              = $rdUsers;
        $this->view->users                = $this->loadModel('user')->getPairs('nodeleted|noclosed');
        $this->view->groups               = $this->loadModel('group')->getPairs();
        $this->view->branches             = $branches;
        $this->view->allProducts          = $linkedObjects->allProducts;
        $this->view->multiBranchProducts  = $this->product->getMultiBranchPairs();
        $this->view->linkedProducts       = $linkedObjects->linkedProducts;
        $this->view->linkedBranches       = $linkedObjects->linkedBranches;
        $this->view->linkedStoryIDList    = $linkedObjects->linkedStoryIDList;
        $this->view->unmodifiableProducts = $linkedObjects->unmodifiableProducts;
        $this->view->unmodifiableBranches = $linkedObjects->unmodifiableBranches;
        $this->view->productPlans         = $linkedObjects->productPlans;
        $this->view->productPlan          = $linkedObjects->productPlan;
        $this->view->currentPlan          = $linkedObjects->currentPlan;
        $this->view->branchGroups         = $this->execution->getBranchByProduct(array_keys($linkedObjects->linkedProducts), $execution->project, 'noclosed', $linkedObjects->linkedBranchList);
        $this->view->teamMembers          = $this->execution->getTeamMembers($executionID);
        $this->view->allProjects          = $this->project->getPairsByModel($project->model, 'noclosed|multiple', $project->id);
        $this->view->parentStageList      = isset($parentStageList) ? $parentStageList : array();
        $this->view->isStage              = isset($project->model) && in_array($project->model, array('waterfall', 'waterfallplus'));
        $this->view->unclosedTasks        = $this->loadModel('task')->getUnclosedTasksByExecution($executionID);
        $this->display();
    }

    /**
     * 批量编辑多个执行。
     * Batch edit the executions.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function batchEdit($executionID = 0)
    {
        $this->app->loadLang('stage');
        $this->app->loadLang('programplan');

        if($this->app->tab == 'project')
        {
            $projectID = $this->session->project;
            $project   = $this->project->getById($projectID);
            $this->project->setMenu($projectID);
            $this->view->project = $project;
            if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->lang->execution->common = $this->lang->execution->stage;
            if($project->model == 'ipd') $this->config->execution->list->customBatchEditFields = 'days,team,desc,PO,QD,PM,RD';
        }
        else
        {
            $this->config->excludeDropmenuList[] = 'execution-batchedit';
        }

        if($this->post->name)
        {
            $postData   = fixer::input('post')->get();
            $allChanges = $this->execution->batchUpdate($postData);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->executeHooks(key($this->post->id));
            if(!empty($allChanges))
            {
                foreach($allChanges as $executionID => $changes)
                {
                    if(empty($changes)) continue;

                    $actionID = $this->loadModel('action')->create($this->objectType, $executionID, 'Edited');
                    $this->action->logHistory($actionID, $changes);
                }
            }

            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $this->session->executionList));
        }

        $projectModel = $this->app->tab == 'project' ? $project->model : 'all';
        $allProjects  = $this->project->getPairsByModel($projectModel, 'noclosed', isset($projectID) ? $projectID : 0);

        if(!$this->post->executionIDList)
        {
            /* Use a fallback link to locate in case session has no related data. */
            $locateLink = !empty($this->session->executionList) ? $this->session->executionList : $this->createLink('execution', 'all');
            return $this->locate($locateLink);
        }

        $executionIDList = $this->post->executionIDList;
        $executions      = $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->in($executionIDList)->fetchAll('id');
        $relatedProjects = $this->dao->select('id,project')->from(TABLE_PROJECT)->where('id')->in($executionIDList)->fetchPairs(); /* 获取执行所属的项目列表。*/
        $projects        = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->in($relatedProjects)->fetchAll('id');        /* 获取执行所属的项目列表中每个项目的项目信息。*/

        list($pmUsers, $poUsers, $qdUsers, $rdUsers) = $this->executionZen->setUserMoreLink($executions);

        /* Set custom fields. */
        foreach(explode(',', $this->config->execution->list->customBatchEditFields) as $field)
        {
            $fieldTitle = $this->app->tab == 'execution' ? str_replace($this->lang->executionCommon, $this->lang->execution->common, $this->lang->execution->$field) : $this->lang->execution->$field;
            if($field == 'lifetime') $fieldTitle = $this->app->tab == 'execution' ? $this->lang->execution->execType : $this->lang->execution->type;
            if($field == 'team') $fieldTitle = $this->lang->execution->teamName;
            $customFields[$field] = $fieldTitle;
        }

        $parentIdList = array();
        foreach($executions as $execution)
        {
            if($execution->parent == 0) continue;
            $parentIdList[] = $execution->parent;
        }

        $this->view->customFields = $customFields;
        $this->view->showFields   = $this->config->execution->custom->batchEditFields;
        $this->view->title        = $this->lang->execution->batchEdit;
        $this->view->executions   = $executions;
        $this->view->allProjects  = $allProjects;
        $this->view->projects     = $projects;
        $this->view->pmUsers      = $pmUsers;
        $this->view->poUsers      = $poUsers;
        $this->view->qdUsers      = $qdUsers;
        $this->view->rdUsers      = $rdUsers;
        $this->view->from         = $this->app->tab;
        $this->view->parents      = $this->execution->getByIdList($parentIdList);
        $this->display();
    }

    /**
     * 批量设置执行状态。
     * Batch change status.
     *
     * @param  string    $status
     * @param  int       $projectID
     * @access public
     * @return void
     */
    public function batchChangeStatus($status, $projectID = 0)
    {
        if($_SERVER['REQUEST_METHOD'] !== 'POST') return $this->sendError($this->lang->error->unsupportedReq);

        $executionIdList = $this->post->executionIDList;
        if(is_string($executionIdList)) $executionIdList = explode(',', $executionIdList);

        $filteredStages = $this->execution->batchChangeStatus($executionIdList, $status);
        if(!$filteredStages) return $this->sendSuccess(array('load' => true));

        $alertMsg = '';
        if($status == 'wait')
        {
            $project = $this->loadModel('project')->getById($projectID);
            /* In execution-all list or waterfall, waterfallplus project's execution list. */
            if(empty($project) or (!empty($project) and strpos($project->model, 'waterfall') !== false))
            {
                $executionLang = (empty($project) or (!empty($project) and $project->model == 'waterfallplus')) ? $this->lang->execution->common : $this->lang->stage->common;
                $alertMsg      = sprintf($this->lang->execution->hasStartedTaskOrSubStage, $executionLang, $filteredStages);
            }
            if(!empty($project) and strpos('agileplus,scrum', $project->model) !== false)
            {
                $executionLang = $project->model == 'scrum' ? $this->lang->executionCommon : $this->lang->execution->common;
                $alertMsg      = sprintf($this->lang->execution->hasStartedTask, $executionLang, $filteredStages);
            }
        }
        if($status == 'suspended') $alertMsg = sprintf($this->lang->execution->hasSuspendedOrClosedChildren, $filteredStages);
        if($status == 'closed') $alertMsg = sprintf($this->lang->execution->hasNotClosedChildren, $filteredStages);

        return $this->sendSuccess(array('message' => $alertMsg, 'load' => true));
    }

    /**
     * 开始一个执行。
     * Start the execution.
     *
     * @param  int    $executionID
     * @param  string $from
     * @access public
     * @return void
     */
    public function start($executionID, $from = 'execution')
    {
        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;
        if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban;

        $this->extendRequireFields($executionID);
        if(!empty($_POST))
        {
            $postData = fixer::input('post')
                ->add('id', $executionID)
                ->setDefault('status', 'doing')
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->setDefault('lastEditedDate', helper::now())
                ->stripTags($this->config->execution->editor->start['id'], $this->config->allowedTags)
                ->get();

            $this->execution->start($executionID, $postData);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $project = $this->loadModel('project')->getById($execution->project);
            if($project->model == 'waterfall' or $project->model == 'waterfallplus') $this->loadModel('programplan')->computeProgress($executionID, 'start');

            $this->loadModel('common')->syncPPEStatus($executionID);
            $this->executeHooks($executionID);

            $response['closeModal'] = true;
            $response['load']       = true;
            if($from == 'kanban') $response['callback'] = "changeStatus('doing')";
            return $this->sendSuccess($response);
        }

        $this->view->title      = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->start;
        $this->view->users      = $this->loadModel('user')->getPairs('noletter');
        $this->view->actions    = $this->loadModel('action')->getList($this->objectType, $executionID);
        $this->display();
    }

    /**
     * 延期一个迭代。
     * Delay the execution.
     *
     * @param  int    $executionID
     * @param  string $from
     * @access public
     * @return void
     */
    public function putoff($executionID, $from = 'execution')
    {
        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;

        $this->extendRequireFields($executionID);
        if(!empty($_POST))
        {
            $postData = fixer::input('post')
                ->add('id', $executionID)
                ->stripTags($this->config->execution->editor->putoff['id'], $this->config->allowedTags)
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->setDefault('lastEditedDate', helper::now())
                ->get();

            $this->execution->putoff($executionID, $postData);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->executeHooks($executionID);

            $response['closeModal'] = true;
            $response['load']       = true;
            if($from == 'kanban') $response['callback'] = "changeStatus('doing')";
            return $this->sendSuccess($response);
        }

        $this->view->title      = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->putoff;
        $this->view->users      = $this->loadModel('user')->getPairs('noletter');
        $this->view->actions    = $this->loadModel('action')->getList($this->objectType, $executionID);
        $this->display();
    }

    /**
     * 挂起一个执行。
     * Suspend a execution.
     *
     * @param  int    $executionID
     * @param  string $from
     * @access public
     * @return void
     */
    public function suspend($executionID, $from = 'execution')
    {
        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;
        if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban;

        $this->extendRequireFields($executionID);
        if(!empty($_POST))
        {
            $postData = fixer::input('post')
                ->add('id', $executionID)
                ->setDefault('status', 'suspended')
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->setDefault('lastEditedDate', helper::now())
                ->setDefault('suspendedDate', helper::today())
                ->stripTags($this->config->execution->editor->suspend['id'], $this->config->allowedTags)
                ->get();

            $this->execution->computeBurn($executionID);
            $this->execution->suspend($executionID, $postData);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $project = $this->loadModel('project')->getById($execution->project);
            if($project->model == 'waterfall' or $project->model == 'waterfallplus') $this->loadModel('programplan')->computeProgress($executionID, 'suspend');

            $this->executeHooks($executionID);

            $response['closeModal'] = true;
            $response['load']       = true;
            if($from == 'kanban') $response['callback'] = "changeStatus('suspended')";
            return $this->sendSuccess($response);
        }

        $this->view->title      = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->suspend;
        $this->view->users      = $this->loadModel('user')->getPairs('noletter');
        $this->view->actions    = $this->loadModel('action')->getList($this->objectType, $executionID);
        $this->display();
    }

    /**
     * 激活一个执行。
     * Activate a execution.
     *
     * @param  int    $executionID
     * @param  string $from
     * @access public
     * @return void
     */
    public function activate($executionID, $from = 'execution')
    {
        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;
        if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban;

        $this->extendRequireFields($executionID);
        if(!empty($_POST))
        {
            $postData = fixer::input('post')
                ->add('id', $executionID)
                ->setDefault('realEnd', null)
                ->setDefault('status', 'doing')
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->setDefault('lastEditedDate', helper::now())
                ->setDefault('closedBy', '')
                ->setDefault('closedDate', null)
                ->stripTags($this->config->execution->editor->activate['id'], $this->config->allowedTags)
                ->get();

            $this->execution->activate($executionID, $postData);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $project = $this->loadModel('project')->getById($execution->project);
            if($project->model == 'waterfall' or $project->model == 'waterfallplus') $this->loadModel('programplan')->computeProgress($executionID, 'activate');

            $this->executeHooks($executionID);

            $response['closeModal'] = true;
            $response['load']       = true;
            if($from == 'kanban') $response['callback'] = "changeStatus('doing')";
            return $this->sendSuccess($response);
        }

        $newBegin = date('Y-m-d');
        $dateDiff = helper::diffDate($newBegin, $execution->begin);
        $newEnd   = date('Y-m-d', strtotime($execution->end) + $dateDiff * 24 * 3600);

        $this->view->title      = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->activate;
        $this->view->execution    = $execution;
        $this->view->users      = $this->loadModel('user')->getPairs('noletter');
        $this->view->actions    = $this->loadModel('action')->getList($this->objectType, $executionID);
        $this->view->newBegin   = $newBegin;
        $this->view->newEnd     = $newEnd;
        $this->display();
    }

    /**
     * 关闭迭代。
     * Close execution.
     *
     * @param  int    $executionID
     * @param  string $from
     * @access public
     * @return void
     */
    public function close($executionID, $from = 'execution')
    {
        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;
        if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban;

        $this->extendRequireFields($executionID);
        if(!empty($_POST))
        {
            $now      = helper::now();
            $postData = fixer::input('post')
                ->add('id', $executionID)
                ->setDefault('status', 'closed')
                ->setDefault('closedBy', $this->app->user->account)
                ->setDefault('closedDate', $now)
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->setDefault('lastEditedDate', $now)
                ->stripTags($this->config->execution->editor->close['id'], $this->config->allowedTags)
                ->get();

            $this->execution->computeBurn($executionID);
            $this->execution->close($executionID, $postData);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $project = $this->loadModel('project')->getById($execution->project);
            if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->loadModel('programplan')->computeProgress($executionID, 'close');

            $this->executeHooks($executionID);

            $response['closeModal'] = true;
            $response['load']       = true;
            if($from == 'kanban') $response['callback'] = "changeStatus('closed')";
            return $this->sendSuccess($response);
        }

        $this->view->title         = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->close;
        $this->view->users         = $this->loadModel('user')->getPairs('noletter');
        $this->view->actions       = $this->loadModel('action')->getList($this->objectType, $executionID);
        $this->view->unclosedTasks = $this->loadModel('task')->getUnclosedTasksByExecution($executionID);
        $this->display();
    }

    /**
     * 执行详情。
     * View a execution.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function view($executionID)
    {
        /* Check execution permission. */
        $executionID = $this->execution->checkAccess((int)$executionID, $this->executions);
        $execution   = $this->execution->getByID($executionID, true);
        $type        = $this->config->vision == 'lite' ? 'kanban' : 'stage,sprint,kanban';
        if($execution->type == 'stage')
        {
            $childExecutions = $this->execution->getChildExecutions($executionID);
            if(!empty($childExecutions)) return $this->app->control->sendError($this->lang->execution->errorParentExecution, helper::createLink('execution', 'all'));
        }

        if(empty($execution) || strpos($type, $execution->type) === false) return $this->send(array('result' => 'success', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->config->vision == 'lite' ? $this->createLink('project', 'index') : $this->createLink('execution', 'all'))));

        if($execution->type == 'kanban' and defined('RUN_MODE') and RUN_MODE == 'api') return print($this->fetch('execution', 'kanban', "executionID=$executionID"));

        /* Load lang and set session. */
        $this->app->loadLang('program');
        $this->app->loadLang('bug');
        $this->app->session->set('teamList', $this->app->getURI(true), 'execution');

        $this->execution->setMenu($execution->id);

        /* Execution not found to prevent searching for. */
        if(!isset($this->executions[$execution->id])) $this->executions = $this->execution->getPairs($execution->project, 'all', 'nocode');
        if(!$this->loadModel('common')->checkPrivByObject('execution', $executionID)) return $this->execution->accessDenied();

        $execution->projectInfo = $this->loadModel('project')->getByID($execution->project);
        $programList = $execution->projectInfo ? array_filter(explode(',', $execution->projectInfo->path)) : array();
        array_pop($programList);

        if(empty($execution->projectInfo->hasProduct)) $this->lang->execution->PO = $this->lang->common->story . $this->lang->execution->owner;

        $this->executionZen->assignViewVars($executionID);

        $this->view->title        = $this->lang->execution->view;
        $this->view->execution    = $execution;
        $this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable.
        $this->view->type         = $type;
        $this->view->features     = $this->execution->getExecutionFeatures($execution);
        $this->view->project      = $execution->projectInfo;
        $this->view->programList  = $this->loadModel('program')->getPairsByList($programList);

        $this->display();
    }

    /**
     * 研发看板。
     * Research and development Kanban.
     *
     * @param  int     $executionID
     * @param  string  $browseType  all|story|bug|task
     * @param  string  $orderBy
     * @param  string  $groupBy     default|pri|category|module|source|assignedTo|type|story|severity
     * @access public
     * @return void
     */
    public function kanban($executionID, $browseType = 'all', $orderBy = 'id_asc', $groupBy = 'default')
    {
        $this->app->loadLang('bug');
        $this->app->loadLang('epic');
        $this->app->loadLang('requirement');
        $this->app->loadLang('kanban');

        if($this->config->vision != 'lite') $this->lang->execution->menu = new stdclass();
        $execution = $this->commonAction($executionID);
        if($execution->type != 'kanban') return $this->locate(inlink('view', "executionID={$execution->id}"));

        /* Save the recently five executions visited in the cookie. */
        $this->executionZen->setRecentExecutions($execution->id);

        /* Set Session. */
        if(empty($groupBy)) $groupBy = 'default';
        $this->session->set('execGroupBy', $groupBy);
        $this->session->set('storyList', $this->app->getURI(true), 'execution');
        $this->session->set('rdSearchValue', '');

        /* Get kanban data and set actions. */
        $executionActions = array();
        foreach($this->config->execution->statusActions as $action)
        {
            if($this->execution->isClickable($execution, $action)) $executionActions[] = $action;
        }

        /* Set lane type. */
        $features = $this->execution->getExecutionFeatures($execution);
        if(!$features['story'] and !$features['qa']) $browseType = 'task';
        if(!$features['story']) unset($this->lang->kanban->group->task['story']);
        $this->session->set('execLaneType', $browseType);

        $taskToOpen = $this->cookie->taskToOpen ? $this->cookie->taskToOpen : 0;
        helper::setcookie('taskToOpen', 0, 0);

        $this->executionZen->assignKanbanVars($executionID);

        $this->view->title            = $this->lang->kanban->view;
        $this->view->execution        = $execution;
        $this->view->executionList    = $this->loadModel('project')->getExecutionList(array($execution->project));
        $this->view->executionID      = $executionID;
        $this->view->kanbanList       = $this->loadModel('kanban')->getRDKanban($executionID, $browseType, $orderBy, 0, $groupBy);
        $this->view->browseType       = $browseType;
        $this->view->orderBy          = $orderBy;
        $this->view->groupBy          = $groupBy;
        $this->view->projectID        = $execution->project;
        $this->view->project          = $this->loadModel('project')->getByID($execution->project);
        $this->view->features         = $features;
        $this->view->executionActions = $executionActions;
        $this->view->kanban           = $this->lang->execution->kanban;
        $this->view->taskToOpen       = $taskToOpen;
        $this->display();
    }

    /**
     * 任务看板
     * Task kanban.
     *
     * @param  int    $executionID
     * @param  string $browseType  story|bug|task|all
     * @param  string $orderBy
     * @param  string $groupBy     default|pri|category|module|source|assignedTo|type|story|severity
     * @access public
     * @return void
     */
    public function taskKanban($executionID, $browseType = 'all', $orderBy = 'order_asc', $groupBy = '')
    {
        if(!$this->loadModel('common')->checkPrivByObject('execution', $executionID)) return $this->execution->accessDenied();
        /* Load model and language. */
        $this->app->loadLang('epic');
        $this->app->loadLang('requirement');
        $this->app->loadLang('task');
        $this->app->loadLang('bug');
        $this->loadModel('kanban');

        if(strpos($this->server->http_user_agent, 'MSIE 8.0') !== false) helper::header('X-UA-Compatible', 'IE=EmulateIE7'); // Compatibility IE8.

        $this->execution->setMenu($executionID);
        $execution = $this->execution->getByID($executionID);
        $features  = $this->execution->getExecutionFeatures($execution);
        if(!$features['story'])
        {
            $browseType = 'task';
            unset($this->lang->kanban->group->task['story']);
        }

        /* Save to session. */
        $uri     = $this->app->getURI(true);
        $groupBy = empty($groupBy) ? 'default' : $groupBy;
        $this->session->set('taskList', $uri, 'execution');
        $this->session->set('storyList', $uri, 'storyList');
        $this->session->set('bugList',  $uri, 'qa');
        $this->session->set('taskSearchValue', '');
        $this->session->set('execGroupBy', $groupBy);
        $this->session->set('execLaneType', $browseType);

        /* Get kanban data. */
        $orderBy = $groupBy == 'story' && $browseType == 'task' && !isset($this->lang->kanban->orderList[$orderBy]) ? 'id_asc' : $orderBy;
        $this->kanban->createLaneIfNotExist($executionID);
        list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy);

        /* Show lanes of the attribute: no story and bug in request, no bug in design. */
        if(!isset($this->lang->execution->menu->story)) unset($kanbanGroup['story']);
        if(!isset($this->lang->execution->menu->qa))    unset($kanbanGroup['bug']);

        $this->executionZen->assignTaskKanbanVars($execution);

        if($this->app->tab == 'project') $this->view->projectID = $execution->project;
        $this->view->storyOrder   = $orderBy;
        $this->view->orderBy      = 'id_asc';
        $this->view->executionID  = $executionID;
        $this->view->browseType   = $browseType;
        $this->view->features     = $features;
        $this->view->kanbanGroup  = $kanbanGroup;
        $this->view->links        = $links;
        $this->view->groupBy      = $groupBy;

        $this->display();
    }

    /**
     * 执行看板。
     * Execution kanban.
     *
     * @access public
     * @return void
     */
    public function executionKanban()
    {
        $projects   = $this->loadModel('project')->getPairsByProgram(0, 'noclosed');
        $executions = $this->execution->getStatData(0, 'all', 0, 0, false, 'withchild', 'id_desc');
        foreach($executions as $execution)
        {
            $execution->name = htmlspecialchars_decode($execution->name);
            $execution->team = htmlspecialchars_decode($execution->team);
        }

        list($projectCount, $statusCount, $myExecutions, $kanbanGroup) = $this->executionZen->buildExecutionKanbanData(array_keys($projects), $executions);
        $kanbanGroup = empty($myExecutions) ? $kanbanGroup : array($myExecutions) + $kanbanGroup;

        $kanbanList  = array();
        $lanes       = array();
        $items       = array();
        $columnCards = array();
        $columns     = array();

        foreach($kanbanGroup as $laneKey => $laneData)
        {
            $laneTitle = zget($projects, $laneKey, $this->lang->execution->myExecutions);

            $laneKey ++;
            $lanes[] = array('name' => $laneKey, 'title' => $laneTitle);
            foreach(array('wait', 'doing', 'suspended', 'closed') as $columnKey)
            {
                $cardList = !empty($laneData[$columnKey]) ? $laneData[$columnKey] : array();
                foreach($cardList as $card)
                {
                    $items[$laneKey][$columnKey][] = array('id' => $card->id, 'name' => $card->id, 'title' => $card->name, 'status' => $card->status, 'delay' => !empty($card->delay) ? $card->delay : 0, 'progress' => $card->progress);

                    if(!isset($columnCards[$columnKey])) $columnCards[$columnKey] = 0;
                    $columnCards[$columnKey] ++;
                }
            }
        }

        foreach(array('wait', 'doing', 'suspended', 'closed') as $columnKey)
        {
            $columns[] = array('name' => $columnKey, 'title' => $this->lang->execution->kanbanColType[$columnKey], 'cards' => !empty($columnCards[$columnKey]) ? $columnCards[$columnKey] : 0);
        }

        if(!empty($kanbanGroup))
        {
            $groupData['key']           = 'executionKanban';
            $groupData['data']['lanes'] = $lanes;
            $groupData['data']['cols']  = $columns;
            $groupData['data']['items'] = $items;
            $kanbanList[] = array('items' => array($groupData), 'key' => 'executionKanban', 'heading' => array('title' => $this->lang->execution->executionKanban));
        }

        $this->view->title      = $this->lang->execution->executionKanban;
        $this->view->kanbanList = $kanbanList;
        $this->display();
    }

    /**
     * 设置看板配置。
     * Set Kanban.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function setKanban($executionID)
    {
        $execution = $this->execution->getByID($executionID);

        if($_POST)
        {
            $executionData = form::data($this->config->execution->form->setkanban)
                ->setIF($this->post->heightType == 'auto', 'displayCards', 0)
                ->setDefault('minColWidth', $execution->minColWidth)
                ->remove('heightType')
                ->get();

            if(!isset($_POST['heightType']) || $this->post->heightType != 'custom' || $this->loadModel('kanban')->checkDisplayCards($executionData->displayCards))
            {
                $this->execution->setKanban($executionID, $executionData);
            }

            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
            return $this->sendSuccess(array('load' => true, 'closeModal' => true));
        }

        $this->view->title         = $this->lang->execution->setKanban;
        $this->view->execution     = $execution;
        $this->view->laneCount     = $this->loadModel('kanban')->getLaneCount($executionID, $execution->type);
        $this->view->heightType    = $execution->displayCards > 2 ? 'custom' : 'auto';
        $this->view->displayCards  = $execution->displayCards ? $execution->displayCards : '';

        $this->display();
    }

    /**
     * 用树状图的形式查看迭代/执行的视图。
     * Show the view of the execution in a tree mode.
     *
     * @param  int    $executionID
     * @param  string $type
     * @access public
     * @return void
     */
    public function tree($executionID, $type = 'task')
    {
        $this->execution->setMenu($executionID);

        /* Save this URI to session for locate it back. */
        $uri = $this->app->getURI(true);
        $this->app->session->set('taskList',      $uri, 'execution');
        $this->app->session->set('storyList',     $uri, 'execution');
        $this->app->session->set('executionList', $uri, 'execution');
        $this->app->session->set('caseList',      $uri, 'qa');
        $this->app->session->set('bugList',       $uri, 'qa');

        $tree = $this->execution->getTree($executionID);
        if($type === 'json') return print(helper::jsonEncode4Parse($tree, JSON_HEX_QUOT | JSON_HEX_APOS));

        $execution = $this->execution->getById($executionID);
        $project   = $this->project->getById($execution->project);

        if($execution->lifetime == 'ops') unset($this->lang->execution->treeLevel['story']);
        if($this->app->tab == 'project') $this->view->projectID = $execution->project;

        $gradeGroup = $this->loadModel('story')->getGradeGroup();

        $this->view->title       = $this->lang->execution->tree;
        $this->view->execution   = $execution;
        $this->view->executionID = $executionID;
        $this->view->level       = $type;
        $this->view->tree        = $this->execution->buildTree($tree, (bool)$project->hasProduct, $gradeGroup);
        $this->view->features    = $this->execution->getExecutionFeatures($execution);
        $this->display();
    }

    /**
     * 看板看板。
     * Print kanban.
     *
     * @param  int    $executionID
     * @param  string $orderBy
     * @access public
     * @return void
     */
    public function printKanban($executionID, $orderBy = 'id_asc')
    {
        $this->view->title = $this->lang->execution->printKanban;

        if($_POST)
        {
            /* Get stories information. */
            $order      = 1;
            $stories    = $this->loadModel('story')->getExecutionStories($executionID, 0, $orderBy);
            $storySpecs = $this->story->getStorySpecs(array_keys($stories));
            foreach($stories as $story) $story->order = $order ++;

            /* Get printed kanban data. */
            list($dataList, $users) = $this->executionZen->getPrintKanbanData($executionID, $stories);

            $originalDataList = $dataList;
            if($this->post->content == 'increment') $dateList = $this->executionZen->processPrintKanbanData($executionID, $dataList);

            /* Close the page when there is no data. */
            $hasData = false;
            foreach($dataList as $data)
            {
                if(empty($data)) continue;
                $hasData = true;
                break;
            }
            if(!$hasData) return $this->sendSuccess(array('message' => $this->lang->execution->noPrintData, 'load' => true));

            $this->execution->saveKanbanData($executionID, $originalDataList);

            /* Get date list. */
            if($this->post->content == 'all')
            {
                $executionInfo  = $this->execution->getByID($executionID);
                list($dateList) = $this->execution->getDateList($executionInfo->begin, $executionInfo->end, 'noweekend');
            }

            $this->view->hasBurn     = $this->post->content == 'all';
            $this->view->datas       = $dataList;
            $this->view->chartData   = $this->post->content == 'all' ? $this->execution->buildBurnData($executionID, $dateList) : array();
            $this->view->storySpecs  = $storySpecs;
            $this->view->realnames   = $this->loadModel('user')->getRealNameAndEmails($users);
            $this->view->executionID = $executionID;

            return $this->display();
        }

        $this->execution->setMenu($executionID);
        $this->view->executionID = $executionID;

        $this->display();
    }

    /**
     * 需求看板。
     * Story kanban.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function storyKanban($executionID)
    {
        $this->app->loadLang('kanban');
        $execution   = $this->commonAction($executionID);
        $executionID = $execution->id;
        $stories     = $this->loadModel('story')->getExecutionStories($executionID);
        $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false);

        $stories = array_filter($stories, function($story) {return in_array($story->stage, $this->config->execution->storyKanbanCols);});
        foreach($stories as $story) unset($story->type);

        /* Get execution's product. */
        $productID = 0;
        $productPairs = $this->loadModel('product')->getProductPairsByProject($executionID);
        if($productPairs)
        {
            $productID = key($productPairs);
        }
        else
        {
            return $this->sendError($this->lang->execution->errorNoLinkedProducts, $this->createLink('execution', 'manageproducts', "executionID=$executionID"));
        }

        $this->app->session->set('executionStoryList', $this->app->getURI(true), 'execution');

        $this->view->title        = $this->lang->execution->storyKanban;
        $this->view->kanbanData   = $this->story->getKanbanGroupData($stories);
        $this->view->realnames    = $this->loadModel('user')->getPairs('noletter');
        $this->view->executionID  = $executionID;
        $this->view->execution    = $execution;
        $this->view->productID    = $productID;
        $this->view->total        = count($stories);
        $this->view->product      = $this->product->getByID($productID);
        $this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable.

        $this->display();
    }

    /**
     * 删除一个执行。
     * Delete a execution.
     *
     * @param  int    $executionID
     * @param  string $confirm      yes|no
     * @access public
     * @return void
     */
    public function delete($executionID, $confirm = 'no')
    {
        if($confirm == 'no')
        {
            /* Get the number of unfinished tasks and unresolved bugs. */
            $unfinishedTasks     = $this->loadModel('task')->getUnfinishTasks($executionID);
            $unresolvedBugs      = $this->loadModel('bug')->getActiveBugs(0, '', (string)$executionID, array());
            $unfinishedTaskCount = count($unfinishedTasks);
            $unresolvedBugCount  = count($unresolvedBugs);

            /* Set prompt information. */
            $tips = '';
            if($unfinishedTaskCount) $tips  = sprintf($this->lang->execution->unfinishedTask, $unfinishedTaskCount);
            if($unresolvedBugCount)  $tips .= sprintf($this->lang->execution->unresolvedBug,  $unresolvedBugCount);
            if($tips)                $tips  = $this->lang->execution->unfinishedExecution . $tips;

            $execution = $this->execution->fetchByID($executionID);
            if($execution->type == 'stage')
            {
                if($tips) $tips = str_replace($this->lang->executionCommon, $this->lang->project->stage, $tips);
                $this->lang->execution->confirmDelete = str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->execution->confirmDelete);
            }
            elseif($execution->type == 'kanban')
            {
                global $lang;
                $lang->executionCommon = $lang->execution->kanban;
                include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php';
            }

            $tips = $tips . sprintf($this->lang->execution->confirmDelete, $this->executions[$executionID]);
            return $this->send(array('callback' => "confirmDeleteExecution({$executionID}, \"{$tips}\")"));
        }
        else
        {
            /* Delete an execution and update the information. */
            $execution = $this->execution->fetchByID($executionID);
            $this->execution->delete(TABLE_EXECUTION, $executionID);
            $this->execution->updateUserView($executionID);

            $project = $this->loadModel('project')->getByID($execution->project);
            if($project->model == 'waterfall' or $project->model == 'waterfallplus') $this->loadModel('programplan')->computeProgress($executionID);

            $this->session->set('execution', '');
            $message = $this->executeHooks($executionID);
            if($message) $this->lang->saveSuccess = $message;

            if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess));
            $locate = true;
            if($execution->type == 'kanban')
            {
                $locate = $this->config->vision == 'lite' ? $this->createLink('project', 'execution', "status=all&projectID=$execution->project") : $this->createLink('execution', 'all');
            }
            if($this->app->tab == 'project' && $this->session->executionList) $locate = $this->session->executionList;
            return $this->send(array('result' => 'success', 'load' => $locate));
        }
    }

    /**
     * 维护关联产品。
     * Manage products.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function manageProducts($executionID)
    {
        /* Use first execution if executionID does not exist. */
        if(!isset($this->executions[$executionID])) $executionID = key($this->executions);

        $execution = $this->execution->getByID($executionID);
        $project   = $this->project->getByID($execution->project);
        if(!$project->hasProduct) return $this->sendError($this->lang->project->cannotManageProducts, true);
        if($project->model == 'waterfall' || $project->model == 'waterfallplus') return $this->sendError(sprintf($this->lang->execution->cannotManageProducts, zget($this->lang->project->modelList, $project->model)), true);

        if($_SERVER['REQUEST_METHOD'] == 'POST')
        {
            $oldProducts = $this->loadModel('product')->getProducts($executionID);
            $oldProducts = array_keys($oldProducts);

            $postData = form::data()->get();
            $this->execution->updateProducts($executionID, $postData);
            if(dao::isError()) return $this->sendError(dao::getError());

            $newProducts  = $this->product->getProducts($executionID);
            $newProducts  = array_keys($newProducts);
            $diffProducts = array_merge(array_diff($oldProducts, $newProducts), array_diff($newProducts, $oldProducts));
            if($diffProducts) $this->loadModel('action')->create($this->objectType, $executionID, 'Managed', '', !empty($_POST['products']) ? implode(',', $_POST['products']) : '');

            return $this->sendSuccess(array('load' => true, 'closeModal' => true));
        }

        /* Set menu. */
        $this->execution->setMenu($execution->id);

        $this->executionZen->assignManageProductsVars($execution);
    }

    /**
     * 维护执行的团队成员。
     * Manage members of the execution.
     *
     * @param  int    $executionID
     * @param  int    $team2Import    the team to import.
     * @param  int    $dept
     * @access public
     * @return void
     */
    public function manageMembers($executionID = 0, $team2Import = 0, $dept = 0)
    {
        $execution = $this->execution->getByID($executionID);
        if(!empty($_POST))
        {
            $memberDataList = $this->executionZen->buildMembersForManageMembers($execution);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->execution->manageMembers($execution, $memberDataList);
            if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->loadModel('action')->create('team', $executionID, 'managedTeam');
            return $this->sendSuccess(array('load' => $this->createLink('execution', 'team', "executionID={$executionID}")));
        }

        $this->loadModel('dept');
        $deptUsers = empty($dept) ? array() : $this->dept->getDeptUserPairs($dept);

        $currentMembers = $this->execution->getTeamMembers($executionID);
        $members2Import = $this->execution->getMembers2Import($team2Import, array_keys($currentMembers));

        /* Append users for get users. */
        $appendUsers = array();
        foreach($currentMembers as $account => $member) $appendUsers[$account] = $account;
        foreach($members2Import as $member) $appendUsers[$member->account] = $member->account;
        foreach($deptUsers as $deptAccount => $userName) $appendUsers[$deptAccount] = $deptAccount;

        /* Set menu. */
        $this->execution->setMenu($execution->id);
        if(!empty($this->config->user->moreLink)) $this->config->moreLinks["accounts[]"] = $this->config->user->moreLink;

        if($execution->type == 'kanban') $this->lang->execution->copyTeamTitle = str_replace($this->lang->execution->common, $this->lang->execution->kanban, $this->lang->execution->copyTeamTitle);

        $this->view->title          = $this->lang->execution->manageMembers . $this->lang->hyphen . $execution->name;
        $this->view->execution      = $execution;
        $this->view->users          = $this->loadModel('user')->getPairs('noclosed|nodeleted|devfirst', $appendUsers);
        $this->view->roles          = $this->user->getUserRoles(array_keys($this->view->users));
        $this->view->dept           = $dept;
        $this->view->depts          = $this->dept->getOptionMenu();
        $this->view->teams2Import   = $this->loadModel('personnel')->getCopiedObjects($executionID, 'sprint', true);
        $this->view->team2Import    = $team2Import;
        $this->view->teamMembers    = $this->executionZen->buildMembers($currentMembers, $members2Import, $deptUsers, $execution->days);
        $this->display();
    }

    /**
     * 移除团队成员。
     * Unlink a memeber.
     *
     * @param  int    $executionID
     * @param  int    $userID
     * @access public
     * @return void
     */
    public function unlinkMember($executionID, $userID)
    {
        $user = $this->loadModel('user')->getById($userID, 'id');
        $this->execution->unlinkMember($executionID, $user->account);

        if(dao::isError()) return $this->sendError(dao::getError());

        $this->loadModel('action')->create('team', $executionID, 'managedTeam');
        return $this->sendSuccess(array('message' => '', 'load' => helper::createLink('execution', 'team', "executionID={$executionID}")));
    }

    /**
     * 在迭代中关联需求。
     * Link stories in the execution.
     *
     * @param  int    $objectID
     * @param  string $browseType
     * @param  int    $param
     * @param  string $orderBy
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @param  string $extra
     * @param  string $storyType
     * @access public
     * @return void
     */
    public function linkStory($objectID = 0, $browseType = '', $param = 0, $orderBy = 'id_desc', $recPerPage = 50, $pageID = 1, $extra = '', $storyType = 'story')
    {
        $this->loadModel('story');
        $this->loadModel('product');
        $this->loadModel('tree');
        $this->loadModel('branch');

        /* Save old objectID to use later. */
        $originObjectID = $objectID;

        /* Use the projectID of Kanban as objectID if vision is lite. */
        if($this->config->vision == 'lite')
        {
            $kanban = $this->project->getByID($objectID, 'kanban');
            if($kanban) $objectID = $kanban->project;
        }

        $products = $this->product->getProducts($objectID);
        if(empty($products)) return $this->sendError($this->lang->execution->errorNoLinkedProducts, $this->createLink('execution', 'manageproducts', "executionID=$objectID"));

        /* 通过objectID获取符合项目、执行、阶段和看板类型的对象。*/
        $object = $this->project->getByID($objectID, 'project,sprint,stage,kanban');

        $browseLink = $this->createLink('execution', 'story', "executionID=$objectID");
        if($this->app->tab == 'project' and $object->multiple) $browseLink = $this->createLink('projectstory', 'story', "objectID=$objectID");

        if($object->type == 'kanban' && !$object->hasProduct) $this->lang->productCommon = $this->lang->project->common;

        $this->session->set('storyList', $this->app->getURI(true), $this->app->tab);

        if(!empty($_POST))
        {
            if($object->type != 'project' and $object->project != 0) $this->execution->linkStory($object->project, $this->post->stories ? $this->post->stories : array(), '', array(), $storyType);
            $this->execution->linkStory($objectID, $this->post->stories ? $this->post->stories : array(), $extra, array(), $storyType);

            if(isInModal()) return $this->sendSuccess(array('closeModal' => true, 'load' => true, 'callback' => "refreshKanban()"));

            return $this->sendSuccess(array('load' => $browseLink));
        }

        if($object->type == 'project') $this->project->setMenu($object->id);
        if(in_array($object->type, array('sprint', 'stage', 'kanban'))) $this->execution->setMenu($object->id);

        /* Set modules and branches. */
        $modules      = array();
        $branchIDList = array(BRANCH_MAIN);
        $branches     = $this->project->getBranchesByProject($objectID);
        $productType  = 'normal';

        if(commonModel::isTutorialMode())
        {
            $modules = $this->loadModel('tutorial')->getModulePairs();
        }
        else
        {
            foreach($products as $product)
            {
                $productModules = $this->tree->getOptionMenu($product->id, 'story', 0, array_keys($branches[$product->id]));
                foreach($productModules as $branchModules)
                {
                    foreach($branchModules as $moduleID => $moduleName) $modules[$moduleID] = ((count($products) >= 2 and $moduleID != 0) ? $product->name : '') . $moduleName;
                }
                if($product->type != 'normal')
                {
                    $productType = $product->type;
                    if(isset($branches[$product->id]))
                    {
                        foreach($branches[$product->id] as $branchID => $branch) $branchIDList[$branchID] = $branchID;
                    }
                }
            }
        }

        /* Build the search form. */
        $actionURL    = $this->createLink($this->app->rawModule, 'linkStory', "objectID=$objectID&browseType=bySearch&queryID=myQueryID&orderBy=$orderBy&recPerPage=$recPerPage&pageID=$pageID&extra=$extra&storyType=$storyType");
        $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products));
        $queryID      = ($browseType == 'bySearch') ? (int)$param : 0;
        $this->execution->buildStorySearchForm($products, $branchGroups, $modules, $queryID, $actionURL, 'linkStory', $object);

        $project   = (strpos('sprint,stage,kanban', $object->type) !== false) ? $this->loadModel('project')->getByID($object->project) : $object;
        $storyType = (($object->type == 'stage' && in_array($object->attribute, array('mix', 'request', 'design'))) || $object->type == 'project' || !$object->multiple) ? ($project->storyType ?? 'story') : 'story';

        if($browseType == 'bySearch') $allStories = $this->story->getBySearch('all', '', $queryID, $orderBy, $objectID, $storyType);
        if($browseType != 'bySearch') $allStories = $this->story->getProductStories(implode(',', array_keys($products)), $branchIDList, '0', 'active,launched', $storyType, $orderBy, true, '', null);
        $linkedStories = $this->story->getExecutionStoryPairs($objectID, 0, 'all', 0, 'full', 'all', $storyType);
        foreach($allStories as $id => $story)
        {
            if(isset($linkedStories[$story->id])) unset($allStories[$id]);

            if(!isset($modules[$story->module]))
            {
                $storyModule = $this->tree->getModulesName((array)$story->module);
                $productName = count($products) > 1 ? $products[$story->product]->name : '';
                $modules[$story->module] = $productName . zget($storyModule, $story->module, '');
            }

            $story->estimate = $story->estimate . $this->config->hourUnit;
        }

        $gradeGroup = array();
        $gradeList  = $this->story->getGradeList('');
        foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;

        /* Set the pager. */
        $this->app->loadClass('pager', true);
        $pager      = new pager(count($allStories), $recPerPage, $pageID);
        $allStories = array_chunk($allStories, $pager->recPerPage);

        $productPairs = array();
        foreach($products as $id => $product) $productPairs[$id] = $product->name;

        $this->view->title        = $object->name . $this->lang->hyphen . $this->lang->execution->linkStory;
        $this->view->objectID     = $originObjectID;
        $this->view->param        = $param;
        $this->view->extra        = $extra;
        $this->view->object       = $object;
        $this->view->orderBy      = $orderBy;
        $this->view->gradeGroup   = $gradeGroup;
        $this->view->showGrade    = $this->config->edition == 'ipd';
        $this->view->productPairs = $productPairs;
        $this->view->allStories   = empty($allStories) ? $allStories : $allStories[$pageID - 1];
        $this->view->pager        = $pager;
        $this->view->browseType   = $browseType;
        $this->view->productType  = $productType;
        $this->view->modules      = $modules;
        $this->view->users        = $this->loadModel('user')->getPairs('noletter');
        $this->view->branchGroups = $branchGroups;
        $this->view->browseLink   = $browseLink;

        $this->view->project      = $project;
        $this->view->executionID  = $object->id;
        $this->view->projectID    = $object->id;
        $this->view->storyType    = $storyType;
        if($this->config->edition == 'ipd') $this->view->roadmaps = $this->loadModel('roadmap')->getPairs($productPairs);

        $this->display();
    }

    /**
     * 取消关联需求（移除需求）。
     * Unlink a story.
     *
     * @param  int    $executionID
     * @param  int    $storyID
     * @param  string $confirm     yes|no
     * @param  string $from        taskkanban
     * @param  int    $laneID
     * @param  int    $columnID
     * @access public
     * @return void
     */
    public function unlinkStory($executionID, $storyID, $confirm = 'no', $from = '', $laneID = 0, $columnID = 0)
    {
        if($confirm == 'no')
        {
            $confirmURL = $this->createLink('execution', 'unlinkstory', "executionID=$executionID&storyID=$storyID&confirm=yes&from=$from&laneID=$laneID&columnID=$columnID");
            $tip        = $this->app->rawModule == 'projectstory' ? $this->lang->execution->confirmUnlinkExecutionStory : $this->lang->execution->confirmUnlinkStory;
            $story      = $this->loadModel('story')->getByID($storyID);
            if($story->type == 'requirement') $tip = str_replace($this->lang->SRCommon, $this->lang->URCommon, $tip);
            return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message: '{$tip}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'}).then((res) => {if(res) $.ajaxSubmit({url: '$confirmURL'});});"));
        }

        $execution = $this->execution->getByID($executionID);
        $this->execution->unlinkStory($executionID, $storyID, $laneID, $columnID);
        if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError()));

        if($execution->type == 'kanban')
        {
            /* Fix bug #29171. */
            $executions       = $this->dao->select('*')->from(TABLE_EXECUTION)->where('parent')->eq($execution->parent)->fetchAll('id');
            $executionStories = $this->dao->select('project,story')->from(TABLE_PROJECTSTORY)->where('story')->eq($storyID)->andWhere('project')->in(array_keys($executions))->fetchAll();
            if(empty($executionStories)) $this->execution->unlinkStory($execution->parent, $storyID, $laneID, $columnID);
        }

        if($this->app->tab == 'execution' and $execution->type == 'kanban' or $from == 'taskkanban') return $this->send(array('result' => 'success', 'closeModal' => true, 'callback' => "refreshKanban()"));

        return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true));
    }

    /**
     * 批量取消关联需求（移除需求）。
     * Batch unlink story.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function batchUnlinkStory($executionID)
    {
        if(isset($_POST['storyIdList']))
        {
            $storyIdList = $this->post->storyIdList;
            $_POST       = array();

            $this->loadModel('gitlab');
            foreach($storyIdList as $storyID)
            {
                /* Delete related issue in gitlab. */
                $relation = $this->gitlab->getRelationByObject('story', (int)$storyID);
                if(!empty($relation)) $this->gitlab->deleteIssue('story', (int)$storyID, $relation->issueID);

                $this->execution->unlinkStory($executionID, (int)$storyID);
            }
        }
        if(!dao::isError()) $this->loadModel('score')->create('ajax', 'batchOther');
        $execution = $this->execution->getByID($executionID);
        return $this->send(array('result' => 'success', 'load' => array('alert' => $this->lang->execution->confirmBatchUnlinkStory, 'locate' => $this->createLink('execution', 'story', "executionID=$executionID") . ($execution->multiple ? '' : '#app=project'))));
    }

    /**
     * 获取执行动态。
     * Get the dynamic of execution.
     *
     * @param  int    $executionID
     * @param  string $type
     * @param  string $param
     * @param  int    $recTotal
     * @param  string $date
     * @param  string $direction  next|pre
     * @access public
     * @return void
     */
    public function dynamic($executionID = 0, $type = 'today', $param = '', $recTotal = 0, $date = '', $direction = 'next')
    {
        /* Save session. */
        $uri = $this->app->getURI(true);
        $this->session->set('productList',     $uri, 'product');
        $this->session->set('productPlanList', $uri, 'product');
        $this->session->set('releaseList',     $uri, 'product');
        $this->session->set('storyList',       $uri, 'product');
        $this->session->set('executionList',   $uri, 'execution');
        $this->session->set('taskList',        $uri, 'execution');
        $this->session->set('buildList',       $uri, 'execution');
        $this->session->set('bugList',         $uri, 'qa');
        $this->session->set('caseList',        $uri, 'qa');
        $this->session->set('testtaskList',    $uri, 'qa');
        $this->session->set('reportList',      $uri, 'qa');

        /* use first execution if executionID does not exist. */
        if(!isset($this->executions[$executionID])) $executionID = key($this->executions);
        $this->execution->setMenu($executionID);
        $execution = $this->execution->getByID($executionID);

        /* Set the user and type. */
        $account = 'all';
        if($type == 'account')
        {
            $user = $this->loadModel('user')->getById((int)$param, 'id'); /* 通过 id 字段获取用户。另一种是通过 account 获取。*/
            if($user) $account = $user->account;
        }

        /* 获取这个时间段的操作日志列表。*/
        $period     = $type == 'account' ? 'all' : $type;
        $orderBy    = $direction == 'next' ? 'date_desc' : 'date_asc';
        $date       = empty($date) ? '' : date('Y-m-d', $date);
        $actions    = $this->loadModel('action')->getDynamic($account, $period, $orderBy, 50, 'all', 'all', $executionID, $date, $direction);
        $dateGroups = $this->action->buildDateGroup($actions, $direction);
        if(empty($recTotal)) $recTotal = count($dateGroups) < 2 ? count($dateGroups, 1) - count($dateGroups) : $this->action->getDynamicCount();

        $this->view->title        = $execution->name . $this->lang->hyphen . $this->lang->execution->dynamic;
        $this->view->userIdPairs  = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted|useid');
        $this->view->accountPairs = $this->loadModel('user')->getPairs('noletter|nodeleted');
        $this->view->executionID  = $executionID;
        $this->view->type         = $type;
        $this->view->orderBy      = $orderBy;
        $this->view->account      = $account;
        $this->view->param        = $param;
        $this->view->dateGroups   = $dateGroups; /* 将日志按照日期分组，以日期为索引，将日志列表作为值。*/
        $this->view->direction    = $direction;
        $this->view->recTotal     = $recTotal;

        $this->display();
    }

    /**
     * 通过执行ID获取产品。
     * AJAX: get products of a execution in html select.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function ajaxGetProducts($executionID)
    {
        $products = $this->loadModel('product')->getProducts($executionID, 'all', '', false);
        return print(html::select('product', $products, '', 'class="form-control"'));
    }

    /**
     * 通过执行ID和指派给获取团队成员。
     * AJAX: get team members of the execution.
     *
     * @param  int    $executionID
     * @param  string $assignedTo
     * @access public
     * @return void
     */
    public function ajaxGetMembers($executionID, $assignedTo = '')
    {
        $users = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution');
        if($this->app->getViewType() === 'json') return print(json_encode($users));

        $items = array();
        foreach($users as $account => $realName) $items[] = array('value' => $account, 'text' => $realName, 'keys' => $account);
        return print(json_encode($items));
    }

    /**
     * 通过项目ID或执行ID获取团队成员。
     * AJAX: get team members by projectID/executionID.
     *
     * @param  int    $objectID
     * @access public
     * @return string
     */
    public function ajaxGetTeamMembers($objectID)
    {
        $objectType = $this->dao->select('`type`')->from(TABLE_PROJECT)->where('id')->eq($objectID)->fetch('type');
        if($objectType != 'project') $objectType = 'execution';

        $members = $this->loadModel('user')->getTeamMemberPairs($objectID, $objectType);

        $selectedMembers = array();
        foreach($members as $account => $realName) $selectedMembers[] = $account;
        return print(json_encode($selectedMembers));
    }

    /**
     * 在用户创建完执行后，给用户发送一个后续操作的提示（比如设置团队、关联软件需求等）方便引导用户使用。
     * Show tips to guide the user to do something after the execution created successfully.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function tips($executionID)
    {
        $execution = $this->execution->getById($executionID);

        $this->view->executionID = $executionID;
        $this->view->execution   = $execution;
        $this->view->projectID   = $execution->project;
        $this->display();
    }

    /**
     * 获取执行下拉列表用于切换不同的执行。
     * Drop menu page.
     *
     * @param  int    $executionID 已经打开的页面对应的执行ID
     * @param  string $module      链接里要访问的模块
     * @param  string $method      链接里要访问的方法
     * @param  string $extra       给链接传入的额外的参数
     * @access public
     * @return void
     */
    public function ajaxGetDropMenu($executionID, $module, $method, $extra = '')
    {
        $this->view->link        = $this->executionZen->getLink($module, $method, $extra);
        $this->view->module      = $module;
        $this->view->method      = $method;
        $this->view->executionID = $executionID;
        $this->view->extra       = $extra;

        $cacheProjectsKey   = $this->config->cacheKeys->execution->ajaxGetDropMenuProjects;
        $cacheExecutionsKey = $this->config->cacheKeys->execution->ajaxGetDropMenuExecutions;

        if(helper::isCacheEnabled())
        {
            $projects          = $this->cache->get($cacheProjectsKey);
            $projectExecutions = $this->cache->get($cacheExecutionsKey);

            if(!empty($projects) && !empty($projectExecutions))
            {
                $this->view->projects          = $projects;
                $this->view->projectExecutions = $projectExecutions;
                $this->display();
                return;
            }
        }

        $projects = $this->loadModel('program')->getProjectList(0, 'all', 0, 'order_asc', '', true); /* 获取所有项目的列表。*/
        $executionGroups = $this->dao->select('*')->from(TABLE_EXECUTION) /* 按照项目分组，获取有权限访问的执行列表。*/
            ->where('deleted')->eq('0')
            ->andWhere('multiple')->eq('1')
            ->andWhere('type')->in('sprint,stage,kanban')
            ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi()
            ->andWhere('project')->in(array_keys($projects))
            ->orderBy('order_asc')
            ->fetchGroup('project', 'id');

        $teams = $this->dao->select('root,account')->from(TABLE_TEAM) /* 按照执行ID分组，获取有权限访问的执行的团队信息。*/
            ->where('root')->in($this->app->user->view->sprints)
            ->andWhere('type')->eq('execution')
            ->fetchGroup('root', 'account');

        $projectPairs      = array(); /* 项目ID为索引，项目名称为值的数组 [projectID => projectName]。 */
        $orderedExecutions = array();
        $parentExecutions  = array();
        foreach($projects as $project)
        {
            $executions = zget($executionGroups, $project->id, array());
            if(isset($project->model) and $project->model == 'waterfall') ksort($executions);

            $topExecutions = array();
            foreach($executions as $execution)
            {
                if($execution->grade == 1) $topExecutions[$execution->id] = $execution->id;
                if($execution->grade > 1 && $execution->parent) $parentExecutions[$execution->parent] = $execution->parent;
            }

            /* 获取排序后的执行列表，并给每个执行设置团队信息。*/
            $executions = $this->execution->resetExecutionSorts($executions, $topExecutions);
            foreach($executions as $execution)
            {
                $execution->teams = zget($teams, $execution->id, array());
                $orderedExecutions[$execution->id] = $execution;
            }

            $projectPairs[$project->id] = $project->name;
        }

        $executionNameList = $this->execution->getFullNameList($orderedExecutions);
        $projectExecutions = array(); /* 项目对应的执行列表 [projectID => execution]。*/
        foreach($orderedExecutions as $execution)
        {
            if(isset($parentExecutions[$execution->id])) continue;

            $execution->name = $executionNameList[$execution->id];
            $projectExecutions[$execution->project][] = $execution;
        }

        if($this->config->cache->enable)
        {
            $this->cache->set($cacheProjectsKey, $projectPairs);
            $this->cache->set($cacheExecutionsKey, $projectExecutions);
        }

        $this->view->projects           = $projectPairs;      /* 项目ID为索引，项目名称为值的数组 [projectID => projectName]。 */
        $this->view->projectExecutions  = $projectExecutions; /* 项目对应的执行列表 [projectID => execution]。*/
        $this->view->executionID        = $executionID;
        $this->display();
    }

    /**
     * 将执行列表内的执行进行排序。
     * Update the order of the executions.
     *
     * @access public
     * @return void
     */
    public function updateOrder()
    {
        if($_SERVER['REQUEST_METHOD'] !== 'POST') return $this->sendError($this->lang->error->unsupportedReq);

        $idList  = explode(',', trim($this->post->executions, ','));
        $orderBy = $this->post->orderBy;
        if(strpos($orderBy, 'order') === false) return false;

        $executions = $this->dao->select('id,`order`')->from(TABLE_EXECUTION)->where('id')->in($idList)->orderBy($orderBy)->fetchPairs('order', 'id');
        foreach($executions as $order => $id)
        {
            $newID = array_shift($idList);
            if($id == $newID) continue;
            $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($order)->where('id')->eq($newID)->exec();
            if(dao::isError()) return $this->sendError(dao::getError());
        }

        return $this->sendSuccess();
    }

    /**
     * 需求排序。
     * Sort stories.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function storySort($executionID)
    {
        if($_SERVER['REQUEST_METHOD'] !== 'POST') return $this->sendError($this->lang->error->unsupportedReq);

        $idList  = explode(',', trim($this->post->storyList, ','));
        $orderBy = $this->post->orderBy;

        $order = $this->dao->select('`order`')->from(TABLE_PROJECTSTORY)->where('story')->in($idList)->andWhere('project')->eq($executionID)->orderBy('order_asc')->fetch('order');
        if(strpos($orderBy, 'order_desc') !== false) $idList = array_reverse($idList);
        foreach($idList as $storyID)
        {
            $this->dao->update(TABLE_PROJECTSTORY)->set('`order`')->eq($order)->where('story')->eq($storyID)->andWhere('project')->eq($executionID)->exec();
            if(dao::isError()) return $this->sendError(dao::getError());
            $order++;
        }

        return $this->sendSuccess();
    }

    /**
     * 在执行需求列表内进行需求估算。
     * Story estimate.
     *
     * @param  int    $executionID
     * @param  int    $storyID
     * @param  int    $round       The round of estimate for stroy 估算的轮次
     * @access public
     * @return void
     */
    public function storyEstimate($executionID, $storyID, $round = 0)
    {
        $this->loadModel('story');

        if($_POST)
        {
            $this->story->saveEstimateInfo($storyID);
            if(dao::isError()) return $this->sendError(dao::getError());

            $this->loadModel('action')->create('story', $storyID, 'estimated', '', $executionID);
            $link = $this->createLink('execution', 'storyEstimate', "executionID={$executionID}&storyID={$storyID}&round={$round}");
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'callback' => "loadModal(\"$link\", '#storyEstimateModal');"));
        }

        $estimateInfo = $this->story->getEstimateInfo($storyID, $round);
        $team         = $this->execution->getTeamMembers($executionID);
        /* If user has been removed from team and has estimate record, join him into team.*/
        if(!empty($estimateInfo->estimate))
        {
            foreach($estimateInfo->estimate as $account => $estimate)
            {
                if(!in_array($account, array_keys($team)))
                {
                    $team[$account] = new stdclass();
                    $team[$account]->account = $account;
                }
            }
        }

        $this->view->estimateInfo = $estimateInfo;
        $this->view->round        = !empty($estimateInfo->round) ? $estimateInfo->round : 0;
        $this->view->rounds       = $this->story->getEstimateRounds($storyID);
        $this->view->users        = $this->loadModel('user')->getPairs('noletter');
        $this->view->team         = $team;
        $this->view->executionID  = $executionID;
        $this->view->storyID      = $storyID;
        $this->view->story        = $this->story->getById($storyID);
        $this->display();
    }

    /**
     * 查看执行列表。
     * All execution.
     *
     * @param  string $status
     * @param  string $orderBy
     * @param  int    $productID
     * @param  string $param
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function all($status = 'undone', $orderBy = 'order_asc', $productID = 0, $param = '', $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        $this->app->loadLang('my');
        $this->app->loadLang('stage');
        $this->app->loadLang('programplan');
        $this->loadModel('product');
        $this->loadModel('datatable');

        $from = $this->app->tab;
        if($from == 'execution') $this->session->set('executionList', $this->app->getURI(true), 'execution');

        if($this->app->viewType == 'mhtml')
        {
            $executionID = $this->execution->checkAccess(0, $this->executions);
            $this->execution->setMenu($executionID);
        }

        /* Load pager and get tasks. */
        $this->app->loadClass('pager', true);
        $pager = new pager($recTotal, $recPerPage, $pageID);

        $queryID   = ($status == 'bySearch') ? (int)$param : 0;
        $actionURL = $this->createLink('execution', 'all', "status=bySearch&orderBy=$orderBy&productID=$productID&param=myQueryID");
        $this->execution->buildSearchForm($queryID, $actionURL);

        $sort = $orderBy;
        if(strpos($sort, 'rawID_') !== false) $sort = str_replace('rawID_', 'id_', $sort);
        if(strpos($sort, 'nameCol_') !== false) $sort = str_replace('nameCol_', 'name_', $sort);

        $executionStats = $this->execution->getStatData(0, $status, $productID, 0, false, $queryID, $sort, $pager);

        $this->view->title            = $this->lang->execution->allExecutions;
        $this->view->executionStats   = $executionStats;
        $this->view->allExecutionsNum = $this->execution->getExecutionCounts(0, 'all');
        $this->view->productList      = $this->loadModel('product')->getProductPairsByProject(0);
        $this->view->productID        = $productID;
        $this->view->pager            = $pager;
        $this->view->orderBy          = $orderBy;
        $this->view->users            = $this->loadModel('user')->getPairs('noletter');
        $this->view->projects         = array('') + $this->project->getPairsByProgram();
        $this->view->status           = $status;
        $this->view->from             = $from;
        $this->view->param            = $param;
        $this->view->showBatchEdit    = $this->cookie->showExecutionBatchEdit;
        $this->view->avatarList       = $this->user->getAvatarPairs('');

        $this->display();
    }

    /**
     * 显示白名单信息。
     * Get white list personnel.
     *
     * @param  int    $executionID
     * @param  string $module
     * @param  string $objectType
     * @param  string $orderby
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function whitelist($executionID = 0, $module='execution', $objectType = 'sprint', $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        /* use first execution if executionID does not exist. */
        if(!isset($this->executions[$executionID])) $executionID = key($this->executions);

        /* Set the menu. If the executionID = 0, use the indexMenu instead. */
        $this->execution->setMenu($executionID);

        $execution = $this->execution->getByID($executionID);
        if(!empty($execution->acl) and $execution->acl != 'private') return $this->sendError($this->lang->whitelistNotNeed, $this->createLink('execution', 'task', "executionID=$executionID"));

        echo $this->fetch('personnel', 'whitelist', "objectID=$executionID&module=$module&browseType=$objectType&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID");
    }

    /**
     * 添加用户到白名单。
     * Adding users to the white list.
     *
     * @param  int     $executionID
     * @param  int     $deptID
     * @param  int     $copyID
     * @access public
     * @return void
     */
    public function addWhitelist($executionID = 0, $deptID = 0, $copyID = 0)
    {
        /* use first execution if executionID does not exist. */
        if(!isset($this->executions[$executionID])) $executionID = key($this->executions);

        /* Set the menu. If the executionID = 0, use the indexMenu instead. */
        $this->execution->setMenu($executionID);

        $execution = $this->execution->getByID($executionID);
        if(!empty($execution->acl) and $execution->acl != 'private') return $this->sendError($this->lang->whitelistNotNeed, $this->createLink('execution', 'task', "executionID=$executionID"));

        echo $this->fetch('personnel', 'addWhitelist', "objectID=$executionID&dept=$deptID&copyID=$copyID&objectType=sprint&module=execution");
    }

    /*
     * 从白名单中移除用户。
     * Removing users from the white list.
     *
     * @param  int     $id
     * @param  string  $confirm
     * @access public
     * @return void
     */
    public function unbindWhitelist($id = 0, $confirm = 'no')
    {
        echo $this->fetch('personnel', 'unbindWhitelist', "id=$id&confirm=$confirm");
    }

    /**
     * 执行列表导出数据。
     * Export execution.
     *
     * @param  string $status
     * @param  int    $productID
     * @param  string $orderBy
     * @param  string $from
     * @access public
     * @return void
     */
    public function export($status, $productID, $orderBy, $from)
    {
        if($_POST)
        {
            $executionLang   = $this->lang->execution;
            $executionConfig = $this->config->execution;

            $projectID = $from == 'project' ? $this->session->project : 0;
            if($projectID) $this->project->setMenu($projectID);
            $project = $this->project->getByID($projectID);

            /* Create field lists. */
            $fields = $this->post->exportFields ? $this->post->exportFields : explode(',', $executionConfig->list->exportFields);
            foreach($fields as $key => $fieldName)
            {
                if($fieldName == 'name' and $this->app->tab == 'project' and ($project->model == 'agileplus' or $project->model == 'waterfallplus')) $fields['method'] = $executionLang->method;

                $fieldName = trim($fieldName);
                $fields[$fieldName] = zget($executionLang, $fieldName);
                unset($fields[$key]);
            }

            $users = $this->loadModel('user')->getPairs('noletter');
            $executionStats = $this->execution->getStatData($projectID, $status == 'byproduct' ? 'all' : $status, $productID, 0, false, 'withchild', $orderBy);
            $executionStats = $this->flattenObjectArray($executionStats);
            foreach($executionStats as $i => $execution)
            {
                $execution->PM            = zget($users, $execution->PM);
                $execution->status        = isset($execution->delay) ? $executionLang->delayed : $this->processStatus('execution', $execution);
                $execution->progress     .= '%';
                $execution->name          = isset($execution->title) ? $execution->title : $execution->name;
                if(isset($executionStats[$execution->parent])) $execution->name = $executionStats[$execution->parent]->name . '/' . $execution->name;
                if($this->app->tab == 'project' and ($project->model == 'agileplus' or $project->model == 'waterfallplus')) $execution->method = zget($executionLang->typeList, $execution->type);

                if($this->post->exportType == 'selected')
                {
                    $checkedItem = $this->cookie->checkedItem;
                    if(strpos(",$checkedItem,", ",pid{$execution->id},") === false) unset($executionStats[$i]);
                }
            }

            if($this->config->edition != 'open') list($fields, $executionStats) = $this->loadModel('workflowfield')->appendDataFromFlow($fields, $executionStats);

            $this->post->set('fields', $fields);
            $this->post->set('rows', $executionStats);
            $this->post->set('kind', $this->lang->execution->common);
            $this->fetch('file', 'export2' . $this->post->fileType, $_POST);
        }

        $executionConcept = $this->lang->execution->common;
        if($this->app->tab == 'project')
        {
            $project = $this->project->getByID($this->session->project);
            if(!empty($project->model) && $project->model == 'waterfall') $executionConcept = $this->lang->project->stage;
            if(!empty($project->model) && $project->model == 'scrum')     $executionConcept = $this->lang->executionCommon;
        }

        $this->view->fileName = (in_array($status, array('all', 'undone')) ? $this->lang->execution->$status : $this->lang->execution->statusList[$status]) . $executionConcept;
        $this->display();
    }

    /**
     * 在迭代中查看迭代的文档。
     * Doc for compatible.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function doc($executionID = 0, $libID = 0, $moduleID = 0, $browseType = 'all', $orderBy = 'order_asc', $param = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1, $mode = 'list', $docID = 0, $search = '')
    {
        echo $this->fetch('doc', 'app', "type=execution&spaceID=$executionID&libID=$libID&moduleID=$moduleID&docID=$docID&mode=$mode&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID&filterType=$browseType&search=$search&noSpace=true");
    }

    /**
     * 将计划中的需求关联到此项目或迭代/执行或看板中。
     * Import the stories to execution/project/kanban by product plan.
     *
     * @param  int    $executionID
     * @param  int    $planID
     * @param  int    $productID
     * @param  string $extra
     * @access public
     * @return void
     */
    public function importPlanStories($executionID, $planID, $productID = 0, $extra = '')
    {
        $execution   = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch();
        $planStories = array();
        $planStory   = $this->loadModel('story')->getPlanStories($planID);
        $draftCount  = 0;
        if(!empty($planStory))
        {
            $projectProducts = $this->loadModel('project')->getBranchesByProject($executionID);
            foreach($planStory as $id => $story)
            {
                $projectBranches = zget($projectProducts, $story->product, array());
                if($story->status != 'active' or (!empty($story->branch) and !empty($projectBranches) and !isset($projectBranches[$story->branch])))
                {
                    $draftCount++;
                    unset($planStory[$id]);
                }
            }

            $planStories = array_keys($planStory);
            if($execution->project != 0) $this->execution->linkStory($execution->project, $planStories); /* Link story to the project of the execution. */
            $this->execution->linkStory($executionID, $planStories, $extra); /* Link story to the execution. */
        }

        $moduleName = 'execution';
        if($execution->type == 'project') $moduleName = 'projectstory';
        if($execution->type == 'kanban')
        {
            global $lang;
            $lang->executionCommon = $lang->execution->kanban;
            include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php';
        }

        /* Check if the product is multiple branch. */
        $multiBranchProduct = $this->executionZen->hasMultipleBranch($productID, $executionID);

        if($draftCount != 0)
        {
            $importPlanStoryTips = $multiBranchProduct ? $this->lang->execution->haveBranchDraft : $this->lang->execution->haveDraft;
            $haveDraft           = sprintf($importPlanStoryTips, $draftCount);
            if(!$execution->multiple || $moduleName == 'projectstory') $haveDraft = str_replace($this->lang->executionCommon, $this->lang->projectCommon, $haveDraft);
            return $this->send(array('closeModal' => true, 'load' => true, 'result' => 'fail', 'message' => $haveDraft));
        }

        return $this->sendSuccess(array('closeModal' => true, 'load' => true));
    }

    /**
     * 在执行/迭代的树状图中查看某个需求的详情。
     * View a story in the tree view of the execution.
     *
     * @param int $storyID
     * @param int $version  The version of story.
     *
     * @access public
     * @return void
     */
    public function treeStory($storyID, $version = 0)
    {
        $this->loadModel('requirement');
        $this->loadModel('epic');

        $story   = $this->loadModel('story')->getById($storyID, $version, true);
        $product = $this->dao->findById($story->product)->from(TABLE_PRODUCT)->fields('name, id, `type`, shadow')->fetch();

        $this->view->story       = $story;
        $this->view->product     = $product;
        $this->view->branches    = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id);
        $this->view->plan        = $this->dao->findById($story->plan)->from(TABLE_PRODUCTPLAN)->fields('title')->fetch('title');
        $this->view->bugs        = $this->dao->select('id,title')->from(TABLE_BUG)->where('story')->eq($storyID)->andWhere('deleted')->eq(0)->fetchAll();
        $this->view->fromBug     = $this->dao->select('id,title')->from(TABLE_BUG)->where('toStory')->eq($storyID)->fetch();
        $this->view->cases       = $this->dao->select('id,title')->from(TABLE_CASE)->where('story')->eq($storyID)->andWhere('deleted')->eq(0)->fetchAll();
        $this->view->modulePath  = $this->loadModel('tree')->getParents($story->module);
        $this->view->users       = $this->loadModel('user')->getPairs('noletter');
        $this->view->executions  = $this->loadModel('execution')->getPairs(0, 'all', 'nocode');
        $this->view->actions     = $this->loadModel('action')->getList('story', $storyID);
        $this->view->version     = $version == 0 ? $story->version : $version;
        $this->view->executionID = $this->session->execution;
        $this->display();
    }

    /**
     * 在执行/迭代的树状图中查看某个任务的详情。
     * View a task in the tree view of the execution.
     *
     * @param int $taskID
     *
     * @access public
     * @return void
     */
    public function treeTask($taskID)
    {
        $this->loadModel('task');
        $task = $this->task->getById($taskID, true);
        if($task->fromBug != 0)
        {
            $bug = $this->loadModel('bug')->getById($task->fromBug);
            $task->bugSteps = '';
            if($bug)
            {
                $task->bugSteps = $this->loadModel('file')->setImgSize($bug->steps);
                foreach($bug->files as $file) $task->files[] = $file;
            }
            $this->view->fromBug = $bug;
        }
        else
        {
            $story = $this->loadModel('story')->getById($task->story);
            $task->storySpec   = empty($story) ? '' : $this->loadModel('file')->setImgSize($story->spec);
            $task->storyVerify = empty($story) ? '' : $this->loadModel('file')->setImgSize($story->verify);
            $task->storyFiles  = $this->loadModel('file')->getByObject('story', $task->story);
        }

        if($task->team) $this->lang->task->assign = $this->lang->task->transfer;

        /* Update action. */
        if($task->assignedTo == $this->app->user->account) $this->loadModel('action')->read('task', $taskID);

        $this->view->task      = $task;
        $this->view->execution = $this->execution->getById($task->execution);
        $this->view->actions   = $this->loadModel('action')->getList('task', $taskID);
        $this->view->users     = $this->loadModel('user')->getPairs('noletter');
        $this->display();
    }

    /**
     * 获取泳道分组下拉。
     * Get the lane group drop-down by ajax.
     *
     * @param  string $type all|syory|task|bug
     * @param  string $group
     * @access public
     * @return void
     */
    public function ajaxGetGroup($type, $group = 'default')
    {
        $this->app->loadLang('kanban');

        return print(html::select("group", $this->lang->kanban->group->$type, $group, 'class="form-control chosen" data-max_drop_width="215"'));
    }

    /**
     * 更新看板数据。
     * Update kanban by ajax.
     *
     * @param  int    $executionID
     * @param  int    $enterTime
     * @param  string $browseType  all|story|task|bug
     * @param  string $groupBy     default|pri|category|module|source|assignedTo|type|story|severity
     * @param  string $from        taskkanban|execution
     * @param  string $searchValue
     * @param  string $orderBy
     * @access public
     * @return void
     */
    public function ajaxUpdateKanban($executionID = 0, $enterTime = 0, $browseType = '', $groupBy = '', $from = 'taskkanban', $searchValue = '', $orderBy = 'id_asc')
    {
        $this->loadModel('kanban');
        if($groupBy == 'story' and $browseType == 'task' and !isset($this->lang->kanban->orderList[$orderBy])) $orderBy = 'pri_asc';

        if($from == 'execution') $this->session->set('taskSearchValue', $searchValue);
        if($from == 'RD')        $this->session->set('rdSearchValue', $searchValue);

        $lastEditedTime = $this->execution->getLaneMaxEditedTime($executionID);
        $enterTime      = date('Y-m-d H:i:s', $enterTime);
        if(in_array(true, array(empty($lastEditedTime), strtotime($lastEditedTime) < 0, $lastEditedTime > $enterTime, $groupBy != 'default', !empty($searchValue))))
        {
            if($from == 'taskkanban')
            {
                list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, $searchValue, $orderBy);
            }
            else
            {
                $kanbanGroup = $this->kanban->getRDKanban($executionID, $browseType, $orderBy, 0, $groupBy, $searchValue);
            }
            return print(json_encode($kanbanGroup));
        }
    }

    /**
     * 创建执行时，根据项目ID获取可以复制的执行。
     * When the create an execution, get execution list that can be copied by the project ID.
     *
     * @param  int    $projectID
     * @param  int    $copyExecutionID
     * @access public
     * @return void
     */
    public function ajaxGetCopyProjectExecutions($projectID = 0, $copyExecutionID = 0)
    {
        $project = $this->loadModel('project')->fetchByID($projectID);
        if($project) $this->executionZen->correctExecutionCommonLang($project, zget($this->config->execution->modelList, $project->model, 'scrum'));

        $this->view->executions      = $this->execution->getList($projectID, 'all', 'all', 0, 0, 0, null, false);
        $this->view->copyExecutionID = $copyExecutionID;
        $this->display();
    }

    /**
     * 获取专业研发看板的数据。
     * Get execution kanban data by ajax.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function ajaxGetExecutionKanban($executionID)
    {
        $execution = $this->execution->getByID($executionID);
        if($this->app->tab == 'execution' and $execution->type == 'kanban')
        {
            $execLaneType  = $this->session->execLaneType ? $this->session->execLaneType : 'all';
            $execGroupBy   = $this->session->execGroupBy ? $this->session->execGroupBy : 'default';
            $rdSearchValue = $this->session->rdSearchValue ? $this->session->rdSearchValue : '';
            $kanbanData    = $this->loadModel('kanban')->getRDKanban($executionID, $execLaneType, 'id_desc', 0, $execGroupBy, $rdSearchValue);
            echo json_encode($kanbanData);
        }
    }

    /**
     * 展示燃尽图。
     * Display burn chart.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function ajaxGetBurn($executionID)
    {
        $execution = $this->execution->getByID($executionID, true);
        $type      = 'noweekend';
        if(((strpos('closed,suspended', $execution->status) === false && helper::today() > $execution->end)
            || ($execution->status == 'closed'    && substr($execution->closedDate ?? '', 0, 10) > $execution->end)
            || ($execution->status == 'suspended' && $execution->suspendedDate > $execution->end))
            && strpos($type, 'delay') === false)
        {
            $type .= ',withdelay';
        }

        $deadline = $execution->status == 'closed' ? substr($execution->closedDate ?? '', 0, 10) : $execution->suspendedDate;
        $deadline = strpos('closed,suspended', $execution->status) === false ? helper::today() : $deadline;
        $endDate  = strpos($type, 'withdelay') !== false ? $deadline : $execution->end;
        list($dateList, $interval) = $this->execution->getDateList($execution->begin, $endDate, $type, 0, 'Y-m-d', $execution->end);

        $executionEnd = strpos($type, 'withdelay') !== false ? $execution->end : '';
        $this->view->chartData = $this->execution->buildBurnData($executionID, $dateList, 'left', $executionEnd);
        $this->view->execution = $execution;

        $this->display();
    }

    /**
     * 展示累计流图。
     * Display cumulative flow diagram.
     *
     * @param  int    $executionID
     * @access public
     * @return void
     */
    public function ajaxGetCFD($executionID)
    {
        $execution = $this->execution->getById($executionID, true);

        $this->app->loadClass('date');
        list($begin, $end) = $this->execution->getBeginEnd4CFD($execution);
        $dateList  = date::getDateList($begin, $end, 'Y-m-d', 'noweekend');
        $chartData = $this->execution->buildCFDData($executionID, $dateList, 'task');
        if(isset($chartData['line'])) $chartData['line'] = array_reverse($chartData['line']);

        $this->view->begin     = helper::safe64Encode(urlencode($begin));
        $this->view->end       = helper::safe64Encode(urlencode($end));
        $this->view->execution = $execution;
        $this->view->chartData = $chartData;

        $this->display();
    }

    /**
     * Ajax get product drop menu.
     *
     * @param  int    $executionID
     * @param  int    $productID
     * @access public
     * @return void
     */
    public function ajaxSwitcherMenu($executionID, $productID, $currentMethod = '')
    {
        $this->view->link        = helper::createLink('execution', $currentMethod, "executionID=$executionID&productID={id}");
        $this->view->productID   = $productID;
        $this->view->products    = $this->loadModel('product')->getProducts($executionID);
        $this->view->executionID = $executionID;
        $this->display();
    }

    /**
     * Ajax get execution drop menu.
     * 通过Ajax获取执行下拉菜单。
     *
     * @param  int    $projectID
     * @param  int    $executionID
     * @param  string $method
     * @access public
     * @return void
     */
    public function ajaxGetExecutionSwitcherMenu($projectID, $executionID, $method = '')
    {
        $this->view->link        = helper::createLink('execution', $method, 'executionID={id}');
        $this->view->executionID = $executionID;
        $this->view->executions  = $this->execution->getList($projectID, 'kanban');

        $this->display();
    }

    /**
     * Flatten Object Array.
     *
     * @param  array  $array
     * @access public
     * @return void
     */
    public function flattenObjectArray($array = array())
    {
        $result = array();

        foreach ($array as $key => $object) {
            $result[$object->id] = $object;

            if (isset($object->children) && is_array($object->children)) {
                $result = array_replace($result, $this->flattenObjectArray($object->children));
            }
        }

        return $result;
    }

    /**
     * AJAX: 获取有未关闭任务的执行。
     * AJAX: get executions that have unclosed tasks.
     *
     * @param  string $executionIdList
     * @access public
     * @return string
     */
    public function ajaxGetNonClosableExecutions($executionIdList)
    {
        $executionIdList = str_replace('pid', '', $executionIdList);
        $tasks           = $this->loadModel('task')->getUnclosedTasksByExecution(explode(',', $executionIdList));
        $tasks           = array_filter($tasks);
        $executions      = !empty($tasks) ? $this->execution->getPairsByList(array_keys($tasks)) : array();

        return print(json_encode(array_values($executions)));
    }
}
