3941 lines
148 KiB
PHP
Executable file
3941 lines
148 KiB
PHP
Executable file
<?php
|
||
declare(strict_types=1);
|
||
/**
|
||
* The model 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 Shujie Tian <tianshujie@easycorp.ltd>
|
||
* @package kanban
|
||
* @version $Id: model.php 5118 2021-10-22 10:18:41Z $
|
||
* @link https://www.zentao.net
|
||
*/
|
||
class kanbanModel extends model
|
||
{
|
||
/**
|
||
* 创建看板分组。
|
||
* Create a kanban group.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $regionID
|
||
* @access public
|
||
* @return int|false
|
||
*/
|
||
public function createGroup(int $kanbanID, int $regionID): int|false
|
||
{
|
||
$maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBANGROUP)
|
||
->where('region')->eq($regionID)
|
||
->fetch('maxOrder');
|
||
|
||
$order = $maxOrder ? $maxOrder + 1 : 1;
|
||
|
||
$group = new stdclass();
|
||
$group->kanban = $kanbanID;
|
||
$group->region = $regionID;
|
||
$group->order = $order;
|
||
|
||
$this->dao->insert(TABLE_KANBANGROUP)->data($group)->autoCheck()->exec();
|
||
if(dao::isError()) return false;
|
||
|
||
return $this->dao->lastInsertID();
|
||
}
|
||
|
||
/**
|
||
* 创建默认看板区域。
|
||
* Create a default kanban region.
|
||
*
|
||
* @param object $kanban
|
||
* @access public
|
||
* @return int|bool
|
||
*/
|
||
public function createDefaultRegion(object $kanban): int|bool
|
||
{
|
||
$region = new stdclass();
|
||
$region->name = $this->lang->kanbanregion->default;
|
||
$region->kanban = $kanban->id;
|
||
$region->space = $kanban->space;
|
||
$region->createdBy = $this->app->user->account;
|
||
$region->createdDate = helper::now();
|
||
|
||
return $this->createRegion($kanban, $region);
|
||
}
|
||
|
||
/**
|
||
* 复制看板区域。
|
||
* Copy kanban regions.
|
||
*
|
||
* @param object $kanban
|
||
* @param int $copyKanbanID
|
||
* @param string $from kanban|execution
|
||
* @param string $param
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function copyRegions(object $kanban, int $copyKanbanID, string $from = 'kanban', string $param = 'withArchived')
|
||
{
|
||
if(empty($kanban) || empty($copyKanbanID)) return;
|
||
|
||
$regions = $this->getRegionPairs($copyKanbanID, 0, $from);
|
||
$order = 1;
|
||
foreach($regions as $copyID => $copyName)
|
||
{
|
||
$region = new stdclass();
|
||
$region->name = $copyName;
|
||
$region->kanban = $kanban->id;
|
||
$region->space = $kanban->space;
|
||
$region->createdBy = $this->app->user->account;
|
||
$region->createdDate = helper::now();
|
||
$region->order = $order;
|
||
|
||
$this->createRegion($kanban, $region, $copyID, $from, $param);
|
||
$order ++;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建区域。
|
||
* Create a new region.
|
||
*
|
||
* @param object $kanban
|
||
* @param object $fromRegion
|
||
* @param int $copyRegionID
|
||
* @param string $from kanban|execution
|
||
* @param string $param
|
||
* @access public
|
||
* @return int
|
||
*/
|
||
public function createRegion(object $kanban, ?object $fromRegion = null, int $copyRegionID = 0, string $from = 'kanban', string $param = '')
|
||
{
|
||
$account = $this->app->user->account;
|
||
$order = 1;
|
||
$region = $fromRegion;
|
||
|
||
if(empty($region))
|
||
{
|
||
$maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBANREGION)
|
||
->where('kanban')->eq($kanban->id)
|
||
->fetch('maxOrder');
|
||
|
||
$order = $maxOrder ? $maxOrder + 1 : 1;
|
||
$region = new stdclass();
|
||
$region->kanban = $kanban->id;
|
||
$region->space = $from == 'kanban' ? $kanban->space : 0;
|
||
$region->name = $this->post->name;
|
||
$region->createdBy = $account;
|
||
$region->createdDate = helper::now();
|
||
}
|
||
|
||
$region->order = isset($region->order) ? $region->order : $order;
|
||
$this->dao->insert(TABLE_KANBANREGION)->data($region)
|
||
->batchCheck($this->config->kanban->require->createregion, 'notempty')
|
||
->check('name', 'unique', "kanban = {$kanban->id} AND deleted = '0' AND space='$region->space'")
|
||
->autoCheck()
|
||
->exec();
|
||
|
||
$regionID = $this->dao->lastInsertID();
|
||
if(dao::isError()) return false;
|
||
|
||
$this->loadModel('action')->create('kanbanRegion', $regionID, 'Created');
|
||
|
||
if($copyRegionID)
|
||
{
|
||
$this->copyRegion($kanban, $regionID, $copyRegionID, $from, $param);
|
||
}
|
||
elseif($from == 'kanban')
|
||
{
|
||
$groupID = $this->createGroup((int)$kanban->id, $regionID);
|
||
if(dao::isError()) return false;
|
||
|
||
$this->createDefaultLane($regionID, $groupID);
|
||
if(dao::isError()) return false;
|
||
|
||
$this->createDefaultColumns($regionID, $groupID);
|
||
if(dao::isError()) return false;
|
||
}
|
||
|
||
return $regionID;
|
||
}
|
||
|
||
/**
|
||
* 复制区域。
|
||
* Copy a region.
|
||
*
|
||
* @param object $kanban
|
||
* @param int $regionID
|
||
* @param int $copyRegionID
|
||
* @param string $from
|
||
* @param string $param
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function copyRegion(object $kanban, int $regionID, int $copyRegionID, string $from = 'kanban', string $param = '')
|
||
{
|
||
/* Gets the groups, lanes and columns of the replication region. */
|
||
$copyGroups = $this->getGroupGroupByRegions((array)$copyRegionID);
|
||
$copyLaneGroup = $this->getLaneGroupByRegions((array)$copyRegionID);
|
||
$copyColumnGroup = $this->getColumnGroupByRegions((array)$copyRegionID, 'id_asc', $param);
|
||
|
||
/* Create groups, lanes, and columns. */
|
||
if(empty($copyGroups)) return $regionID;
|
||
|
||
foreach($copyGroups[$copyRegionID] as $copyGroupID => $copyGroup)
|
||
{
|
||
$newGroupID = $this->createGroup((int)$kanban->id, $regionID);
|
||
if(dao::isError()) return false;
|
||
|
||
$copyLanes = isset($copyLaneGroup[$copyGroupID]) ? $copyLaneGroup[$copyGroupID] : array();
|
||
$copyColumns = isset($copyColumnGroup[$copyGroupID]) ? $copyColumnGroup[$copyGroupID] : array();
|
||
|
||
$lanePairs = $this->copyLanes($kanban, $copyLanes, $regionID, $newGroupID);
|
||
$this->copyColumns($copyColumns, $regionID, $newGroupID, $from);
|
||
if(dao::isError()) return false;
|
||
|
||
if($param == 'updateTaskCell')
|
||
{
|
||
foreach($lanePairs as $oldLaneID => $newLaneID)
|
||
{
|
||
$cards = $this->dao->select('id,cards')->from(TABLE_KANBANCELL)->where('lane')->eq($oldLaneID)->andWhere('type')->eq('task')->fetchPairs();
|
||
$cards = implode(',', $cards);
|
||
$cards = preg_replace('/[,]+/', ',',$cards);
|
||
$cards = trim($cards, ',');
|
||
|
||
$group = $this->dao->select('`group`')->from(TABLE_KANBANLANE)->where('id')->eq($newLaneID)->fetch();
|
||
$waitColumn = $this->dao->select('id')->from(TABLE_KANBANCOLUMN)->where('type')->eq('wait')->andWhere('`group`')->eq($group->group)->fetch();
|
||
|
||
if(!empty($waitColumn)) $this->addKanbanCell($kanban->id, $newLaneID, $waitColumn->id, 'task', $cards);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 复制区域中的泳道。
|
||
* Copy lanes.
|
||
*
|
||
* @param object $kanban
|
||
* @param array $copyLanes
|
||
* @param int $regionID
|
||
* @param int $newGroupID
|
||
* @access public
|
||
* @return array|false
|
||
*/
|
||
public function copyLanes(object $kanban, array $copyLanes, int $regionID, int $newGroupID): array|false
|
||
{
|
||
$lanePairs = array();
|
||
foreach($copyLanes as $copyLane)
|
||
{
|
||
if(is_array($copyLane)) $copyLane = (object)$copyLane;
|
||
|
||
$laneID = $copyLane->id;
|
||
$copyLane->name = $copyLane->title;
|
||
|
||
unset($copyLane->id);
|
||
unset($copyLane->actionList);
|
||
unset($copyLane->title);
|
||
|
||
$copyLane->region = $regionID;
|
||
$copyLane->group = $newGroupID;
|
||
$copyLane->lastEditedTime = helper::now();
|
||
$lanePairs[$laneID] = $this->createLane((int)$kanban->id, $regionID, $copyLane, 'copy');
|
||
if(dao::isError()) return false;
|
||
}
|
||
|
||
return $lanePairs;
|
||
}
|
||
|
||
/**
|
||
* 复制区域中的列。
|
||
* Copy columns.
|
||
*
|
||
* @param array $copyColumns
|
||
* @param int $regionID
|
||
* @param int $newGroupID
|
||
* @param string $from
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function copyColumns(array $copyColumns, int $regionID, int $newGroupID, string $from = 'kanban')
|
||
{
|
||
$parentColumns = array();
|
||
foreach($copyColumns as $copyColumn)
|
||
{
|
||
if(is_array($copyColumn)) $copyColumn = (object)$copyColumn;
|
||
|
||
$copyColumnID = $copyColumn->id;
|
||
$copyColumn->name = $copyColumn->title;
|
||
|
||
unset($copyColumn->id);
|
||
unset($copyColumn->title);
|
||
unset($copyColumn->actionList);
|
||
unset($copyColumn->asParent);
|
||
unset($copyColumn->parentName);
|
||
|
||
$copyColumn->region = $regionID;
|
||
$copyColumn->group = $newGroupID;
|
||
|
||
if($copyColumn->parent > 0 && isset($parentColumns[$copyColumn->parent]))
|
||
{
|
||
$copyColumn->parent = $parentColumns[$copyColumn->parent];
|
||
}
|
||
|
||
$parentColumnID = $this->createColumn($regionID, $copyColumn, $from, 'copy');
|
||
|
||
if($copyColumn->parent < 0) $parentColumns[$copyColumnID] = $parentColumnID;
|
||
if(dao::isError()) return false;
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 创建默认泳道。
|
||
* Create default lane.
|
||
*
|
||
* @param int $regionID
|
||
* @param int $groupID
|
||
* @access public
|
||
* @return int
|
||
*/
|
||
public function createDefaultLane(int $regionID, int $groupID)
|
||
{
|
||
$lane = new stdclass();
|
||
$lane->name = $this->lang->kanbanlane->default;
|
||
$lane->group = $groupID;
|
||
$lane->region = $regionID;
|
||
$lane->type = 'common';
|
||
$lane->lastEditedTime = helper::now();
|
||
$lane->color = '#7ec5ff';
|
||
$lane->order = 1;
|
||
$lane->groupby = '';
|
||
$lane->extra = '';
|
||
|
||
$this->dao->insert(TABLE_KANBANLANE)->data($lane)->exec();
|
||
$laneID = $this->dao->lastInsertId();
|
||
|
||
return $laneID;
|
||
}
|
||
|
||
/**
|
||
* 创建默认看板列。
|
||
* Create default kanban columns.
|
||
*
|
||
* @param int $regionID
|
||
* @param int $groupID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function createDefaultColumns(int $regionID, int $groupID)
|
||
{
|
||
$order = 1;
|
||
foreach($this->lang->kanban->defaultColumn as $columnName)
|
||
{
|
||
$column = new stdclass();
|
||
$column->region = $regionID;
|
||
$column->group = $groupID;
|
||
$column->name = $columnName;
|
||
$column->order = $order;
|
||
$column->limit = -1;
|
||
$column->color = '#333';
|
||
$column->type = '';
|
||
|
||
$this->createColumn($regionID, $column, 'kanban', 'copy');
|
||
$order ++;
|
||
}
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 创建看板列。
|
||
* Create a column.
|
||
*
|
||
* @param int $regionID
|
||
* @param object $column
|
||
* @param string $from kanban|execution
|
||
* @param string $mode new|copy
|
||
* @access public
|
||
* @return int|false
|
||
*/
|
||
public function createColumn(int $regionID, ?object $column = null, string $from = 'kanban', string $mode = 'new'): int|false
|
||
{
|
||
if($mode == 'new')
|
||
{
|
||
if(!$column->limit && empty($column->noLimit)) dao::$errors['limit'][] = sprintf($this->lang->error->notempty, $this->lang->kanban->WIP);
|
||
if(!preg_match("/^-?\d+$/", (string)$column->limit) || (!isset($column->noLimit) && $column->limit <= 0)) dao::$errors['limit'] = $this->lang->kanban->error->mustBeInt;
|
||
if(dao::isError()) return false;
|
||
}
|
||
|
||
$limit = $column->limit;
|
||
if(isset($column->parent) && $column->parent > 0)
|
||
{
|
||
/* Create a child column. */
|
||
$parentColumn = $this->getColumnByID((int)$column->parent);
|
||
if($parentColumn->limit != -1)
|
||
{
|
||
/* The WIP of the child column is infinite or greater than the WIP of the parent column. */
|
||
$sumChildLimit = $this->dao->select('SUM(`limit`) AS sumChildLimit')->from(TABLE_KANBANCOLUMN)->where('parent')->eq($column->parent)->andWhere('deleted')->eq(0)->fetch('sumChildLimit');
|
||
if($limit == -1 || (((int)$limit + (int)$sumChildLimit) > $parentColumn->limit)) dao::$errors['limit'][] = $this->lang->kanban->error->parentLimitNote;
|
||
if(dao::isError()) return false;
|
||
}
|
||
}
|
||
|
||
if($mode == 'new' && $column->order)
|
||
{
|
||
$this->dao->update(TABLE_KANBANCOLUMN)
|
||
->set('`order` = `order` + 1')
|
||
->where('`group`')->eq($column->group)
|
||
->andWhere('`order`')->ge($column->order)
|
||
->exec();
|
||
}
|
||
|
||
$this->dao->insert(TABLE_KANBANCOLUMN)->data($column, 'noLimit,position,copyItems')
|
||
->batchCheck($this->config->kanban->require->createcolumn, 'notempty')
|
||
->autoCheck()
|
||
->exec();
|
||
if(dao::isError()) return false;
|
||
|
||
$columnID = $this->dao->lastInsertID();
|
||
$this->loadModel('action')->create('kanbanColumn', $columnID, 'Created');
|
||
if($from == 'kanban') $this->dao->update(TABLE_KANBANCOLUMN)->set('type')->eq("column{$columnID}")->where('id')->eq($columnID)->exec();
|
||
|
||
/* Add kanban cell. */
|
||
$lanes = $this->dao->select('id,type')->from(TABLE_KANBANLANE)->where('`group`')->eq($column->group)->fetchPairs();
|
||
$kanbanID = $this->dao->select('kanban')->from(TABLE_KANBANREGION)->where('id')->eq($regionID)->fetch('kanban');
|
||
foreach($lanes as $laneID => $laneType) $this->addKanbanCell((int)$kanbanID, $laneID, $columnID, $laneType);
|
||
|
||
return $columnID;
|
||
}
|
||
|
||
/**
|
||
* 拆分看板列。
|
||
* Split column.
|
||
*
|
||
* @param int $columnID
|
||
* @param array $columns
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function splitColumn(int $columnID, array $columns)
|
||
{
|
||
$this->loadModel('action');
|
||
$column = $this->getColumnByID($columnID);
|
||
$maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBANCOLUMN)->where('`group`')->eq($column->group)->fetch('maxOrder');
|
||
$order = $maxOrder ? $maxOrder + 1 : 1;
|
||
$sumChildLimit = 0;
|
||
|
||
$childrenColumn = array();
|
||
foreach($columns as $childColumn)
|
||
{
|
||
$childColumn->parent = $column->id;
|
||
$childColumn->region = $column->region;
|
||
$childColumn->group = $column->group;
|
||
$childColumn->limit = $childColumn->noLimit == -1 ? -1 : $childColumn->limit;
|
||
$childColumn->order = $order;
|
||
|
||
$sumChildLimit += $childColumn->limit == -1 ? 0 : $childColumn->limit;
|
||
if(!$this->checkChildColumn($column, $childColumn, $sumChildLimit)) return false;
|
||
|
||
$order ++;
|
||
$childrenColumn[] = $childColumn;
|
||
}
|
||
|
||
foreach($childrenColumn as $i => $childColumn)
|
||
{
|
||
$this->dao->insert(TABLE_KANBANCOLUMN)->data($childColumn, 'noLimit')
|
||
->autoCheck()
|
||
->batchCheck($this->config->kanban->splitcolumn->requiredFields, 'notempty')
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$childColumnID = $this->dao->lastInsertID();
|
||
$this->dao->update(TABLE_KANBANCOLUMN)->set('type')->eq("column{$childColumnID}")->where('id')->eq($childColumnID)->exec();
|
||
$this->action->create('kanbanColumn', $childColumnID, 'created');
|
||
|
||
$this->kanbanTao->addChildColumnCell($columnID, $childColumnID, $i);
|
||
}
|
||
|
||
$this->dao->update(TABLE_KANBANCOLUMN)->set('parent')->eq(-1)->where('id')->eq($columnID)->exec();
|
||
}
|
||
|
||
/**
|
||
* 检查看板列信息是否合法。
|
||
* Check the column information is legal.
|
||
*
|
||
* @param object $column
|
||
* @param object $childColumn
|
||
* @param int $sumChildLimit
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function checkChildColumn(object $column, object $childColumn, int $sumChildLimit): bool
|
||
{
|
||
if(empty($childColumn->name))
|
||
{
|
||
dao::$errors['name'] = sprintf($this->lang->error->notempty, $this->lang->kanbancolumn->name);
|
||
return false;
|
||
}
|
||
|
||
if(!preg_match("/^-?\d+$/", (string)$childColumn->limit) || (!$childColumn->noLimit && $childColumn->limit <= 0))
|
||
{
|
||
dao::$errors['limit'] = $this->lang->kanban->error->mustBeInt;
|
||
return false;
|
||
}
|
||
|
||
if($column->limit != -1 && ($childColumn->limit == -1 || ($column->limit < $sumChildLimit)))
|
||
{
|
||
dao::$errors['limit'] = $this->lang->kanban->error->parentLimitNote;
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 创建看板卡片。
|
||
* Create a kanban card.
|
||
*
|
||
* @param int $columnID
|
||
* @param object $card
|
||
* @access public
|
||
* @return bool|int
|
||
*/
|
||
public function createCard(int $columnID, object $card): bool|int
|
||
{
|
||
if($card->estimate < 0)
|
||
{
|
||
dao::$errors['estimate'] = $this->lang->kanbancard->error->recordMinus;
|
||
return false;
|
||
}
|
||
|
||
if($card->end && $card->begin > $card->end)
|
||
{
|
||
dao::$errors['end'] = $this->lang->kanbancard->error->endSmall;
|
||
return false;
|
||
}
|
||
|
||
$card = $this->loadModel('file')->processImgURL($card, $this->config->kanban->editor->createcard['id'], $this->post->uid);
|
||
|
||
$this->dao->insert(TABLE_KANBANCARD)->data($card)->autoCheck()
|
||
->checkIF($card->estimate != '', 'estimate', 'float')
|
||
->batchCheck($this->config->kanban->createcard->requiredFields, 'notempty')
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$cardID = $this->dao->lastInsertID();
|
||
$this->loadModel('action')->create('kanbanCard', $cardID, 'created');
|
||
$this->file->saveUpload('kanbancard', $cardID);
|
||
$this->file->updateObjectID($this->post->uid, $cardID, 'kanbancard');
|
||
$this->addKanbanCell((int)$card->kanban, (int)$this->post->lane, $columnID, 'common', (string)$cardID);
|
||
|
||
return $cardID;
|
||
}
|
||
|
||
/**
|
||
* 转入其它看板的卡片。
|
||
* Import card.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $regionID
|
||
* @param int $groupID
|
||
* @param int $columnID
|
||
* @access public
|
||
* @return array|false
|
||
*/
|
||
public function importCard(int $kanbanID, int $regionID, int $groupID, int $columnID): array|false
|
||
{
|
||
$importIDList = $this->post->cards;
|
||
$targetLaneID = $this->post->targetLane;
|
||
|
||
if(!$importIDList || !$targetLaneID) return false;
|
||
|
||
$updateData = new stdclass();
|
||
$updateData->kanban = $kanbanID;
|
||
$updateData->region = $regionID;
|
||
$updateData->group = $groupID;
|
||
$this->dao->update(TABLE_KANBANCARD)->data($updateData)->where('id')->in($importIDList)->exec();
|
||
|
||
$kanban = $this->getByID($kanbanID);
|
||
$oldCardsKanban = array();
|
||
$kanbanUsers = trim($kanban->owner) . ',' . trim($kanban->team);
|
||
$users = $this->loadModel('user')->getPairs('noclosed|nodeleted', '', 0, $kanbanUsers);
|
||
|
||
$cardList = $this->dao->select('*')->from(TABLE_KANBANCARD)->where('id')->in($importIDList)->fetchAll('id');
|
||
foreach($cardList as $cardID => $card)
|
||
{
|
||
$oldCardsKanban[$cardID] = $card->kanban;
|
||
if(empty($card->assignedTo)) continue;
|
||
$this->kanbanTao->updateCardAssignedTo($cardID, $card->assignedTo, $users);
|
||
}
|
||
|
||
if(!dao::isError())
|
||
{
|
||
$this->removeKanbanCell('common', $importIDList, $oldCardsKanban);
|
||
|
||
$cards = implode(',', $importIDList);
|
||
$this->addKanbanCell($kanbanID, (int)$targetLaneID, $columnID, 'common', $cards);
|
||
|
||
return $importIDList;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 转入其它对象作为看板卡片
|
||
* Import object.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $regionID
|
||
* @param int $groupID
|
||
* @param int $columnID
|
||
* @param string $objectType
|
||
* @access public
|
||
* @return array|false
|
||
*/
|
||
public function importObject(int $kanbanID, int $regionID, int $groupID, int $columnID, string $objectType): array|false
|
||
{
|
||
$objectIDList = $this->post->{$objectType . 's'};
|
||
$targetLaneID = $this->post->targetLane;
|
||
|
||
$objectCards = array();
|
||
$now = helper::now();
|
||
foreach($objectIDList as $objectID)
|
||
{
|
||
$cardData = new stdclass();
|
||
$cardData->kanban = $kanbanID;
|
||
$cardData->region = $regionID;
|
||
$cardData->group = $groupID;
|
||
$cardData->fromID = $objectID;
|
||
$cardData->fromType = $objectType;
|
||
$cardData->createdBy = $this->app->user->account;
|
||
$cardData->createdDate = $now;
|
||
$this->dao->insert(TABLE_KANBANCARD)->data($cardData)->exec();
|
||
|
||
$cardID = $this->dao->lastInsertID();
|
||
$objectCards[$cardID] = $objectID;
|
||
}
|
||
|
||
if(!dao::isError())
|
||
{
|
||
$cards = implode(',', array_keys($objectCards));
|
||
$this->addKanbanCell($kanbanID, (int)$targetLaneID, $columnID, 'common', $cards);
|
||
|
||
return $objectCards;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 批量创建卡片。
|
||
* Batch create kanban cards.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $regionID
|
||
* @param int $groupID
|
||
* @param int $columnID
|
||
* @param array $cards
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function batchCreateCard(int $kanbanID, int $regionID, int $groupID, int $columnID, array $cards)
|
||
{
|
||
$now = helper::now();
|
||
foreach($cards as $i => $card)
|
||
{
|
||
if($card->estimate < 0)
|
||
{
|
||
dao::$errors["estimate[$i]"] = $this->lang->kanbancard->error->recordMinus;
|
||
return false;
|
||
}
|
||
if($card->end && $card->begin > $card->end)
|
||
{
|
||
dao::$errors["end[$i]"] = $this->lang->kanbancard->error->endSmall;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
foreach($cards as $card)
|
||
{
|
||
$card->kanban = $kanbanID;
|
||
$card->region = $regionID;
|
||
$card->group = $groupID;
|
||
$card->createdBy = $this->app->user->account;
|
||
$card->createdDate = $now;
|
||
$card->assignedDate = $now;
|
||
$card->color = '#fff';
|
||
|
||
$this->dao->insert(TABLE_KANBANCARD)->data($card, 'lane')->autoCheck()
|
||
->checkIF($card->estimate != '', 'estimate', 'float')
|
||
->batchCheck($this->config->kanban->createcard->requiredFields, 'notempty')
|
||
->exec();
|
||
|
||
if(!dao::isError())
|
||
{
|
||
$cardID = $this->dao->lastInsertID();
|
||
$this->addKanbanCell($kanbanID, $card->lane, $columnID, 'common', (string)$cardID);
|
||
$this->loadModel('action')->create('kanbancard', $cardID, 'created');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取看板数据。
|
||
* Get kanban by id.
|
||
*
|
||
* @param int $kanbanID
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function getByID(int $kanbanID): object
|
||
{
|
||
$kanban = $this->dao->findByID($kanbanID)->from(TABLE_KANBAN)->fetch();
|
||
$kanban = $this->loadModel('file')->replaceImgURL($kanban, 'desc');
|
||
|
||
return $kanban;
|
||
}
|
||
|
||
/**
|
||
* 获取看板键值对。
|
||
* Get kanban pairs.
|
||
*
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getPairs()
|
||
{
|
||
$idList = $this->getCanViewObjects();
|
||
return $this->dao->select('id,name')->from(TABLE_KANBAN)
|
||
->where('id')->in($idList)
|
||
->andWhere('deleted')->eq('0')
|
||
->fetchPairs();
|
||
}
|
||
|
||
/**
|
||
* 获取看板所有数据。
|
||
* Get kanban data.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param mixed $regionIDList
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getKanbanData(int $kanbanID, mixed $regionIDList = ''): array
|
||
{
|
||
$regions = $this->getRegionPairs($kanbanID);
|
||
|
||
if(empty($regionIDList))
|
||
{
|
||
$regionIDList = array_keys($regions);
|
||
}
|
||
elseif(!is_array($regionIDList))
|
||
{
|
||
$regionIDList = array($regionIDList);
|
||
}
|
||
|
||
$groupGroup = $this->getGroupGroupByRegions($regionIDList);
|
||
$laneGroup = $this->getLaneGroupByRegions($regionIDList);
|
||
$columnGroup = $this->getColumnGroupByRegions($regionIDList);
|
||
$cardGroup = $this->getCardGroupByKanban($kanbanID);
|
||
|
||
$kanbanList = array();
|
||
foreach($regionIDList as $regionID)
|
||
{
|
||
$regionData = array();
|
||
|
||
$heading = new stdclass();
|
||
$heading->title = new stdClass();
|
||
$heading->title->html = zget($regions, $regionID, '') . '<span class="icon icon-angle-top"></span>';
|
||
$heading->actions = $this->getRegionActions($kanbanID, $regionID, count($regions));
|
||
|
||
$regionData['key'] = "region{$regionID}";
|
||
$regionData['id'] = $regionID;
|
||
$regionData['heading'] = $heading;
|
||
$regionData['toggleFromHeading'] = true;
|
||
|
||
$groups = zget($groupGroup, $regionID, array());
|
||
$kanbanList[] = $this->kanbanTao->buildRegionData($regionData, $groups, $laneGroup, $columnGroup, $cardGroup);
|
||
}
|
||
|
||
return $kanbanList;
|
||
}
|
||
|
||
/**
|
||
* 获取看板区域上的操作按钮。
|
||
* Get region actions.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int|string $regionID
|
||
* @param int $regionCount
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRegionActions(int $kanbanID, int|string $regionID, int $regionCount): array
|
||
{
|
||
$action = array();
|
||
$actions = array();
|
||
|
||
$action['type'] = 'dropdown';
|
||
$action['icon'] = 'ellipsis-v';
|
||
$action['caret'] = false;
|
||
$action['items'] = array();
|
||
|
||
if(common::hasPriv('kanban', 'createRegion')) $action['items'][] = array('text' => $this->lang->kanban->createRegion, 'url' => helper::createLink('kanban', 'createRegion', "kanbanID=$kanbanID"), 'data-toggle' => 'modal', 'icon' => 'plus');
|
||
if(common::hasPriv('kanban', 'editRegion')) $action['items'][] = array('text' => $this->lang->kanban->editRegion, 'url' => helper::createLink('kanban', 'editRegion', "regionID=$regionID"), 'data-toggle' => 'modal', 'icon' => 'edit');
|
||
if(common::hasPriv('kanban', 'sortRegion')) $action['items'][] = array('text' => $this->lang->kanban->sortRegion, 'url' => 'javascript:;', 'icon' => 'move', 'data-on' => 'click', 'data-call' => 'sortItems', 'data-params' => 'event', 'data-type' => 'region', 'data-id' => $regionID);
|
||
if(common::hasPriv('kanban', 'createLane')) $action['items'][] = array('text' => $this->lang->kanban->createLane, 'url' => helper::createLink('kanban', 'createLane', "kanbanID=$kanbanID®ionID=$regionID"), 'data-toggle' => 'modal', 'icon' => 'plus');
|
||
if(common::hasPriv('kanban', 'deleteRegion') and ($regionCount > 1)) $action['items'][] = array('text' => $this->lang->kanban->deleteRegion, 'url' => helper::createLink('kanban', 'deleteRegion', "regionID=$regionID"), 'data-confirm' => $this->lang->kanbanregion->confirmDelete, 'icon' => 'trash', 'innerClass' => 'ajax-submit');
|
||
|
||
$action['items'][] = array('type' => 'divider');
|
||
|
||
if(commonModel::hasPriv('kanban', 'viewArchivedCard')) $action['items'][] = array('text' => $this->lang->kanban->viewArchivedCard, 'url' => "javascript:loadMore(\"Card\", $regionID)", 'icon' => 'card-archive');
|
||
if(commonModel::hasPriv('kanban', 'viewArchivedColumn')) $action['items'][] = array('text' => $this->lang->kanban->viewArchivedColumn, 'url' => "javascript:loadMore(\"Column\", $regionID)", 'icon' => 'col-archive');
|
||
|
||
$actions[] = $action;
|
||
|
||
return $actions;
|
||
}
|
||
|
||
/**
|
||
* 获取专业研发看板区域上的操作按钮。
|
||
* Get RD region actions.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $regionID
|
||
* @param int $regionCount
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRDRegionActions(int $kanbanID, int $regionID, int $regionCount = 1): array
|
||
{
|
||
$action = array();
|
||
$actions = array();
|
||
|
||
$action['type'] = 'dropdown';
|
||
$action['icon'] = 'ellipsis-v';
|
||
$action['caret'] = false;
|
||
$action['items'] = array();
|
||
|
||
if(common::hasPriv('kanban', 'createRegion')) $action['items'][] = array('text' => $this->lang->kanban->createRegion, 'url' => helper::createLink('kanban', 'createRegion', "kanbanID=$kanbanID&from=execution"), 'data-toggle' => 'modal', 'icon' => 'plus', 'class' => 'kanban-createRegion-btn');
|
||
if(common::hasPriv('kanban', 'editRegion')) $action['items'][] = array('text' => $this->lang->kanban->editRegion, 'url' => helper::createLink('kanban', 'editRegion', "regionID=$regionID"), 'data-toggle' => 'modal', 'icon' => 'edit');
|
||
if(common::hasPriv('kanban', 'createLane')) $action['items'][] = array('text' => $this->lang->kanban->createLane, 'url' => helper::createLink('kanban', 'createLane', "kanbanID=$kanbanID®ionID=$regionID&from=execution"), 'data-toggle' => 'modal', 'icon' => 'plus');
|
||
if(common::hasPriv('kanban', 'deleteRegion') && ($regionCount > 1)) $action['items'][] = array('text' => $this->lang->kanban->deleteRegion, 'url' => helper::createLink('kanban', 'deleteRegion', "regionID=$regionID"), 'data-confirm' => $this->lang->kanbanregion->confirmDelete, 'icon' => 'trash', 'innerClass' => 'ajax-submit');
|
||
|
||
$actions[] = $action;
|
||
return $actions;
|
||
}
|
||
|
||
/**
|
||
* 获取计划看板。
|
||
* Get plan kanban.
|
||
*
|
||
* @param object $product
|
||
* @param string $branchID
|
||
* @param array $planGroup
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getPlanKanban(object $product, string $branchID = '0', array $planGroup = array()): array
|
||
{
|
||
$this->loadModel('productplan');
|
||
|
||
$lanes = $columns = $kanbanData = $planList = $lanes = $columns = $columnCards = array();
|
||
$colorIndex = 0;
|
||
$laneOrder = 1;
|
||
|
||
$cardActions = array('view', 'createExecution', 'linkStory', 'linkBug', 'edit', 'start', 'finish', 'close', 'activate', 'delete');
|
||
$branches = $this->kanbanTao->getBranchesForPlanKanban($product, $branchID);
|
||
foreach($branches as $id => $name)
|
||
{
|
||
if($product->type != 'normal') $plans = isset($planGroup[$product->id][$id]) ? array_filter($planGroup[$product->id][$id]) : array();
|
||
if($product->type == 'normal') $plans = $planGroup;
|
||
|
||
foreach($plans as $plan)
|
||
{
|
||
if(empty($plan) || $plan->parent == -1) continue;
|
||
$plan->isParent = false;
|
||
|
||
$planBegin = $plan->begin == $this->config->productplan->future ? $this->lang->productplan->future : date('m-d', strtotime($plan->begin));
|
||
$planEnd = $plan->end == $this->config->productplan->future ? $this->lang->productplan->future : date('m-d', strtotime($plan->end));
|
||
|
||
$item = array('id' => $plan->id, 'name' => $plan->id, 'title' => htmlspecialchars_decode($plan->title), 'status' => $plan->status);
|
||
$item['statusLabel'] = zget($this->lang->productplan->statusList, $plan->status);
|
||
$item['delay'] = helper::today() > $plan->end ? true : false;
|
||
$item['desc'] = strip_tags(htmlspecialchars_decode($plan->desc));
|
||
$item['dateLine'] = in_array($this->lang->productplan->future, array($planBegin, $planEnd)) ? $this->lang->productplan->future : $planBegin . ' ' . $this->lang->productplan->to . ' ' . $planEnd;
|
||
$item['actionList'] = array();
|
||
foreach($cardActions as $action)
|
||
{
|
||
if($this->productplan->isClickable($plan, $action)) $item['actionList'][] = $action;
|
||
}
|
||
$planList['lane' . $id][$plan->status][] = $item;
|
||
|
||
if(!isset($columnCards[$plan->status])) $columnCards[$plan->status] = 0;
|
||
$columnCards[$plan->status] ++;
|
||
}
|
||
|
||
$lanes[] = array('id' => $id, 'name' => 'lane' . $id, 'title' => $name, 'color' => $this->config->productplan->laneColorList[$colorIndex], 'order' => $laneOrder);
|
||
$laneOrder ++;
|
||
$colorIndex ++;
|
||
if($colorIndex == count($this->config->productplan->laneColorList)) $colorIndex = 0;
|
||
}
|
||
|
||
foreach($this->lang->kanban->defaultColumn as $columnType => $columnName) $columns[] = array('id' => $columnType, 'name' => $columnType, 'title' => $columnName);
|
||
foreach($columns as $key => $column) $columns[$key]['cards'] = !empty($columnCards[$column['name']]) ? $columnCards[$column['name']] : 0;
|
||
|
||
$groupData['key'] = 'planKanban';
|
||
$groupData['data'] = array('lanes' => $lanes, 'cols' => $columns, 'items' => $planList);
|
||
$kanbanData[] = array('items' => array($groupData), 'key' => 'planKanban');
|
||
return $kanbanData;
|
||
}
|
||
|
||
/**
|
||
* 获取专业研发看板。
|
||
* Get a RD kanban data.
|
||
*
|
||
* @param int $executionID
|
||
* @param string $browseType all|story|task|bug
|
||
* @param string $orderBy
|
||
* @param int $regionID
|
||
* @param string $groupBy
|
||
* @param string $searchValue
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRDKanban(int $executionID, string $browseType = 'all', string $orderBy = 'id_desc', int $regionID = 0, string $groupBy = 'default', string $searchValue = '')
|
||
{
|
||
$kanbanList = array();
|
||
$execution = $this->loadModel('execution')->getByID($executionID);
|
||
|
||
if($groupBy != 'default' && $groupBy != '') return $this->getRDKanbanByGroup($execution, $browseType, $orderBy, $regionID, $groupBy, $searchValue);
|
||
|
||
$regions = $this->getRegionPairs($executionID, $regionID, 'execution');
|
||
$regionIDList = $regionID == 0 ? array_keys($regions) : array(0 => $regionID);
|
||
$groupGroup = $this->getGroupGroupByRegions($regionIDList);
|
||
$laneGroup = $this->getLaneGroupByRegions($regionIDList, $browseType);
|
||
|
||
foreach($laneGroup as $lanes)
|
||
{
|
||
foreach($lanes as $lane)
|
||
{
|
||
$lane['execution'] = $executionID;
|
||
if(in_array($execution->attribute, array('request', 'design', 'review')) && $lane['type'] == 'bug') continue 2;
|
||
if(in_array($execution->attribute, array('request', 'review')) && $lane['type'] == 'story') continue 2;
|
||
$this->refreshCards((array)$lane);
|
||
$lane['defaultCardType'] = $lane['type'];
|
||
}
|
||
}
|
||
|
||
$columnGroup = $this->getRDColumnGroupByRegions($regionIDList, array_keys($laneGroup));
|
||
$cardGroup = $this->getCardGroupByExecution($executionID, $browseType, $orderBy, $searchValue);
|
||
|
||
foreach($regions as $regionID => $regionName)
|
||
{
|
||
$regionData = array();
|
||
|
||
$heading = new stdclass();
|
||
$heading->title = new stdClass();
|
||
$heading->title->html = $regionName . '<span class="icon icon-angle-top"></span>';
|
||
$heading->actions = $this->getRDRegionActions($executionID, $regionID, count($regions));
|
||
|
||
$regionData['key'] = "region{$regionID}";
|
||
$regionData['id'] = $regionID;
|
||
$regionData['heading'] = $heading;
|
||
$regionData['toggleFromHeading'] = true;
|
||
$regionData['items'] = array();
|
||
|
||
$groups = zget($groupGroup, $regionID, array());
|
||
|
||
$regionData = $this->kanbanTao->buildRDRegionData($regionData, $groups, $laneGroup, $columnGroup, $cardGroup, $searchValue);
|
||
|
||
$kanbanList[] = $regionData;
|
||
}
|
||
|
||
return $kanbanList;
|
||
}
|
||
|
||
/**
|
||
* 获取专业研发看板分组视图下的看板数据。
|
||
* Get kanban data for group view of RD kanban.
|
||
*
|
||
* @param object $execution
|
||
* @param string $browseType
|
||
* @param string $groupBy
|
||
* @param string $searchValue
|
||
* @param string $orderBy
|
||
* @access private
|
||
* @return array
|
||
*/
|
||
private function getRDKanbanByGroup(object $execution, string $browseType, string $orderBy, int $regionID, string $groupBy, string $searchValue): array
|
||
{
|
||
$regionData = array();
|
||
$heading = new stdclass();
|
||
$heading->title = $execution->name;
|
||
$heading->actions = $this->getRDRegionActions($execution->id, $regionID, 1);
|
||
|
||
$regionData['key'] = "region{$execution->id}";
|
||
$regionData['id'] = $execution->id;
|
||
$regionData['heading'] = $heading;
|
||
$regionData['toggleFromHeading'] = true;
|
||
$regionData['items'] = $this->getKanban4Group($execution->id, $browseType, $groupBy, $searchValue, $orderBy);
|
||
|
||
$kanbanList[] = $regionData;
|
||
return $kanbanList;
|
||
}
|
||
|
||
/**
|
||
* 获取看板区域。
|
||
* Get region by id.
|
||
*
|
||
* @param int $regionID
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function getRegionByID(int $regionID)
|
||
{
|
||
return $this->dao->findByID($regionID)->from(TABLE_KANBANREGION)->fetch();
|
||
}
|
||
|
||
/**
|
||
* 获取看板区域的键值对。
|
||
* Get ordered region pairs.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $regionID
|
||
* @param string $from kanban|execution
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRegionPairs(int $kanbanID, int $regionID = 0, string $from = 'kanban'): array
|
||
{
|
||
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getRegionPairs();
|
||
|
||
return $this->dao->select('id,name')->from(TABLE_KANBANREGION)
|
||
->where('kanban')->eq($kanbanID)
|
||
->andWhere('deleted')->eq('0')
|
||
->beginIF($regionID)->andWhere('id')->eq($regionID)->fi()
|
||
->beginIF($from == 'execution')->andWhere('space')->eq(0)->fi()
|
||
->beginIF($from == 'kanban')->andWhere('space')->ne(0)->fi()
|
||
->orderBy('order_asc')
|
||
->fetchPairs();
|
||
}
|
||
|
||
/**
|
||
* 根据区域获取看板ID。
|
||
* Get kanban id by region id.
|
||
*
|
||
* @param int $regionID
|
||
* @access public
|
||
* @return int
|
||
*/
|
||
public function getKanbanIDByRegion(int $regionID): int
|
||
{
|
||
return (int)$this->dao->select('kanban')->from(TABLE_KANBANREGION)->where('id')->eq($regionID)->fetch('kanban');
|
||
}
|
||
|
||
/**
|
||
* 分组获取看板组。
|
||
* Get kanban group by regions.
|
||
*
|
||
* @param array $regions
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getGroupGroupByRegions(array $regions): array
|
||
{
|
||
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getGroups();
|
||
|
||
return $this->dao->select('*')->from(TABLE_KANBANGROUP)
|
||
->where('region')->in($regions)
|
||
->orderBy('order')
|
||
->fetchGroup('region', 'id');
|
||
}
|
||
|
||
/**
|
||
* 获取区域中的泳道分组。
|
||
* Get lane group by regions.
|
||
*
|
||
* @param array $regions
|
||
* @param string $browseType
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getLaneGroupByRegions(array $regions, string $browseType = 'all'): array
|
||
{
|
||
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getLaneGroup();
|
||
|
||
$lanes = $this->dao->select('*')->from(TABLE_KANBANLANE)
|
||
->where('deleted')->eq('0')
|
||
->andWhere('region')->in($regions)
|
||
->beginIf($browseType != 'all')->andWhere('type')->eq($browseType)->fi()
|
||
->orderBy('order_asc')
|
||
->fetchAll();
|
||
|
||
$actions = array('sortLane', 'deleteLane', 'editLaneName', 'editLaneColor');
|
||
$laneGroup = array();
|
||
foreach($lanes as $lane)
|
||
{
|
||
$item = array();
|
||
$item['id'] = $lane->id;
|
||
$item['execution'] = $lane->execution;
|
||
$item['type'] = $lane->type;
|
||
$item['name'] = $lane->id;
|
||
$item['region'] = $lane->region;
|
||
$item['title'] = htmlspecialchars_decode($lane->name);
|
||
$item['color'] = $lane->color;
|
||
$item['order'] = $lane->order;
|
||
|
||
foreach($actions as $action)
|
||
{
|
||
if($this->isClickable($lane, $action)) $item['actionList'][] = $action;
|
||
}
|
||
|
||
$laneGroup[$lane->group][] = $item;
|
||
}
|
||
|
||
return $laneGroup;
|
||
}
|
||
|
||
/**
|
||
* 根据分组ID获取泳道键值对。
|
||
* Get kanban lane pairs by group id.
|
||
*
|
||
* @param int $groupID
|
||
* @param string $orderBy
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getLanePairsByGroup(int $groupID, string $orderBy = '`order`_asc'): array
|
||
{
|
||
return $this->dao->select('id,name')->from(TABLE_KANBANLANE)
|
||
->where('deleted')->eq(0)
|
||
->andWhere('`group`')->eq($groupID)
|
||
->orderBy($orderBy)
|
||
->fetchPairs();
|
||
}
|
||
|
||
/**
|
||
* 根据分组ID获取看板列键值对。
|
||
* Get kanban column pairs by group id.
|
||
*
|
||
* @param int $groupID
|
||
* @param int $parentID
|
||
* @param string $orderBy
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getColumnPairsByGroup(int $groupID, int $parentID = 0, string $orderBy = '`order`_asc'): array
|
||
{
|
||
return $this->dao->select('id,name')->from(TABLE_KANBANCOLUMN)
|
||
->where('deleted')->eq(0)
|
||
->andWhere('`group`')->eq($groupID)
|
||
->beginIf($parentID > 0)->andWhere('parent')->eq($parentID)->fi()
|
||
->beginIf($parentID <= 0)->andWhere('parent')->le(0)->fi()
|
||
->orderBy($orderBy)
|
||
->fetchPairs();
|
||
}
|
||
|
||
/**
|
||
* Get column group by regions.
|
||
*
|
||
* @param array $regions
|
||
* @param string $order order|id_asc
|
||
* @param string $param
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getColumnGroupByRegions(array $regions, string $order = '`order`', string $param = ''): array
|
||
{
|
||
$columns = $this->dao->select("*")->from(TABLE_KANBANCOLUMN)
|
||
->where('deleted')->eq('0')
|
||
->andWhere('region')->in($regions)
|
||
->beginIF(strpos(",$param,", ',withArchived,') === false)->andWhere('archived')->eq('0')->fi()
|
||
->orderBy($order)
|
||
->fetchAll();
|
||
|
||
$actions = array('createColumn', 'setColumn', 'setWIP', 'sortColumn', 'archiveColumn', 'restoreColumn', 'deleteColumn', 'createCard', 'batchCreateCard', 'splitColumn', 'sortColumn');
|
||
|
||
/* Group by parent. */
|
||
$columnGroup = array();
|
||
foreach($columns as $column)
|
||
{
|
||
$item = array();
|
||
$item['title'] = htmlspecialchars_decode($column->name);
|
||
$item['name'] = $column->id;
|
||
$item['id'] = $column->id;
|
||
$item['type'] = $column->type;
|
||
$item['limit'] = $column->limit;
|
||
$item['region'] = $column->region;
|
||
$item['group'] = $column->group;
|
||
$item['parent'] = $column->parent;
|
||
$item['color'] = $column->color;
|
||
$item['order'] = (int)$column->order;
|
||
if($column->parent > 0) $item['parentName'] = $column->parent;
|
||
|
||
/* Judge column action priv. */
|
||
foreach($actions as $action)
|
||
{
|
||
if($this->isClickable($column, $action)) $item['actionList'][] = $action;
|
||
}
|
||
|
||
$columnGroup[$column->group][] = $item;
|
||
}
|
||
|
||
return $columnGroup;
|
||
}
|
||
|
||
/**
|
||
* 获取看板下的卡片。
|
||
* Get card group by kanban id.
|
||
*
|
||
* @param int $kanbanID
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getCardGroupByKanban(int $kanbanID): array
|
||
{
|
||
/* Get card data.*/
|
||
$cards = $this->dao->select('*')->from(TABLE_KANBANCARD)
|
||
->where('deleted')->eq(0)
|
||
->andWhere('kanban')->eq($kanbanID)
|
||
->andWhere('archived')->eq(0)
|
||
->andWhere('fromID')->eq(0)
|
||
->fetchAll('id', false);
|
||
|
||
foreach($this->config->kanban->fromType as $fromType) $cards = $this->getImportedCards($kanbanID, $cards, $fromType);
|
||
|
||
$cellList = $this->dao->select('*')->from(TABLE_KANBANCELL)
|
||
->where('kanban')->eq($kanbanID)
|
||
->andWhere('type')->eq('common')
|
||
->fetchAll('id', false);
|
||
|
||
$actions = array('editCard', 'archiveCard', 'deleteCard', 'moveCard', 'setCardColor', 'viewCard', 'sortCard', 'viewExecution', 'viewPlan', 'viewRelease', 'viewBuild', 'viewTicket', 'activateCard', 'finishCard');
|
||
$cardGroup = array();
|
||
$avatarPairs = $this->loadModel('user')->getAvatarPairs();
|
||
$users = $this->loadModel('user')->getPairs('noletter');
|
||
foreach($cellList as $cell)
|
||
{
|
||
$cardIdList = array_filter(explode(',', $cell->cards));
|
||
if(empty($cardIdList)) continue;
|
||
|
||
$order = 0;
|
||
foreach($cardIdList as $cardID)
|
||
{
|
||
if(!isset($cards[$cardID])) continue;
|
||
|
||
$card = zget($cards, $cardID);
|
||
$item = $this->kanbanTao->initCardItem($card, $cell, $order, $avatarPairs, $users);
|
||
|
||
$order ++;
|
||
|
||
foreach($actions as $action)
|
||
{
|
||
if(in_array($action, array('viewExecution', 'viewPlan', 'viewRelease', 'viewBuild', 'viewTicket')))
|
||
{
|
||
if($card->fromType == 'execution')
|
||
{
|
||
if($card->execType == 'kanban' && common::hasPriv('execution', 'kanban')) $item['actionList'][] = $action;
|
||
if($card->execType != 'kanban' && common::hasPriv('execution', 'view')) $item['actionList'][] = $action;
|
||
}
|
||
else
|
||
{
|
||
if(common::hasPriv($fromType, 'view')) $item['actionList'][] = $action;
|
||
}
|
||
continue;
|
||
}
|
||
if(common::hasPriv('kanban', $action)) $item['actionList'][] = $action;
|
||
}
|
||
|
||
$cardGroup[$card->group][$cell->lane][$cell->column][] = $item;
|
||
}
|
||
}
|
||
|
||
return $cardGroup;
|
||
}
|
||
|
||
/**
|
||
* 获取已经导入的卡片。
|
||
* Get imported cards.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param array $cards
|
||
* @param array $fromType
|
||
* @param int $archived
|
||
* @param int $regionID
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getImportedCards(int $kanbanID, array $cards, string $fromType, int $archived = 0, int $regionID = 0): array
|
||
{
|
||
/* Get imported cards based on imported object type. */
|
||
$objectCards = $this->dao->select('*')->from(TABLE_KANBANCARD)
|
||
->where('deleted')->eq(0)
|
||
->andWhere('kanban')->eq($kanbanID)
|
||
->andWhere('archived')->eq($archived)
|
||
->andWhere('fromType')->eq($fromType)
|
||
->beginIF($regionID)->andWhere('region')->eq($regionID)->fi()
|
||
->fetchGroup('fromID', 'id');
|
||
|
||
if(!empty($objectCards))
|
||
{
|
||
/* Get imported objects. */
|
||
$table = $this->config->objectTables[$fromType];
|
||
$objects = $this->dao->select('*')->from($table)
|
||
->where('id')->in(array_keys($objectCards))
|
||
->fetchAll('id', false);
|
||
|
||
$creators = array();
|
||
if($fromType == 'productplan' || $fromType == 'release')
|
||
{
|
||
$creators = $this->dao->select('objectID, actor')->from(TABLE_ACTION)
|
||
->where('objectID')->in(array_keys($objectCards))
|
||
->andWhere('objectType')->eq($fromType)
|
||
->andWhere('action')->eq('opened')
|
||
->fetchPairs();
|
||
}
|
||
|
||
/* Data for constructing the card. */
|
||
foreach($objectCards as $objectID => $cardsInfo)
|
||
{
|
||
foreach($cardsInfo as $cardID => $objectCard)
|
||
{
|
||
$object = $objects[$objectID];
|
||
$fieldType = $fromType . 'Field';
|
||
|
||
if($fromType == 'execution') $objectCard->progress = $object->progress;
|
||
|
||
foreach($this->config->kanban->$fieldType as $field) $objectCard->$field = $object->$field;
|
||
|
||
$objectCard = $this->kanbanTao->buildObjectCard($objectCard, $object, $fromType, $creators);
|
||
$cards[$cardID] = $objectCard;
|
||
}
|
||
}
|
||
}
|
||
return $cards;
|
||
}
|
||
|
||
/**
|
||
* 获取专业研发看板的列。
|
||
* Get RD column group by regions.
|
||
*
|
||
* @param array $regions
|
||
* @param array $groupIDList
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRDColumnGroupByRegions(array $regions, array $groupIDList = array())
|
||
{
|
||
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getColumns();
|
||
|
||
$columnGroup = $this->dao->select("*")->from(TABLE_KANBANCOLUMN)
|
||
->where('deleted')->eq('0')
|
||
->andWhere('region')->in($regions)
|
||
->beginIF(!empty($groupIDList))->andWhere('`group`')->in($groupIDList)->fi()
|
||
->orderBy('id_asc')
|
||
->fetchGroup('group');
|
||
|
||
$actions = array('setColumn', 'setWIP', 'deleteColumn');
|
||
|
||
/* Group by parent. */
|
||
$columnData = array();
|
||
foreach($columnGroup as $group => $columns)
|
||
{
|
||
foreach($columns as $column)
|
||
{
|
||
$item = array();
|
||
$item['title'] = htmlspecialchars_decode($column->name);
|
||
$item['name'] = $column->id;
|
||
$item['id'] = $column->id;
|
||
$item['type'] = $column->type;
|
||
$item['limit'] = $column->limit;
|
||
$item['region'] = $column->region;
|
||
$item['group'] = $column->group;
|
||
$item['parent'] = $column->parent;
|
||
$item['color'] = $column->color;
|
||
if($column->parent > 0) $item['parentName'] = $column->parent;
|
||
|
||
/* Judge column action priv. */
|
||
foreach($actions as $action)
|
||
{
|
||
if($this->isClickable($column, $action)) $item['actionList'][] = $action;
|
||
}
|
||
|
||
$columnData[$group][] = $item;
|
||
}
|
||
}
|
||
|
||
return $columnData;
|
||
}
|
||
|
||
/**
|
||
* 获取执行中的看板卡片分组。
|
||
* Get card group by execution id.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param string $browseType all|task|bug|story
|
||
* @param string $orderBy
|
||
* @param string $searchValue
|
||
*
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getCardGroupByExecution(int $executionID, string $browseType = 'all', string $orderBy = 'id_asc', string $searchValue = ''): array
|
||
{
|
||
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getCardGroup();
|
||
|
||
$cards = $this->dao->select('t1.*, t2.type as columnType, t2.group')
|
||
->from(TABLE_KANBANCELL)->alias('t1')
|
||
->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column=t2.id')
|
||
->where('t1.kanban')->eq($executionID)
|
||
->beginIF($browseType != 'all')->andWhere('t1.type')->eq($browseType)->fi()
|
||
->orderby($orderBy)
|
||
->fetchgroup('lane', 'column');
|
||
|
||
/* Get group objects. */
|
||
if($browseType == 'all' || $browseType == 'parentStory') $objectGroup['parentStory'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
|
||
if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
|
||
if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID);
|
||
if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id");
|
||
|
||
$cardGroup = array();
|
||
|
||
$avatarPairs = $this->loadModel('user')->getAvatarPairs();
|
||
$users = $this->loadModel('user')->getPairs('noletter');
|
||
foreach($cards as $laneID => $cells)
|
||
{
|
||
foreach($cells as $cell)
|
||
{
|
||
$order = 0;
|
||
$cardIdList = array_filter(explode(',', $cell->cards));
|
||
foreach($cardIdList as $cardID)
|
||
{
|
||
$cardData = array();
|
||
$objects = zget($objectGroup, $cell->type, array());
|
||
$object = zget($objects, $cardID, array());
|
||
|
||
if(empty($object)) continue;
|
||
if($cell->type == 'parentStory' && $object->isParent != '1') continue;
|
||
if($cell->type == 'story' && $object->isParent == '1') continue;
|
||
|
||
$cardData = $this->kanbanTao->initCardItem($object, $cell, $order, $avatarPairs, $users);
|
||
$cardData['acl'] = 'open';
|
||
$order ++;
|
||
|
||
if($searchValue != '' && strpos($cardData['title'], $searchValue) === false) continue;
|
||
$cardGroup[$cell->group][$laneID][$cell->column][] = $cardData;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $cardGroup;
|
||
}
|
||
|
||
/**
|
||
* 获取执行看板的数据。
|
||
* Get Kanban by execution id.
|
||
*
|
||
* @param int $executionID
|
||
* @param string $browseType all|story|bug|task
|
||
* @param string $groupBy default|pri|category|module|source|assignedTo|type|story|severity
|
||
* @param string $searchValue
|
||
* @param string $orderBy
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getExecutionKanban(int $executionID, string $browseType = 'all', string $groupBy = 'default', string $searchValue = '', string $orderBy = 'id_asc'): array
|
||
{
|
||
if($groupBy != 'default') return array($this->getKanban4Group($executionID, $browseType, $groupBy, $searchValue, $orderBy), array());
|
||
|
||
$execution = $this->loadModel('execution')->fetchByID($executionID);
|
||
$lanes = $this->dao->select('*')->from(TABLE_KANBANLANE)
|
||
->where('execution')->eq($executionID)
|
||
->andWhere('deleted')->eq(0)
|
||
->beginIF($browseType != 'all')->andWhere('type')->eq($browseType)->fi()
|
||
->beginIF(!empty($execution->attribute) && !in_array($execution->attribute, array('mix', 'request', 'design')))->andWhere('type')->notIn('epic,requirement')->fi()
|
||
->orderBy('order_asc')
|
||
->fetchAll('id');
|
||
|
||
if(empty($lanes)) return array();
|
||
|
||
foreach($lanes as $lane) $this->refreshCards((array)$lane);
|
||
|
||
$columns = $this->dao->select('t1.cards, t1.lane, t2.id, t2.type, t2.name, t2.color, t2.limit, t2.parent')->from(TABLE_KANBANCELL)->alias('t1')
|
||
->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column = t2.id')
|
||
->where('t2.deleted')->eq(0)
|
||
->andWhere('t1.lane')->in(array_keys($lanes))
|
||
->orderBy('id_asc')
|
||
->fetchGroup('lane', 'id');
|
||
|
||
$geMax = in_array($this->config->edition, array('max', 'ipd'));
|
||
/* Get group objects. */
|
||
if($browseType == 'all' || $browseType == 'epic') $objectGroup['epic'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'epic');
|
||
if($browseType == 'all' || $browseType == 'requirement') $objectGroup['requirement'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'requirement');
|
||
if($browseType == 'all' || $browseType == 'parentStory') $objectGroup['parentStory'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
|
||
if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
|
||
if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID);
|
||
if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id");
|
||
if($geMax && ($browseType == 'all' || $browseType == 'risk')) $objectGroup['risk'] = $this->loadModel('risk')->getKanbanRisks($executionID);
|
||
|
||
/* Get objects cards menus. */
|
||
$menus = array();
|
||
$menus['epic'] = $browseType == 'all' || $browseType == 'epic' ? $this->getKanbanCardMenu($executionID, $objectGroup['epic'], 'epic') : array();
|
||
$menus['requirement'] = $browseType == 'all' || $browseType == 'requirement' ? $this->getKanbanCardMenu($executionID, $objectGroup['requirement'], 'requirement') : array();
|
||
$menus['parentStory'] = $browseType == 'all' || $browseType == 'parentStory' ? $this->getKanbanCardMenu($executionID, $objectGroup['parentStory'], 'parentStory') : array();
|
||
$menus['story'] = $browseType == 'all' || $browseType == 'story' ? $this->getKanbanCardMenu($executionID, $objectGroup['story'], 'story') : array();
|
||
$menus['bug'] = $browseType == 'all' || $browseType == 'bug' ? $this->getKanbanCardMenu($executionID, $objectGroup['bug'], 'bug') : array();
|
||
$menus['task'] = $browseType == 'all' || $browseType == 'task' ? $this->getKanbanCardMenu($executionID, $objectGroup['task'], 'task') : array();
|
||
$menus['risk'] = ($geMax && ($browseType == 'all' || $browseType == 'risk')) ? $this->getKanbanCardMenu($executionID, $objectGroup['risk'], 'risk') : array();
|
||
|
||
/* 获取看板连线的fromKanbanID. */
|
||
$storyFromKanbanID = '';
|
||
$taskFromKanbanID = '';
|
||
foreach($lanes as $lane)
|
||
{
|
||
if($lane->type == 'parentStory') $storyFromKanbanID = 'group' . $lane->id;
|
||
if($lane->type == 'task') $taskFromKanbanID = 'group' . $lane->id;
|
||
}
|
||
|
||
/* Build kanban group data. */
|
||
$kanbanGroup = array();
|
||
$links = array();
|
||
$execution = $this->loadModel('execution')->fetchById($executionID);
|
||
$project = $this->loadModel('project')->fetchById($execution->project);
|
||
foreach($lanes as $lane)
|
||
{
|
||
$laneType = $lane->type;
|
||
if(in_array($laneType, array('epic', 'requirement')) && strpos($project->storyType, $laneType) === false) continue;
|
||
list($laneData, $columnData, $cardsData) = $this->buildExecutionGroup($lane, $columns, $objectGroup, $searchValue, $menus);
|
||
|
||
if($lane->type == 'risk' && in_array($this->config->edition, array('max', 'ipd'))) $cardsData = $this->appendRiskField($cardsData, $objectGroup['risk']);
|
||
|
||
$kanbanID = 'group' . $lane->id;
|
||
|
||
$kanbanGroup[$laneType]['id'] = $lane->id;
|
||
$kanbanGroup[$laneType]['key'] = $kanbanID;
|
||
$kanbanGroup[$laneType]['data'] = array();
|
||
$kanbanGroup[$laneType]['data']['lanes'] = array($laneData);
|
||
$kanbanGroup[$laneType]['data']['cols'] = $columnData;
|
||
$kanbanGroup[$laneType]['data']['items'] = $cardsData;
|
||
|
||
foreach($cardsData as $columnCard)
|
||
{
|
||
foreach($columnCard as $cards)
|
||
{
|
||
foreach($cards as $card)
|
||
{
|
||
if($card['parent'] == 0) continue;
|
||
/* 获取看板卡片的连线关系,业需、用需和父需求共用同一组看板列,所以fromKanban和toKanban是一样的。 */
|
||
$link = array('from' => $card['parent'], 'to' => $card['id']);
|
||
if($laneType == 'task')
|
||
{
|
||
$link['fromKanban'] = $taskFromKanbanID;
|
||
$link['toKanban'] = $taskFromKanbanID;
|
||
}
|
||
else
|
||
{
|
||
$link['fromKanban'] = $storyFromKanbanID;
|
||
$link['toKanban'] = in_array($laneType, array('epic', 'requirement')) ? $storyFromKanbanID : $kanbanID;
|
||
}
|
||
|
||
$links[] = $link;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 展示所有泳道的时候,业务、用需与父需求泳道共用看板列。 */
|
||
if($browseType == 'all')
|
||
{
|
||
foreach(array('requirement', 'epic') as $laneType)
|
||
{
|
||
if(!isset($kanbanGroup[$laneType])) continue;
|
||
|
||
array_unshift($kanbanGroup['parentStory']['data']['lanes'], $kanbanGroup[$laneType]['data']['lanes'][0]);
|
||
$kanbanGroup['parentStory']['data']['items'] += $kanbanGroup[$laneType]['data']['items'];
|
||
unset($kanbanGroup[$laneType]);
|
||
}
|
||
}
|
||
|
||
return array(array_values($kanbanGroup), $links);
|
||
}
|
||
|
||
/**
|
||
* 构建迭代看板的泳道组数据。
|
||
* Build the laneGroup data for the execution Kanban.
|
||
*
|
||
* @param object $lane
|
||
* @param array $columns
|
||
* @param array $objectGroup
|
||
* @param string $searchValue
|
||
* @param array $menus array(story => array, bug => array, task => array)
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function buildExecutionGroup(object $lane, array $columns, array $objectGroup, string $searchValue = '', array $menus = array()): array
|
||
{
|
||
$laneData = array();
|
||
$columnsData = array();
|
||
$cardsData = array();
|
||
|
||
$laneData['id'] = $lane->id;
|
||
$laneData['type'] = $lane->type;
|
||
$laneData['name'] = $lane->id;
|
||
$laneData['region'] = $lane->region;
|
||
$laneData['title'] = $lane->name;
|
||
$laneData['color'] = $lane->color;
|
||
|
||
foreach($columns[$lane->id] as $columnID => $column)
|
||
{
|
||
$cardIdList = array_unique(array_filter(explode(',', $column->cards)));
|
||
|
||
$columnsData[$column->id]['id'] = $columnID;
|
||
$columnsData[$column->id]['type'] = $column->type;
|
||
$columnsData[$column->id]['name'] = $columnID;
|
||
$columnsData[$column->id]['title'] = $column->name;
|
||
$columnsData[$column->id]['color'] = $column->color;
|
||
$columnsData[$column->id]['limit'] = $column->limit;
|
||
$columnsData[$column->id]['region'] = $lane->region;
|
||
$columnsData[$column->id]['laneName'] = $column->lane;
|
||
$columnsData[$column->id]['group'] = $lane->type;
|
||
$columnsData[$column->id]['cards'] = 0;
|
||
$columnsData[$column->id]['actionList'] = array('setColumn', 'setWIP');
|
||
|
||
if($column->parent > 0) $columnsData[$column->id]['parentName'] = $column->parent;
|
||
if($cardIdList)
|
||
{
|
||
$cardsData = $this->buildExecutionCards($cardsData, $column, $lane->type, $cardIdList, $objectGroup, $searchValue, $menus);
|
||
$columnsData[$column->id]['cards'] = empty($cardsData[$column->lane][$column->id]) ? 0 : count($cardsData[$column->lane][$column->id]);
|
||
}
|
||
}
|
||
|
||
foreach($columnsData as $columnData)
|
||
{
|
||
if(isset($columnData['parentName'])) $columnsData[$columnData['parentName']]['cards'] += $columnData['cards'];
|
||
}
|
||
|
||
return array($laneData, array_values($columnsData), $cardsData);
|
||
}
|
||
|
||
/**
|
||
* 构建迭代看板的卡片数据。
|
||
* Build the card data for the execution Kanban.
|
||
*
|
||
* @param object $lane
|
||
* @param array $laneData
|
||
* @param string $columnType
|
||
* @param array $cardIdList
|
||
* @param array $objectGroup
|
||
* @param string $searchValue
|
||
* @param array $menus array(story => array, bug => array, task => array)
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function buildExecutionCards(array $cardsData, object $column, string $laneType, array $cardIdList, array $objectGroup, string $searchValue = '', array $menus = array()): array
|
||
{
|
||
foreach($cardIdList as $cardID)
|
||
{
|
||
$cardData = array();
|
||
$objects = zget($objectGroup, $laneType, array());
|
||
$object = zget($objects, $cardID, array());
|
||
|
||
if(empty($object)) continue;
|
||
if($laneType == 'story' && $object->isParent == '1') continue;
|
||
$cardData = $this->buildExecutionCard($object, $column, $laneType, $searchValue, $menus);
|
||
if(empty($cardData)) continue;
|
||
|
||
$cardsData[$column->lane][$column->id][] = $cardData;
|
||
}
|
||
|
||
return $cardsData;
|
||
}
|
||
|
||
/**
|
||
* 构造执行看板卡片数据。
|
||
* Build execution card.
|
||
*
|
||
* @param object $object
|
||
* @param object $column
|
||
* @param string $laneType
|
||
* @param string $searchValue
|
||
* @param array $menus array(story => array, bug => array, task => array)
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function buildExecutionCard(object $object, object $column, string $laneType, string $searchValue = '', $menus = array()): array
|
||
{
|
||
if(empty($object)) return array();
|
||
|
||
$cardData = array();
|
||
$cardData['id'] = $object->id;
|
||
$cardData['lane'] = $column->lane;
|
||
$cardData['column'] = zget($column, 'column', $column->id);
|
||
$cardData['pri'] = zget($object, 'pri', 0);
|
||
$cardData['group'] = $laneType;
|
||
$cardData['parent'] = zget($object, 'originParent', 0);
|
||
$cardData['status'] = zget($object, 'status', '');
|
||
$cardData['estimate'] = zget($object, 'estimate', 0);
|
||
$cardData['assignedTo'] = $object->assignedTo;
|
||
$cardData['deadline'] = zget($object, 'deadline', '');
|
||
$cardData['severity'] = zget($object, 'severity', 0);
|
||
$cardData['actionList'] = zget(zget($menus, $laneType, array()), $object->id, array());
|
||
$cardData['canAssignTo'] = true;
|
||
|
||
$cardData['title'] = zget($object, 'title', '');
|
||
if($laneType == 'task')
|
||
{
|
||
$cardData['title'] = $object->name;
|
||
$cardData['status'] = $object->status;
|
||
$cardData['parent'] = $object->parent;
|
||
$cardData['isParent'] = $object->isParent;
|
||
$cardData['left'] = $object->left;
|
||
$cardData['estStarted'] = $object->estStarted;
|
||
$cardData['mode'] = $object->mode;
|
||
$cardData['canAssignTo'] = common::hasPriv('task', 'assignTo') && common::hasDBPriv($object, 'task', 'assignto');
|
||
if($object->mode == 'multi') $cardData['teamMembers'] = $object->teamMembers;
|
||
}
|
||
|
||
if($searchValue != '' && !str_contains($cardData['title'], $searchValue)) return array();
|
||
|
||
return $cardData;
|
||
}
|
||
|
||
/**
|
||
* 获取执行看板分组视图的数据。
|
||
* Get kanban for group view.
|
||
*
|
||
* @param int $executionID
|
||
* @param string $browseType
|
||
* @param string $groupBy
|
||
* @param string $searchValue
|
||
* @param string $orderBy
|
||
*
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getKanban4Group(int $executionID, string $browseType, string $groupBy, string $searchValue = '', string $orderBy = 'id_asc'): array
|
||
{
|
||
/* Get card data. */
|
||
$cardList = array();
|
||
if(in_array($browseType, array('story', 'parentStory'))) $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
|
||
|
||
if($browseType == 'epic') $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'epic');
|
||
if($browseType == 'requirement') $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'requirement');
|
||
if($browseType == 'bug') $cardList = $this->loadModel('bug')->getExecutionBugs($executionID);
|
||
if($browseType == 'task') $cardList = $this->loadModel('execution')->getKanbanTasks($executionID);
|
||
|
||
if($browseType == 'task' && $groupBy == 'assignedTo') $cardList = $this->appendTeamMember($cardList);
|
||
|
||
/* Get objects cards menus. */
|
||
$menus = array();
|
||
if($browseType == 'story') $menus['story'] = $this->getKanbanCardMenu($executionID, $cardList, 'story');
|
||
if($browseType == 'bug') $menus['bug'] = $this->getKanbanCardMenu($executionID, $cardList, 'bug');
|
||
if($browseType == 'task') $menus['task'] = $this->getKanbanCardMenu($executionID, $cardList, 'task');
|
||
|
||
if($groupBy == 'story' && $browseType == 'task' && !isset($this->lang->kanban->orderList[$orderBy])) $orderBy = 'id_asc';
|
||
$lanes = $this->getLanes4Group($executionID, $browseType, $groupBy, $cardList, $orderBy);
|
||
if(empty($lanes)) return array();
|
||
|
||
$columns = $this->getCols4Group($executionID, $browseType);
|
||
|
||
$cardGroup = array();
|
||
foreach($columns as $column)
|
||
{
|
||
if(empty($column->cards)) continue;
|
||
foreach($cardList as $card)
|
||
{
|
||
if(strpos($column->cards, ",$card->id,") !== false) $cardGroup[$column->columnType][$card->id] = $card;
|
||
}
|
||
}
|
||
|
||
/* Build kanban group data. */
|
||
$kanbanGroup = array();
|
||
list($laneData, $columnData, $cardData) = $this->buildGroupKanban($lanes, $columns, $cardGroup, $searchValue, $groupBy, $browseType, $menus);
|
||
|
||
$kanbanGroup['id'] = $executionID;
|
||
$kanbanGroup['key'] = 'group' . $executionID;
|
||
$kanbanGroup['data'] = array();
|
||
$kanbanGroup['data']['lanes'] = $laneData;
|
||
$kanbanGroup['data']['cols'] = array_values($columnData);
|
||
$kanbanGroup['data']['items'] = $cardData;
|
||
|
||
return array($kanbanGroup);
|
||
}
|
||
|
||
/**
|
||
* 构建分组视图的看板泳道。
|
||
* Build lanes for group kanban.
|
||
*
|
||
* @access public
|
||
* @param int $executionID
|
||
* @param string $browseType
|
||
* @param string $groupBy
|
||
* @param array $cardList
|
||
* @param string $orderBy
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getLanes4Group(int $executionID, string $browseType, string $groupBy, array $cardList, string $orderBy = 'id_asc'): array
|
||
{
|
||
$lanes = array();
|
||
$groupByList = array();
|
||
foreach($cardList as $item)
|
||
{
|
||
if(!isset($groupByList[$item->$groupBy])) $groupByList[$item->$groupBy] = $item->$groupBy;
|
||
|
||
if($groupBy == 'assignedTo' && !empty($item->teamMember))
|
||
{
|
||
foreach($item->teamMember as $account => $name)
|
||
{
|
||
if(!isset($groupByList[$account])) $groupByList[$account] = $account;
|
||
}
|
||
}
|
||
}
|
||
|
||
$objectPairs = $this->getObjectPairs($groupBy, $groupByList, $browseType, $orderBy);
|
||
if($groupBy == 'story') $objects = $this->dao->select('*')->from(TABLE_STORY)->where('deleted')->eq(0)->andWhere('id')->in($groupByList)->orderBy($orderBy)->fetchAll('id');
|
||
|
||
$laneColor = 0;
|
||
$order = 1;
|
||
foreach($objectPairs as $objectType => $objectName)
|
||
{
|
||
if(!isset($groupByList[$objectType]) && $objectType && !in_array($objectType, array('feature', 'design'))) continue;
|
||
|
||
$lane = new stdclass();
|
||
$lane->id = $groupBy . $objectType;
|
||
$lane->type = $browseType;
|
||
$lane->execution = $executionID;
|
||
$lane->name = $objectName;
|
||
$lane->order = (int)$order;
|
||
$lane->color = $this->config->kanban->laneColorList[$laneColor];
|
||
$lane->pri = (isset($objects) && isset($objects[$objectType]->pri)) ? $objects[$objectType]->pri : '';
|
||
$lane->assignedTo = (isset($objects) && isset($objects[$objectType]->assignedTo)) ? $objects[$objectType]->assignedTo : '';
|
||
|
||
$order += 1;
|
||
$laneColor += 1;
|
||
if($laneColor == count($this->config->kanban->laneColorList)) $laneColor = 0;
|
||
$lanes[$objectType] = $lane;
|
||
}
|
||
|
||
return $lanes;
|
||
}
|
||
|
||
/**
|
||
* 获取分组视图的看板列数据。
|
||
* Get columns for group kanban.
|
||
*
|
||
* @param int $executionID
|
||
* @param string $browseType
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getCols4Group(int $executionID, string $browseType): array
|
||
{
|
||
$execution = $this->loadModel('execution')->getByID($executionID);
|
||
$cards = $this->dao->select('t1.*, t1.cards, t2.`type` as columnType, t2.parent, t2.limit, t2.name as columnName, t2.color')->from(TABLE_KANBANCELL)->alias('t1')
|
||
->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id')
|
||
->leftJoin(TABLE_KANBANLANE)->alias('t3')->on('t1.lane = t3.id')
|
||
->leftJoin(TABLE_KANBANREGION)->alias('t4')->on('t1.kanban = t4.kanban')
|
||
->where('t1.kanban')->eq($executionID)
|
||
->andWhere('t1.`type`')->eq($browseType)
|
||
->beginIF(isset($execution->type) && $execution->type == 'kanban')
|
||
->andWhere('t3.deleted')->eq(0)
|
||
->andWhere('t4.deleted')->eq(0)
|
||
->fi()
|
||
->orderBy('column_asc')
|
||
->fetchAll();
|
||
|
||
$columns = array();
|
||
foreach($cards as $card)
|
||
{
|
||
if(!isset($columns[$card->columnType])) $columns[$card->columnType] = $card;
|
||
|
||
$columns[$card->columnType]->cards .= $card->cards;
|
||
$columns[$card->columnType]->cards = ',' . trim($columns[$card->columnType]->cards, ',') . ',';
|
||
}
|
||
return $columns;
|
||
}
|
||
|
||
/**
|
||
* 获取看板空间列表。
|
||
* Get space list.
|
||
*
|
||
* @param string $browseType private|cooperation|public|involved
|
||
* @param object $pager
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getSpaceList(string $browseType, ?object $pager = null): array
|
||
{
|
||
$account = $this->app->user->account;
|
||
$spaceIdList = $this->getCanViewObjects('kanbanspace', $browseType);
|
||
$spaceList = $this->dao->select('*')->from(TABLE_KANBANSPACE)
|
||
->where('deleted')->eq(0)
|
||
->beginIF(in_array($browseType, array('private', 'cooperation', 'public')))->andWhere('type')->eq($browseType)->fi()
|
||
->beginIF($browseType == 'private' && !$this->app->user->admin)->andWhere('owner')->eq($account)->fi()
|
||
->beginIF($this->cookie->showClosed == 0)->andWhere('status')->ne('closed')->fi()
|
||
->andWhere('id')->in($spaceIdList)
|
||
->orderBy('id_desc')
|
||
->page($pager)
|
||
->fetchAll('id');
|
||
|
||
$kanbanIdList = $this->getCanViewObjects('kanban', $browseType);
|
||
$kanbanGroup = $this->getGroupBySpaceList(array_keys($spaceList), $kanbanIdList);
|
||
foreach($spaceList as $spaceID => $space)
|
||
{
|
||
if(isset($kanbanGroup[$spaceID])) $space->kanbans = $kanbanGroup[$spaceID];
|
||
}
|
||
|
||
return $spaceList;
|
||
}
|
||
|
||
/**
|
||
* 获取看板空间键值对。
|
||
* Get space pairs.
|
||
*
|
||
* @param string $browseType private|cooperation|public|involved
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getSpacePairs(string $browseType = 'private'): array
|
||
{
|
||
$spaceIdList = $this->getCanViewObjects('kanbanspace', $browseType);
|
||
|
||
return $this->dao->select('id,name')->from(TABLE_KANBANSPACE)
|
||
->where('deleted')->eq(0)
|
||
->andWhere('id')->in($spaceIdList)
|
||
->beginIF(in_array($browseType, array('private', 'cooperation', 'public')))->andWhere('type')->eq($browseType)->fi()
|
||
->beginIF($this->cookie->showClosed == 0 && $browseType != 'showClosed')->andWhere('status')->ne('closed')->fi()
|
||
->orderBy('id_desc')
|
||
->fetchPairs('id');
|
||
}
|
||
|
||
/**
|
||
* 获取看板键值对。
|
||
* Get Kanban pairs.
|
||
*
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getKanbanPairs(): array
|
||
{
|
||
$kanbanIdList = $this->getCanViewObjects('kanban');
|
||
|
||
return $this->dao->select('id,name')->from(TABLE_KANBAN)
|
||
->where('deleted')->eq(0)
|
||
->beginIF(!$this->app->user->admin)->andWhere('id')->in($kanbanIdList)->fi()
|
||
->orderBy('id_desc')
|
||
->fetchPairs('id');
|
||
}
|
||
|
||
/**
|
||
* 获取有权查看的对象。
|
||
* Get can view objects.
|
||
*
|
||
* @param string $objectType kanbanspace|kanban
|
||
* @param string $param all|noclosed|private|cooperation|public|involved
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getCanViewObjects(string $objectType = 'kanban', string $param = 'all'): array
|
||
{
|
||
$table = $this->config->objectTables[$objectType];
|
||
$objects = $this->dao->select('*')->from($table)
|
||
->where('deleted')->eq(0)
|
||
->beginIF(strpos($param, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi()
|
||
->fetchAll('id', false);
|
||
|
||
$spaceList = $objectType == 'kanban' ? $this->dao->select('id,owner,type')->from(TABLE_KANBANSPACE)->fetchAll('id') : array();
|
||
|
||
if($param && $this->app->user->admin && strpos('private,involved', $param) === false) return array_keys($objects);
|
||
|
||
$account = $this->app->user->account;
|
||
foreach($objects as $objectID => $object)
|
||
{
|
||
if($objectType == 'kanbanspace' && $object->type == 'public' && $param != 'involved') continue;
|
||
|
||
$remove = true;
|
||
|
||
if($object->owner == $account) $remove = false;
|
||
if(strpos(",{$object->team},", ",$account,") !== false) $remove = false;
|
||
if(strpos(",{$object->whitelist},", ",$account,") !== false) $remove = false;
|
||
|
||
if($objectType == 'kanban')
|
||
{
|
||
$spaceOwner = isset($spaceList[$object->space]->owner) ? $spaceList[$object->space]->owner : '';
|
||
$spaceType = isset($spaceList[$object->space]->type) ? $spaceList[$object->space]->type : '';
|
||
if(strpos(",$spaceOwner,", ",$account,") !== false) $remove = false;
|
||
if($spaceType == 'public' && $param != 'involved') $remove = false;
|
||
}
|
||
|
||
if($remove) unset($objects[$objectID]);
|
||
}
|
||
|
||
return array_keys($objects);
|
||
}
|
||
|
||
/**
|
||
* 创建看板空间。
|
||
* Create a space.
|
||
*
|
||
* @param object $space
|
||
* @access public
|
||
* @return int|false
|
||
*/
|
||
public function createSpace(object $space): int|false
|
||
{
|
||
$account = $this->app->user->account;
|
||
|
||
if($space->type == 'private') $space->owner = $account;
|
||
if($space->type != 'private') $this->config->kanban->createspace->requiredFields .= ',owner';
|
||
if(strpos(",{$space->team},", ",$account,") === false) $space->team .= ",$account";
|
||
if(strpos(",{$space->team},", ",$space->owner,") === false) $space->team .= ",$space->owner";
|
||
|
||
$this->dao->insert(TABLE_KANBANSPACE)->data($space)
|
||
->autoCheck()
|
||
->batchCheck($this->config->kanban->createspace->requiredFields, 'notempty')
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$spaceID = $this->dao->lastInsertID();
|
||
$this->dao->update(TABLE_KANBANSPACE)->set('`order`')->eq($spaceID)->where('id')->eq($spaceID)->exec();
|
||
$this->loadModel('file')->saveUpload('kanbanspace', $spaceID);
|
||
$this->file->updateObjectID($this->post->uid, $spaceID, 'kanbanspace');
|
||
$this->loadModel('action')->create('kanbanSpace', $spaceID, 'created');
|
||
|
||
return $spaceID;
|
||
}
|
||
|
||
/**
|
||
* 编辑看板空间。
|
||
* Update a space.
|
||
*
|
||
* @param object $space
|
||
* @param int $spaceID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function updateSpace(object $space, int $spaceID): bool
|
||
{
|
||
$oldSpace = $this->getSpaceById($spaceID);
|
||
|
||
if($space->type == 'cooperation' || $space->type == 'public') $space->whitelist = '';
|
||
if($space->type != 'private') $this->config->kanban->editspace->requiredFields .= ',owner';
|
||
|
||
$this->dao->update(TABLE_KANBANSPACE)->data($space)
|
||
->autoCheck()
|
||
->batchCheck($this->config->kanban->editspace->requiredFields, 'notempty')
|
||
->where('id')->eq($spaceID)
|
||
->exec();
|
||
|
||
if($oldSpace->type == 'private' && (in_array($space->type, array('cooperation', 'public'))))
|
||
{
|
||
$this->dao->update(TABLE_KANBAN)->set('team = whitelist')->set('whitelist')->eq('')->where('space')->eq($spaceID)->andWhere('deleted')->eq('0')->exec();
|
||
}
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$this->loadModel('file')->saveUpload('kanbanspace', $spaceID);
|
||
$this->file->updateObjectID($this->post->uid, $spaceID, 'kanbanspace');
|
||
$changes = common::createChanges($oldSpace, $space);
|
||
|
||
if($changes)
|
||
{
|
||
$actionID = $this->loadModel('action')->create('kanbanSpace', $spaceID, 'edited');
|
||
$this->action->logHistory($actionID, $changes);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 关闭看板空间。
|
||
* Close a space.
|
||
*
|
||
* @param int $spaceID
|
||
* @param object $space
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
function closeSpace(int $spaceID, object $space): bool
|
||
{
|
||
$oldSpace = $this->getSpaceById($spaceID);
|
||
|
||
$this->dao->update(TABLE_KANBANSPACE)->data($space)
|
||
->autoCheck()
|
||
->where('id')->eq($spaceID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
$changes = common::createChanges($oldSpace, $space);
|
||
if($changes)
|
||
{
|
||
$actionID = $this->loadModel('action')->create('kanbanSpace', $spaceID, 'closed', $this->post->comment);
|
||
$this->action->logHistory($actionID, $changes);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 激活看板空间。
|
||
* Activate a space.
|
||
*
|
||
* @param int $spaceID
|
||
* @param object $space
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
function activateSpace(int $spaceID, object $space): bool
|
||
{
|
||
$oldSpace = $this->getSpaceById($spaceID);
|
||
|
||
$this->dao->update(TABLE_KANBANSPACE)->data($space)
|
||
->autoCheck()
|
||
->where('id')->eq($spaceID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$changes = common::createChanges($oldSpace, $space);
|
||
if($changes)
|
||
{
|
||
$actionID = $this->loadModel('action')->create('kanbanSpace', $spaceID, 'activated', $this->post->comment);
|
||
$this->action->logHistory($actionID, $changes);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 获取看板泳道键值对。
|
||
* Get lane pairs by region id.
|
||
*
|
||
* @param array|int $regionID
|
||
* @param string $type all|story|task|bug|common
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getLanePairsByRegion(int $regionID, string $type = 'all'): array
|
||
{
|
||
return $this->dao->select('id, name')->from(TABLE_KANBANLANE)
|
||
->where('deleted')->eq('0')
|
||
->andWhere('region')->in($regionID)
|
||
->beginIF($type != 'all')->andWhere('type')->eq($type)->fi()
|
||
->fetchPairs();
|
||
}
|
||
|
||
/**
|
||
* 获取看板泳道分组。
|
||
* Get lane by region id.
|
||
*
|
||
* @param array|int $regionID
|
||
* @param string $type all|story|task|bug|common
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getLaneGroupByRegion(array|int $regionID, string $type = 'all'): array
|
||
{
|
||
return $this->dao->select('*')->from(TABLE_KANBANLANE)
|
||
->where('deleted')->eq('0')
|
||
->andWhere('region')->in($regionID)
|
||
->beginIF($type != 'all')->andWhere('type')->eq($type)->fi()
|
||
->fetchGroup('region');
|
||
}
|
||
|
||
/**
|
||
* 创建看板泳道。
|
||
* Create a lane.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $regionID
|
||
* @param object $lane
|
||
* @param string $mode new|copy
|
||
* @access public
|
||
* @return int|bool
|
||
*/
|
||
public function createLane(int $kanbanID, int $regionID, ?object $lane = null, string $mode = 'new'): int|bool
|
||
{
|
||
if($mode == 'new')
|
||
{
|
||
$laneType = isset($_POST['laneType']) ? $_POST['laneType'] : 'common';
|
||
if($laneType == 'common')
|
||
{
|
||
$sameNameLane = $this->dao->select('id')->from(TABLE_KANBANLANE)->where('region')->eq($regionID)->andWhere('name')->eq($lane->name)->andWhere('deleted')->eq('0')->limit(1)->fetch();
|
||
if($sameNameLane)
|
||
{
|
||
dao::$errors['name'][] = $this->lang->kanbanlane->error->hasExist;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/* 共享看板列不能为空。 */
|
||
if($lane->mode == 'sameAsOther' && $lane->otherLane == 0)
|
||
{
|
||
dao::$errors['otherLane'][] = $this->lang->kanbanlane->error->emptyOtherLane;
|
||
return false;
|
||
}
|
||
|
||
$maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBANLANE)
|
||
->where('region')->eq($regionID)
|
||
->fetch('maxOrder');
|
||
|
||
$lane->order = $maxOrder ? $maxOrder + 1 : 1;
|
||
$lane->type = $laneType;
|
||
$lane->execution = isset($_POST['laneType']) ? $kanbanID : 0;
|
||
|
||
if($lane->mode == 'sameAsOther')
|
||
{
|
||
if($lane->otherLane) $lane->group = $this->dao->select('`group`')->from(TABLE_KANBANLANE)->where('id')->eq($lane->otherLane)->fetch('group');
|
||
}
|
||
elseif($lane->mode == 'independent')
|
||
{
|
||
$lane->group = $this->createGroup($kanbanID, $regionID);
|
||
if($lane->type == 'common') $this->createDefaultColumns($regionID, $lane->group);
|
||
}
|
||
}
|
||
|
||
$this->dao->insert(TABLE_KANBANLANE)->data($lane, $skip = 'mode,otherLane')
|
||
->batchCheck($this->config->kanban->require->createlane, 'notempty')
|
||
->autoCheck()
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$laneID = $this->dao->lastInsertID();
|
||
if($mode == 'new')
|
||
{
|
||
if($lane->type != 'common' && isset($lane->mode) && $lane->mode == 'independent') $this->createRDColumn($regionID, $lane->group, $laneID, $lane->type, $kanbanID);
|
||
|
||
if($lane->mode == 'sameAsOther' || ($lane->type == 'common' && $lane->mode == 'independent'))
|
||
{
|
||
$columnIDList = $this->dao->select('id')->from(TABLE_KANBANCOLUMN)->where('deleted')->eq(0)->andWhere('archived')->eq(0)->andWhere('`group`')->eq($lane->group)->fetchPairs();
|
||
foreach($columnIDList as $columnID)
|
||
{
|
||
$this->addKanbanCell($kanbanID, $laneID, $columnID, $lane->type);
|
||
|
||
if(dao::isError()) return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
$this->loadModel('action')->create('kanbanLane', $laneID, 'created');
|
||
return $laneID;
|
||
}
|
||
|
||
/*
|
||
* 创建看板。
|
||
* Create a kanban.
|
||
*
|
||
* @param object $kanban
|
||
* @access public
|
||
* @return int|false
|
||
*/
|
||
public function create(object $kanban): int|false
|
||
{
|
||
$account = $this->app->user->account;
|
||
|
||
if($this->post->import == 'on') $kanban->object = implode(',', $this->post->importObjectList);
|
||
|
||
if(strpos(",{$kanban->team},", ",$account,") === false) $kanban->team .= ",$account";
|
||
if(strpos(",{$kanban->team},", ",$kanban->owner,") === false) $kanban->team .= ",$kanban->owner";
|
||
|
||
if(!empty($kanban->space))
|
||
{
|
||
$maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBAN)
|
||
->where('space')->eq($kanban->space)
|
||
->fetch('maxOrder');
|
||
$kanban->order = $maxOrder ? $maxOrder + 1 : 1;
|
||
|
||
$space = $this->getSpaceById($kanban->space);
|
||
if(!empty($space) && $space->type == 'private') $kanban->owner = $account;
|
||
}
|
||
|
||
$this->kanbanTao->createKanban($kanban);
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$kanbanID = $this->dao->lastInsertID();
|
||
$kanbanInfo = $this->getByID($kanbanID);
|
||
|
||
$this->loadModel('action')->create('kanban', $kanbanID, 'created');
|
||
$this->loadModel('file')->saveUpload('kanban', $kanbanID);
|
||
$this->file->updateObjectID($this->post->uid, $kanbanID, 'kanban');
|
||
|
||
!empty($kanban->copyRegion) ? $this->copyRegions($kanbanInfo, $kanban->copyKanbanID): $this->createDefaultRegion($kanbanInfo);
|
||
|
||
if(!empty($kanbanInfo->team) || !empty($kanbanInfo->whitelist))
|
||
{
|
||
$type = !empty($kanbanInfo->team) ? 'team' : 'whitelist';
|
||
$kanbanMembers = empty($kanbanInfo->{$type}) ? array() : explode(',', $kanbanInfo->{$type});
|
||
$this->addSpaceMembers($kanbanInfo->space, $type, $kanbanMembers);
|
||
}
|
||
|
||
return $kanbanID;
|
||
}
|
||
|
||
/**
|
||
* 编辑看板。
|
||
* Update a kanban.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param object $kanban
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function update(int $kanbanID, object $kanban): bool
|
||
{
|
||
$oldKanban = $this->getByID($kanbanID);
|
||
|
||
if(strpos(",{$kanban->team},", ",$kanban->owner,") === false) $kanban->team .= ",$kanban->owner";
|
||
|
||
$this->dao->update(TABLE_KANBAN)->data($kanban)
|
||
->autoCheck()
|
||
->batchCheck($this->config->kanban->edit->requiredFields, 'notempty')
|
||
->where('id')->eq($kanbanID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$this->loadModel('file')->saveUpload('kanban', $kanbanID);
|
||
$this->file->updateObjectID($this->post->uid, $kanbanID, 'kanban');
|
||
|
||
if(!empty($kanban->team) || !empty($kanban->whitelist))
|
||
{
|
||
$type = !empty($kanban->team) ? 'team' : 'whitelist';
|
||
$kanbanMembers = empty($kanban->{$type}) ? array() : explode(',', $kanban->{$type});
|
||
$this->addSpaceMembers($kanban->space, $type, $kanbanMembers);
|
||
}
|
||
|
||
$changes = common::createChanges($oldKanban, $kanban);
|
||
$actionID = $this->loadModel('action')->create('kanban', $kanbanID, 'edited');
|
||
$this->action->logHistory($actionID, $changes);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 设置看板。
|
||
* Setting kanban.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param object $kanban
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function setting(int $kanbanID, object $kanban): bool
|
||
{
|
||
$oldKanban = $this->getByID($kanbanID);
|
||
|
||
if($this->post->import == 'off') $kanban->object = '';
|
||
if($this->post->heightType == 'auto') $kanban->displayCards = 0;
|
||
if(empty($kanban->displayCards)) $kanban->displayCards = 0;
|
||
|
||
if($this->post->import == 'on') $kanban->object = $this->post->importObjectList ? implode(',', $this->post->importObjectList) : '';
|
||
if(isset($_POST['heightType']) && $this->post->heightType == 'custom' && !$this->checkDisplayCards($kanban->displayCards)) return false;
|
||
|
||
$this->dao->update(TABLE_KANBAN)->data($kanban)
|
||
->autoCheck()
|
||
->batchCheck($this->config->kanban->edit->requiredFields, 'notempty')
|
||
->checkIF(!$kanban->fluidBoard, 'colWidth', 'ge', $this->config->minColWidth)
|
||
->batchCheckIF($kanban->fluidBoard, 'minColWidth', 'ge', $this->config->minColWidth)
|
||
->checkIF($kanban->minColWidth >= $this->config->minColWidth && $kanban->fluidBoard, 'maxColWidth', 'gt', $kanban->minColWidth)
|
||
->where('id')->eq($kanbanID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$changes = common::createChanges($oldKanban, $kanban);
|
||
|
||
if($changes)
|
||
{
|
||
$actionID = $this->loadModel('action')->create('kanban', $kanbanID, 'edited');
|
||
$this->action->logHistory($actionID, $changes);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 激活看板。
|
||
* Activate a kanban.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param object $kanban
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
function activate(int $kanbanID, object $kanban): bool
|
||
{
|
||
$oldKanban = $this->getByID($kanbanID);
|
||
|
||
$this->dao->update(TABLE_KANBAN)->data($kanban)
|
||
->autoCheck()
|
||
->where('id')->eq($kanbanID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$changes = common::createChanges($oldKanban, $kanban);
|
||
$actionID = $this->loadModel('action')->create('kanban', $kanbanID, 'activated', $this->post->comment);
|
||
$this->action->logHistory($actionID, $changes);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 关闭看板。
|
||
* Close a kanban.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param object $kanban
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
function close(int $kanbanID, object $kanban): bool
|
||
{
|
||
$oldKanban = $this->getByID($kanbanID);
|
||
|
||
$this->dao->update(TABLE_KANBAN)->data($kanban)
|
||
->autoCheck()
|
||
->where('id')->eq($kanbanID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$changes = common::createChanges($oldKanban, $kanban);
|
||
if($changes)
|
||
{
|
||
$actionID = $this->loadModel('action')->create('kanban', $kanbanID, 'closed', $this->post->comment);
|
||
$this->action->logHistory($actionID, $changes);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/* 执行看板新增列和泳道。
|
||
* Check whether kanban lane exists.
|
||
*
|
||
* @param object $execution
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function createLaneIfNotExist(object $execution): void
|
||
{
|
||
$kanbanColumns = $this->dao->select('*')->from(TABLE_KANBANLANE)
|
||
->where('execution')->eq($execution->id)
|
||
->andWhere('deleted')->eq(0)
|
||
->fetchAll('type');
|
||
$types = array_keys($kanbanColumns);
|
||
|
||
/* 如果没有任意一种类型的 kanban, 生成一份新的 */
|
||
if(empty($types))
|
||
{
|
||
$this->createExecutionLane($execution, 'all');
|
||
}
|
||
else
|
||
{
|
||
/* 如果缺少某种类型的 kanban,单独生成缺少的 */
|
||
$kanbanTypes = $this->lang->kanban->type;
|
||
unset($kanbanTypes['all']);
|
||
unset($kanbanTypes['epic']);
|
||
unset($kanbanTypes['requirement']);
|
||
foreach($kanbanTypes as $type => $typeName)
|
||
{
|
||
if(!in_array($type, $types)) $this->createExecutionLane($execution, $type);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行看板新增列和泳道。
|
||
* Add execution Kanban lanes and columns.
|
||
*
|
||
* @param object $execution
|
||
* @param string $type all|parentStory|story|bug|task|risk
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function createExecutionLane(object $execution, string $type = 'all'): bool
|
||
{
|
||
if(!$execution) return false;
|
||
|
||
/* e.g $defaults = array('risk' => (object)array('name' => 'risk', 'color' => '#FF0000', 'order' => 20)); */
|
||
$defaults = ($type != 'all') ? array($type => $this->config->kanban->default->$type) : (array)$this->config->kanban->default;
|
||
|
||
$ERURLanes = array();
|
||
$parentStoryLaneID = 0;
|
||
|
||
foreach($defaults as $type => $lane)
|
||
{
|
||
/* 只有综合、需求、设计阶段,才可关联业需、用需。 */
|
||
if($execution->type != 'stage' && !in_array($execution->attribute, array('mix', 'request', 'design')) && in_array($type, array('epic', 'requirement'))) continue;
|
||
|
||
$lane->type = $type;
|
||
$lane->execution = $execution->id;
|
||
$lane->region = 0;
|
||
$lane->group = 0;
|
||
$lane->groupby = '';
|
||
$lane->extra = '';
|
||
$this->dao->insert(TABLE_KANBANLANE)->data($lane)->exec();
|
||
|
||
$laneID = $this->dao->lastInsertId();
|
||
if($type == 'parentStory') $parentStoryLaneID = $laneID;
|
||
|
||
if(in_array($type, array('epic', 'requirement')))
|
||
{
|
||
$ERURLanes[$type] = $laneID;
|
||
}
|
||
else
|
||
{
|
||
if($type != 'risk') $this->createExecutionColumns($laneID, $type, $execution->id);
|
||
else $this->createExecutionRiskColumns($laneID, $type, $execution->id);
|
||
}
|
||
}
|
||
|
||
if($ERURLanes)
|
||
{
|
||
$columnIDList = $this->dao->select('`column`')->from(TABLE_KANBANCELL)->where('lane')->eq($parentStoryLaneID)->fetchPairs();
|
||
foreach($ERURLanes as $type => $laneID)
|
||
{
|
||
foreach($columnIDList as $columnID)
|
||
{
|
||
$this->addKanbanCell($execution->id, $laneID, $columnID, $type);
|
||
if(dao::isError()) return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 创建执行看板列。
|
||
* Create execution columns.
|
||
*
|
||
* @param int|array $laneID
|
||
* @param string $type story|bug|task
|
||
* @param int $executionID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function createExecutionColumns(int|array $laneID, string $type, int $executionID): void
|
||
{
|
||
$designColumnID = $devColumnID = $testColumnID = $resolvingColumnID = 0;
|
||
|
||
$columns = array();
|
||
if(in_array($type, array('epic', 'requirement', 'parentStory'))) $columns = $this->lang->kanban->ERURColumn;
|
||
if($type == 'story') $columns = $this->lang->kanban->storyColumn;
|
||
if($type == 'bug') $columns = $this->lang->kanban->bugColumn;
|
||
if($type == 'task') $columns = $this->lang->kanban->taskColumn;
|
||
if(empty($columns)) return;
|
||
|
||
foreach($columns as $colType => $name)
|
||
{
|
||
$data = new stdclass();
|
||
$data->name = $name;
|
||
$data->color = '#333';
|
||
$data->type = $colType;
|
||
$data->region = 0;
|
||
|
||
if(str_contains(',designing,designed,', ",{$colType},")) $data->parent = $designColumnID;
|
||
if(str_contains(',developing,developed,', ",{$colType},")) $data->parent = $devColumnID;
|
||
if(str_contains(',testing,tested,', ",{$colType},")) $data->parent = $testColumnID;
|
||
if(str_contains(',fixing,fixed,', ",{$colType},")) $data->parent = $resolvingColumnID;
|
||
if(str_contains(',resolving,develop,test,', ",{$colType},")) $data->parent = -1;
|
||
|
||
$this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec();
|
||
|
||
$colID = $this->dao->lastInsertId();
|
||
if($colType == 'design') $designColumnID = $colID;
|
||
if($colType == 'develop') $devColumnID = $colID;
|
||
if($colType == 'test') $testColumnID = $colID;
|
||
if($colType == 'resolving') $resolvingColumnID = $colID;
|
||
|
||
if(is_array($laneID))
|
||
{
|
||
foreach($laneID as $id) $this->addKanbanCell($executionID, $id, $colID, $type);
|
||
}
|
||
else
|
||
{
|
||
$this->addKanbanCell($executionID, $laneID, $colID, $type);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 新增看板Cell。
|
||
* Add kanban cell for new lane.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $laneID
|
||
* @param int $colID
|
||
* @param string $type story|task|bug|card
|
||
* @param string $cardID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function addKanbanCell(int $kanbanID, int $laneID, int $colID, string $type, string $cardID = ''): void
|
||
{
|
||
$cell = $this->dao->select('id, cards')->from(TABLE_KANBANCELL)
|
||
->where('kanban')->eq($kanbanID)
|
||
->andWhere('lane')->eq($laneID)
|
||
->andWhere('`column`')->eq($colID)
|
||
->andWhere('type')->eq($type)
|
||
->fetch();
|
||
|
||
if(empty($cell))
|
||
{
|
||
$cell = new stdclass();
|
||
$cell->kanban = $kanbanID;
|
||
$cell->lane = $laneID;
|
||
$cell->column = $colID;
|
||
$cell->type = $type;
|
||
$cell->cards = $cardID ? ",$cardID," : '';
|
||
|
||
$this->dao->insert(TABLE_KANBANCELL)->data($cell)->exec();
|
||
}
|
||
else
|
||
{
|
||
$cell->cards = $cell->cards ? ",$cardID" . $cell->cards : ",$cardID,";
|
||
$this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cell->cards)->where('id')->eq($cell->id)->exec();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 向看板空间添加成员。
|
||
* Add space members.
|
||
*
|
||
* @param int $spaceID
|
||
* @param string $type team|whitelist
|
||
* @param array $kanbanMembers
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function addSpaceMembers(int $spaceID, string $type, array $kanbanMembers = array()): void
|
||
{
|
||
$space = $this->getSpaceById($spaceID);
|
||
if(empty($space)) return;
|
||
|
||
$spaceMembers = empty($space->{$type}) ? array() : explode(',', $space->{$type});
|
||
$members = $space->{$type};
|
||
$addMembers = array_diff($kanbanMembers, $spaceMembers);
|
||
|
||
if(!empty($addMembers))
|
||
{
|
||
$addMembers = implode(',', $addMembers);
|
||
$members .= ',' . trim($addMembers, ',');
|
||
$this->dao->update(TABLE_KANBANSPACE)->set($type)->eq($members)->where('id')->eq($spaceID)->exec();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从看板格子中移除卡片。
|
||
* Remove kanban cell.
|
||
*
|
||
* @param string $type
|
||
* @param int|array $removeCardID
|
||
* @param array $kanbanList
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function removeKanbanCell(string $type, int|array $removeCardID, array $kanbanList): void
|
||
{
|
||
$removeIDList = is_array($removeCardID) ? $removeCardID : array($removeCardID);
|
||
foreach($removeIDList as $cardID)
|
||
{
|
||
if(empty($cardID)) continue;
|
||
|
||
$this->dao->update(TABLE_KANBANCELL)->set("cards = REPLACE(cards, ',$cardID,', ',')")->where('type')->eq($type)->andWhere('kanban')->eq($kanbanList[$cardID])->exec();
|
||
}
|
||
|
||
$this->dao->update(TABLE_KANBANCELL)
|
||
->set('cards')->eq('')
|
||
->where('cards')->eq(',')
|
||
->andWhere('type')->eq($type)
|
||
->andWhere('kanban')->in($kanbanList)
|
||
->exec();
|
||
}
|
||
|
||
/**
|
||
* 创建默认的专业研发看板。
|
||
* Create a default RD kanban.
|
||
*
|
||
* @param object $execution
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function createRDKanban(object $execution): bool
|
||
{
|
||
$regionID = $this->createRDRegion($execution);
|
||
if(dao::isError()) return false;
|
||
|
||
$this->createGroup((int)$execution->id, $regionID);
|
||
if(dao::isError()) return false;
|
||
|
||
$this->createRDLane((int)$execution->id, $regionID);
|
||
if(dao::isError()) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 创建默认的专业研发看板区域。
|
||
* Create a default RD region.
|
||
*
|
||
* @param object $execution
|
||
*
|
||
* @access public
|
||
* @return int|bool
|
||
*/
|
||
public function createRDRegion(object $execution): int|bool
|
||
{
|
||
$region = new stdclass();
|
||
$region->name = $this->lang->kanbanregion->default;
|
||
$region->kanban = $execution->id;
|
||
$region->createdBy = $this->app->user->account;
|
||
$region->createdDate = helper::now();
|
||
$region->order = 1;
|
||
|
||
$this->dao->insert(TABLE_KANBANREGION)->data($region)
|
||
->check('name', 'unique', "kanban={$execution->id} AND deleted='0' AND space = '0'")
|
||
->autoCheck()
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
return $this->dao->lastInsertId();
|
||
}
|
||
|
||
/**
|
||
* 创建默认的专业研发看板泳道。
|
||
* Create default RD lanes.
|
||
*
|
||
* @param int $executionID
|
||
* @param int $regionID
|
||
*
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function createRDLane(int $executionID, int $regionID): bool
|
||
{
|
||
$execution = $this->loadModel('execution')->fetchByID($executionID);
|
||
$project = $this->loadModel('project')->fetchByID($execution->project);
|
||
$index = 0;
|
||
foreach($this->lang->kanban->laneTypeList as $type => $name)
|
||
{
|
||
$groupID = $this->createGroup($executionID, $regionID);
|
||
if(dao::isError()) return false;
|
||
|
||
$lane = new stdclass();
|
||
$lane->execution = $executionID;
|
||
$lane->type = $type;
|
||
$lane->region = $regionID;
|
||
$lane->group = $groupID;
|
||
$lane->name = $name;
|
||
$lane->color = $this->config->kanban->laneColorList[$index];
|
||
$lane->order = $this->config->kanban->RDLaneOrder[$type];
|
||
|
||
$this->dao->insert(TABLE_KANBANLANE)->data($lane)->autoCheck()->exec();
|
||
if(dao::isError()) return false;
|
||
$laneID = $this->dao->lastInsertId();
|
||
|
||
$this->createRDColumn($regionID, $groupID, $laneID, $type, $executionID);
|
||
|
||
$index ++;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 创建默认值的专业研发看板列。
|
||
* Create default RD columns.
|
||
*
|
||
* @param int $regionID
|
||
* @param int $groupID
|
||
* @param int $laneID
|
||
* @param string $laneType
|
||
* @param int $executionID
|
||
*
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function createRDColumn(int $regionID, int $groupID, int $laneID, string $laneType, int $executionID): bool
|
||
{
|
||
$designColumnID = $devColumnID = $testColumnID = $resolvingColumnID = 0;
|
||
if($laneType == 'parentStory') $columnList = $this->lang->kanban->ERURColumn;
|
||
if($laneType == 'story') $columnList = $this->lang->kanban->storyColumn;
|
||
if($laneType == 'bug') $columnList = $this->lang->kanban->bugColumn;
|
||
if($laneType == 'task') $columnList = $this->lang->kanban->taskColumn;
|
||
|
||
foreach($columnList as $type => $name)
|
||
{
|
||
$data = new stdclass();
|
||
$data->name = $name;
|
||
$data->color = '#333';
|
||
$data->type = $type;
|
||
$data->group = $groupID;
|
||
$data->region = $regionID;
|
||
|
||
if(strpos(',designing,designed,', $type) !== false) $data->parent = $designColumnID;
|
||
if(strpos(',developing,developed,', $type) !== false) $data->parent = $devColumnID;
|
||
if(strpos(',testing,tested,', $type) !== false) $data->parent = $testColumnID;
|
||
if(strpos(',fixing,fixed,', $type) !== false) $data->parent = $resolvingColumnID;
|
||
if(strpos(',design,develop,test,resolving,', $type) !== false) $data->parent = -1;
|
||
|
||
$this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec();
|
||
if(dao::isError()) return false;
|
||
|
||
$columnID = $this->dao->lastInsertId();
|
||
if($type == 'design') $designColumnID = $columnID;
|
||
if($type == 'develop') $devColumnID = $columnID;
|
||
if($type == 'test') $testColumnID = $columnID;
|
||
if($type == 'resolving') $resolvingColumnID = $columnID;
|
||
|
||
$this->addKanbanCell($executionID, $laneID, $columnID, $laneType);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 编辑区域。
|
||
* Update a region.
|
||
*
|
||
* @param int $regionID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function updateRegion(int $regionID): bool
|
||
{
|
||
$region = new stdclass();
|
||
$region->lastEditedBy = $this->app->user->account;
|
||
$region->lastEditedDate = helper::now();
|
||
$region->name = trim($this->post->name);
|
||
|
||
$oldRegion = $this->getRegionById($regionID);
|
||
|
||
$this->dao->update(TABLE_KANBANREGION)->data($region)
|
||
->autoCheck()
|
||
->batchcheck($this->config->kanban->editregion->requiredFields, 'notempty')
|
||
->where('id')->eq($regionID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$changes = common::createChanges($oldRegion, $region);
|
||
if($changes)
|
||
{
|
||
$actionID = $this->loadModel('action')->create('kanbanregion', $regionID, 'edited');
|
||
$this->action->logHistory($actionID, $changes);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 编辑看板泳道。
|
||
* Update kanban lane.
|
||
*
|
||
* @param int $executionID
|
||
* @param string $laneType
|
||
* @param int $cardID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function updateLane(int $executionID, string $laneType = '', int $cardID = 0): void
|
||
{
|
||
$execution = $this->loadModel('execution')->getByID($executionID);
|
||
if($execution->type == 'kanban')
|
||
{
|
||
$lanes = $this->dao->select('t2.*')->from(TABLE_KANBANREGION)->alias('t1')
|
||
->leftJoin(TABLE_KANBANLANE)->alias('t2')->on('t1.id=t2.region')
|
||
->leftJoin(TABLE_KANBANCELL)->alias('t3')->on('t2.id=t3.lane')
|
||
->where('t1.deleted')->eq(0)
|
||
->andWhere('t2.deleted')->eq(0)
|
||
->andWhere('t1.kanban')->eq($executionID)
|
||
->andWhere('t2.execution')->eq($executionID)
|
||
->beginIF($laneType)->andWhere('t2.type')->eq($laneType)->fi()
|
||
->beginIF(!empty($cardID))->andWhere('t3.cards')->like("%,$cardID,%")->fi()
|
||
->orderBy('t1.`order` asc, t2.`order` asc')
|
||
->fetchAll('id');
|
||
|
||
if(count($lanes) > 1) $lanes = array_slice($lanes, 0, 2);
|
||
}
|
||
else
|
||
{
|
||
$lanes = $this->dao->select('*')->from(TABLE_KANBANLANE)
|
||
->where('execution')->eq($executionID)
|
||
->beginIF($laneType)->andWhere('type')->eq($laneType)->fi()
|
||
->fetchAll('id');
|
||
}
|
||
|
||
foreach($lanes as $lane) $this->refreshCards((array)$lane);
|
||
}
|
||
|
||
/**
|
||
* 更新看板泳道上的卡片。
|
||
* Refresh column cards.
|
||
*
|
||
* @param array $lane
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function refreshCards(array $lane): void
|
||
{
|
||
if(common::isTutorialMode()) return;
|
||
|
||
$laneID = zget($lane, 'id');
|
||
$laneType = zget($lane, 'type');
|
||
$executionID = zget($lane, 'execution');
|
||
$otherCardList = '';
|
||
$otherLanes = $this->dao->select('t2.id, t2.cards')->from(TABLE_KANBANLANE)->alias('t1')
|
||
->leftJoin(TABLE_KANBANCELL)->alias('t2')->on('t1.id=t2.lane')
|
||
->where('t1.id')->ne($laneID)
|
||
->andWhere('t1.execution')->eq($executionID)
|
||
->andWhere('t2.`type`')->eq($laneType)
|
||
->fetchPairs();
|
||
|
||
foreach($otherLanes as $cardIDList)
|
||
{
|
||
$cardIDList = trim($cardIDList, ',');
|
||
if(!empty($cardIDList)) $otherCardList .= ',' . $cardIDList;
|
||
}
|
||
|
||
$cardPairs = $this->dao->select('t2.type, t1.cards')->from(TABLE_KANBANCELL)->alias('t1')
|
||
->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id')
|
||
->where('t1.kanban')->eq($executionID)
|
||
->andWhere('t1.lane')->eq($laneID)
|
||
->fetchPairs();
|
||
|
||
if(empty($cardPairs)) return;
|
||
$sourceCards = $cardPairs;
|
||
|
||
if(in_array($laneType, array('epic', 'requirement', 'parentStory'))) $cardPairs = $this->kanbanTao->refreshERURCards($cardPairs, $executionID, $otherCardList, $laneType);
|
||
if($laneType == 'story') $cardPairs = $this->kanbanTao->refreshStoryCards($cardPairs, $executionID, $otherCardList);
|
||
if($laneType == 'bug') $cardPairs = $this->kanbanTao->refreshBugCards($cardPairs, $executionID, $otherCardList);
|
||
if($laneType == 'task') $cardPairs = $this->kanbanTao->refreshTaskCards($cardPairs, $executionID, $otherCardList);
|
||
|
||
if($laneType == 'risk' && in_array($this->config->edition, array('max', 'ipd'))) $cardPairs = $this->refreshRiskCards($cardPairs, $executionID, $otherCardList);
|
||
|
||
$colPairs = $this->dao->select('t2.type, t2.id')->from(TABLE_KANBANCELL)->alias('t1')
|
||
->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id')
|
||
->where('t1.kanban')->eq($executionID)
|
||
->andWhere('t1.lane')->eq($laneID)
|
||
->fetchPairs();
|
||
|
||
$updated = false;
|
||
foreach($cardPairs as $colType => $cards)
|
||
{
|
||
if(!isset($colPairs[$colType])) continue;
|
||
if($sourceCards[$colType] == $cards) continue;
|
||
|
||
$this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cards)->where('lane')->eq($laneID)->andWhere('`column`')->eq($colPairs[$colType])->exec();
|
||
if(!$updated) $updated = true;
|
||
}
|
||
|
||
if($updated) $this->dao->update(TABLE_KANBANLANE)->set('lastEditedTime')->eq(helper::now())->where('id')->eq($laneID)->exec();
|
||
}
|
||
|
||
/**
|
||
* 编辑看板列。
|
||
* Update column.
|
||
*
|
||
* @param int $columnID
|
||
* @param object $column
|
||
* @access public
|
||
* @return array|false
|
||
*/
|
||
public function updateColumn(int $columnID, object $column): array|false
|
||
{
|
||
$oldColumn = $this->getColumnById($columnID);
|
||
$this->dao->update(TABLE_KANBANCOLUMN)->data($column)
|
||
->autoCheck()
|
||
->batchcheck($this->config->kanban->setColumn->requiredFields, 'notempty')
|
||
->where('id')->eq($columnID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$changes = common::createChanges($oldColumn, $column);
|
||
return $changes;
|
||
}
|
||
|
||
/**
|
||
* 激活卡片。
|
||
* Activate a card.
|
||
*
|
||
* @param int $cardID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function activateCard(int $cardID): bool
|
||
{
|
||
if($this->post->progress >= 100 || $this->post->progress < 0)
|
||
{
|
||
dao::$errors['progress'][] = $this->lang->kanbancard->error->progressIllegal;
|
||
return false;
|
||
}
|
||
|
||
$oldCard = $this->getCardByID($cardID);
|
||
$this->dao->update(TABLE_KANBANCARD)->set('progress')->eq($this->post->progress ? $this->post->progress : 0)->set('status')->eq('doing')->where('id')->eq($cardID)->exec();
|
||
$card = $this->getCardByID($cardID);
|
||
|
||
$changes = common::createChanges($oldCard, $card);
|
||
$actionID = $this->loadModel('action')->create('kanbanCard', $cardID, 'activated');
|
||
$this->action->logHistory($actionID, $changes);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 编辑看板卡片。
|
||
* Update a card.
|
||
*
|
||
* @param int $cardID
|
||
* @param object $card
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function updateCard(int $cardID, object $card): bool
|
||
{
|
||
if($card->estimate < 0)
|
||
{
|
||
dao::$errors['estimate'] = $this->lang->kanbancard->error->recordMinus;
|
||
return false;
|
||
}
|
||
|
||
if($card->end && ($card->begin > $card->end))
|
||
{
|
||
dao::$errors['end'] = $this->lang->kanbancard->error->endSmall;
|
||
return false;
|
||
}
|
||
|
||
if($card->progress > 100 || $card->progress < 0)
|
||
{
|
||
dao::$errors['progress'] = $this->lang->kanbancard->error->progressIllegal;
|
||
return false;
|
||
}
|
||
|
||
$oldCard = $this->getCardByID($cardID);
|
||
|
||
if(!empty($this->post->assignedTo) && $oldCard->assignedTo != $this->post->assignedTo) $card->assignedDate = helper::now();
|
||
$card->status = $card->progress == 100 ? 'done' : 'doing';
|
||
|
||
$card = $this->loadModel('file')->processImgURL($card, $this->config->kanban->editor->editcard['id'], $this->post->uid);
|
||
|
||
$this->dao->update(TABLE_KANBANCARD)->data($card)
|
||
->autoCheck()
|
||
->checkIF($card->estimate != '', 'estimate', 'float')
|
||
->batchcheck($this->config->kanban->editcard->requiredFields, 'notempty')
|
||
->where('id')->eq($cardID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$this->file->saveUpload('kanbancard', $cardID);
|
||
$this->file->updateObjectID($this->post->uid, $cardID, 'kanbancard');
|
||
|
||
$changes = common::createChanges($oldCard, $card);
|
||
|
||
if($changes)
|
||
{
|
||
$actionID = $this->loadModel('action')->create('kanbanCard', $cardID, 'edited');
|
||
$this->action->logHistory($actionID, $changes);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 设置看板在制品限制。
|
||
* Set WIP limit.
|
||
*
|
||
* @param int $columnID
|
||
* @param object $WIP
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function setWIP(int $columnID, object $WIP): bool
|
||
{
|
||
$oldColumn = $this->getColumnById($columnID);
|
||
if(!preg_match("/^-?\d+$/", (string)$WIP->limit) || (!$WIP->noLimit && $WIP->limit <= 0))
|
||
{
|
||
dao::$errors['limit'] = $this->lang->kanban->error->mustBeInt;
|
||
return false;
|
||
}
|
||
|
||
/* Check column limit. */
|
||
$sumChildLimit = 0;
|
||
if($oldColumn->parent == -1 && $WIP->limit != -1)
|
||
{
|
||
$childColumns = $this->dao->select('id,`limit`')->from(TABLE_KANBANCOLUMN)->where('parent')->eq($columnID)->andWhere('deleted')->eq(0)->fetchAll();
|
||
foreach($childColumns as $childColumn) $sumChildLimit += $childColumn->limit;
|
||
|
||
if($sumChildLimit > $WIP->limit)
|
||
{
|
||
dao::$errors['limit'] = $this->lang->kanban->error->parentLimitNote;
|
||
}
|
||
}
|
||
elseif($oldColumn->parent > 0)
|
||
{
|
||
$parentColumn = $this->getColumnByID($oldColumn->parent);
|
||
if($parentColumn->limit != -1)
|
||
{
|
||
$siblingLimit = $this->dao->select('`limit`')->from(TABLE_KANBANCOLUMN)
|
||
->where('`parent`')->eq($oldColumn->parent)
|
||
->andWhere('id')->ne($columnID)
|
||
->fetch('limit');
|
||
|
||
$sumChildLimit = (int)$siblingLimit + (int)$WIP->limit;
|
||
|
||
if($WIP->limit == -1 || $siblingLimit == -1 || $sumChildLimit > $parentColumn->limit)
|
||
{
|
||
dao::$errors['limit'] = $this->lang->kanban->error->childLimitNote;
|
||
}
|
||
}
|
||
}
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$this->dao->update(TABLE_KANBANCOLUMN)->data($WIP, 'noLimit')
|
||
->autoCheck()
|
||
->checkIF($WIP->limit != -1, 'limit', 'gt', 0)
|
||
->where('id')->eq($columnID)
|
||
->exec();
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 设置看板列。
|
||
* Set lane info.
|
||
*
|
||
* @param int $laneID
|
||
* @param object $lane
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function setLane(int $laneID, object $lane): bool
|
||
{
|
||
$this->dao->update(TABLE_KANBANLANE)->data($lane)
|
||
->autoCheck()
|
||
->batchcheck($this->config->kanban->setlane->requiredFields, 'notempty')
|
||
->where('id')->eq($laneID)
|
||
->exec();
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 获取看板右上角的工具栏。
|
||
* Get kanban pageToolBar.
|
||
*
|
||
* @param object $kanban
|
||
* @access public
|
||
* @return string
|
||
*/
|
||
public function getPageToolBar(object $kanban)
|
||
{
|
||
$actions = '';
|
||
$actions .= "<a href='javascript:$(\"#kanbanList\").fullscreen();' id='fullScreenBtn' class='toolbar-item ghost btn btn-default'><i class='icon icon-fullscreen'></i> {$this->lang->kanban->fullScreen}</a>";
|
||
|
||
$printKanbanBtn = (common::hasPriv('kanban', 'edit') || ($kanban->status == 'active' && common::hasPriv('kanban', 'close')) || common::hasPriv('kanban', 'delete') || ($kanban->status == 'closed' && common::hasPriv('kanban', 'activate')));
|
||
|
||
if($printKanbanBtn)
|
||
{
|
||
$actions .= "<a class='toolbar-item ghost btn btn-default setting' type='button' data-toggle='dropdown' data-target='#kanbanActionMenu'>" . '<i class="icon icon-edit"></i> ' . $this->lang->edit . '</a>';
|
||
$actions .= "<menu id='kanbanActionMenu' class='dropdown-menu text-left'>";
|
||
|
||
$columnActions = '';
|
||
$actions .= $columnActions;
|
||
|
||
$commonActions = '';
|
||
|
||
if($columnActions && $commonActions)
|
||
{
|
||
$actions .= "<li class='divider'></li>";
|
||
}
|
||
$actions .= $commonActions;
|
||
|
||
$kanbanActions = '';
|
||
if(common::hasPriv('kanban', 'edit')) $kanbanActions .= '<li class="menu-item item">' . html::a(helper::createLink('kanban', 'edit', "kanbanID=$kanban->id", '', true), '<i class="icon icon-edit"></i>' . $this->lang->kanban->edit, '', "class='listitem item-inner menu-item-inner state' data-toggle='modal'") . '</li>';
|
||
if(common::hasPriv('kanban', 'close') && $kanban->status == 'active') $kanbanActions .= '<li class="menu-item item">' . html::a(helper::createLink('kanban', 'close', "kanbanID=$kanban->id", '', true), '<i class="icon icon-off"></i>' . $this->lang->kanban->close, '', "class='listitem item-inner menu-item-inner state' data-toggle='modal'") . '</li>';
|
||
if(common::hasPriv('kanban', 'activate') && $kanban->status == 'closed') $kanbanActions .= '<li class="menu-item item">' . html::a(helper::createLink('kanban', 'activate', "kanbanID=$kanban->id", '', true), '<i class="icon icon-magic"></i>' . $this->lang->kanban->activate, '', "class='listitem item-inner menu-item-inner state' data-toggle='modal'") . '</li>';
|
||
if(common::hasPriv('kanban', 'delete')) $kanbanActions .= '<li class="menu-item item">' . html::a(helper::createLink('kanban', 'delete', "kanbanID=$kanban->id"), '<i class="icon icon-trash"></i>' . $this->lang->kanban->delete, 'hiddenwin', "class='listitem item-inner menu-item-inner state ajax-submit' data-confirm={$this->lang->kanban->confirmDeleteKanban}") . '</li>';
|
||
|
||
if($commonActions && $kanbanActions)
|
||
{
|
||
$actions .= "<li class='divider'></li>";
|
||
}
|
||
$actions .= $kanbanActions;
|
||
|
||
$actions .= "</menu>";
|
||
}
|
||
|
||
if(common::hasPriv('kanban', 'setting'))
|
||
{
|
||
$actions .= html::a(helper::createLink('kanban', 'setting', "kanbanID=$kanban->id", '', true), '<i class="icon icon-cog-outline"></i> ' . $this->lang->kanban->setting, '', "class='toolbar-item ghost btn btn-default' data-toggle='modal'");
|
||
}
|
||
|
||
return $actions;
|
||
}
|
||
|
||
/**
|
||
* 排序看板分组。
|
||
* Sort kanban group.
|
||
*
|
||
* @param int $region
|
||
* @param array $groups
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function sortGroup(int $region, array $groups): bool
|
||
{
|
||
$groupList = $this->getGroupList($region);
|
||
|
||
$order = 1;
|
||
foreach($groups as $groupID)
|
||
{
|
||
if(!$groupID) continue;
|
||
if(!isset($groupList[$groupID])) continue;
|
||
|
||
$this->dao->update(TABLE_KANBANGROUP)->set('`order`')->eq($order)->where('id')->eq($groupID)->exec();
|
||
|
||
$order++;
|
||
}
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 移动看板卡片。
|
||
* Move a card.
|
||
*
|
||
* @param int $cardID
|
||
* @param int $fromColID
|
||
* @param int $toColID
|
||
* @param int $fromLaneID
|
||
* @param int $toLaneID
|
||
* @param int $kanbanID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function moveCard(int $cardID, int $fromColID, int $toColID, int $fromLaneID, int $toLaneID, int $kanbanID = 0)
|
||
{
|
||
$groupBy = ($this->session->execGroupBy && ($this->app->tab == 'execution' || $this->config->vision == 'lite')) ? $this->session->execGroupBy : '';
|
||
|
||
$fromCell = $this->dao->select('id,cards,lane')->from(TABLE_KANBANCELL)
|
||
->where('`column`')->eq($fromColID)
|
||
->beginIF((!$groupBy || $groupBy == 'default') && $fromLaneID)->andWhere('lane')->eq($fromLaneID)->fi()
|
||
->beginIF($groupBy && $groupBy != 'default')
|
||
->andWhere('type')->eq($this->session->execLaneType)
|
||
->andWhere('cards')->like("%,$cardID,%")
|
||
->fi()
|
||
->fetch();
|
||
|
||
if($groupBy && $groupBy != 'default') $fromLaneID = $toLaneID = $fromCell->lane;
|
||
if(empty($fromLaneID) && $fromCell) $fromLaneID = $toLaneID = $fromCell->lane;
|
||
$fromCells[$fromCell->id] = $fromCell;
|
||
|
||
/* Remove all cells with cardID in fromCell. */
|
||
$fromLane = $this->getLaneById($fromLaneID);
|
||
$fromCells += $this->dao->select('t1.id as id,t1.cards,t1.lane')->from(TABLE_KANBANCELL)->alias('t1')
|
||
->leftJoin(TABLE_KANBANLANE)->alias('t2')->on('t1.lane=t2.id')
|
||
->where('t1.type')->eq($fromLane->type)
|
||
->andWhere('t1.id')->ne($fromCell->id)
|
||
->andWhere('t2.region')->eq($fromLane->region)
|
||
->andWhere('t1.cards')->like("%,$cardID,%")
|
||
->fetchAll('id');
|
||
|
||
foreach($fromCells as $fromCell)
|
||
{
|
||
$fromCellCards = explode(',', $fromCell->cards);
|
||
$fromCellCards = array_unique($fromCellCards);
|
||
$fromCellCards = array_filter($fromCellCards);
|
||
$fromCellCards = implode(',', $fromCellCards);
|
||
$fromCardList = str_replace(",$cardID,", ',', ",$fromCellCards,");
|
||
if($fromCardList == ',') $fromCardList = '';
|
||
$this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($fromCardList)->where('id')->eq($fromCell->id)->exec();
|
||
}
|
||
|
||
/* Add cardID to toCell. */
|
||
$toCell = $this->dao->select('*')->from(TABLE_KANBANCELL)->where('lane')->eq($toLaneID)->andWhere('`column`')->eq($toColID)->fetch();
|
||
$toCellCards = $this->dao->select('cards')->from(TABLE_KANBANCELL)->where('lane')->eq($toLaneID)->andWhere('`column`')->eq($toColID)->fetch('cards');
|
||
$kanbanID = $kanbanID == 0 ? $toCell->kanban : $kanbanID;
|
||
if(!$toCell) $this->addKanbanCell($kanbanID, $toLaneID, $toColID, 'common');
|
||
|
||
$toCardList = rtrim($toCellCards, ',') . ",$cardID,";
|
||
$this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($toCardList)->where('`column`')->eq($toColID)->andWhere('lane')->eq($toLaneID)->exec();
|
||
|
||
$toLane = $this->getLaneById($toLaneID);
|
||
$this->dao->update(TABLE_KANBANCARD)->set('group')->eq($toLane->group)->where('id')->eq($cardID)->exec();
|
||
}
|
||
|
||
/**
|
||
* 更新卡片的颜色。
|
||
* Update a card's color.
|
||
*
|
||
* @param int $cardID
|
||
* @param string $color
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function updateCardColor(int $cardID, string $color)
|
||
{
|
||
$this->dao->update(TABLE_KANBANCARD)->set('`color`')->eq('#' . $color)->where('id')->eq($cardID)->exec();
|
||
}
|
||
|
||
/**
|
||
* 归档看板列。
|
||
* Archive a column.
|
||
*
|
||
* @param int $columnID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function archiveColumn(int $columnID)
|
||
{
|
||
$this->dao->update(TABLE_KANBANCOLUMN)
|
||
->set('archived')->eq(1)
|
||
->where('id')->eq($columnID)
|
||
->exec();
|
||
|
||
if(!dao::isError()) $this->loadModel('action')->create('kanbancolumn', $columnID, 'archived');
|
||
}
|
||
|
||
/**
|
||
* 还原看板列。
|
||
* Restore a column.
|
||
*
|
||
* @param int $columnID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function restoreColumn(int $columnID)
|
||
{
|
||
$column = $this->getColumnByID($columnID);
|
||
|
||
if($column->parent)
|
||
{
|
||
$parent = $this->getColumnByID($column->parent);
|
||
|
||
/* If the parent column is normal now, put its card into child column. */
|
||
if($parent && $parent->parent != -1)
|
||
{
|
||
$parentCells = $this->dao->select('*')->from(TABLE_KANBANCELL)
|
||
->where('`column`')->eq($column->parent)
|
||
->andWhere('type')->eq('common')
|
||
->fetchAll('id');
|
||
|
||
foreach($parentCells as $cell)
|
||
{
|
||
$cards = $this->dao->select('cards')->from(TABLE_KANBANCELL)
|
||
->where('lane')->eq($cell->lane)
|
||
->andWhere('`column`')->eq($columnID)
|
||
->andWhere('type')->eq('common')
|
||
->fetch('cards');
|
||
|
||
$cards = $cards ? $cards . ltrim($cell->cards, ',') : $cell->cards;
|
||
|
||
$this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cards)
|
||
->where('lane')->eq($cell->lane)
|
||
->andWhere('`column`')->eq($columnID)
|
||
->andWhere('type')->eq('common')
|
||
->exec();
|
||
|
||
$this->dao->update(TABLE_KANBANCELL)->set('cards')->eq('')
|
||
->where('lane')->eq($cell->lane)
|
||
->andWhere('`column`')->eq($cell->column)
|
||
->andWhere('type')->eq('common')
|
||
->exec();
|
||
}
|
||
|
||
$this->dao->update(TABLE_KANBANCOLUMN)->set('parent')->eq(-1)->where('id')->eq($column->parent)->exec();
|
||
}
|
||
}
|
||
|
||
$this->dao->update(TABLE_KANBANCOLUMN)
|
||
->set('archived')->eq(0)
|
||
->where('id')->eq($columnID)
|
||
->exec();
|
||
|
||
if(!dao::isError()) $this->loadModel('action')->create('kanbancolumn', $columnID, 'restore');
|
||
}
|
||
|
||
/**
|
||
* 归档看板卡片。
|
||
* Archive a card.
|
||
*
|
||
* @param int $cardID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function archiveCard(int $cardID): bool
|
||
{
|
||
$oldCard = $this->getCardByID($cardID);
|
||
|
||
$this->dao->update(TABLE_KANBANCARD)
|
||
->set('archived')->eq(1)
|
||
->set('archivedBy')->eq($this->app->user->account)
|
||
->set('archivedDate')->eq(helper::now())
|
||
->where('id')->eq($cardID)
|
||
->exec();
|
||
|
||
$card = $this->getCardByID($cardID);
|
||
|
||
if(dao::isError()) return false;
|
||
$changes = common::createChanges($oldCard, $card);
|
||
|
||
if($changes)
|
||
{
|
||
$actionID = $this->loadModel('action')->create('kanbancard', $cardID, 'archived');
|
||
$this->action->logHistory($actionID, $changes);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 还原看板卡片。
|
||
* Restore a card.
|
||
*
|
||
* @param int $cardID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function restoreCard(int $cardID): bool
|
||
{
|
||
$oldCard = $this->getCardByID($cardID);
|
||
|
||
$this->dao->update(TABLE_KANBANCARD)
|
||
->set('archived')->eq(0)
|
||
->set('archivedBy')->eq('')
|
||
->set('archivedDate')->eq(null)
|
||
->where('id')->eq($cardID)
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$card = $this->getCardByID($cardID);
|
||
$changes = common::createChanges($oldCard, $card);
|
||
if($changes)
|
||
{
|
||
$actionID = $this->loadModel('action')->create('kanbancard', $cardID, 'restore');
|
||
$this->action->logHistory($actionID, $changes);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 删除子列时,将子列的卡片移动到第一个子列,或者移动到父列。
|
||
* Process cards when delete a column.
|
||
*
|
||
* @param object $column
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function processCards(object $column)
|
||
{
|
||
$firstColumnID = $this->dao->select('id')->from(TABLE_KANBANCOLUMN)
|
||
->where('parent')->eq($column->parent)
|
||
->andWhere('id')->ne($column->id)
|
||
->andWhere('deleted')->eq('0')
|
||
->andWhere('archived')->eq('0')
|
||
->orderBy('`order` asc')
|
||
->fetch('id');
|
||
|
||
$extendID = $firstColumnID;
|
||
if(!$firstColumnID)
|
||
{
|
||
$extendID = $column->parent;
|
||
$this->dao->update(TABLE_KANBANCOLUMN)->set('parent')->eq(0)->where('id')->eq($column->parent)->exec();
|
||
}
|
||
|
||
$cells = $this->dao->select('*')->from(TABLE_KANBANCELL)
|
||
->where('`column`')->eq($column->id)
|
||
->andWhere('type')->eq('common')
|
||
->fetchAll('id', false);
|
||
|
||
foreach($cells as $cell)
|
||
{
|
||
$extendCards = $this->dao->select('cards')->from(TABLE_KANBANCELL)
|
||
->where('lane')->eq($cell->lane)
|
||
->andWhere('`column`')->eq($extendID)
|
||
->andWhere('type')->eq('common')
|
||
->fetch('cards');
|
||
|
||
$extendCards = $extendCards ? $extendCards . ltrim($cell->cards, ',') : $cell->cards;
|
||
|
||
$this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($extendCards)
|
||
->where('lane')->eq($cell->lane)
|
||
->andWhere('`column`')->eq($extendID)
|
||
->andWhere('type')->eq('common')
|
||
->exec();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取看板空间。
|
||
* Get space by id.
|
||
*
|
||
* @param int $spaceID
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function getSpaceById(int $spaceID)
|
||
{
|
||
$space = $this->dao->findById($spaceID)->from(TABLE_KANBANSPACE)->fetch();
|
||
if($space) $space = $this->loadModel('file')->replaceImgURL($space, 'desc');
|
||
return $space;
|
||
}
|
||
|
||
/**
|
||
* Get kanban group by space id list.
|
||
*
|
||
* @param array $spaceIdList
|
||
* @param array $kanbanIdList
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getGroupBySpaceList(array $spaceIdList, array $kanbanIdList = array()): array
|
||
{
|
||
$spaceList = $this->dao->select('*')->from(TABLE_KANBAN)
|
||
->where('deleted')->eq(0)
|
||
->andWhere('space')->in($spaceIdList)
|
||
->beginIF($kanbanIdList)->andWhere('id')->in($kanbanIdList)->fi()
|
||
->fetchGroup('space', 'id');
|
||
|
||
$kanbanIDList = array();
|
||
foreach($spaceList as $kanbanList) $kanbanIDList = array_merge_recursive($kanbanIDList, array_keys($kanbanList));
|
||
$cardsCount = $this->dao->select('kanban, COUNT(1) AS count')->from(TABLE_KANBANCARD)
|
||
->where('deleted')->eq(0)
|
||
->andWhere('kanban')->in($kanbanIDList)
|
||
->groupBy('kanban')
|
||
->fetchPairs('kanban');
|
||
|
||
foreach($spaceList as $kanbanList)
|
||
{
|
||
foreach($kanbanList as $kanban)
|
||
{
|
||
$kanban->cardsCount = zget($cardsCount, $kanban->id, 0);
|
||
}
|
||
}
|
||
|
||
return $spaceList;
|
||
}
|
||
|
||
/**
|
||
* 获取看板组。
|
||
* Get group list by region.
|
||
*
|
||
* @param int $regionID
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getGroupList(int $regionID): array
|
||
{
|
||
return $this->dao->select('*')->from(TABLE_KANBANGROUP)
|
||
->where('region')->eq($regionID)
|
||
->orderBy('order')
|
||
->fetchAll('id');
|
||
}
|
||
|
||
/**
|
||
* 获取看板列。
|
||
* Get column by id.
|
||
*
|
||
* @param int $columnID
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function getColumnByID(int $columnID)
|
||
{
|
||
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getColumn();
|
||
|
||
$column = $this->dao->select('t1.*, t2.type as laneType')->from(TABLE_KANBANCOLUMN)->alias('t1')
|
||
->leftJoin(TABLE_KANBANCELL)->alias('t2')->on('t1.id=t2.column')
|
||
->where('t1.id')->eq($columnID)
|
||
->fetch();
|
||
|
||
if(empty($column)) return false;
|
||
if($column->parent > 0) $column->parentName = $this->dao->findById($column->parent)->from(TABLE_KANBANCOLUMN)->fetch('name');
|
||
|
||
return $column;
|
||
}
|
||
|
||
/**
|
||
* 根据字段获取看板列。
|
||
* Get columns by field.
|
||
*
|
||
* @param string $field parent|region|group
|
||
* @param int $fieldID
|
||
* @param string $archived
|
||
* @param string $deleted
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getColumnsByField(string $field = '', int $fieldID = 0, string $archived = '0', string $deleted = '0'): array
|
||
{
|
||
return $this->dao->select('*')->from(TABLE_KANBANCOLUMN)
|
||
->where('1 = 1')
|
||
->beginIF($field)->andWhere($field)->eq($fieldID)->fi()
|
||
->beginIF($archived != '')->andWhere('archived')->eq($archived)->fi()
|
||
->beginIF($deleted != '')->andWhere('deleted')->eq($deleted)->fi()
|
||
->orderBy('order')
|
||
->fetchAll('id');
|
||
}
|
||
|
||
/**
|
||
* 获取泳道的看板列ID。
|
||
* Get column ID by lane ID.
|
||
*
|
||
* @param int $laneID
|
||
* @param string $columnType
|
||
* @access public
|
||
* @return string|int
|
||
*/
|
||
public function getColumnIDByLaneID(int $laneID, string $columnType)
|
||
{
|
||
return $this->dao->select('t1.column')->from(TABLE_KANBANCELL)->alias('t1')
|
||
->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column = t2.id')
|
||
->where('t1.lane')->eq($laneID)
|
||
->andWhere('t2.type')->eq($columnType)
|
||
->fetch('column');
|
||
}
|
||
|
||
/**
|
||
* 根据ID获取看板泳道。
|
||
* Get lane by id.
|
||
*
|
||
* @param int $laneID
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function getLaneById(int $laneID)
|
||
{
|
||
return $this->dao->findById($laneID)->from(TABLE_KANBANLANE)->fetch();
|
||
}
|
||
|
||
/**
|
||
* 获取看板卡片。
|
||
* Get card by id.
|
||
*
|
||
* @param int $cardID
|
||
* @access public
|
||
* @return object|false
|
||
*/
|
||
public function getCardByID(int $cardID): object|false
|
||
{
|
||
$card = $this->dao->select('*')->from(TABLE_KANBANCARD)->where('id')->eq($cardID)->fetch();
|
||
if(!$card) return false;
|
||
|
||
$card = $this->loadModel('file')->replaceImgURL($card, 'desc');
|
||
|
||
return $card;
|
||
}
|
||
|
||
/**
|
||
* 获取某个对象的看板卡片。
|
||
* Get cards by object id.
|
||
*
|
||
* @param string $objectType kanban|region|group|lane|column
|
||
* @param int|string $objectID
|
||
* @param string $archived
|
||
* @param string $deleted
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getCardsByObject(string $objectType = '', int|string $objectID = 0, string $archived = '0', string $deleted = '0')
|
||
{
|
||
return $this->dao->select('*')->from(TABLE_KANBANCARD)
|
||
->where('1 = 1')
|
||
->beginIF($objectType)->andWhere($objectType)->eq($objectID)->fi()
|
||
->beginIF($archived != '')->andWhere('archived')->eq($archived)->fi()
|
||
->beginIF($deleted != '')->andWhere('deleted')->eq($deleted)->fi()
|
||
->orderBy('order')
|
||
->fetchAll('id', false);
|
||
}
|
||
|
||
/**
|
||
* 保存看板卡片到期提醒。
|
||
* Save kanban card due reminders.
|
||
*
|
||
* @param array $cards
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function saveCardNotice(array $cards)
|
||
{
|
||
if(empty($cards)) $cards = $this->loadModel('report')->getUserKanbanCards();
|
||
if(empty($cards)) return;
|
||
|
||
$today = helper::today();
|
||
foreach($cards as $user => $cardList)
|
||
{
|
||
foreach($cardList as $card)
|
||
{
|
||
$url = helper::createLink('kanban', 'view', "kanbanID={$card->kanban}") . "#app=kanban";
|
||
$data = $today > $card->deadline ? $this->lang->kanban->due : $this->lang->kanban->nearing;
|
||
$data .= html::a($url, "#{$card->id} {$card->name}");
|
||
|
||
$notify = new stdclass();
|
||
$notify->objectType = 'message';
|
||
$notify->action = 0;
|
||
$notify->toList = ",{$user},";
|
||
$notify->data = $data;
|
||
$notify->status = 'wait';
|
||
$notify->createdBy = 'admin';
|
||
$notify->createdDate = helper::now();
|
||
$notify->sendTime = null;
|
||
|
||
$this->dao->insert(TABLE_NOTIFY)->data($notify)->exec();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取可转入的看板卡片。
|
||
* Get cards to import.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $excludedID
|
||
* @param object $pager
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getCards2Import(int $kanbanID = 0, int $excludedID = 0, ?object $pager = null): array
|
||
{
|
||
$kanbanIdList = $this->getCanViewObjects();
|
||
|
||
return $this->dao->select('*')->from(TABLE_KANBANCARD)
|
||
->where('deleted')->eq(0)
|
||
->andWhere('archived')->eq(0)
|
||
->andWhere('fromID')->eq(0)
|
||
->andWhere('kanban')->in($kanbanIdList)
|
||
->beginIF($kanbanID)->andWhere('kanban')->eq($kanbanID)->fi()
|
||
->beginIF($excludedID)->andWhere('kanban')->ne($excludedID)->fi()
|
||
->orderBy('order')
|
||
->page($pager)
|
||
->fetchAll('id', false);
|
||
}
|
||
|
||
/**
|
||
* 获取看板卡片的操作按钮。
|
||
* Get Kanban cards menus by execution id.
|
||
*
|
||
* @param int $executionID
|
||
* @param array $objects
|
||
* @param string $objecType story|bug|task
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getKanbanCardMenu(int $executionID, array $objects, string $objecType): array
|
||
{
|
||
$this->app->loadLang('execution');
|
||
$execution = $this->loadModel('execution')->getByID($executionID);
|
||
if(!common::canModify('execution', $execution)) return array();
|
||
|
||
$menus = array();
|
||
switch ($objecType)
|
||
{
|
||
case 'epic':
|
||
$menus = $this->kanbanTao->getERURCardMenu($executionID, $objects);
|
||
break;
|
||
case 'requirement':
|
||
$menus = $this->kanbanTao->getERURCardMenu($executionID, $objects);
|
||
break;
|
||
case 'parentStory':
|
||
$menus = $this->kanbanTao->getERURCardMenu($executionID, $objects);
|
||
break;
|
||
case 'story':
|
||
if($execution) $menus = $this->kanbanTao->getStoryCardMenu($execution, $objects);
|
||
break;
|
||
case 'bug':
|
||
$menus = $this->kanbanTao->getBugCardMenu($objects);
|
||
break;
|
||
case 'task':
|
||
if(!isset($_SESSION['limitedExecutions'])) $this->loadModel('execution')->getLimitedExecution();
|
||
$menus = $this->kanbanTao->getTaskCardMenu($objects, $executionID);
|
||
break;
|
||
case 'risk':
|
||
$menus = $this->kanbanTao->getRiskCardMenu($objects);
|
||
break;
|
||
}
|
||
return $menus;
|
||
}
|
||
|
||
/**
|
||
* 获取卡片的发送人和抄送人。
|
||
* Get toList and ccList.
|
||
*
|
||
* @param object $card
|
||
* @access public
|
||
* @return bool|array
|
||
*/
|
||
public function getToAndCcList(object $card): array|bool
|
||
{
|
||
/* Set toList and ccList. */
|
||
$toList = $card->createdBy;
|
||
$ccList = trim($card->assignedTo, ',');
|
||
|
||
if(empty($toList))
|
||
{
|
||
if(empty($ccList)) return false;
|
||
if(strpos($ccList, ',') === false)
|
||
{
|
||
$toList = $ccList;
|
||
$ccList = '';
|
||
}
|
||
else
|
||
{
|
||
$commaPos = strpos($ccList, ',');
|
||
$toList = substr($ccList, 0, $commaPos);
|
||
$ccList = substr($ccList, $commaPos + 1);
|
||
}
|
||
}
|
||
|
||
return array($toList, $ccList);
|
||
}
|
||
|
||
/**
|
||
* 检查操作按钮是否可点。
|
||
* Check if user can execute an action.
|
||
*
|
||
* @param object $object
|
||
* @param string $action
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function isClickable(object $object, string $action): bool
|
||
{
|
||
$action = strtolower($action);
|
||
$clickable = commonModel::hasPriv('kanban', $action);
|
||
if(!$clickable) return false;
|
||
|
||
switch($action)
|
||
{
|
||
case 'sortlane' :
|
||
case 'deletelane' :
|
||
if($object->deleted != '0') return false;
|
||
|
||
$count = $this->dao->select('COUNT(id) AS count')->from(TABLE_KANBANLANE)
|
||
->where('deleted')->eq('0')
|
||
->andWhere('region')->eq($object->region)
|
||
->beginIF($action == 'sortlane')->andWhere('`group`')->eq($object->group)->fi()
|
||
->fetch('count');
|
||
return $count > 1;
|
||
case 'splitcolumn' :
|
||
return $object->parent == 0;
|
||
case 'restorecolumn' :
|
||
if($object->parent > 0)
|
||
{
|
||
$parent = $this->getColumnByID($object->parent);
|
||
if(!empty($parent) && ($parent->deleted == '1' || $parent->archived == '1')) return false;
|
||
}
|
||
return $object->archived == '1';
|
||
case 'archivecolumn' :
|
||
return $object->archived == '0'; // The column has been archived.
|
||
case 'deletecolumn' :
|
||
return $object->deleted == '0';
|
||
case 'sortcolumn':
|
||
return $object->deleted == '0';
|
||
case 'editcard':
|
||
return $object->deleted == '0' && $object->archived == '0';
|
||
case 'finishcard':
|
||
return $object->status != 'done' && $object->deleted == '0' && $object->archived == '0';
|
||
case 'activatecard':
|
||
return $object->status == 'done' && $object->deleted == '0' && $object->archived == '0';
|
||
case 'deletecard':
|
||
return $object->deleted == '0';
|
||
case 'archivecard':
|
||
return $object->archived == '0';
|
||
case 'restorecard':
|
||
return $object->archived == '1';
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 获取泳道的数量。
|
||
* Get kanban lane count.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param string $type
|
||
* @access public
|
||
* @return int
|
||
*/
|
||
public function getLaneCount(int $kanbanID, string $type = 'common')
|
||
{
|
||
if($type == 'common' || $type == 'kanban')
|
||
{
|
||
return $this->dao->select('COUNT(t2.id) as count')->from(TABLE_KANBANREGION)->alias('t1')
|
||
->leftJoin(TABLE_KANBANLANE)->alias('t2')->on('t1.id=t2.region')
|
||
->where('t1.kanban')->eq($kanbanID)
|
||
->andWhere('t1.deleted')->eq(0)
|
||
->andWhere('t2.deleted')->eq(0)
|
||
->beginIF($type == 'common')->andWhere('t2.type')->eq('common')->fi()
|
||
->beginIF($type != 'common')->andWhere('t2.type')->ne('common')->fi()
|
||
->fetch('count');
|
||
}
|
||
else
|
||
{
|
||
return $this->dao->select('COUNT(id) as count')->from(TABLE_KANBANLANE)
|
||
->where('execution')->eq($kanbanID)
|
||
->andWhere('deleted')->eq(0)
|
||
->fetch('count');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查卡片数量是否合法。
|
||
* Check display card count.
|
||
*
|
||
* @param int $count
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function checkDisplayCards(int $count): bool
|
||
{
|
||
if(!preg_match("/^-?\d+$/", (string)$count) || $count <= DEFAULT_CARDCOUNT || $count > MAX_CARDCOUNT) dao::$errors['displayCards'] = $this->lang->kanbanlane->error->mustBeInt;
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 获取看板动态刷新的回调函数数组。
|
||
* Get kanban callback data.
|
||
*
|
||
* @param int $kanbanID
|
||
* @param int $regionID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function getKanbanCallback(int $kanbanID, int $regionID)
|
||
{
|
||
$kanbanData = $this->getKanbanData($kanbanID, $regionID);
|
||
$kanbanData = reset($kanbanData);
|
||
return array('name' => 'updateKanbanRegion', 'params' => array('region' . $regionID, $kanbanData));
|
||
}
|
||
|
||
/**
|
||
* 获取卡片所在的单元格。
|
||
* Get card cell.
|
||
*
|
||
* @param int $cardID
|
||
* @param int $kanbanID
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function getCellByCard(int $cardID, int $kanbanID): object|false
|
||
{
|
||
return $this->dao->select('id,cards,lane,`column`')->from(TABLE_KANBANCELL)
|
||
->where('kanban')->eq($kanbanID)
|
||
->andWhere('type')->eq('common')
|
||
->andWhere('cards')->like("%,$cardID,%")
|
||
->fetch();
|
||
}
|
||
}
|