<?php
/**
 * The control file of kanban module of ZenTaoPMS.
 *
 * @copyright   Copyright 2009-2023 禅道软件（青岛）有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
 * @license     ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
 * @author      Guangming Sun<sunguangming@easycorp.ltd>
 * @package     kanban
 * @version     $Id: control.php 4460 2021-10-26 11:03:02Z chencongzhi520@gmail.com $
 * @link        https://www.zentao.net
 */
class kanban extends control
{
    /**
     * 看板空间列表。
     * Kanban space.
     *
     * @param  string $browseType involved|cooperation|public|private
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function space($browseType = 'involved', $recTotal = 0, $recPerPage = 15, $pageID = 1)
    {
        $this->session->set('regionID', 'all', 'kanban');

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

        $this->view->title         = $this->lang->kanbanspace->common;
        $this->view->browseType    = $browseType;
        $this->view->pager         = $pager;
        $this->view->spaceList     = $this->kanban->getSpaceList($browseType, $pager, $this->cookie->showClosed);
        $this->view->unclosedSpace = $this->kanban->getCanViewObjects('kanbanspace', 'noclosed');
        $this->view->users         = $this->loadModel('user')->getPairs('noletter|nodeleted');
        $this->view->userIdPairs   = $this->user->getPairs('noletter|nodeleted|showid');
        $this->view->usersAvatar   = $this->user->getAvatarPairs();

        $this->display();
    }

    /**
     * 创建看板空间。
     * Create a space.
     *
     * @param  string $type
     * @access public
     * @return void
     */
    public function createSpace($type = 'private')
    {
        if(!empty($_POST))
        {
            $this->lang->kanban->name = $this->lang->kanbanspace->name;
            $space = form::data($this->config->kanban->form->createSpace)
                ->setDefault('createdBy', $this->app->user->account)
                ->setDefault('createdDate', helper::now())
                ->get();

            $this->kanban->createSpace($space);

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

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

        $this->view->title = $this->lang->kanban->createSpace;
        $this->view->users = $this->loadModel('user')->getPairs('noclosed|nodeleted');
        $this->view->type  = $type;

        $this->display();
    }

    /**
     * 编辑看板空间。
     * Edit a space.
     *
     * @param  int    $spaceID
     * @access public
     * @return void
     */
    public function editSpace($spaceID)
    {
        if(!empty($_POST))
        {
            $this->lang->kanban->name = $this->lang->kanbanspace->name;
            $space = form::data($this->config->kanban->form->editSpace)
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->setDefault('lastEditedDate', helper::now())
                ->get();

            $this->kanban->updateSpace($space, $spaceID);

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

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

        $space = $this->kanban->getSpaceById($spaceID);
        if(in_array($space->type, array('cooperation', 'public'))) unset($this->lang->kanbanspace->typeList['private']);

        $this->view->spaceID     = $spaceID;
        $this->view->space       = $space;
        $this->view->users       = $this->loadModel('user')->getPairs('noclosed');
        $this->view->typeList    = $this->lang->kanbanspace->typeList;
        $this->view->team        = ($space->type == 'private') ? $space->whitelist : $space->team;
        $this->view->defaultType = $space->type;

        $this->display();
    }

    /**
     * 激活看板空间。
     * Activate a space.
     *
     * @param  int    $spaceID
     * @access public
     * @return void
     */
    public function activateSpace($spaceID)
    {
        $this->loadModel('action');

        if(!empty($_POST))
        {
            $space = form::data($this->config->kanban->form->activateSpace)
                ->add('closedBy', '')
                ->add('closedDate', null)
                ->add('activatedBy', $this->app->user->account)
                ->add('lastEditedBy', $this->app->user->account)
                ->get();

            $this->kanban->activateSpace($spaceID, $space);

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

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

        $this->view->space   = $this->kanban->getSpaceById($spaceID);
        $this->view->actions = $this->action->getList('kanbanSpace', $spaceID);
        $this->view->users   = $this->loadModel('user')->getPairs('noletter|noclosed|nodeleted');

        $this->display();
    }

    /*
     * 关闭看板空间。
     * Close a space.
     *
     * @param  int    $spaceID
     * @access public
     * @return void
     */
    /**
     * @param int $spaceID
     */
    public function closeSpace($spaceID)
    {
        if(!empty($_POST))
        {
            $space = form::data($this->config->kanban->form->closeSpace)
                ->add('activatedBy', '')
                ->add('activatedDate', null)
                ->add('closedBy', $this->app->user->account)
                ->add('lastEditedBy', $this->app->user->account)
                ->get();

            $this->kanban->closeSpace($spaceID, $space);

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

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

        $this->view->space   = $this->kanban->getSpaceById($spaceID);
        $this->view->actions = $this->loadModel('action')->getList('kanbanSpace', $spaceID);
        $this->view->users   = $this->loadModel('user')->getPairs('noletter');

        $this->display();
    }

    /**
     * 删除看板空间。
     * Delete a space.
     *
     * @param  int    $spaceID
     * @access public
     * @return void
     */
    public function deleteSpace($spaceID)
    {
        $this->kanban->delete(TABLE_KANBANSPACE, $spaceID);
        return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'closeModal' => true));
    }

    /**
     * 创建看板。
     * Create a kanban.
     *
     * @param  int    $spaceID
     * @param  string $type
     * @param  int    $copyKanbanID
     * @param  string $extra
     * @access public
     * @return void
     */
    public function create($spaceID = 0, $type = 'private', $copyKanbanID = 0, $extra = '')
    {
        if(!empty($_POST))
        {
            $kanban = form::data($this->config->kanban->form->create)
                ->setDefault('createdBy', $this->app->user->account)
                ->get();
            $this->kanban->create($kanban);

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

        $this->kanbanZen->assignCreateVars($spaceID, $type, $copyKanbanID, $extra);
    }

    /**
     * 编辑看板。
     * Edit a kanban.
     *
     * @param  int    $kanbanID
     * @access public
     * @return void
     */
    public function edit($kanbanID = 0)
    {
        if(!empty($_POST))
        {
            $kanban = form::data($this->config->kanban->form->edit)
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->get();
            $this->kanban->update($kanbanID, $kanban);

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

            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => "loadCurrentPage('#header,#kanban-{$kanbanID}')"));
        }

        $kanban = $this->kanban->getByID($kanbanID);

        $space      = $this->kanban->getSpaceById($kanban->space);
        $spaceUsers = trim($space->owner) . ',' . trim($space->team);
        $users      = $this->loadModel('user')->getPairs('noclosed|nodeleted');
        $ownerPairs = $this->user->getPairs('noclosed|nodeleted', '', 0, $spaceUsers);

        $this->view->users      = $users;
        $this->view->ownerPairs = $ownerPairs;
        $this->view->spacePairs = array($kanban->space => $space->name) + $this->kanban->getSpacePairs($space->type);
        $this->view->kanban     = $kanban;
        $this->view->type       = $space->type;

        $this->display();
    }

    /**
     * Setting kanban.
     *
     * @param  int    $kanbanID
     * @access public
     * @return void
     */
    public function setting($kanbanID = 0)
    {
        if(!empty($_POST))
        {
            $kanban = form::data($this->config->kanban->form->setting)->get();
            $this->kanban->setting($kanbanID, $kanban);

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

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

        $kanban = $this->kanban->getByID($kanbanID);

        $this->view->kanban        = $kanban;
        $this->view->laneCount     = $this->kanban->getLaneCount($kanbanID);
        $this->view->heightType    = $kanban->displayCards > 2 ? 'custom' : 'auto';
        $this->view->displayCards  = $kanban->displayCards ? $kanban->displayCards : '';
        $this->view->enableImport  = empty($kanban->object) ? 'off' : 'on';
        $this->view->importObjects = empty($kanban->object) ? array() : explode(',', $kanban->object);

        $this->display();
    }

    /*
     * 激活看板。
     * Activate a kanban.
     *
     * @param  int    $kanbanID
     * @access public
     * @return void
     */
    /**
     * @param int $kanbanID
     */
    public function activate($kanbanID)
    {
        if(!empty($_POST))
        {
            $kanban = form::data($this->config->kanban->form->activate)
                ->setDefault('closedBy', '')
                ->setDefault('closedDate', null)
                ->setDefault('activatedBy', $this->app->user->account)
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->get();
            $this->kanban->activate($kanbanID, $kanban);

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

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

        $this->view->kanban  = $this->kanban->getByID($kanbanID);
        $this->view->actions = $this->loadModel('action')->getList('kanban', $kanbanID);
        $this->view->users   = $this->loadModel('user')->getPairs('noletter');

        $this->display();
    }

    /*
     * 关闭看板。
     * Close a kanban.
     *
     * @param  int    $kanbanID
     * @access public
     * @return void
     */
    /**
     * @param int $kanbanID
     */
    public function close($kanbanID)
    {
        if(!empty($_POST))
        {
            $kanban = form::data($this->config->kanban->form->close)
                ->setDefault('activatedBy', '')
                ->setDefault('activatedDate', null)
                ->setDefault('closedBy', $this->app->user->account)
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->get();
            $this->kanban->close($kanbanID, $kanban);

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

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

        $this->view->kanban  = $this->kanban->getByID($kanbanID);
        $this->view->actions = $this->loadModel('action')->getList('kanban', $kanbanID);
        $this->view->users   = $this->loadModel('user')->getPairs('noletter');

        $this->display();
    }

     /**
      * 看板视图页面。
      * View a kanban.
      *
      * @param  int        $kanbanID
      * @param  string|int $regionID
      * @access public
      * @return void
      */
    public function view($kanbanID, $regionID = '')
    {
        $kanban = $this->kanban->getByID($kanbanID);

        if(!$kanban)
        {
            if(defined('RUN_MODE') && RUN_MODE == 'api') return $this->send(array('status' => 'fail', 'code' => 404, 'message' => '404 Not found'));
            return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->createLink('kanban', 'space'))));
        }

        $kanbanIdList = $this->kanban->getCanViewObjects();
        if(!$this->app->user->admin and !in_array($kanbanID, $kanbanIdList)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->kanban->accessDenied, 'locate' => $this->createLink('kanban', 'space'))));

        $regions = $this->kanban->getRegionPairs($kanbanID);
        if(!$regionID) $regionID = $this->session->regionID ? $this->session->regionID : 'all';
        $regionID = !isset($regions[$regionID]) ? 'all' : $regionID;
        $this->session->set('regionID', $regionID, 'kanban');

        $this->kanbanZen->setUserAvatar();

        $this->view->title       = $this->lang->kanban->view;
        $this->view->kanban      = $kanban;
        $this->view->regions     = $regions;
        $this->view->kanbanList  = $this->kanban->getKanbanData($kanbanID, $regionID == 'all' ? '' : array($regionID));
        $this->view->regionID    = $regionID;
        $this->view->pageToolbar = $this->kanban->getPageToolBar($kanban);

        $this->display();
    }

    /**
     * 删除看板。
     * Delete a kanban.
     *
     * @param  int    $kanbanID
     * @access public
     * @return void
     */
    public function delete($kanbanID)
    {
        $this->kanban->delete(TABLE_KANBAN, $kanbanID);
        return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $this->createLink('kanban', 'space')));
    }

    /**
     * 创建看板区域。
     * Create a region.
     *
     * @param  int    $kanbanID
     * @param  string $from kanban|execution
     * @access public
     * @return void
     */
    public function createRegion($kanbanID, $from = 'kanban')
    {
        if(!empty($_POST))
        {
            $kanban       = $from == 'execution' ? $this->loadModel('execution')->getByID($kanbanID) : $this->kanban->getByID($kanbanID);
            $copyRegionID = $this->post->region == 'custom' ? 0 : $this->post->region;

            $regionID = $this->kanban->createRegion($kanban, null, (int)$copyRegionID, $from);

            $this->session->set('regionID', $regionID, 'kanban');
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'closeModal' => true));
        }

        $regionPairs = array();
        $regions     = $this->kanban->getRegionPairs($kanbanID, 0, $from);
        foreach($regions as $regionID => $region)
        {
            if(mb_strlen($region, 'UTF-8') > 20) $region = mb_substr($region, 0, 20, 'UTF-8') . '...';
            $regionPairs[$regionID] = $this->lang->kanban->copy . '"' . $region . '"' . $this->lang->kanban->styleCommon;
        }

        $this->view->regions = array('custom' => $this->lang->kanban->custom) + $regionPairs;
        $this->display();
    }

    /*
     * 编辑区域。
     * Edit a region
     *
     * @param  int    $regionID
     * @access public
     * @return void
     */
    /**
     * @param int $regionID
     */
    public function editRegion($regionID = 0)
    {
        if(!empty($_POST))
        {
            $this->kanban->updateRegion($regionID);

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

        $this->view->region = $this->kanban->getRegionByID($regionID);
        $this->display();
    }

    /**
     * 排序区域。
     * Sort regions.
     *
     * @param  string $regions
     * @access public
     * @return void
     */
    public function sortRegion($regions = '')
    {
        if(empty($regions)) return;
        $regionIdList = explode(',', trim($regions, ','));

        $this->kanban->updateRegionSort($regionIdList);

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

    /**
     * 排序分组。
     * Sort group.
     *
     * @param  int    $region
     * @param  string $groups
     * @access public
     * @return void
     */
    public function sortGroup($region, $groups)
    {
        $groups = array_filter(explode(',', trim($groups, ',')));
        if(empty($groups)) return $this->send(array('result' => 'fail', 'message' => 'No groups to sort.'));

        $this->kanban->sortGroup($region, $groups);
        if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

        return $this->send(array('result' => 'success'));
    }

    /**
     * 删除区域。
     * Delete a region
     *
     * @param  int    $regionID
     * @access public
     * @return void
     */
    public function deleteRegion($regionID)
    {
        $this->kanban->delete(TABLE_KANBANREGION, $regionID);
        return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true));
    }

    /**
     * 创建看板泳道。
     * Create a lane for a kanban.
     *
     * @param  int    $kanbanID
     * @param  int    $regionID
     * @param  string $from kanban|execution
     * @access public
     * @return void
     */
    public function createLane($kanbanID, $regionID, $from = 'kanban')
    {
        if(!empty($_POST))
        {
            $this->lang->kanban->name = $this->lang->kanbanlane->name;
            $lane = form::data($this->config->kanban->form->createLane)->add('region', $regionID)->get();
            $this->kanban->createLane($kanbanID, $regionID, $lane, 'new');

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

        $laneType = $from == 'kanban' ? 'all' : 'story';
        if($this->config->vision == 'lite') $laneType = 'task';

        $this->view->lanes    = $this->kanban->getLanePairsByRegion($regionID, $laneType);
        $this->view->from     = $from;
        $this->view->regionID = $regionID;

        if($from == 'kanban') $this->display();
        if($from == 'execution') $this->display('kanban', 'createexeclane');
    }

    /**
     * 泳道排序。
     * Sort lanes.
     *
     * @param  int    $regionID
     * @param  string $lanes
     * @access public
     * @return void
     */
    public function sortLane($regionID, $lanes = '')
    {
        if(empty($lanes)) return;
        $lanes = explode(',', trim($lanes, ','));

        $this->kanban->updateLaneSort($regionID, $lanes);
        if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

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

    /**
     * 排序看板列。
     * Sort columns.
     *
     * @param  int    $regionID
     * @param  string $columns
     * @access public
     * @return void
     */
    public function sortColumn($regionID, $columns = '')
    {
        if(empty($columns)) return;
        $columns = explode(',', trim($columns, ','));

        $this->kanban->updateColumnSort($regionID, $columns);
        if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

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

    /**
     * 删除看板泳道。
     * Delete a lane.
     *
     * @param  int    $regionID
     * @param  int    $laneID
     * @access public
     * @return void
     */
    public function deleteLane($regionID, $laneID)
    {
        $this->kanban->delete(TABLE_KANBANLANE, $laneID);

        if($this->app->tab == 'execution')
        {
            if(dao::isError()) return $this->sendError(dao::getError());
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true));
        }

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

    /**
     * 创建看板列。
     * Create a column for a kanban.
     *
     * @param  int    $fromColumnID
     * @param  string $position left|right
     * @access public
     * @return void
     */
    public function createColumn($fromColumnID, $position = 'left')
    {
        $fromColumn = $this->kanban->getColumnByID($fromColumnID);

        if($_POST)
        {
            $this->lang->kanban->name = $this->lang->kanbancolumn->name;
            $order  = $position == 'left' ? $fromColumn->order : $fromColumn->order + 1;
            $column = form::data($this->config->kanban->form->createColumn)
                ->setDefault('order', $order)
                ->setDefault('region', $fromColumn->region)
                ->get();
            $this->kanban->createColumn($fromColumn->region, $column, 'kanban', 'new');
            if(dao::isError()) $this->send(array('message' => dao::getError(), 'result' => 'fail'));

            $region   = $this->kanban->getRegionByID($fromColumn->region);
            $callback = $this->kanban->getKanbanCallback($region->kanban, $region->id);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        $this->view->title      = $this->lang->kanban->createColumn;
        $this->view->fromColumn = $fromColumn;
        $this->view->position   = $position;
        $this->display();
    }

    /**
     * 拆分看板列。
     * Split column.
     *
     * @param  int    $columnID
     * @access public
     * @return void
     */
    public function splitColumn($columnID)
    {
        if(!empty($_POST))
        {
            $this->lang->kanban->name = $this->lang->kanbancolumn->childName;
            $columns = form::batchData($this->config->kanban->form->splitColumn)->get();
            $this->kanban->splitColumn($columnID, $columns);
            if(dao::isError()) $this->send(array('message' => dao::getError(), 'result' => 'fail'));

            $column   = $this->kanban->getColumnById($columnID);
            $region   = $this->kanban->getRegionByID($column->region);
            $callback = $this->kanban->getKanbanCallback($region->kanban, $region->id);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        $this->view->column = $this->kanban->getColumnByID($columnID);
        $this->display();
    }

    /**
     * 归档看板列。
     * Archive a column.
     *
     * @param  int    $columnID
     * @access public
     * @return void
     */
    public function archiveColumn($columnID)
    {
        $column = $this->kanban->getColumnById($columnID);

        if($column->parent) $this->kanban->updateColumnParent($column);

        $this->kanban->archiveColumn($columnID);
        if(dao::isError()) $this->send(array('message' => dao::getError(), 'result' => 'fail'));

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

    /**
     * 还原看板列。
     * Restore a column.
     *
     * @param  int    $columnID
     * @access public
     * @return void
     */
    public function restoreColumn($columnID)
    {
        $this->kanban->restoreColumn($columnID);
        if(dao::isError()) $this->send(array('message' => dao::getError(), 'result' => 'fail'));

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

    /**
     * 查看归档看板列。
     * View archived columns.
     *
     * @param  int    $regionID
     * @access public
     * @return void
     */
    public function viewArchivedColumn($regionID)
    {
        $region = $this->kanban->getRegionByID($regionID);

        $this->view->kanban  = $this->kanban->getByID($region->kanban);
        $this->view->columns = $this->kanban->getColumnsByField('region', $regionID, '1');

        $this->display();
    }

    /**
     * 删除看板列。
     * Delete a column.
     *
     * @param  int    $columnID
     * @access public
     * @return void
     */
    public function deleteColumn($columnID)
    {
        $column = $this->kanban->getColumnById($columnID);
        if($column->parent > 0) $this->kanban->processCards($column);

        $this->dao->delete()->from(TABLE_KANBANCOLUMN)->where('id')->eq($columnID)->exec();

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

    /**
     * Create a card.
     *
     * @param  int    $kanbanID
     * @param  int    $regionID
     * @param  int    $groupID
     * @param  int    $columnID
     * @access public
     * @return void
     */
    public function createCard($kanbanID = 0, $regionID = 0, $groupID = 0, $columnID = 0)
    {
        if($_POST)
        {
            $this->lang->kanban->name = $this->lang->kanbancard->name;

            $card = form::data($this->config->kanban->form->createCard)
                ->add('kanban', $kanbanID)
                ->add('region', $regionID)
                ->add('group', $groupID)
                ->setDefault('createdBy', $this->app->user->account)
                ->get();
            $this->kanban->createCard($columnID, $card);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

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

        $kanban      = $this->kanban->getById($kanbanID);
        $kanbanUsers = $kanbanID == 0 ? ',' : trim($kanban->owner) . ',' . trim($kanban->team);
        $users       = $this->loadModel('user')->getPairs('noclosed|nodeleted', '', 0, $kanbanUsers);

        $this->view->users     = $users;
        $this->view->lanePairs = $this->kanban->getLanePairsByGroup($groupID);

        $this->display();
    }

    /**
     * 批量创建卡片。
     * Batch create cards.
     *
     * @param  int    $kanbanID
     * @param  int    $regionID
     * @param  int    $groupID
     * @param  int    $columnID
     * @access public
     * @return void
     */
    public function batchCreateCard($kanbanID = 0, $regionID = 0, $groupID = 0, $columnID = 0)
    {
        $kanban = $this->kanban->getById($kanbanID);

        if($_POST)
        {
            $cards = form::batchData($this->config->kanban->form->batchCreateCard)->get();
            $this->kanban->batchCreateCard($kanbanID, $regionID, $groupID, $columnID, $cards);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $callback = $this->kanban->getKanbanCallback($kanbanID, $regionID);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        $kanbanUsers = $kanbanID == 0 ? ',' : trim($kanban->owner) . ',' . trim($kanban->team);
        $users       = $this->loadModel('user')->getPairs('noclosed|nodeleted', '', 0, $kanbanUsers);

        $this->view->title     = $this->lang->kanban->batchCreateCard;
        $this->view->users     = $users;
        $this->view->lanePairs = $this->kanban->getLanePairsByGroup($groupID);

        $this->display();
    }

    /**
     * 编辑看板卡片。
     * Edit a card.
     *
     * @param  int    $cardID
     * @access public
     * @return void
     */
    public function editCard($cardID)
    {
        if(!empty($_POST))
        {
            $this->lang->kanban->name = $this->lang->kanbancard->name;
            $card = form::data($this->config->kanban->form->editCard)
                ->setDefault('lastEditedBy', $this->app->user->account)
                ->get();
            $this->kanban->updateCard($cardID, $card);

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

            $card     = $this->kanban->getCardByID($cardID);
            $callback = $this->kanban->getKanbanCallback($card->kanban, $card->region);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        $card        = $this->kanban->getCardByID($cardID);
        $kanban      = $this->kanban->getById($card->kanban);
        $kanbanUsers = $card->kanban == 0 ? ',' : trim($kanban->owner) . ',' . trim($kanban->team);
        $kanbanUsers = $this->loadModel('user')->getPairs('noclosed|nodeleted', '', 0, $kanbanUsers);
        $users       = $this->user->getPairs('noclosed|nodeleted');

        $this->view->card        = $card;
        $this->view->users       = $users;
        $this->view->kanbanUsers = $kanbanUsers;
        $this->view->kanban      = $kanban;

        $this->display();
    }

    /**
     * 完成卡片。
     * Finish a card.
     *
     * @param  int    $cardID
     * @access public
     * @return void
     */
    public function finishCard($cardID)
    {
        $oldCard = $this->kanban->getCardByID($cardID);
        $this->dao->update(TABLE_KANBANCARD)->set('progress')->eq(100)->set('status')->eq('done')->where('id')->eq($cardID)->exec();
        $card = $this->kanban->getCardByID($cardID);

        $changes = common::createChanges($oldCard, $card);

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

        $actionID = $this->loadModel('action')->create('kanbanCard', $cardID, 'finished');
        $this->action->logHistory($actionID, $changes);

        $callback = $this->kanban->getKanbanCallback($card->kanban, $card->region);
        return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
    }

    /**
     * 激活卡片。
     * Activate a card.
     *
     * @param  int    $cardID
     * @access public
     * @return void
     */
    public function activateCard($cardID)
    {
        $card = $this->kanban->getCardByID($cardID);

        if(!empty($_POST))
        {
            $this->kanban->activateCard($cardID);

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

            $callback = $this->kanban->getKanbanCallback($card->kanban, $card->region);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        $this->view->card    = $card;
        $this->view->actions = $this->loadModel('action')->getList('kanbancard', $cardID);
        $this->view->users   = $this->loadModel('user')->getPairs('noclosed|nodeleted');

        $this->display();
    }

    /**
     * 查看看板卡片详情。
     * View a card.
     *
     * @param  int    $cardID
     * @access public
     * @return void
     */
    public function viewCard($cardID)
    {
        $card   = $this->kanban->getCardByID($cardID);
        $kanban = $this->kanban->getByID($card->kanban);
        $space  = $this->kanban->getSpaceById($kanban->space);

        $this->view->card        = $card;
        $this->view->actions     = $this->loadModel('action')->getList('kanbancard', $cardID);
        $this->view->users       = $this->loadModel('user')->getPairs('noletter|nodeleted');
        $this->view->space       = $space;
        $this->view->kanban      = $kanban;
        $this->view->usersAvatar = $this->user->getAvatarPairs();

        $this->display();
    }

    /**
     * 看板移动卡片。
     * Move a card.
     *
     * @param  int    $cardID
     * @param  int    $fromColID
     * @param  int    $toColID
     * @param  int    $fromLaneID
     * @param  int    $toLaneID
     * @param  int    $kanbanID
     * @param  bool   $showModal
     * @access public
     * @return void
     */
    public function moveCard($cardID, $fromColID, $toColID, $fromLaneID, $toLaneID, $kanbanID = 0, $showModal = false)
    {
        if($showModal)
        {
            $this->kanbanZen->moveCardByModal($cardID);
            if(!$_POST) return;
        }
        else
        {
            $this->kanban->moveCard($cardID, $fromColID, $toColID, $fromLaneID, $toLaneID, $kanbanID);
        }
        if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

        $this->loadModel('action')->create('kanbanCard', $cardID, 'moved');

        $card     = $this->kanban->getCardByID($cardID);
        $callback = $this->kanban->getKanbanCallback($card->kanban, $card->region);
        return $this->send(array('result' => 'success', 'callback' => $callback, 'closeModal' => true));
    }

    /**
     * 转入其它看板的卡片。
     * Import card.
     *
     * @param  int $kanbanID
     * @param  int $regionID
     * @param  int $groupID
     * @param  int $columnID
     * @param  int $selectedKanbanID
     * @param  int $recTotal
     * @param  int $recPerPage
     * @param  int $pageID
     * @access public
     * @return void
     */
    public function importCard($kanbanID = 0, $regionID = 0, $groupID = 0, $columnID = 0, $selectedKanbanID = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        /* Load pager. */
        $this->app->loadClass('pager', true);
        $pager = new pager($recTotal, $recPerPage, $pageID);

        $cards2Imported = $this->kanban->getCards2Import($selectedKanbanID, $kanbanID, $pager);

        if($_POST)
        {
            $importedIDList = $this->kanban->importCard($kanbanID, $regionID, $groupID, $columnID);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            foreach($importedIDList as $cardID)
            {
                $this->loadModel('action')->create('kanbancard', (int)$cardID, 'importedcard', '', $cards2Imported[$cardID]->kanban);
            }

            $callback = $this->kanban->getKanbanCallback($kanbanID, $regionID);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        /* Find Kanban other than this kanban. */
        $kanbanPairs = $this->kanban->getKanbanPairs();
        unset($kanbanPairs[$kanbanID]);

        $this->view->cards2Imported   = $cards2Imported;
        $this->view->kanbanPairs      = array($this->lang->kanban->allKanban) + $kanbanPairs;
        $this->view->lanePairs        = $this->kanban->getLanePairsByGroup($groupID);
        $this->view->users            = $this->loadModel('user')->getPairs('noletter|nodeleted');
        $this->view->pager            = $pager;
        $this->view->selectedKanbanID = $selectedKanbanID;
        $this->view->kanbanID         = $kanbanID;
        $this->view->regionID         = $regionID;
        $this->view->groupID          = $groupID;
        $this->view->columnID         = $columnID;

        $this->display();
    }

    /**
     * 导入产品计划。
     * Import plan.
     *
     * @param  int    $kanbanID
     * @param  int    $regionID
     * @param  int    $groupID
     * @param  int    $columnID
     * @param  int    $selectedProductID
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function importPlan($kanbanID = 0, $regionID = 0, $groupID = 0, $columnID = 0, $selectedProductID = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        if($_POST)
        {
            $importedIDList = $this->kanban->importObject($kanbanID, $regionID, $groupID, $columnID, 'productplan');
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            foreach($importedIDList as $cardID => $planID)
            {
                $this->loadModel('action')->create('kanbancard', $cardID, 'importedProductplan', '', $planID);
            }

            $callback = $this->kanban->getKanbanCallback($kanbanID, $regionID);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

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

        $productPairs      = $this->kanban->getCanImportProducts('productplan');
        $selectedProductID = empty($selectedProductID) ? key($productPairs) : $selectedProductID;

        $this->view->products          = $productPairs;
        $this->view->selectedProductID = $selectedProductID;
        $this->view->lanePairs         = $this->kanban->getLanePairsByGroup($groupID);
        $this->view->plans2Imported    = $this->loadModel('productplan')->getList($selectedProductID, '0', 'all', $pager, 'begin_desc', 'skipparent|noproduct');
        $this->view->pager             = $pager;
        $this->view->kanbanID          = $kanbanID;
        $this->view->regionID          = $regionID;
        $this->view->groupID           = $groupID;
        $this->view->columnID          = $columnID;

        $this->display();
    }

    /**
     * 导入产品发布。
     * Import release.
     *
     * @param  int    $kanbanID
     * @param  int    $regionID
     * @param  int    $groupID
     * @param  int    $columnID
     * @param  int    $selectedProductID
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function importRelease($kanbanID = 0, $regionID = 0, $groupID = 0, $columnID = 0, $selectedProductID = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        if($_POST)
        {
            $importedIDList = $this->kanban->importObject($kanbanID, $regionID, $groupID, $columnID, 'release');
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            foreach($importedIDList as $cardID => $releaseID)
            {
                $this->loadModel('action')->create('kanbancard', $cardID, 'importedRelease', '', $releaseID);
            }

            $callback = $this->kanban->getKanbanCallback($kanbanID, $regionID);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

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

        $this->view->products          = $this->kanban->getCanImportProducts('release');
        $this->view->selectedProductID = $selectedProductID;
        $this->view->lanePairs         = $this->kanban->getLanePairsByGroup($groupID);
        $this->view->releases2Imported = $this->loadModel('release')->getList($selectedProductID, 'all', 'all', 't1.date_desc', '', $pager);
        $this->view->pager             = $pager;
        $this->view->kanbanID          = $kanbanID;
        $this->view->regionID          = $regionID;
        $this->view->groupID           = $groupID;
        $this->view->columnID          = $columnID;
        $this->view->appList           = $this->loadModel('system')->getPairs();

        $this->display();
    }

    /**
     * 导入项目版本。
     * Import build.
     *
     * @param  int    $kanbanID
     * @param  int    $regionID
     * @param  int    $groupID
     * @param  int    $columnID
     * @param  int    $selectedProjectID
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function importBuild($kanbanID = 0, $regionID = 0, $groupID = 0, $columnID = 0, $selectedProjectID = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        if($_POST)
        {
            $importedIDList = $this->kanban->importObject($kanbanID, $regionID, $groupID, $columnID, 'build');
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            foreach($importedIDList as $cardID => $buildID)
            {
                $this->loadModel('action')->create('kanbancard', $cardID, 'importedBuild', '', $buildID);
            }

            $callback = $this->kanban->getKanbanCallback($kanbanID, $regionID);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

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

        $builds2Imported = array();
        $projects        = array($this->lang->kanban->allProjects);
        $projects       += $this->loadModel('project')->getPairsByProgram(0, 'all', false, 'order_asc', 'kanban');
        $builds2Imported = $this->loadModel('build')->getProjectBuilds($selectedProjectID, 'all', '0', 't1.date_desc,t1.id_desc', $pager);

        $this->view->projects          = $projects;
        $this->view->selectedProjectID = $selectedProjectID;
        $this->view->builds2Imported   = $builds2Imported;
        $this->view->lanePairs         = $this->kanban->getLanePairsByGroup($groupID);
        $this->view->users             = $this->loadModel('user')->getPairs('noletter|nodeleted');
        $this->view->pager             = $pager;
        $this->view->kanbanID          = $kanbanID;
        $this->view->regionID          = $regionID;
        $this->view->groupID           = $groupID;
        $this->view->columnID          = $columnID;
        $this->view->appList           = $this->loadModel('system')->getPairs();

        $this->display();
    }

    /**
     * 导入执行。
     * Import execution.
     *
     * @param  int $kanbanID
     * @param  int $regionID
     * @param  int $groupID
     * @param  int $columnID
     * @param  int $selectedProjectID
     * @param  int $recTotal
     * @param  int $recPerPage
     * @param  int $pageID
     * @access public
     * @return void
     */
    public function importExecution($kanbanID = 0, $regionID = 0, $groupID = 0, $columnID = 0, $selectedProjectID = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        if($_POST)
        {
            $importedIDList = $this->kanban->importObject($kanbanID, $regionID, $groupID, $columnID, 'execution');
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            foreach($importedIDList as $cardID => $executionID)
            {
                $this->loadModel('action')->create('kanbancard', $cardID, 'importedExecution', '', $executionID);
            }

            $callback = $this->kanban->getKanbanCallback($kanbanID, $regionID);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

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

        $this->view->projects            = array($this->lang->kanban->allProjects) + $this->loadModel('project')->getPairsByProgram(0, 'all', false, '', '', '', 'multiple');
        $this->view->selectedProjectID   = $selectedProjectID;
        $this->view->lanePairs           = $this->kanban->getLanePairsByGroup($groupID);
        $this->view->executions2Imported = $this->loadModel('execution')->getStatData($selectedProjectID, 'undone', 0, 0, false, 'hasParentName', 'id_asc', $pager);
        $this->view->users               = $this->loadModel('user')->getPairs('noletter|nodeleted');
        $this->view->pager               = $pager;
        $this->view->kanbanID            = $kanbanID;
        $this->view->regionID            = $regionID;
        $this->view->groupID             = $groupID;
        $this->view->columnID            = $columnID;

        $this->display();
    }

    /**
     * 导入工单。
     * Import ticket.
     *
     * @param  int    $kanbanID
     * @param  int    $regionID
     * @param  int    $groupID
     * @param  int    $columnID
     * @param  int    $selectedProductID
     * @param  int    $recTotal
     * @param  int    $recPerPage
     * @param  int    $pageID
     * @access public
     * @return void
     */
    public function importTicket($kanbanID = 0, $regionID = 0, $groupID = 0, $columnID = 0, $selectedProductID = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1)
    {
        if($_POST)
        {
            $importedIDList = $this->kanban->importObject($kanbanID, $regionID, $groupID, $columnID, 'ticket');
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            foreach($importedIDList as $cardID => $ticketID)
            {
                $this->loadModel('action')->create('kanbancard', $cardID, 'importedTicket', '', $ticketID);
            }

            return print(js::locate($this->createLink('kanban', 'view', "kanbanID=$kanbanID"), 'parent.parent'));
        }

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

        $this->view->products          = array('all' => $this->lang->kanban->allProducts) + $this->loadModel('feedback')->getGrantProducts();
        $this->view->selectedProductID = $selectedProductID;
        $this->view->lanePairs         = $this->kanban->getLanePairsByGroup($groupID);
        $this->view->tickets2Imported  = $this->loadModel('ticket')->getTicketByProduct($selectedProductID, 'noclosed|nodone', 'id_desc', $pager);
        $this->view->users             = $this->loadModel('user')->getPairs('noletter');
        $this->view->pager             = $pager;
        $this->view->kanbanID          = $kanbanID;
        $this->view->regionID          = $regionID;
        $this->view->groupID           = $groupID;
        $this->view->columnID          = $columnID;

        $this->display();
    }

    /**
     * 设置卡片颜色。
     * Set a card's color.
     *
     * @param  int    $cardID
     * @param  string $color
     * @access public
     * @return string
     */
    public function setCardColor($cardID, $color)
    {
        $this->kanban->updateCardColor($cardID, $color);
        if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

        $card     = $this->kanban->getCardByID($cardID);
        $callback = $this->kanban->getKanbanCallback($card->kanban, $card->region);
        return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
    }

    /**
     * 排序卡片。
     * Sort cards.
     *
     * @param  int    $kanbanID
     * @param  int    $laneID
     * @param  int    $columnID
     * @param  string $cards
     * @access public
     * @return void
     */
    public function sortCard($kanbanID, $laneID, $columnID, $cards = '')
    {
        if(empty($cards)) return;

        $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq(",$cards,")->where('kanban')->eq($kanbanID)->andWhere('lane')->eq($laneID)->andWhere('`column`')->eq($columnID)->exec();

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

        $lane     = $this->kanban->getLaneById($laneID);
        $callback = $this->kanban->getKanbanCallback($kanbanID, $lane->region);
        return $this->send(array('result' => 'success', 'callback' => $callback));
    }

    /**
     * 归档看板卡片。
     * Archive a card.
     *
     * @param  int    $cardID
     * @access public
     * @return void
     */
    public function archiveCard($cardID)
    {
        $this->kanban->archiveCard($cardID);
        if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

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

    /**
     * 查看归档的卡片。
     * View archived cards.
     *
     * @param  int    $regionID
     * @access public
     * @return void
     */
    public function viewArchivedCard($regionID)
    {
        $region = $this->kanban->getRegionByID($regionID);

        $cards = $this->kanban->getCardsByObject('region', $regionID, '1');
        foreach($this->config->kanban->fromType as $fromType)
        {
            $cards = $this->kanban->getImportedCards($region->kanban, $cards, $fromType, 1, $regionID);
        }

        $this->view->kanban      = $this->kanban->getByID($region->kanban);
        $this->view->cards       = $cards;
        $this->view->users       = $this->loadModel('user')->getPairs('noletter|nodeleted');
        $this->view->userIdPairs = $this->user->getPairs('noletter|nodeleted|showid');
        $this->view->usersAvatar = $this->user->getAvatarPairs();

        $this->display();
    }

    /**
     * 还原看板卡片。
     * Restore a card.
     *
     * @param  int    $cardID
     * @access public
     * @return void
     */
    public function restoreCard($cardID)
    {
        $this->kanban->restoreCard($cardID);
        if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

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

    /**
     * 删除看板卡片。
     * Delete a card.
     *
     * @param  int    $cardID
     * @access public
     * @return void
     */
    public function deleteCard($cardID)
    {
        $card = $this->kanban->getCardByID($cardID);
        if($card->fromType == '') $this->kanban->delete(TABLE_KANBANCARD, $cardID);
        if($card->fromType != '') $this->dao->delete()->from(TABLE_KANBANCARD)->where('id')->eq($cardID)->exec();

        return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => true, 'callback' => array('name' => 'updateKanbanRegion', 'params' => array('region' . $card->region, array('items' => array(array('key' => 'group' . $card->group, 'data' => array('items' => array(array('id' => $cardID, 'name' => $cardID, 'deleted' => true))))))))));
    }

    /**
     * 设置看板在制品限制。
     * Set WIP.
     *
     * @param  int    $columnID
     * @param  int    $executionID
     * @param  string $from kanban|execution
     * @access public
     * @return void
     */
    public function setWIP($columnID, $executionID = 0, $from = 'kanban')
    {
        $this->app->loadLang('story');
        $column = $this->kanban->getColumnById($columnID);
        if(!$column) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->createLink('execution', 'kanban', "executionID=$executionID"))));

        if($_POST)
        {
            $WIP = form::data($this->config->kanban->form->setWIP)->get();
            $this->kanban->setWIP($columnID, $WIP);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->loadModel('action')->create('kanbancolumn', $columnID, 'Edited', '', $executionID);

            if($from != 'kanban') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => "refreshKanban()"));

            $region   = $this->kanban->getRegionByID($column->region);
            $callback = $this->kanban->getKanbanCallback($region->kanban, $region->id);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        $this->view->column = $column;
        $this->view->from   = $from;
        if($from != 'kanban') $this->view->status = zget($this->config->kanban->{$column->laneType . 'ColumnStatusList'}, $column->type);
        $this->display();
    }

    /**
     * 设置泳道。
     * Set lane info.
     *
     * @param  int    $laneID
     * @param  int    $executionID
     * @param  string $from kanban|execution
     * @access public
     * @return void
     */
    public function setLane($laneID, $executionID = 0, $from = 'kanban')
    {
        if($_POST)
        {
            $lane = form::data($this->config->kanban->form->setLane)->get();
            $this->kanban->setLane($laneID, $lane);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->loadModel('action')->create('kanbanlane', $laneID, 'Edited', '', $executionID);

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

        $lane = $this->kanban->getLaneById($laneID);
        if(!$lane) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->createLink('execution', 'kanban', "executionID=$executionID"))));

        $this->view->lane  = $lane;
        $this->view->from  = $from;

        $this->display();
    }

    /**
     * 编辑泳道名称。
     * Edit lane's name
     *
     * @param  int    $laneID
     * @param  int    $executionID
     * @param  string $from
     * @access public
     * @return void
     */
    public function editLaneName($laneID, $executionID = 0, $from = 'kanban')
    {
        if($_POST)
        {
            $lane = form::data($this->config->kanban->form->setLane)->remove('color')->get();
            $this->kanban->setLane($laneID, $lane);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->loadModel('action')->create('kanbanlane', $laneID, 'Edited', '', $executionID);

            if($this->config->vision == 'lite') return $this->send(array('result' => 'success', 'closeModal' => true, 'load' => true));

            $lane     = $this->kanban->getLaneById($laneID);
            $region   = $this->kanban->getRegionByID($lane->region);
            $callback = $this->kanban->getKanbanCallback($region->kanban, $region->id);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        $lane = $this->kanban->getLaneById($laneID);
        if(!$lane) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->createLink('execution', 'kanban', "executionID=$executionID"))));

        $this->view->lane = $lane;
        $this->view->from = $from;
        $this->display();
    }

    /**
     * Edit lane's color
     *
     * @param  int    $laneID
     * @param  int    $executionID
     * @param  string $from
     * @access public
     * @return void
     */
    public function editLaneColor($laneID, $executionID = 0, $from = 'kanban')
    {
        if($_POST)
        {
            $lane = form::data($this->config->kanban->form->setLane)->remove('name')->get();
            $this->kanban->setLane($laneID, $lane);
            if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));

            $this->loadModel('action')->create('kanbanlane', $laneID, 'Edited', '', $executionID);

            $lane     = $this->kanban->getLaneById($laneID);
            $region   = $this->kanban->getRegionByID($lane->region);
            $callback = $this->kanban->getKanbanCallback($region->kanban, $region->id);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        $lane = $this->kanban->getLaneById($laneID);
        if(!$lane) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->createLink('execution', 'kanban', "executionID=$executionID"))));

        $this->view->lane  = $lane;
        $this->view->from  = $from;

        $this->display();
    }

    /**
     * 编辑看板列。
     * Set lane column info.
     *
     * @param  int $columnID
     * @param  int $executionID
     * @param  string $from kanban|execution
     * @access public
     * @return void
     */
    public function setColumn($columnID, $executionID = 0, $from = 'kanban')
    {
        $column = $this->kanban->getColumnByID($columnID);
        if($_POST)
        {
            $this->lang->kanban->name = $this->lang->kanbancolumn->name;
            $formData = form::data($this->config->kanban->form->setColumn)->get();
            $changes  = $this->kanban->updateColumn($columnID, $formData);
            if(dao::isError()) return $this->sendError(dao::getError());

            if($changes)
            {
                $actionID = $this->loadModel('action')->create('kanbancolumn', $columnID, 'edited', '', $executionID);
                $this->action->logHistory($actionID, $changes);
            }

            if($from == 'RDKanban') return $this->send(array('result' => 'success', 'closeModal' => true, 'callback' => "refreshKanban()"));

            $region   = $this->kanban->getRegionByID($column->region);
            $callback = $this->kanban->getKanbanCallback($region->kanban, $region->id);
            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => $callback));
        }

        $this->view->canEdit = $from == 'RDKanban' ? 0 : 1;
        $this->view->column  = $column;
        $this->display();
    }

    /**
     * 拖动看板卡片。
     * Ajax move card.
     *
     * @param  int    $cardID
     * @param  int    $fromColID
     * @param  int    $toColID
     * @param  int    $fromLaneID
     * @param  int    $toLaneID
     * @param  int    $executionID
     * @param  string $browseType
     * @param  string $groupBy
     * @param  int    $regionID
     * @param  string $orderBy
     * @access public
     * @return void
     */
    public function ajaxMoveCard($cardID = 0, $fromColID = 0, $toColID = 0, $fromLaneID = 0, $toLaneID = 0, $executionID = 0, $browseType = 'all', $groupBy = '', $regionID = 0, $orderBy = '')
    {
        $fromCell = $this->dao->select('id, cards, lane')->from(TABLE_KANBANCELL)
            ->where('kanban')->eq($executionID)
            ->andWhere('`column`')->eq($fromColID)
            ->beginIF(!$groupBy or $groupBy == 'default')->andWhere('lane')->eq($fromLaneID)->fi()
            ->beginIF($groupBy and $groupBy != 'default')
            ->andWhere('type')->eq($browseType)
            ->andWhere('cards')->like("%,$cardID,%")
            ->fi()
            ->fetch();

        if($groupBy and $groupBy != 'default') $fromLaneID = $toLaneID = $fromCell->lane;

        $toCell = $this->dao->select('id, cards')->from(TABLE_KANBANCELL)
            ->where('kanban')->eq($executionID)
            ->andWhere('lane')->eq($toLaneID)
            ->andWhere('`column`')->eq($toColID)
            ->fetch();

        $fromCards = str_replace(",$cardID,", ',', $fromCell->cards);
        $fromCards = $fromCards == ',' ? '' : $fromCards;
        $toCards   = ',' . implode(',', array_unique(array_filter(explode(',', $toCell->cards)))) . ",$cardID,";

        $this->kanban->updateExecutionCell($executionID, $fromColID, $fromLaneID, $fromCards);
        $this->kanban->updateExecutionCell($executionID, $toColID, $toLaneID, $toCards);

        $toColumn = $this->kanban->getColumnByID($toColID);
        if($toColumn->laneType == 'story' and in_array($toColumn->type, array('tested', 'verified', 'released', 'closed')))
        {
            $data = new stdclass();
            $data->stage = $toColumn->type;
            if($toColumn->type == 'released')
            {
                $fromColumn = $this->kanban->getColumnByID($fromColID);
                if($fromColumn->type == 'closed') $data->status = 'active';
            }
            $this->dao->update(TABLE_STORY)->data($data)->where('id')->eq($cardID)->exec();
            $this->dao->update(TABLE_STORYSTAGE)->set('stage')->eq($toColumn->type)->where('story')->eq($cardID)->exec();
        }

        $taskSearchValue = $this->session->taskSearchValue ? $this->session->taskSearchValue : '';
        $rdSearchValue   = $this->session->rdSearchValue ? $this->session->rdSearchValue : '';
        if($regionID == 0)
        {
            list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, $taskSearchValue);
        }
        else
        {
            $kanbanGroup = $this->kanban->getRDKanban($executionID, $browseType, $orderBy, $regionID, $groupBy, $rdSearchValue);
        }
        echo json_encode($kanbanGroup);
    }

    /**
     * 获取看板左上角下拉菜单。
     * Ajax get kanban menu.
     *
     * @param  int    $kanbanID
     * @param  string $moduleName
     * @param  string $methodName
     * @access public
     * @return void
     */
    public function ajaxGetDropMenu($kanbanID, $moduleName, $methodName)
    {
        $kanbanIdList = $this->kanban->getCanViewObjects();
        $spacePairs   = $this->kanban->getSpacePairs('showClosed');

        $this->view->kanbanList = $this->dao->select('*')->from(TABLE_KANBAN)
            ->where('deleted')->eq('0')
            ->andWhere('id')->in($kanbanIdList)
            ->andWhere('space')->in(array_keys($spacePairs))
            ->fetchGroup('space');

        $this->view->kanbanID  = $kanbanID;
        $this->view->spaceList = $spacePairs;
        $this->view->module    = $moduleName;
        $this->view->method    = $methodName;
        $this->view->link      = $this->createLink('kanban', 'view', 'id={id}');
        $this->display();
    }

    /**
     * 获取区域中的泳道。
     * Ajax get lanes by region id.
     *
     * @param  int    $regionID
     * @param  string $type     all|story|task|bug
     * @param  string $field    otherLane|lane
     * @param  string $pageType
     * @access public
     * @return string
     */
    public function ajaxGetLanes($regionID, $type = 'all', $field = 'otherLane', $pageType = '')
    {
        $lanes = $this->kanban->getLanePairsByRegion($regionID, $type);

        if($this->viewType == 'json') return print($lanes);

        $laneList = array();
        foreach($lanes as $laneID => $laneName) $laneList[] = array('value' => $laneID, 'text' => $laneName);

        if($pageType == 'batch') return $this->send($laneList);
        return print(json_encode(array('items' => $laneList, 'name' => $field)));
    }

    /**
     * 获取泳道的列。
     * Ajax get columns by lane id.
     *
     * @param  int    $laneID
     * @access public
     * @return string
     */
    public function ajaxGetColumns($laneID)
    {
        $lane    = $this->kanban->getLaneByID($laneID);
        $columns = $this->dao->select('id,name')->from(TABLE_KANBANCOLUMN)
              ->where('deleted')->eq(0)
              ->andWhere('`group`')->eq($lane->group)
              ->andWhere('parent')->ne('-1')
              ->orderBy('id_asc')
              ->fetchPairs();

        $columnList = array();
        foreach($columns as $columnID => $columnName) $columnList[] = array('value' => $columnID, 'text' => $columnName);

        return print(json_encode($columnList));
    }

    /**
     * Ajax load space users.
     *
     * @param  int    $spaceID
     * @param  string $field team|whitelist|owner
     * @param  string $space all|space
     * @access public
     * @return string
     * @param string $type
     */
    public function ajaxLoadUsers($spaceID, $field = '', $type = 'space')
    {
        $space    = $this->kanban->getSpaceById($spaceID);
        $accounts = '';

        if(!empty($space) and $field == 'owner' and $type != 'all') $accounts = trim($space->owner) . ',' . trim($space->team);

        $users = $this->loadModel('user')->getPairs('noclosed|nodeleted', '', 0, $accounts);

        $userList = array();
        foreach($users as $account => $user) $userList[] = array('text' => $user, 'value' => $account, 'key' => $user . $account);

        return print(json_encode($userList));
    }

    /**
     * 保存当前区域到Session。
     * Ajax save regionID.
     *
     * @param  int    $regionID
     * @access public
     * @return void
     */
    public function ajaxSaveRegionID($regionID)
    {
        $this->session->set('regionID', $regionID, 'kanban');
    }

    /**
     * 获取排序的列表。
     * Ajax Get sort items.
     *
     * @param  string $objectType
     * @param  int    $objectID
     * @access public
     * @return void
     */
    public function ajaxGetSortItems($objectType, $objectID)
    {
        $itemList = array();
        if($objectType == 'region')
        {
            $region      = $this->kanban->getRegionByID($objectID);
            $regionPairs = $this->kanban->getRegionPairs($region->kanban);
            foreach($regionPairs as $regionID => $regionName) $itemList[] = array('id' => $regionID, 'text' => $regionName);
        }
        else if($objectType == 'column')
        {
            $column      = $this->kanban->getColumnByID($objectID);
            $columnPairs = $this->kanban->getColumnPairsByGroup($column->group, $column->parent);
            foreach($columnPairs as $columnID => $columnName) $itemList[] = array('id' => $columnID, 'text' => $columnName);
        }
        else if($objectType == 'lane')
        {
            $lane      = $this->kanban->getLaneById($objectID);
            $lanePairs = $this->kanban->getLanePairsByGroup($lane->group);
            foreach($lanePairs as $laneID => $laneName) $itemList[] = array('id' => $laneID, 'text' => $laneName);
        }
        return print(json_encode($itemList));
    }

    /**
     * 获取空间的团队成员。
     * Ajax get space team memebers.
     *
     * @param  int    $spaceID
     * @access public
     * @return void
     */
    public function ajaxGetSpaceTeam($spaceID)
    {
        $space = $this->loadModel('kanban')->getSpaceById($spaceID);
        return print(json_encode($space->team));
    }
}
