diff --git a/module/execution/css/view.ui.css b/module/execution/css/view.ui.css new file mode 100644 index 0000000000..40939c2d85 --- /dev/null +++ b/module/execution/css/view.ui.css @@ -0,0 +1,25 @@ +.timeline > li {position: relative; list-style: none} +.timeline > li:before, .timeline > li > a:after, .timeline > li > div:after {position: absolute; left: -20px; display: block; width: 15px; height: 15px; content: ' '; border-radius: 50%} +.timeline > li:before {top: 12px; left: -16px; z-index: 3; width: 7px; height: 7px; background-color: #c4c4c4; border: none; border: 1px solid #c4c4c4} +.timeline > li > a:after, .timeline > li > div:after {top: 11px; left: -17px; z-index: 3; width: 9px; height: 9px; background-color: #2e7fff; border-radius: 50%; opacity: 0} +.timeline > li + li:after {position: absolute; top: -12px; bottom: 20px; left: -13px; z-index: 1; display: block; content: ' '; border-left: 1px solid #eee} +.timeline > li.active > a:after, .timeline > li.active > div:after {opacity: 1} +.timeline > li.active:before {top: 8px; left: -20px; width: 15px; height: 15px; background-color: rgba(46,127,255, .2); border: none} +.timeline > li > a, .timeline > li > div {display: block; padding: 5px; line-height: 20px} +.timeline > li.active > a {color: #313c52} + +.timeline-tag {position: absolute; top: 5px; left: -115px; font-size: 12px} +.timeline-tag-left {padding-left: 115px} + +.timeline-sm {font-size: 12px} +.timeline-sm > li:before, .timeline-sm > li > a:after, .timeline-sm > li > div:after {top: 10px; left: -20px; width: 11px; height: 11px} +.timeline-sm > li.active:before, .timeline-sm > li:before {top: 10px; left: -18px; width: 11px; height: 11px; background: 0 0; border: 1px solid #eee} +.timeline-sm > li > a, .timeline-sm > li > div {line-height: 20px} +.timeline-sm > li > a:after, .timeline-sm > li > div:after {top: 13px; left: -15px; width: 5px; height: 5px} + +.linked-products > .items-center {justify-content: space-between;} +.execution-statitic-hours .table-data-body, .execution-basic-info .table-data-body {display: flex; flex-wrap: wrap;} +.execution-statitic-hours .table-data-body .table-data-tr {flex: 0 1 50%;} +.execution-statitic-hours .table-data .table-data-th {min-width: 60px; text-align: right;} +.execution-basic-info .table-data-body .table-data-tr {flex: 0 1 33%;} +.execution-basic-info .table-data .table-data-th {min-width: 36px;} diff --git a/module/execution/js/view.ui.js b/module/execution/js/view.ui.js new file mode 100644 index 0000000000..445f064878 --- /dev/null +++ b/module/execution/js/view.ui.js @@ -0,0 +1,18 @@ +$(function() +{ + setTimeout(function() + { + blocks.forEach(block => + { + if(block.domID) + { + block.content = {html: $(`#${block.domID}`).html()}; + $(`#${block.domID}`).remove(); + delete(block.domID); + } + }); + + const $dashboard = $('#executionDashBoard').data('zui.Dashboard') + $dashboard.render({blocks: blocks}); + }, 10); +}); diff --git a/module/execution/ui/view.html.php b/module/execution/ui/view.html.php new file mode 100644 index 0000000000..6fe7ff9328 --- /dev/null +++ b/module/execution/ui/view.html.php @@ -0,0 +1,497 @@ + + * @package execution + * @link https://www.zentao.net + */ +namespace zin; + +$blocks = array( + array( + 'id' => 1, + 'size' => 'sm', + 'domID' => 'burnBlock' + ), + array( + 'id' => 2, + 'size' => 'sm', + 'domID' => 'dynamicBlock' + ), + array( + 'id' => 3, + 'size' => 'xl', + 'domID' => 'basicBlock' + ), + array( + 'id' => 4, + 'size' => 'sm', + 'domID' => 'memberBlock' + ), + array( + 'id' => 5, + 'size' => 'sm', + 'domID' => 'docBlock' + ), + array( + 'id' => 6, + 'size' => 'smWide', + 'domID' => 'historyBlock', + ) +); +jsVar('blocks', $blocks); + +dashboard +( + setID('executionDashBoard'), + set::blocks($blocks), + set::blockMenu(false) +); + +/* Dynamic list. */ +$dynamicDom = array(); +foreach($dynamics as $action) +{ + $dynamicDom[] = li + ( + setClass($action->major ? 'active': ''), + div + ( + span( + setClass('timeline-tag'), + $action->date + ), + span( + setClass('timeline-text clip'), + zget($users, $action->actor), + span + ( + setClass('text-gray'), + " {$action->actionLabel} " + ), + span(" {$action->objectLabel} "), + a + ( + setClass('clip'), + set::href($action->objectLink), + set::title($action->objectName), + $action->objectName + ) + ) + ) + ); +} + +div +( + setID('dynamicBlock'), + setClass('hidden'), + panel + ( + to::heading + ( + div + ( + set('class', 'panel-title'), + $lang->execution->latestDynamic, + ) + ), + to::headingActions + ( + common::hasPriv('execution', 'dynamic') ? btn + ( + setClass('ghost text-gray'), + set::url(createLink('execution', 'dynamic', "executionID={$execution->id}&type=all")), + $lang->more + ) : null + ), + set::bodyClass('pt-0 overflow-x-hidden'), + ul + ( + setClass('timeline timeline-tag-left no-margin'), + $dynamicDom + ) + ) +); + +/* Related members. */ +$membersDom = array(); +foreach(array('PM', 'PO', 'QD', 'RD') as $field) +{ + if(empty($execution->$field)) continue; + + $membersDom[] = div + ( + setClass('flex-initial w-1/2 py-1'), + icon('person', setClass('mr-2')), + zget($users, $execution->$field), + span + ( + setClass('text-gray ml-2'), + "( {$lang->execution->$field} )" + ) + ); + + unset($teamMembers[$execution->$field]); +} +$memberCount = count($membersDom); +foreach($teamMembers as $teamMember) +{ + if($memberCount >= 10) break; + + $membersDom[] = div + ( + setClass('flex-initial w-1/2 py-1'), + icon('person', setClass('mr-2')), + zget($users, $teamMember->account), + ); + $memberCount ++; +} + +if(common::hasPriv('execution', 'manageMembers')) +{ + $membersDom[] = div + ( + setClass('flex-initial w-1/2 py-1'), + a + ( + setClass('ghost text-gray'), + icon('plus', setClass('bg-primary-50 text-primary mr-2')), + span($lang->execution->manageMembers), + set::href(createLink('execution', 'manageMembers', "executionID={$execution->id}")) + ) + ); +} + +div +( + setID('memberBlock'), + setClass('hidden'), + panel + ( + to::heading + ( + div + ( + set('class', 'panel-title'), + $lang->execution->relatedMember + ) + ), + to::headingActions + ( + common::hasPriv('execution', 'team') ? btn + ( + setClass('ghost text-gray'), + set::url(createLink('execution', 'team', "executionID={$execution->id}")), + $lang->more + ) : null + ), + set::bodyClass('flex flex-wrap pt-0'), + $membersDom + ) +); + +/* History list. */ +div +( + setID('historyBlock'), + setClass('hidden'), + div + ( + setClass('overflow-y-auto h-full'), + history() + ) +); + +$programDom = null; +if($execution->projectInfo->grade > 1) +{ + foreach($programList as $programID => $name) + { + if(common::hasPriv('program', 'product')) + { + $programList[$programID] = html::a + ( + $this->createLink('program', 'product', "programID={$programID}"), + $name + ); + } + else + { + $programList[$programID] = span($name); + } + } + + $programDom = div + ( + icon('program mr-2'), + html(implode('/ ', $programList)) + ); +} + +$productsDom = null; +if($execution->projectInfo->hasProduct) +{ + foreach($products as $productID => $product) + { + foreach($product->branches as $branchID) + { + $branchName = isset($branchGroups[$productID][$branchID]) ? '/' . $branchGroups[$productID][$branchID] : ''; + $productsDom[] = div + ( + setClass('flex-initial w-1/2 py-1'), + icon('product mr-2'), + a + ( + set::href(createLink('product', 'browse', "productID={$productID}&branch={$branchID}")), + span($product->name . $branchName) + ) + ); + } + } +} + +$plansDom = null; +if($features['plan']) +{ + foreach($products as $productID => $product) + { + foreach($product->plans as $planIDList) + { + $planIDList = explode(',', $planIDList); + foreach($planIDList as $planID) + { + if(!isset($planGroups[$productID][$planID])) continue; + $plansDom[] = div + ( + setClass('flex-initial py-1'), + icon('calendar mr-2'), + a + ( + set::href(createLink('productplan', 'view', "planID={$planID}")), + span($product->name . '/' . $planGroups[$productID][$planID]) + ) + ); + } + } + } +} + +$progress = ($execution->totalConsumed + $execution->totalLeft) ? floor($execution->totalConsumed / ($execution->totalConsumed + $execution->totalLeft) * 1000) / 1000 * 100 : 0; +$hoursDom = div + ( + div + ( + span($lang->execution->progress . ' ' . $progress . $lang->percent), + div + ( + set('class', 'progress'), + div + ( + set('class', 'progress-bar'), + set('role', 'progressbar'), + setStyle(['width' => $progress . $lang->percent]), + ) + ) + ), + tableData + ( + set::useTable(false), + item + ( + set::name($lang->execution->begin), + $execution->begin + ), + item + ( + set::name($lang->execution->realBeganAB), + helper::isZeroDate($execution->realBegan) ? '' : $execution->realBegan + ), + item + ( + set::name($lang->execution->end), + $execution->end + ), + item + ( + set::name($lang->execution->realEndAB), + helper::isZeroDate($execution->realEndAB) ? '' : $execution->realEndAB + ), + item + ( + set::name($lang->execution->totalEstimate), + (float)$execution->totalEstimate . $lang->execution->workHour + ), + item + ( + set::name($lang->execution->totalDays), + $execution->days . $lang->execution->day + ), + item + ( + set::name($lang->execution->totalConsumed), + (float)$execution->totalConsumed . $lang->execution->workHour + ), + item + ( + set::name($lang->execution->totalHours), + (float)$execution->totalHours . $lang->execution->workHour + ), + item + ( + set::name($lang->execution->totalLeft), + (float)$execution->totalLeft . $lang->execution->workHour + ), + ) + ); + +$basicInfoDom = tableData + ( + set::useTable(false), + $features['story'] ? item + ( + set::name($lang->story->common), + $statData->storyCount + ) : null, + item + ( + set::name($lang->task->common), + $statData->taskCount + ), + $features['qa'] ? item + ( + set::name($lang->bug->common), + $statData->bugCount + ) : null, + ); + + +/* Baseic information. */ +div +( + setID('basicBlock'), + setClass('hidden'), + sectionList + ( + section + ( + setClass('border-b pb-4'), + div + ( + label + ( + setClass('text-dark'), + $execution->id + ), + !empty($config->setCode) ? label + ( + setClass('dark-outline text-dark mx-2'), + $execution->code + ) : null, + span(setClass('article-h2'), $execution->name), + ), + div + ( + set::class('detail-content article-content'), + span($execution->desc), + ), + div + ( + setClass('mt-4'), + $execution->deleted ? label + ( + setClass('danger-outline text-danger'), + $lang->execution->deleted + ) : null, + !empty($execution->lifetime) && $execution->type != 'kanban' && $project->model != 'waterfall' && $project->model != 'waterfallplus' ? label + ( + setClass('secondary-outline text-primary'), + setClass($execution->deleted ? 'ml-2' : ''), + zget($lang->execution->lifeTimeList, $execution->lifetime, '') + ) : null, + isset($execution->delay) ? label + ( + setClass('danger-outline text-danger ml-2'), + $lang->execution->delayed + ) : label + ( + setClass("success-pale ring-success ml-2"), + $this->processStatus('execution', $execution) + ), + ) + ), + $config->systemMode == 'ALM' ? section + ( + setClass('border-b pb-4'), + set::title($lang->project->parent), + $programDom + ) : null, + section + ( + setClass('border-b pb-4'), + set::title($lang->project->project), + div + ( + icon('project mr-2'), + html + ( + common::hasPriv('project', 'index') ? html::a + ( + $this->createLink('project', 'index', "projectID={$execution->project}"), + $execution->projectInfo->name + ) : span($execution->projectInfo->name), + ) + ) + ), + $execution->projectInfo->hasProduct ? section + ( + setClass('border-b pb-4 linked-products'), + set::title($lang->execution->manageProducts), + common::hasPriv('execution', 'manageproducts') && $execution->type != 'stage' && $project->model != 'waterfallplus' ? to::actions(btn + ( + setClass('ghost text-gray'), + set::url(createLink('execution', 'manageproducts', "projectID={$execution->id}")), + $lang->more + )) : null, + div + ( + setClass('flex flex-wrap'), + $productsDom + ) + ) : null, + $features['plan'] ? section + ( + setClass('border-b pb-4 linked-plans'), + set::title($lang->execution->linkPlan), + div + ( + setClass('flex flex-wrap'), + $plansDom + ) + ) : null, + section + ( + setClass('border-b pb-4 execution-statitic-hours'), + set::title($lang->execution->lblStats), + $hoursDom, + ), + section + ( + setClass('border-b pb-4 execution-basic-info'), + set::title($lang->execution->basicInfo), + $basicInfoDom + ), + section + ( + setClass('pb-4'), + set::title($lang->execution->acl), + $lang->execution->aclList[$execution->acl] + ), + ) +); + +/* ====== Render page ====== */ +render();