Merge branch 'master' of git.zcorp.cc:easycorp/zentaopms

This commit is contained in:
zhaoke
2023-05-05 19:15:32 +08:00
26 changed files with 2735 additions and 2323 deletions
+1 -12
View File
@@ -26,7 +26,7 @@ class form extends baseFixer
* 错误信息列表。
* Error list.
*
* @var object
* @var array
*/
public $errors;
@@ -138,17 +138,6 @@ class form extends baseFixer
$this->data->$field = $data;
}
/**
* 打印表单数据。
* Print the form data.
*
* @return void
*/
public function a()
{
a($this->data);
}
/**
* 过滤表单字段数据。
* Filter the form field data.
+29 -52
View File
@@ -3,29 +3,28 @@ namespace zin;
require_once dirname(__DIR__) . DS . 'formgroup' . DS . 'v1.php';
require_once dirname(__DIR__) . DS . 'formrow' . DS . 'v1.php';
require_once dirname(__DIR__) . DS . 'formbase' . DS . 'v1.php';
class form extends wg
/**
* 通用表单(form)部件类,支持 Ajax 提交
* The common form widget class
*/
class form extends formBase
{
protected static $defineProps = array(
'method?: string',
'url?: string',
'actions?: array',
'target?: string',
protected static $defineProps = array
(
'items?: array',
'grid?: bool',
'labelWidth?: int',
'submitBtnText?: string',
'cancelBtnText?: string',
'labelWidth?: int'
);
protected static $defaultProps = array(
protected static $defaultProps = array
(
'grid' => true,
'method' => 'post',
'target' => 'ajax',
'actions' => ['submit', 'cancel'],
'actionsClass' => 'form-group gap-4 no-label'
);
public function onBuildItem($item)
public function onBuildItem(item|array $item): wg
{
if(!($item instanceof item))
{
@@ -38,32 +37,26 @@ class form extends wg
return new formGroup(inherit($item));
}
protected function buildFormActions()
protected function buildActions(): wg
{
$actions = $this->prop('actions');
if(empty($actions)) return NULL;
$actions = parent::buildActions();
if($this->prop('grid') && !empty($actions)) $actions = div(setClass('form-row'), $actions);
return $actions;
}
protected function buildProps(): array
{
list($grid, $labelWidth) = $this->prop(array('grid', 'labelWidth'));
$props = parent::buildProps();
if($grid) $props[] = set::class('form-grid');
if(!empty($labelWidth)) $props[] = setCssVar('form-label-width', $labelWidth);
global $lang;
foreach($actions as $key => $action)
{
if($action === 'submit') $actions[$key] = ['text' => $this->prop('submitBtnText') ?? $lang->save, 'btnType' => 'submit', 'type' => 'primary'];
elseif($action === 'cancel') $actions[$key] = ['text' => $this->prop('cancelBtnText') ?? $lang->goback, 'url' => html::getGobackLink()];
elseif(is_string($action)) $actions[$key] = ['text' => $action];
}
return toolbar
(
set::class('form-actions form-group gap-4 no-label'),
set::items($actions)
);
return $props;
}
protected function build()
protected function buildContent(): array
{
list($items, $grid, $labelWidth, $url, $target, $method, $id) = $this->prop(['items', 'grid', 'labelWidth', 'url', 'target', 'method', 'id']);
$actions = $this->buildFormActions();
if($grid && !empty($actions)) $actions = div(setClass('form-row'), $actions);
list($items, $grid) = $this->prop(['items', 'grid']);
$list = is_array($items) ? array_map(array($this, 'onBuildItem'), $items) : [];
$children = $this->children();
@@ -76,23 +69,7 @@ class form extends wg
if($item instanceof formGroup) $list[$key] = new formRow($item);
}
}
$isAjax = $target === 'ajax';
if($isAjax)
{
$target = NULL;
if(empty($id)) $id = $this->gid;
}
if(empty($url)) $url = $_SERVER['REQUEST_URI'];
return h::form
(
set::class('form load-indicator', $grid ? 'form-grid' : NULL, $isAjax ? 'form-ajax' : ''),
set(['id' => $id, 'action' => $url, 'target' => $target, 'method' => $method]),
set($this->getRestProps()),
empty($labelWidth) ? NULL : setCssVar('form-label-width', $labelWidth),
$list,
$actions,
$isAjax ? zui::ajaxForm(set::_to("#$id")) : NULL
);
return $list;
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/**
* The formBase widget class file of zin module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author sunhao<sunhao@easycorp.ltd>
* @package zin
* @link http://www.zentao.net
*/
namespace zin;
/**
* 基础表单(formBase)部件类,支持 Ajax 提交
* The formBase widget class
*/
class formBase extends wg
{
protected static $defineProps = array
(
'method?: string',
'url?: string',
'actions?: array',
'actionsClass?: string',
'target?: string',
'submitBtnText?: string',
'cancelBtnText?: string',
);
protected static $defaultProps = array
(
'method' => 'post',
'target' => 'ajax',
'actions' => ['submit', 'cancel'],
);
protected function buildActions(): wg
{
$actions = $this->prop('actions');
if(empty($actions)) return NULL;
global $lang;
foreach($actions as $key => $action)
{
if($action === 'submit') $actions[$key] = ['text' => $this->prop('submitBtnText') ?? $lang->save, 'btnType' => 'submit', 'type' => 'primary'];
elseif($action === 'cancel') $actions[$key] = ['text' => $this->prop('cancelBtnText') ?? $lang->goback, 'url' => html::getGobackLink()];
elseif(is_string($action)) $actions[$key] = ['text' => $action];
}
return toolbar
(
set::class('form-actions', $this->prop('actionsClass')),
set::items($actions)
);
}
protected function buildContent(): array|wg
{
return $this->children();
}
protected function buildProps(): array
{
return array();
}
protected function build(): wg
{
list($url, $target, $method, $id) = $this->prop(['url', 'target', 'method', 'id']);
$isAjax = $target === 'ajax';
if($isAjax)
{
$target = NULL;
if(empty($id)) $id = $this->gid;
}
if(empty($url)) $url = $_SERVER['REQUEST_URI'];
return h::form
(
set::class('form load-indicator', $isAjax ? 'form-ajax' : ''),
set(['id' => $id, 'action' => $url, 'target' => $target, 'method' => $method]),
set($this->getRestProps()),
$this->buildProps(),
$this->buildContent(),
$this->buildActions(),
$isAjax ? zui::ajaxForm(set::_to("#$id")) : NULL
);
}
}
+2
View File
@@ -0,0 +1,2 @@
.nav-tabs>.nav-item.active>a {color: var(--color-primary-500);}
.nav-tabs>.nav-item.active>a:after {border-bottom-color: var(--color-primary-500) !important; border-bottom-width: 2px;}
+64 -28
View File
@@ -10,6 +10,11 @@ class tabs extends wg
'activeID?string'
);
public static function getPageCSS()
{
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
}
protected function getItemID(array $item): string
{
return isset($item['id']) ? $item['id'] : $this->gid;
@@ -17,7 +22,8 @@ class tabs extends wg
protected function buildLabelView(string $id, array $item)
{
$isActive = $this->prop('activeID') == $id;
if(!isset($item['label'])) return null;
$isActive = $this->prop('activeID') == $id || (isset($item['active']) && $item['active']);
return li
(
setClass('nav-item'),
@@ -25,6 +31,7 @@ class tabs extends wg
a
(
set('data-toggle', 'tab'),
setClass('font-medium'),
set::href("#$id"),
$item['label']
)
@@ -33,57 +40,86 @@ class tabs extends wg
protected function buildContentView(string $id, array $item)
{
$isActive = $this->prop('activeID') == $id;
if(!isset($item['data'])) return null;
$isActive = $this->prop('activeID') == $id || (isset($item['active']) && $item['active']);
return div
(
setClass('tab-pane'),
setID($id),
$isActive ? setClass('active') : null,
isset($item['data']) ? $item['data'] : null
$item['data']
);
}
protected function buildTabHeader($labelViews)
{
if(empty($labelViews)) return null;
$isVertical = $this->prop('direction') === 'v';
return ul
(
setClass('nav nav-tabs'),
$isVertical ? setClass('nav-stacked') : null,
$labelViews
);
}
protected function buildTabBody($contentViews)
{
if(empty($contentViews)) return null;
return div
(
setClass('tab-content'),
$contentViews
);
}
protected function build()
{
$items = $this->prop('items');
$direction = $this->prop('direction');
$activeID = $this->prop('activeID');
$items = $this->prop('items');
// $activeID = $this->prop('activeID');
$isVertical = $this->prop('direction') === 'v';
if(empty($items)) return null;
if(empty($items))
{
return div
(
set($this->props->skip(array_keys(static::getDefinedProps()))),
$isVertical ? setClass('flex') : null,
$this->children()
);
}
$labelViews = array();
$contentViews = array();
foreach($items as $item)
{
$id = $this->getItemID($item);
$labelViews[] = $this->buildLabelView($id, $item);
$contentViews[] = $this->buildContentView($id, $item);
$labelView = $this->buildLabelView($id, $item);
$contentView = $this->buildContentView($id, $item);
if(!empty($labelView)) $labelViews[] = $labelView;
if(!empty($contentView)) $contentViews[] = $contentView;
}
/* There is no active item, then set index 0 to be actived. */
if(empty($activeID))
{
$labelViews[0]->setProp('class', 'active');
$contentViews[0]->setProp('class', 'active');
}
// if(empty($activeID))
// {
// $labelViews[0]->setProp('class', 'active');
// $contentViews[0]->setProp('class', 'active');
// }
return div
(
set($this->props->skip(array_keys(static::getDefinedProps()))),
$direction == 'v' ? setClass('flex') : null,
/* Tabs. */
ul
(
setClass('nav nav-tabs'),
$direction == 'v' ? setClass('nav-stacked') : null,
$labelViews
),
/* Content. */
div
(
setClass('tab-content'),
$contentViews
)
$isVertical ? setClass('flex') : null,
$this->buildTabHeader($labelViews),
$this->buildTabBody($contentViews),
$this->children()
);
}
}
+123 -1872
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
.projectstatistic-block .tab-content {width: 100%}
+367 -34
View File
@@ -11,47 +11,380 @@ declare(strict_types=1);
namespace zin;
$isChineseLang = in_array($this->app->getClientLang(), array('zh-cn','zh-tw'));
$blockNavID = 'nav-' . uniqid();
$selected = key($projects);
$navTabs = array();
foreach($projects as $project)
{
$navTabs[] = li
(
set('class', 'nav-item' . ($project->id == $selected ? ' active' : '')),
a
(
set('data-toggle', 'tab'),
set('href', "#tab3{$blockNavID}Content{$project->id}"),
$project->name
)
);
}
$tabItems = array();
foreach($projects as $project)
{
$tabItems[] = div
(
set('class', 'tab-pane' . ($project->id == $selected ? ' active' : '')),
set('id', "tab3{$blockNavID}Content{$project->id}"),
in_array($project->model, array('scrum', 'kanban', 'agileplus')) ? div
(
div
(
set('class', 'flex justify-around'),
div
(
set('class', 'text-center'),
h4($lang->block->storyCount),
div
(
div
(
set('class', 'statistic-title'),
$lang->block->allStories . ":",
),
div
(
set('class', 'statistic-data'),
$project->allStories,
)
),
div
(
div
(
set('class', 'statistic-title'),
$lang->block->finish . ":",
),
div
(
set('class', 'statistic-data'),
$project->doneStories,
)
),
div
(
div
(
set('class', 'statistic-title'),
$lang->project->surplus . ":",
),
div
(
set('class', 'statistic-data'),
$project->leftStories,
)
)
),
div
(
set('class', 'text-center'),
h4($lang->block->investment),
div
(
div
(
set('class', 'statistic-title'),
$lang->block->totalPeople . ":",
),
div
(
set('class', 'statistic-data'),
$project->teamCount,
)
),
div
(
div
(
set('class', 'statistic-title'),
$lang->block->estimate . ":",
),
div
(
set('class', 'statistic-data'),
$project->estimate . $lang->execution->workHourUnit,
)
),
div
(
div
(
set('class', 'statistic-title'),
$lang->block->consumedHours . ":",
),
div
(
set('class', 'statistic-data'),
$project->consumed . $lang->execution->workHourUnit,
)
)
),
div
(
set('class', 'text-center'),
h4($lang->block->taskCount),
div
(
div
(
set('class', 'statistic-title'),
$lang->block->wait . ":",
),
div
(
set('class', 'statistic-data'),
$project->waitTasks,
)
),
div
(
div
(
set('class', 'statistic-title'),
$lang->block->doing . ":",
),
div
(
set('class', 'statistic-data'),
$project->doingTasks,
)
),
div
(
div
(
set('class', 'statistic-title'),
$lang->block->done . ":",
),
div
(
set('class', 'statistic-data'),
$project->rndDoneTasks,
)
)
),
div
(
set('class', 'text-center'),
h4($lang->block->bugCount),
div
(
div
(
set('class', 'col col-title'),
$lang->block->totalBug . ":",
),
div
(
set('class', 'col col-data'),
$project->allBugs,
)
),
div
(
div
(
set('class', 'col col-title'),
$lang->bug->statusList['resolved'] . ":",
),
div
(
set('class', 'col col-data'),
$project->doneBugs,
)
),
div
(
div
(
set('class', 'col col-title'),
$lang->bug->unResolved . ":",
),
div
(
set('class', 'col col-data'),
$project->leftBugs,
)
)
)
),
(!empty($project->executions) and $project->multiple) ? div
(
set('class', 'flex'),
div
(
set('class', 'flex-1 text-right'),
h4($lang->block->last)
),
div
(
set('class', 'flex-1 text-center'),
a
(
set('href', $this->createLink('execution', 'task', "executionID={$project->executions[0]->id}")),
set('title', $project->executions[0]->name),
$project->executions[0]->name,
)
),
div
(
set('class', 'flex-1'),
div
(
set('class', 'progress'),
span
(
set('class', 'mr-4'),
$project->executions[0]->hours->progress . '%'
),
div
(
set('class', 'progress-bar'),
set('role', 'progressbar'),
setStyle(['width' => $project->executions[0]->hours->progress . '%']),
)
)
)
) : null
) : div
(
set('class', 'weekly-row'),
div
(
span
(
set('class', 'weekly-title'),
$lang->project->weekly
),
span
(
set('class', 'weekly-stage'),
$project->current
)
),
div
(
set('class', 'flex'),
div
(
set('class', 'flex-1'),
div
(
set('class', 'progress'),
span
(
set('class', 'mr-4'),
$lang->project->progress . ' : ' . $project->progress . '%'
),
div
(
set('class', 'progress-bar'),
set('role', 'progressbar'),
setStyle(['width' => $project->progress . '%']),
)
)
),
div
(
set('class', 'flex-1 text-center'),
$lang->project->teamCount . ' : ' . $project->teamCount
),
div
(
set('class', 'flex-1 text-left'),
$lang->project->budget . ' : ' . ($project->budget != 0 ? $project->budget : $this->lang->project->future)
),
div(set('class', 'flex-1'))
),
div
(
set('class', 'flex'),
div
(
set('class', 'flex-1'),
$isChineseLang ? $lang->project->pv . '(' . $lang->project->pvTitle . ')' : $lang->project->pv
),
div
(
set('class', 'flex-1'),
$isChineseLang ? $lang->project->ev . '(' . $lang->project->evTitle . ')' : $lang->project->ev
),
div
(
set('class', 'flex-1'),
$isChineseLang ? $lang->project->ac . '(' . $lang->project->acTitle . ')' : $lang->project->ac
),
div
(
set('class', 'flex-1'),
$isChineseLang ? $lang->project->sv . '(' . $lang->project->svTitle . ')' : $lang->project->sv
),
div
(
set('class', 'flex-1'),
$isChineseLang ? $lang->project->cv . '(' . $lang->project->cvTitle . ')' : $lang->project->cv
)
),
div
(
set('class', 'flex'),
div
(
set('class', 'flex-1'),
$project->pv
),
div
(
set('class', 'flex-1'),
$project->ev
),
div
(
set('class', 'flex-1'),
$project->ac
),
div
(
set('class', 'flex-1'),
$project->sv
),
div
(
set('class', 'flex-1'),
$project->cv
)
)
)
);
}
panel
(
set('class', 'welcome-block'),
ul
(
set('class', 'nav nav-tabs'),
li
(
set('class', 'nav-item'),
a
(
set('href', '#tabCount1'),
'标签1'
)
),
li
(
set('class', 'nav-item'),
a
(
set('href', '#tabCount2'),
'标签2'
)
)
),
set('class', 'projectstatistic-block'),
div
(
set('class', 'tab-content'),
set('class', 'flex'),
ul
(
set('class', 'nav nav-tabs nav-stacked'),
$navTabs
),
div
(
set('class', 'tab-pane'),
set('id', 'tabCount1'),
"我是标签1"
)
div
(
set('class', 'tab-pane'),
set('id', 'tabCount2'),
"我是标签2"
set('class', 'tab-content'),
$tabItems
)
)
)
);
render();
@@ -88,7 +88,6 @@ html[lang="en"] .product-info .type-info {color: #A6AAB8; text-align: center; po
.forty-percent {width: 40%;}
</style>
<script>
<?php $blockNavId = 'nav-' . uniqid(); ?>
$(function()
{
var $nav = $('#<?php echo $blockNavId;?>');
+1683
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -391,7 +391,7 @@ class product extends control
if(!empty($_POST))
{
$data = form::data($this->config->product->form->create);
$product = $this->productZen->prepareCreateExtras($data);
$product = $this->productZen->prepareCreateExtras($data, $this->post->acl, $this->post->uid);
$productID = $this->product->create($product, $this->post->uid, zget($_POST, 'lineName', ''));
if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError()));
@@ -422,7 +422,7 @@ class product extends control
if(!empty($_POST))
{
$data = form::data($this->config->product->form->edit);
$product = $this->productZen->prepareEditExtras($data);
$product = $this->productZen->prepareEditExtras($data, $this->post->acl, $this->post->uid);
$changes = $this->product->update($productID, $product, $this->post->uid);
if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError()));
+11 -53
View File
@@ -1115,6 +1115,7 @@ class productModel extends model
}
/**
* 获取与该产品关联的项目列表。
* Get project list by product.
*
* @param int $productID
@@ -1143,6 +1144,7 @@ class productModel extends model
}
/**
* 根据产品,获取与该产品关联的项目的统计信息。
* Get project stats by product.
*
* @param int $productID
@@ -1152,7 +1154,7 @@ class productModel extends model
* @param string $orderBy
* @param object $pager
* @access public
* @return array
* @return int[]
*/
public function getProjectStatsByProduct(int $productID, string $browseType = 'all', string $branch = '0', bool $involved = false, string $orderBy = 'order_desc', object|null $pager = null): array
{
@@ -1161,8 +1163,6 @@ class productModel extends model
$projectKeys = array_keys($projects);
$stats = array();
$hours = array();
$emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0);
/* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */
$tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason')
@@ -1171,59 +1171,17 @@ class productModel extends model
->andWhere('parent')->lt(1)
->andWhere('deleted')->eq(0)
->fetchGroup('project', 'id');
/* Compute totalEstimate, totalConsumed, totalLeft. */
foreach($tasks as $projectID => $projectTasks)
{
$hour = (object)$emptyHour;
foreach($projectTasks as $task)
{
if($task->status != 'cancel')
{
$hour->totalEstimate += $task->estimate;
$hour->totalConsumed += $task->consumed;
}
if($task->status != 'cancel' and $task->status != 'closed') $hour->totalLeft += $task->left;
}
$hours[$projectID] = $hour;
}
/* Compute totalReal and progress. */
$progressList = $this->loadModel('project')->getWaterfallProgress(array_keys($hours));
foreach($hours as $projectID => $hour)
{
$hour->totalEstimate = round($hour->totalEstimate, 1) ;
$hour->totalConsumed = round($hour->totalConsumed, 1);
$hour->totalLeft = round($hour->totalLeft, 1);
$hour->totalReal = $hour->totalConsumed + $hour->totalLeft;
$hour->progress = $projects[$projectID]->model == 'waterfall' ? $progressList[$projectID] : ($hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 2) * 100 : 0);
}
$hours = $this->loadModel('program')->computeProjectHours($tasks);
/* Get the number of project teams. */
$teams = $this->dao->select('root,count(*) as teams')->from(TABLE_TEAM)
->where('root')->in($projectKeys)
->andWhere('type')->eq('project')
->groupBy('root')
->fetchAll('root');
$teams = $this->dao->select('t1.root,t1.account')->from(TABLE_TEAM)->alias('t1')
->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account')
->where('t1.root')->in($projectKeys)
->andWhere('t1.type')->eq('project')
->andWhere('t2.deleted')->eq(0)
->fetchGroup('root', 'account');
/* Process projects. */
foreach($projects as $key => $project)
{
if($project->end == '0000-00-00') $project->end = '';
/* Judge whether the project is delayed. */
if($project->status != 'done' and $project->status != 'closed' and $project->status != 'suspended')
{
$delay = helper::diffDate(helper::today(), $project->end);
if($delay > 0) $project->delay = $delay;
}
/* Process the hours. */
$project->hours = isset($hours[$project->id]) ? $hours[$project->id] : (object)$emptyHour;
$project->teamCount = isset($teams[$project->id]) ? $teams[$project->id]->teams : 0;
$stats[] = $project;
}
$stats = $this->program->appendStatToProjects($projects, 'hours,teamCount', array('hours' => $hours, 'teams' => $teams));
return $stats;
}
@@ -46,8 +46,29 @@ $product->app->user->admin = true;
$product->app->moduleName = 'product';
$product->app->methodName = 'project';
r(count($product->getProjectListByProduct($productID = 0, 'all', '', 'order_desc'))) && p() && e('0');
r(count($product->getProjectListByProduct($productID = 1, 'all', '', 'order_desc'))) && p() && e('20');
r(count($product->getProjectListByProduct($productID = 1, 'all', '0', 'order_desc'))) && p() && e('17');
r(count($product->getProjectListByProduct($productID = 1, 'undone', '', 'order_desc'))) && p() && e('15');
r(count($product->getProjectListByProduct($productID = 1, 'wait', 'all', 'order_desc'))) && p() && e('5');
r(count($product->getProjectListByProduct($productID = 0, 'all', '', false, 'order_desc'))) && p() && e('0');
r(count($product->getProjectListByProduct($productID = 1, 'all', '', false, 'order_desc'))) && p() && e('20');
r(count($product->getProjectListByProduct($productID = 1, 'all', '0', false, 'order_desc'))) && p() && e('17');
r(count($product->getProjectListByProduct($productID = 1, 'undone', '', false, 'order_desc'))) && p() && e('15');
r(count($product->getProjectListByProduct($productID = 1, 'wait', 'all', false, 'order_desc'))) && p() && e('5');
$product->app->loadClass('pager', $static = true);
$pager = new pager(0, 50, 1);
r(count($product->getProjectListByProduct($productID = 1, 'all', '', false, 'order_desc', $pager))) && p() && e('20');
$pager = new pager(0, 5, 1);
r(count($product->getProjectListByProduct($productID = 1, 'all', '', false, 'order_desc', $pager))) && p() && e('5');
$product->app->user->admin = false;
$product->app->user->view->projects = '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,48,49,50';
r(count($product->getProjectListByProduct($productID = 1, 'all', '', true, 'order_desc'))) && p() && e('4');
r(count($product->getProjectListByProduct($productID = 1, 'all', '0', true, 'order_desc'))) && p() && e('4');
r(count($product->getProjectListByProduct($productID = 1, 'undone', '', true, 'order_desc'))) && p() && e('2');
r(count($product->getProjectListByProduct($productID = 1, 'wait', 'all', true, 'order_desc'))) && p() && e('0');
$product->app->loadClass('pager', $static = true);
$pager = new pager(0, 50, 1);
r(count($product->getProjectListByProduct($productID = 1, 'all', '', true, 'order_desc', $pager))) && p() && e('4');
$pager = new pager(0, 5, 1);
r(count($product->getProjectListByProduct($productID = 1, 'all', '', true, 'order_desc', $pager))) && p() && e('4');
@@ -3,55 +3,48 @@
include dirname(__FILE__, 5) . "/test/lib/init.php";
include dirname(__FILE__, 2) . '/product.class.php';
zdTable('product')->gen(50);
zdTable('user')->gen(50);
$project = zdTable('project');
$project->PM->range('user2,user3');
$project->gen(50);
$task = zdTable('task');
$task->project->range('1-20');
$task->gen(100);
$team = zdTable('team');
$team->root->range('1-50');
$team->type->range('project');
$team->account->range('admin,admin,user1,user1');
$team->days->range('5');
$team->hours->range('6,7');
$team->gen(50);
$stakeholder = zdTable('stakeholder');
$stakeholder->objectType->range('project');
$stakeholder->objectID->range('1-50');
$stakeholder->user->range('user4,user5');
$stakeholder->type->range('inside');
$stakeholder->from->range('');
$stakeholder->gen(50);
$projectproduct = zdTable('projectproduct');
$projectproduct->product->range('1-2');
$projectproduct->project->range('1-50');
$projectproduct->gen(50);
/**
title=productModel->getProjectStatsByProduct();
title=productModel->getProjectStatsByProductTest();
cid=1
pid=1
测试获取产品1的 browseType为all的信息 >> 项目1;项目11
测试获取产品1的 browseType为unclosed的信息 >> 0
测试获取产品1的 browseType为unplan的信息 >> 0
测试获取产品1的 browseType为allstory的信息 >> 0
测试获取产品2的 browseType为all的信息 >> 项目2;项目12
测试获取产品2的 browseType为assignedtome的信息 >> 0
测试获取产品2的 browseType为openedbyme的信息 >> 0
测试获取产品2的 browseType为reviewedbyme的信息 >> 0
测试获取产品3的 browseType为all的信息 >> 项目3;项目13
测试获取产品3的 browseType为reviewbyme的信息 >> 0
测试获取产品3的 browseType为closedbyme的信息 >> 0
测试获取产品3的 browseType为draftstory的信息 >> 0
测试获取产品4的 browseType为all的信息 >> 项目4;项目14
测试获取产品4的 browseType为activestory的信息 >> 0
测试获取产品4的 browseType为changedstory的信息 >> 0
测试获取产品5的 browseType为all的信息 >> 项目5;项目15
测试获取产品5的 browseType为willclose的信息 >> 0
测试获取产品5的 browseType为closedstory的信息 >> 0
测试获取不存在产品的信息 >> 0
*/
$productIDList = array('1', '2', '3', '4', '5', '1000001');
$browseType = array('unclosed', 'unplan', 'allstory', 'assignedtome', 'openedbyme', 'reviewedbyme', 'reviewbyme', 'closedbyme', 'draftstory', 'activestory', 'changedstory', 'willclose', 'closedstory');
$product = new productTest('admin');
r($product->getProjectStatsByProductTest($productIDList[0])) && p('11;21') && e('项目1;项目11'); // 测试获取产品1的 browseType为all的信息
r($product->getProjectStatsByProductTest($productIDList[0], $browseType[0])) && p() && e('0'); // 测试获取产品1的 browseType为unclosed的信息
r($product->getProjectStatsByProductTest($productIDList[0], $browseType[1])) && p() && e('0'); // 测试获取产品1的 browseType为unplan的信息
r($product->getProjectStatsByProductTest($productIDList[0], $browseType[2])) && p() && e('0'); // 测试获取产品1的 browseType为allstory的信息
r($product->getProjectStatsByProductTest($productIDList[1])) && p('12;22') && e('项目2;项目12'); // 测试获取产品2的 browseType为all的信息
r($product->getProjectStatsByProductTest($productIDList[1], $browseType[3])) && p() && e('0'); // 测试获取产品2的 browseType为assignedtome的信息
r($product->getProjectStatsByProductTest($productIDList[1], $browseType[4])) && p() && e('0'); // 测试获取产品2的 browseType为openedbyme的信息
r($product->getProjectStatsByProductTest($productIDList[1], $browseType[5])) && p() && e('0'); // 测试获取产品2的 browseType为reviewedbyme的信息
r($product->getProjectStatsByProductTest($productIDList[2])) && p('13;23') && e('项目3;项目13'); // 测试获取产品3的 browseType为all的信息
r($product->getProjectStatsByProductTest($productIDList[2], $browseType[6])) && p() && e('0'); // 测试获取产品3的 browseType为reviewbyme的信息
r($product->getProjectStatsByProductTest($productIDList[2], $browseType[7])) && p() && e('0'); // 测试获取产品3的 browseType为closedbyme的信息
r($product->getProjectStatsByProductTest($productIDList[2], $browseType[8])) && p() && e('0'); // 测试获取产品3的 browseType为draftstory的信息
r($product->getProjectStatsByProductTest($productIDList[3])) && p('14;24') && e('项目4;项目14'); // 测试获取产品4的 browseType为all的信息
r($product->getProjectStatsByProductTest($productIDList[3], $browseType[9])) && p() && e('0'); // 测试获取产品4的 browseType为activestory的信息
r($product->getProjectStatsByProductTest($productIDList[3], $browseType[10])) && p() && e('0'); // 测试获取产品4的 browseType为changedstory的信息
r($product->getProjectStatsByProductTest($productIDList[4])) && p('15;25') && e('项目5;项目15'); // 测试获取产品5的 browseType为all的信息
r($product->getProjectStatsByProductTest($productIDList[4], $browseType[11])) && p() && e('0'); // 测试获取产品5的 browseType为willclose的信息
r($product->getProjectStatsByProductTest($productIDList[4], $browseType[12])) && p() && e('0'); // 测试获取产品5的 browseType为closedstory的信息
r($product->getProjectStatsByProductTest($productIDList[5])) && p() && e('0'); // 测试获取不存在产品的信息
r($product->getProjectStatsByProductTest($productID = 0, 'all', '', false, 'order_desc')) && p() && e('0');
r($product->getProjectStatsByProductTest($productID = 1, 'all', '', false, 'order_desc')) && p('11:totalConsumed,totalEstimate,totalLeft,progress,teamCount') && e('15,30,10,60,1');
r($product->getProjectStatsByProductTest($productID = 1, 'all', '', true, 'order_desc')) && p('17:totalConsumed,totalEstimate,totalLeft,progress,teamCount') && e('45,27,11,80,1');
+23 -14
View File
@@ -699,28 +699,37 @@ class productTest
}
/**
* Test get project stats by product.
* 测试 getProjectStatsByProduct 方法。
* Test getProjectStatsByProduct method.
*
* @param int $productID
* @param string $browseType
* @param int $productID
* @param string $browseType
* @param string $branch
* @param bool $involved
* @param string $order
* @param object|null $pager
* @access public
* @return array
*/
public function getProjectStatsByProductTest($productID, $browseType = 'all')
public function getProjectStatsByProductTest(int $productID, string $browseType = 'all', string $branch = '', bool $involved = false, string $orderBy = 'order_desc', object|null $pager = null): array
{
$objects = $this->objectModel->getProjectStatsByProduct($productID, $browseType);
$objects = $this->objectModel->getProjectStatsByProduct($productID, $browseType, $branch, $involved, $orderBy, $pager);
foreach($objects as $object) $projects[$object->id] = $object->name;
if(dao::isError())
$projects = array();
foreach($objects as $object)
{
return dao::getError();
}
else
{
return isset($projects) ? $projects : array();
$project = new stdclass();
$project->id = $object->id;
$project->totalConsumed = $object->hours->totalConsumed;
$project->totalEstimate = $object->hours->totalEstimate;
$project->totalLeft = $object->hours->totalLeft;
$project->progress = $object->hours->progress;
$project->teamCount = $object->teamCount;
$projects[$project->id] = $project;
}
if(dao::isError()) return dao::getError();
return $projects;
}
/**
+12 -8
View File
@@ -417,38 +417,42 @@ class productZen extends product
* 追加创建信息,处理白名单、项目集字段,还有富文本内容处理。
* Prepare data for create.
*
* @param form $data
* @param form $data
* @param string $acl
* @param string $uid
* @access protected
* @return object
*/
protected function prepareCreateExtras(form $data): object
protected function prepareCreateExtras(form $data, string $acl, string $uid = ''): object
{
$product = $data->setDefault('createdBy', $this->app->user->account)
->setDefault('createdDate', helper::now())
->setDefault('createdVersion', $this->config->version)
->setIF($this->config->systemMode == 'light', 'program', (int)zget($this->config->global, 'defaultProgram', 0))
->setIF($data->rawdata->acl == 'open', 'whitelist', '')
->setIF($acl == 'open', 'whitelist', '')
->stripTags($this->config->product->editor->create['id'], $this->config->allowedTags)
->get();
return $this->loadModel('file')->processImgURL($product, $this->config->product->editor->create['id'], $data->rawdata->uid);
return $this->loadModel('file')->processImgURL($product, $this->config->product->editor->create['id'], $uid);
}
/**
* 处理白名单和富文本内容。
* Prepare data for edit.
*
* @param form $data
* @param form $data
* @param string $acl
* @param string $uid
* @access protected
* @return object
*/
protected function prepareEditExtras(form $data): object
protected function prepareEditExtras(form $data, string $acl, string $uid = ''): object
{
$product = fixer::input('post')->setIF($data->rawdata->acl == 'open', 'whitelist', '')
$product = fixer::input('post')->setIF($acl == 'open', 'whitelist', '')
->stripTags($this->config->product->editor->edit['id'], $this->config->allowedTags)
->get();
return $this->loadModel('file')->processImgURL($product, $this->config->product->editor->edit['id'], zget($data->rawdata, 'uid', ''));
return $this->loadModel('file')->processImgURL($product, $this->config->product->editor->edit['id'], $uid);
}
/**
+67 -64
View File
@@ -344,7 +344,7 @@ class programModel extends model
->andWhere('deleted')->eq(0)
->fetchGroup('execution', 'id');
$hours = $this->computeProgress($tasks);
$hours = $this->computeProjectHours($tasks);
$projectHours = $this->getProgressList();
/* Group data by product. */
@@ -517,43 +517,97 @@ class programModel extends model
}
/**
* Compute progress for project or execution.
* 根据按照项目分组的任务,统计各个项目的工时和进度
* Compute hours and progress for project or execution.
*
* @param array $tasks
* @access public
* @return void
* @return array
*/
public function computeProgress($tasks)
public function computeProjectHours(array $tasks): array
{
if(empty($tasks)) return array();
$hours = array();
foreach($tasks as $projectID => $projectTasks)
{
/* Init hour. */
$hour = new stdclass();
$hour->totalConsumed = 0;
$hour->totalEstimate = 0;
$hour->totalLeft = 0;
/* Compute totalEstimate, totalConsumed, totalLeft to them. */
foreach($projectTasks as $task)
{
$hour->totalConsumed += $task->consumed;
$hour->totalEstimate += $task->estimate;
if($task->status != 'cancel' and $task->status != 'closed') $hour->totalLeft += $task->left;
if(strpos('|cancel|closed|done|', "|{$task->status}|") === false) $hour->totalLeft += $task->left;
}
$hours[$projectID] = $hour;
}
foreach($hours as $hour)
/* 计算进度。四舍五入totalEstimate, totalConsumed, totalLeft. */
$progressList = $this->loadModel('project')->getWaterfallProgress(array_keys($hours));
foreach($hours as $projectID => $hour)
{
$hour->totalEstimate = round($hour->totalEstimate, 1) ;
$hour->totalConsumed = round($hour->totalConsumed, 1);
$hour->totalLeft = round($hour->totalLeft, 1);
$hour->totalReal = $hour->totalConsumed + $hour->totalLeft;
$hour->progress = $hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 2) * 100 : 0;
$hour->progress = zget($progressList, $projectID, ($hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 2) * 100 : 0)) ;
}
return $hours;
}
/**
* 将工时、团队人数、剩余任务数、团队成员等统计信息,追加到对应的项目中。
* Append statistics fields to projects.
*
* @param array $projects
* @param string $appendFields hours,teamCount,leftTasks,teamMembers
* @param array $data array keys are hours, teams and leftTasks.
* @access public
* @return array
*/
public function appendStatToProjects(array $projects, string $appendFields = '', array $data = array()): array
{
if(empty($projects)) return array();
if(empty($appendFields) or empty($data)) return $projects;
$appendFields = explode(',', $appendFields);
$emptyHour = json_decode(json_encode(array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0)));
/* Process projects. */
$stats = array();
foreach($projects as $projectID => $project)
{
if(helper::isZeroDate($project->end)) $project->end = '';
/* Judge whether the project is delayed. */
if($project->status != 'done' and $project->status != 'closed' and $project->status != 'suspended')
{
$delay = empty($project->end) ? 0 : helper::diffDate(helper::today(), $project->end);
if($delay > 0) $project->delay = $delay;
}
/* Merge the hours. */
if(in_array('hours', $appendFields))
{
$project->hours = $emptyHour;
if(isset($data['hours'])) $project->hours = zget($data['hours'], $project->id, $emptyHour);
}
/* Merge the team and left tasks. */
if(in_array('teamCount', $appendFields)) $project->teamCount = isset($data['teams'][$project->id]) ? count($data['teams'][$project->id]) : 0;
if(in_array('teamMembers', $appendFields)) $project->teamMembers = isset($data['teams'][$project->id]) ? array_keys($data['teams'][$project->id]) : array();
if(in_array('leftTasks', $appendFields)) $project->leftTasks = isset($data['leftTasks'][$project->id]) ? $data['leftTasks'][$project->id]->tasks : '—';
$stats[$projectID] = $project;
}
return $stats;
}
/**
* Get project list data.
*
@@ -1418,15 +1472,10 @@ class programModel extends model
/* Init vars. */
$projects = $this->getProjectList($programID, $browseType, $queryID, $orderBy, $pager, $programTitle, $involved, $queryAll);
if(empty($projects)) return array();
$projectKeys = array_keys($projects);
$stats = array();
$hours = array();
$emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0);
$leftTasks = array();
$teamMembers = array();
$executions = $this->dao->select('id')->from(TABLE_EXECUTION)
->where('deleted')->eq(0)
@@ -1438,37 +1487,11 @@ class programModel extends model
$tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution')
->from(TABLE_TASK)
->where('project')->in($projectKeys)
->andWhere('execution')->in(array_keys($executions))
->andWhere('parent')->lt(1)
->andWhere('deleted')->eq(0)
->fetchGroup('project', 'id');
/* Compute totalEstimate, totalConsumed, totalLeft. */
foreach($tasks as $projectID => $projectTasks)
{
$hour = (object)$emptyHour;
foreach($projectTasks as $task)
{
if(isset($executions[$task->execution]))
{
$hour->totalConsumed += $task->consumed;
$hour->totalEstimate += $task->estimate;
if(strpos('cancel,closed', $task->status) === false) $hour->totalLeft += $task->left;
}
}
$hours[$projectID] = $hour;
}
/* Compute totalReal and progress. */
$progressList = $this->loadModel('project')->getWaterfallProgress(array_keys($hours));
foreach($hours as $projectID => $hour)
{
$hour->totalEstimate = round($hour->totalEstimate, 1) ;
$hour->totalConsumed = round($hour->totalConsumed, 1);
$hour->totalLeft = round($hour->totalLeft, 1);
$hour->totalReal = $hour->totalConsumed + $hour->totalLeft;
$hour->progress = $projects[$projectID]->model == 'waterfall' ? $progressList[$projectID] : ($hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 2) * 100 : 0);
}
$hours = $this->computeProjectHours($tasks);
/* Get the number of left tasks. */
if($this->cookie->projectType and $this->cookie->projectType == 'bycard')
@@ -1489,30 +1512,10 @@ class programModel extends model
->andWhere('t2.deleted')->eq(0)
->fetchGroup('root', 'account');
/* Process projects. */
foreach($projects as $key => $project)
{
if($project->end == '0000-00-00') $project->end = '';
$stats = $this->appendStatToProjects($projects, 'hours,teamCount,teamMembers,leftTasks', array('hours' => $hours, 'teams' => $teamMembers, 'leftTasks' => $leftTasks));
/* Convert predefined HTML entities to characters. */
foreach($stats as $project) $project->name = htmlspecialchars_decode($project->name, ENT_QUOTES);
/* Judge whether the project is delayed. */
if($project->status != 'done' and $project->status != 'closed' and $project->status != 'suspended')
{
$delay = helper::diffDate(helper::today(), $project->end);
if($delay > 0) $project->delay = $delay;
}
/* Process the hours. */
$project->hours = isset($hours[$project->id]) ? $hours[$project->id] : (object)$emptyHour;
$project->teamCount = isset($teamMembers[$project->id]) ? count($teamMembers[$project->id]) : 0;
$project->leftTasks = isset($leftTasks[$project->id]) ? $leftTasks[$project->id]->tasks : '—';
$project->teamMembers = isset($teamMembers[$project->id]) ? array_keys($teamMembers[$project->id]) : array();
/* Convert predefined HTML entities to characters. */
$project->name = htmlspecialchars_decode($project->name, ENT_QUOTES);
$stats[$key] = $project;
}
return $stats;
}
@@ -1778,7 +1781,7 @@ class programModel extends model
$actionsMap[] = $item;
}
if($canSuspendProgram or ($canClose && $program->status != 'doing') or ($canActivateProgram and $program->status != 'closed'))
if($canSuspendProgram or ($canCloseProgram and $program->status != 'doing') or ($canActivateProgram and $program->status != 'closed'))
{
$other = new stdclass();
$other->name = 'other';
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . "/test/lib/init.php";
zdTable('project')->gen(50);
$task = zdTable('task');
$task->project->range('1-20');
$task->gen(100);
/**
title=测试 programModel::create();
cid=1
pid=1
*/
global $tester;
$tester->loadModel('program');
$projectID = 11;
$hour = new stdclass();
$hour->totalConsumed = 15;
$hour->totalEstimate = 30;
$hour->totalLeft = 10;
$hour->progress = 60;
$hours[$projectID] = $hour;
$projects = $tester->program->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetchAll('id');
$teams = $tester->program->dao->select('t1.root,t1.account')->from(TABLE_TEAM)->alias('t1')
->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account')
->where('t1.root')->eq($projectID)
->andWhere('t1.type')->eq('project')
->andWhere('t2.deleted')->eq(0)
->fetchGroup('root', 'account');
$leftTasks = $tester->program->dao->select('t2.parent as project, count(*) as tasks')->from(TABLE_TASK)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
->where('t1.project')->eq($projectID)
->andWhere('t1.status')->notIn('cancel,closed')
->groupBy('t2.parent')
->fetchAll('project');
r($tester->program->appendStatToProjects(array())) && p() && e('0'); //不传入任何数据
r($tester->program->appendStatToProjects($projects)) && p('11:id') && e('11'); //只传入项目数据
$stats = $tester->program->appendStatToProjects($projects, 'hours');
r(isset($stats[$projectID]->hours)) && p() && e('0'); //只传入项目数据和hours追加字段
$stats = $tester->program->appendStatToProjects($projects, 'teamCount');
r(isset($stats[$projectID]->teamCount)) && p() && e('0'); //只传入项目数据和teamCount追加字段
$stats = $tester->program->appendStatToProjects($projects, 'teamMembers');
r(isset($stats[$projectID]->teamMembers)) && p() && e('0'); //只传入项目数据和teamMembers追加字段
$stats = $tester->program->appendStatToProjects($projects, 'leftTasks');
r(isset($stats[$projectID]->leftTasks)) && p() && e('0'); //只传入项目数据和leftTasks追加字段
$stats = $tester->program->appendStatToProjects($projects, 'hours,teamCount,teamMembers,leftTasks', array('hours' => $hours, 'teams' => $teams, 'leftTasks' => $leftTasks));
$stat = $stats[$projectID];
r(isset($stat->hours)) && p() && e('1'); //正常传入数据,检查hours字段
r(isset($stat->teamCount)) && p() && e('1'); //正常传入数据,检查teamCount字段
r(isset($stat->teamMembers)) && p() && e('1'); //正常传入数据,检查teamMembers字段
r(isset($stat->leftTasks)) && p() && e('1'); //正常传入数据,检查leftTasks字段
@@ -1,51 +0,0 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . "/test/lib/init.php";
/**
title=测试 programModel::create();
cid=1
pid=1
传入任务数组,返回计算后的总消耗 >> 13
传入任务数组,返回计算后的总预计 >> 15
传入任务数组,返回计算后的总剩余 >> 7
传入任务数组,返回计算后的总工时 >> 20
传入任务数组,返回计算后的进度 >> 65
*/
global $tester;
$tester->loadModel('program');
$task1 = new stdclass();
$task1->consumed = 2;
$task1->left = 2;
$task1->estimate = 2;
$task1->status = 'active';
$task2 = new stdclass();
$task2->consumed = 4;
$task2->left = 5;
$task2->estimate = 6;
$task2->status = 'active';
$task3 = new stdclass();
$task3->consumed = 7;
$task3->left = 7;
$task3->estimate = 7;
$task3->status = 'closed';
$tasks[] = $task1;
$tasks[] = $task2;
$tasks[] = $task3;
$projectTasks[1] = $tasks;
$hours = $tester->program->computeProgress($projectTasks);
r($hours) && p('1:totalConsumed') && e('13'); // 传入任务数组,返回计算后的总消耗
r($hours) && p('1:totalEstimate') && e('15'); // 传入任务数组,返回计算后的总预计
r($hours) && p('1:totalLeft') && e('7'); // 传入任务数组,返回计算后的总剩余
r($hours) && p('1:totalReal') && e('20'); // 传入任务数组,返回计算后的总工时
r($hours) && p('1:progress') && e('65'); // 传入任务数组,返回计算后的进度
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . "/test/lib/init.php";
zdTable('project')->gen(50);
$task = zdTable('task');
$task->project->range('1-20');
$task->gen(100);
/**
title=测试 programModel::create();
cid=1
pid=1
*/
global $tester;
$tester->loadModel('program');
$tasks = $tester->program->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution')
->from(TABLE_TASK)
->where('project')->eq('11')
->andWhere('parent')->lt(1)
->andWhere('deleted')->eq(0)
->fetchGroup('project', 'id');
$hours = $tester->program->computeProjectHours($tasks);
r($tester->program->computeProjectHours(array())) && p() && e('0');
r($tester->program->computeProjectHours($tasks)) && p('11:totalConsumed,totalEstimate,totalLeft,progress') && e('15,30,10,60');
+33 -18
View File
@@ -150,6 +150,7 @@ class programplanModel extends model
}
/**
* 获取甘特图页面数据。
* Get gantt data.
*
* @param int $executionID
@@ -160,27 +161,14 @@ class programplanModel extends model
* @access public
* @return string
*/
public function getDataForGantt($executionID, $productID, $baselineID = 0, $selectCustom = '', $returnJson = true)
public function getDataForGantt(int $executionID, int $productID, int $baselineID = 0, string $selectCustom = '', bool $returnJson = true): string
{
$this->loadModel('stage');
$this->loadModel('execution');
$plans = $this->getStage($executionID, $productID, 'all', 'order');
if($baselineID)
{
$baseline = $this->loadModel('cm')->getByID($baselineID);
$oldData = json_decode($baseline->data);
$oldPlans = $oldData->stage;
foreach($oldPlans as $id => $oldPlan)
{
if(!isset($plans[$id])) continue;
$plans[$id]->version = $oldPlan->version;
$plans[$id]->name = $oldPlan->name;
$plans[$id]->milestone = $oldPlan->milestone;
$plans[$id]->begin = $oldPlan->begin;
$plans[$id]->end = $oldPlan->end;
}
}
if($baselineID) $plans = $this->getBaselineData($baselineID, $plans);
$today = helper::today();
$datas = array();
@@ -380,6 +368,33 @@ class programplanModel extends model
return $returnJson ? json_encode($datas) : $datas;
}
/**
* 获取基线数据。
* Get baseline data.
*
* @param int $baselineID
* @param array $plans
* @access private
* @return array
*/
private function getBaselineData(int $baselineID, array $plans): array
{
$baseline = $this->loadModel('cm')->getByID($baselineID);
$oldData = json_decode($baseline->data);
$oldPlans = $oldData->stage;
foreach($oldPlans as $id => $oldPlan)
{
if(!isset($plans[$id])) continue;
$plans[$id]->version = $oldPlan->version;
$plans[$id]->name = $oldPlan->name;
$plans[$id]->milestone = $oldPlan->milestone;
$plans[$id]->begin = $oldPlan->begin;
$plans[$id]->end = $oldPlan->end;
}
return $plans;
}
/**
* Get gantt data group by assigned.
*
@@ -617,11 +632,11 @@ class programplanModel extends model
/**
* Process plans.
*
* @param int $plans
* @param array $plans
* @access public
* @return object
*/
public function processPlans($plans)
private function processPlans(array $plans)
{
foreach($plans as $planID => $plan) $plans[$planID] = $this->processPlan($plan);
return $plans;
+10 -8
View File
@@ -238,12 +238,12 @@ class todoModel extends model
/**
* Get todo list of a user.
*
* @param string $type
* @param string $account
* @param string $status all|today|thisweek|lastweek|before, or a date.
* @param int $limit
* @param object $pager
* @param string $orderBy
* @param string $type
* @param string $account
* @param string|array $status all|today|thisweek|lastweek|before, or a date.
* @param int $limit
* @param ?object $pager
* @param string $orderBy
* @access public
* @return array
*/
@@ -362,11 +362,12 @@ class todoModel extends model
if(!isset($validUsers[$todo->account])) continue;
$todo->config = json_decode($todo->config);
$begin = $todo->config->begin;
$end = $todo->config->end;
$beforeDays = (int)$todo->config->beforeDays;
if(!empty($beforeDays) && $beforeDays > 0) $begin = date('Y-m-d', strtotime("$begin -{$beforeDays} days"));
if($today < $begin or (!empty($end) && $today > $end)) continue;
if($today < $begin || (!empty($end) && $today > $end)) continue;
$newTodo = $this->todoTao->buildCycleTodo($todo);
if(isset($todo->assignedTo) && $todo->assignedTo) $newTodo->assignedDate = $now;
@@ -560,7 +561,8 @@ class todoModel extends model
* 根据待办ID更新待办数据。
* Update todo data by id.
*
* @param array $todoIdList
* @param int $todoID
* @param object $todo
* @access public
* @return bool
*/
+5 -8
View File
@@ -279,7 +279,7 @@ class todoTao extends todoModel
$todo->account = $this->app->user->account;
$todo->date = $formData->rawdata->date;
if($formData->rawdata->switchDate == 'on' || $formData->rawdata->date == false) $todo->date = '2030-01-01';
if($formData->rawdata->switchDate == 'on' || !$formData->rawdata->date) $todo->date = '2030-01-01';
$todo->type = $todos->types[$loop];
$todo->pri = $todos->pris[$loop];
@@ -368,8 +368,8 @@ class todoTao extends todoModel
}
/**
* 设置周期待办数据。
* Set cycle todo data.
* 设置周期待办数据
*
* @param object $todoData
* @access private
@@ -397,12 +397,12 @@ class todoTao extends todoModel
if($todoData->config['type'] == 'week')
{
unset($todoData->config['day'], $todoData->config['month']);
$todoData->config['week'] = join(',', $todoData->config['week']);
$todoData->config['week'] = implode(',', $todoData->config['week']);
}
if($todoData->config['type'] == 'month')
{
unset($todoData->config['day'], $todoData->config['week']);
$todoData->config['month'] = join(',', $todoData->config['month']);
$todoData->config['month'] = implode(',', $todoData->config['month']);
}
if($todoData->config['beforeDays'] and !validater::checkInt($todoData->config['beforeDays']))
@@ -476,10 +476,7 @@ class todoTao extends todoModel
if($todo->type == 'opportunity') $todo->name = $this->dao->findByID($todo->objectID)->from(TABLE_OPPORTUNITY)->fetch('name');
}
if($this->config->edition != 'open')
{
if($todo->type == 'feedback') $todo->name = $this->dao->findByID($todo->objectID)->from(TABLE_FEEDBACK)->fetch('title');
}
if($this->config->edition != 'open' && $todo->type == 'feedback') $todo->name = $this->dao->findByID($todo->objectID)->from(TABLE_FEEDBACK)->fetch('title');
return $todo;
}
+8 -24
View File
@@ -1,39 +1,23 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . "/test/lib/init.php";
include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/todo.class.php';
su('admin');
function initData()
{
zdTable('todo')->config('createbycycle')->gen(3);
}
/**
title=测试 todoModel->createByCycle();
cid=1
pid=1
>> + days
测试创建周期日待办 >> 11
测试创建周期周待办 >> 9
测试创建周期月待办 >> `%d`
*/
$days = 31;
$begin = call_user_func_array('date', array('Y-m-d'));
$end = call_user_func_array('date', array('Y-m-d', strtotime("+$days days")));
$todo1 = new stdclass();
$todo1->name = 'cycle生成的待办1';
$todo1->config = '{"specify":{"month":"0","day":"1"},"day":"1","type":"day","beforeDays":11,"end":"' . $end . '","begin":"' . $begin . '"}';
$todo2 = new stdclass();
$todo2->name = 'cycle生成的待办2';
$todo2->config = '{"specify":{"month":"0","day":"1"},"week":"2,4,7","type":"week","beforeDays":31,"end":"' . $end . '","begin":"' . $begin . '"}';
$todo3 = new stdclass();
$todo3->name = 'cycle生成的待办3';
$todo3->config = '{"specify":{"month":"0","day":"1"},"month":"1,8,13,15,27,29,30","type":"month","beforeDays":301,"end":"' . $end . '","begin":"' . $begin . '"}';
initData();
$todo = new todoTest();
r($todo->createByCycleTest($todo1)) && p() && e('11'); // 测试创建周期日待办
r($todo->createByCycleTest($todo2)) && p() && e('9'); // 测试创建周期周待办
r($todo->createByCycleTest($todo3)) && p() && e('`%d`'); // 测试创建周期月待办
r($todo->createByCycleTest()) && p() && e('1');
@@ -0,0 +1,35 @@
title: Create cycle todos for todoModel::createCycle UTC.
desc: Create cycle todos for todoModel::createCycle UTC.
author: xushenjie
version: 1.0.0
fields:
- field: account
range: 'admin'
- field: name
range: '`周期待办:提前1天,间隔一天生成`, `周期待办:每月的每天,提前1天生成`, `周期待办:每周的每天,提前1天生成`'
- field: date
range: 1-5
prefix: '2023-05-0'
- field: type
range: 'cycle'
- field: cycle
range: 1
- field: begin
range: 0800-1100
- field: end
range: 1800-2000
- field: assignedTo
range: 'admin'
- field: desc
range: '周期待办描述'
- field: status
range: 'doing'
- field: objectID
range: 0
- field: assignedBy
range: 'admin'
- field: pri
range: 1-4
- field: config
range: '`{"specify":{"month":"0","day":"1"},"day":"1","type":"day","beforeDays":1,"end":"2030-05-31","begin":"2023-05-01"}`, `{"specify":{"month":"0","day":"1"},"month":"1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31","type":"month","beforeDays":1,"end":"2030-05-31","begin":"2023-05-01"}`, `{"specify":{"month":"0","day":"1"},"week":"1,2,3,4,5,6,0","type":"week","beforeDays":1,"end":"2030-05-31","begin":"2023-05-01"}`'
+12 -26
View File
@@ -243,36 +243,22 @@ class todoTest
return $object ? 1 : 2;
}
public function createByCycleTest($todo)
/**
* Create by cycle test.
*
* @access public
* @return int
*/
public function createByCycleTest()
{
$todo->cycle = '1';
$todo->type = 'cycle';
$todo->pri = 3;
$todo->desc = '';
$todo->status = 'wait';
$todo->begin = '0830';
$todo->end = '0900';
$todo->account = 'admin';
$todo->objectID = '0';
$todo->vision = 'rnd';
$todo->assignedTo = 'admin';
$todo->assignedBy = 'admin';
$todo->assignedDate = date('Y-m-d', time());
$todo->date = date('Y-m-d', time());
$todo->config = str_replace('2022-03-23', date('Y-m-d', time()), $todo->config);
$todoList = $this->objectModel->getValidCycleList();
$this->objectModel->createByCycle($todoList);
global $tester;
$tester->dao->insert(TABLE_TODO)->data($todo)->autoCheck()->exec();
$todoID = $tester->dao->lastInsertID();
$todoIDList = array_keys($todoList);
$count = $tester->dao->select('count(`id`) as count')->from(TABLE_TODO)->where('objectID')->in($todoIDList)->andWhere('deleted')->eq('0')->fetch('count');
$this->objectModel->createByCycle(array($todoID => $todo));
$objects = $tester->dao->select('id')->from(TABLE_TODO)->where('objectID')->eq($todoID)->andWhere('deleted')->eq('0')->fetchAll();
if(dao::isError()) return dao::getError();
return count($objects);
return dao::isError() ? 0 :1;
}
/**