diff --git a/config/zentaopms.php b/config/zentaopms.php index a10d6a941c..5e57c970f7 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -224,6 +224,7 @@ $config->openMethods[] = 'personnel.unbindwhitelist'; $config->openMethods[] = 'tree.viewhistory'; $config->openMethods[] = 'doc.createbasicinfo'; $config->openMethods[] = 'project.createguide'; +$config->openMethods[] = 'task.editteam'; /* Define the tables. */ define('TABLE_COMPANY', '`' . $config->db->prefix . 'company`'); @@ -265,6 +266,7 @@ define('TABLE_PROJECT', '`' . $config->db->prefix . 'project`'); define('TABLE_EXECUTION', '`' . $config->db->prefix . 'project`'); define('TABLE_TASK', '`' . $config->db->prefix . 'task`'); define('TABLE_TASKSPEC', '`' . $config->db->prefix . 'taskspec`'); +define('TABLE_TASKTEAM', '`' . $config->db->prefix . 'taskteam`'); define('TABLE_TEAM', '`' . $config->db->prefix . 'team`'); define('TABLE_PROJECTADMIN', '`' . $config->db->prefix . 'projectadmin`'); define('TABLE_PROJECTPRODUCT','`' . $config->db->prefix . 'projectproduct`'); diff --git a/db/update17.6.sql b/db/update17.6.sql new file mode 100644 index 0000000000..4774842924 --- /dev/null +++ b/db/update17.6.sql @@ -0,0 +1,13 @@ +CREATE TABLE `zt_taskteam` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `task` mediumint(8) unsigned NOT NULL, + `account` char(30) NOT NULL, + `estimate` decimal(12,2) NOT NULL, + `consumed` decimal(12,2) NOT NULL, + `left` decimal(12,2) NOT NULL, + `transfer` char(30) NOT NULL, + `status` enum('wait','doing','done') NOT NULL DEFAULT 'wait', + `order` tinyint(3) NOT NULL, + PRIMARY KEY (`id`), + KEY `task` (`task`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/db/zentao.sql b/db/zentao.sql index ac6389009a..a986e9e742 100755 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -1606,6 +1606,20 @@ CREATE TABLE IF NOT EXISTS `zt_taskspec` ( `deadline` date NOT NULL, UNIQUE KEY `task` (`task`,`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_taskteam`; +CREATE TABLE `zt_taskteam` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `task` mediumint(8) unsigned NOT NULL, + `account` char(30) NOT NULL, + `estimate` decimal(12,2) NOT NULL, + `consumed` decimal(12,2) NOT NULL, + `left` decimal(12,2) NOT NULL, + `transfer` char(30) NOT NULL, + `status` enum('wait','doing','done') NOT NULL DEFAULT 'wait', + `order` tinyint(3) NOT NULL, + PRIMARY KEY (`id`), + KEY `task` (`task`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_team`; CREATE TABLE IF NOT EXISTS `zt_team` ( `id` mediumint(8) unsigned NOT NULL auto_increment, diff --git a/module/block/control.php b/module/block/control.php old mode 100644 new mode 100755 index e88e1bfca3..18ba598678 --- a/module/block/control.php +++ b/module/block/control.php @@ -1206,6 +1206,8 @@ class block extends control ->andWhere('parent')->lt(1) ->fetch(); + $this->weekly->save($this->session->project, $date); + $this->view->pv = $this->weekly->getPV($this->session->project, $today); $this->view->ev = $this->weekly->getEV($this->session->project, $today); $this->view->ac = $this->weekly->getAC($this->session->project, $today); @@ -1301,23 +1303,17 @@ class block extends control $projectID = $this->session->project; $project = $this->loadModel('project')->getByID($projectID); - $begin = $project->begin; - $today = helper::today(); - $end = date('Y-m-d', strtotime($today)); + $projectWeekly = $this->dao->select('*')->from(TABLE_WEEKLYREPORT)->where('project')->eq($projectID)->orderBy('weekStart_asc')->fetchAll('weekStart'); $charts['PV'] = '['; $charts['EV'] = '['; $charts['AC'] = '['; - $i = 1; - while($begin < $end) + foreach($projectWeekly as $weekStart => $data) { - $charts['labels'][] = $this->lang->block->time . $i . $this->lang->block->month; - $charts['PV'] .= $this->weekly->getPV($projectID, $begin) . ','; - $charts['EV'] .= $this->weekly->getEV($projectID, $begin) . ','; - $charts['AC'] .= $this->weekly->getAC($projectID, $begin) . ','; - $stageEnd = $this->weekly->getThisSunday($begin); - $begin = date('Y-m-d', strtotime("$stageEnd + 30 day")); - $i ++; + $charts['labels'][] = $weekStart; + $charts['PV'] .= $data->pv . ','; + $charts['EV'] .= $data->ev . ','; + $charts['AC'] .= $data->ac . ','; } $charts['PV'] .= ']'; diff --git a/module/block/model.php b/module/block/model.php index 232e965288..3086c24bfb 100644 --- a/module/block/model.php +++ b/module/block/model.php @@ -171,7 +171,7 @@ class blockModel extends model $tasks = $this->dao->select("count(t1.id) as tasks, count(if(t1.status = 'done', 1, null)) as doneTasks")->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on("t1.project = t2.id") ->leftJoin(TABLE_EXECUTION)->alias('t3')->on("t1.execution = t3.id") - ->leftJoin(TABLE_TEAM)->alias('t4')->on("t4.root = t1.id and t4.type = 'task' and t4.account = '{$this->app->user->account}'") + ->leftJoin(TABLE_TASKTEAM)->alias('t4')->on("t4.task = t1.id and t4.account = '{$this->app->user->account}'") ->where("(t1.assignedTo = '{$this->app->user->account}' or (t1.mode = 'multi' and t4.`account` = '{$this->app->user->account}') )") ->andWhere('(t2.status')->ne('suspended') ->orWhere('t3.status')->ne('suspended') diff --git a/module/common/view/sortable.html.php b/module/common/view/sortable.html.php index 45f3f38175..239f719745 100644 --- a/module/common/view/sortable.html.php +++ b/module/common/view/sortable.html.php @@ -18,7 +18,7 @@ $(document).ready(function() var $tbody = $(this); $tbody.sortable( { - selector: 'tr', + selector: typeof(sortSelector) == 'undefined' ? 'tr' : sortSelector, dragCssClass: 'drag-row', trigger: $tbody.find('.sort-handler').length ? '.sort-handler' : null, finish: function(e) diff --git a/module/execution/config.php b/module/execution/config.php index af413a93f7..e3faacb40e 100644 --- a/module/execution/config.php +++ b/module/execution/config.php @@ -71,7 +71,6 @@ $config->execution->search['fields']['canceledBy'] = $lang->task->canceledBy $config->execution->search['fields']['lastEditedBy'] = $lang->task->lastEditedBy; $config->execution->search['fields']['mailto'] = $lang->task->mailto; -$config->execution->search['fields']['finishedList'] = $lang->task->finishedList; $config->execution->search['fields']['openedDate'] = $lang->task->openedDate; $config->execution->search['fields']['deadline'] = $lang->task->deadline; @@ -107,7 +106,6 @@ $config->execution->search['params']['cancelBy'] = array('operator' => '=' $config->execution->search['params']['lastEditedBy'] = array('operator' => '=', 'control' => 'select', 'values' => 'users'); $config->execution->search['params']['mailto'] = array('operator' => 'include', 'control' => 'select', 'values' => 'users'); -$config->execution->search['params']['finishedList'] = array('operator' => 'include', 'control' => 'select', 'values' => 'users'); $config->execution->search['params']['openedDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date'); $config->execution->search['params']['deadline'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date'); diff --git a/module/execution/model.php b/module/execution/model.php index 77f71b2a11..32b6aaa4e0 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -2343,7 +2343,8 @@ class executionModel extends model { $this->loadModel('task'); - $tasks = $this->dao->select('id,execution,assignedTo,story,consumed,status')->from(TABLE_TASK)->where('id')->in($this->post->tasks)->fetchAll('id'); + $execution = $this->getByID($executionID); + $tasks = $this->dao->select('id,execution,assignedTo,story,consumed,status')->from(TABLE_TASK)->where('id')->in($this->post->tasks)->fetchAll('id'); foreach($tasks as $task) { /* Save the assignedToes and stories, should linked to execution. */ @@ -2351,8 +2352,9 @@ class executionModel extends model $stories[$task->story] = $task->story; $data = new stdclass(); + $data->project = $execution->project; $data->execution = $executionID; - $data->status = $task->consumed > 0 ? 'doing' : 'wait'; + $data->status = $task->consumed > 0 ? 'doing' : 'wait'; if($task->status == 'cancel') { @@ -2931,29 +2933,6 @@ class executionModel extends model ->fetchGroup('root'); } - /** - * Get the skip members of the team. - * - * @param array $teams - * @param string $begin - * @param string $end - * @access public - * @return array - */ - public function getTeamSkip($teams, $begin, $end) - { - $members = array(); - foreach($teams as $account => $team) - { - if($account == $end) break; - if(!empty($begin) and $account != $begin and empty($members)) continue; - - $members[$account] = $team; - } - - return $members; - } - /** * Get teams which can be imported. * @@ -3669,7 +3648,7 @@ class executionModel extends model if(empty($tasks)) return array(); - $taskTeam = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->in(array_keys($tasks))->andWhere('type')->eq('task')->fetchGroup('root'); + $taskTeam = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task'); if(!empty($taskTeam)) { foreach($taskTeam as $taskID => $team) $tasks[$taskID]->team = $team; @@ -4642,7 +4621,7 @@ class executionModel extends model echo ''; common::printIcon('execution', 'start', "executionID={$execution->id}", $execution, 'list', '', '', 'iframe', true); $class = !empty($execution->children) ? 'disabled' : ''; - common::printIcon('task', 'create', "executionID={$execution->id}", $execution, 'list', '', '', $class, false, "data-app='execution'"); + common::printIcon('task', 'create', "executionID={$execution->id}", '', 'list', '', '', $class, false, "data-app='execution'"); if($execution->type == 'stage') { diff --git a/module/kanban/model.php b/module/kanban/model.php index 50bca7f45d..511694c98a 100755 --- a/module/kanban/model.php +++ b/module/kanban/model.php @@ -1534,19 +1534,29 @@ class kanbanModel extends model if($browseType == 'bug') $cardList = $this->loadModel('bug')->getExecutionBugs($executionID); if($browseType == 'task') $cardList = $this->loadModel('execution')->getKanbanTasks($executionID); + $multiTasks = array(); if($browseType == 'task' and $groupBy == 'assignedTo') { foreach($cardList as $id => $task) { - if($task->mode == 'multi') - { - $task->team = $this->dao->select('t1.account,t2.realname')->from(TABLE_TEAM)->alias('t1') - ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') - ->where('t1.root')->eq($id)->andWhere('t1.type')->eq('task')->orderBy('t1.order')->fetchPairs('account'); - } + if($task->mode == 'multi') $multiTasks[$id] = $task; } } + $taskTeams = $this->dao->select('t1.account,t1.task,t2.realname')->from(TABLE_TASKTEAM)->alias('t1') + ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') + ->where('t1.task')->in(array_keys($multiTasks)) + ->orderBy('t1.order') + ->fetchGroup('task', 'account'); + foreach($multiTasks as $taskID => $task) + { + if(!isset($taskTeams[$taskID])) continue; + + $teamPairs = array(); + foreach($taskTeams[$taskID] as $account => $team) $teamPairs[$account] = $team->realname; + $task->team = $teamPairs; + } + /* Get objects cards menus. */ if($browseType == 'story') $storyCardMenu = $this->getKanbanCardMenu($executionID, $cardList, 'story'); if($browseType == 'bug') $bugCardMenu = $this->getKanbanCardMenu($executionID, $cardList, 'bug'); diff --git a/module/my/model.php b/module/my/model.php index 89101f568a..14bc238ccf 100644 --- a/module/my/model.php +++ b/module/my/model.php @@ -588,6 +588,7 @@ class myModel extends model ->leftJoin(TABLE_EXECUTION)->alias('t2')->on("t1.execution = t2.id") ->leftJoin(TABLE_STORY)->alias('t3')->on('t1.story = t3.id') ->leftJoin(TABLE_PROJECT)->alias('t4')->on("t1.project = t4.id") + ->leftJoin(TABLE_TASKTEAM)->alias('t5')->on("t1.id = t5.task") ->where($query) ->andWhere('t1.deleted')->eq(0) ->andWhere('t2.deleted')->eq(0) @@ -598,7 +599,7 @@ class myModel extends model ->orWhere('t1.closedBy')->eq($account) ->orWhere('t1.canceledBy')->eq($account) ->orWhere('t1.finishedby', 1)->eq($account) - ->orWhere('t1.finishedList')->like("%,{$account},%") + ->orWhere('t5.status')->eq("done") ->orWhere('t1.id')->in($taskIDList) ->markRight(1) ->fi() @@ -612,7 +613,7 @@ class myModel extends model $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'task', false); - $taskTeam = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->in(array_keys($tasks))->andWhere('type')->eq('task')->fetchGroup('root'); + $taskTeam = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task'); if(!empty($taskTeam)) { foreach($taskTeam as $taskID => $team) $tasks[$taskID]->team = $team; diff --git a/module/programplan/model.php b/module/programplan/model.php index 61d186ffdf..4efccd5daa 100755 --- a/module/programplan/model.php +++ b/module/programplan/model.php @@ -224,7 +224,7 @@ class programplanModel extends model if(empty($selectCustom)) $selectCustom = $this->loadModel('setting')->getItem("owner={$owner}&module={$module}§ion={$section}&key={$object}"); $tasks = $this->dao->select('*')->from(TABLE_TASK)->where('deleted')->eq(0)->andWhere('execution')->in($planIdList)->orderBy('execution_asc, order_asc, id_desc')->fetchAll('id'); - $taskTeams = $this->dao->select('root,account')->from(TABLE_TEAM)->where('type')->eq('task')->andWhere('root')->in(array_keys($tasks))->fetchGroup('root', 'account'); + $taskTeams = $this->dao->select('task,account')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task', 'account'); $users = $this->loadModel('user')->getPairs('noletter'); if($baselineID) @@ -381,33 +381,42 @@ class programplanModel extends model $tasksGroup = $this->dao->select('*')->from(TABLE_TASK)->where('deleted')->eq(0)->andWhere('execution')->in($planIdList)->fetchGroup('assignedTo','id'); $users = $this->loadModel('user')->getPairs('noletter'); - $tasksMap = array(); + $tasksMap = array(); + $multiTasks = array(); foreach($tasksGroup as $group => $tasks) { foreach($tasks as $id => $task) { - if($task->mode == 'multi') - { - $team = $this->dao->select('t1.*,t2.realname')->from(TABLE_TEAM)->alias('t1') - ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') - ->where('t1.root')->eq($id)->andWhere('t1.type')->eq('task')->orderBy('t1.order')->fetchAll('account'); - foreach($team as $account => $member) - { - if($account == $group) continue; - if(!isset($taskGroups[$account])) $taskGroups[$account] = array(); - - $taskGroups[$account][$id] = clone $task; - $taskGroups[$account][$id]->id = $id . '_' . $account; - $taskGroups[$account][$id]->realID = $id; - $taskGroups[$account][$id]->assignedTo = $account; - $taskGroups[$account][$id]->realname = $member->realname; - } - } - + if($task->mode == 'multi') $multiTasks[$id] = $group; $tasksMap[$task->id] = $task; } } + if($multiTasks) + { + $taskTeams = $this->dao->select('t1.*,t2.realname')->from(TABLE_TASKTEAM)->alias('t1') + ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') + ->where('t1.task')->in(array_keys($multiTasks)) + ->orderBy('t1.order') + ->fetchGroup('task', 'id'); + foreach($taskTeams as $taskID => $team) + { + $group = $multiTasks[$taskID]; + foreach($team as $member) + { + $account = $member->account; + if($account == $group) continue; + if(!isset($taskGroups[$account])) $taskGroups[$account] = array(); + + $taskGroups[$account][$taskID] = clone $tasksMap[$taskID]; + $taskGroups[$account][$taskID]->id = $taskID . '_' . $account; + $taskGroups[$account][$taskID]->realID = $taskID; + $taskGroups[$account][$taskID]->assignedTo = $account; + $taskGroups[$account][$taskID]->realname = $member->realname; + } + } + } + $groupID = 0; foreach($tasksGroup as $group => $tasks) { diff --git a/module/report/model.php b/module/report/model.php index 4d17beaf4a..52e00c69f4 100644 --- a/module/report/model.php +++ b/module/report/model.php @@ -358,10 +358,9 @@ class reportModel extends model $taskGroups[$task->assignedTo][$task->id] = $task; } - $multiTaskTeams = $this->dao->select('*')->from(TABLE_TEAM)->where('type')->eq('task') - ->andWhere('root')->in(array_keys($allTasks)) + $multiTaskTeams = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($allTasks)) ->beginIF($dept)->andWhere('account')->in(array_keys($deptUsers))->fi() - ->fetchGroup('account', 'root'); + ->fetchGroup('account', 'task'); foreach($multiTaskTeams as $assignedTo => $multiTasks) { foreach($multiTasks as $task) diff --git a/module/task/control.php b/module/task/control.php index 2da2dbf5b3..3a01acf696 100755 --- a/module/task/control.php +++ b/module/task/control.php @@ -719,12 +719,12 @@ class task extends control /* Get edited tasks. */ $tasks = $this->dao->select('*')->from(TABLE_TASK)->where('id')->in($taskIDList)->fetchAll('id'); - $teams = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->in($taskIDList)->andWhere('type')->eq('task')->fetchGroup('root', 'account'); + $teams = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in($taskIDList)->fetchGroup('task', 'id'); /* Get execution teams. */ $executionIDList = array(); foreach($tasks as $task) if(!in_array($task->execution, $executionIDList)) $executionIDList[] = $task->execution; - $executionTeams = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->in($executionIDList)->andWhere('type')->eq('execution')->fetchGroup('root', 'account'); + $executionTeams = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in($executionIDList)->fetchGroup('task', 'id'); /* Judge whether the editedTasks is too large and set session. */ $countInputVars = count($tasks) * (count(explode(',', $this->config->task->custom->batchEditFields)) + 3); @@ -787,9 +787,9 @@ class task extends control $this->commonAction($taskID); $task = $this->task->getByID($taskID); - if(!empty($task->team) and $task->mode == 'multi') + if(!empty($task->team) and $task->mode == 'multi' and strpos('done,cencel,closed', $task->status) === false) { - return $this->editTeam($executionID, $task, $kanbanGroup); + return $this->editTeam($executionID, $taskID, $kanbanGroup, $from); } if(!empty($_POST)) @@ -841,9 +841,9 @@ class task extends control $members = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted'); /* Compute next assignedTo. */ - if(!empty($task->team)) + if(!empty($task->team) and strpos('done,cencel,closed', $task->status) === false) { - $task->nextUser = $this->task->getNextUser(array_keys($task->team), $task->assignedTo); + $task->nextUser = $this->task->getAssignedTo4Multi($task->team, $task, 'next'); $members = $this->task->getMemberPairs($task); } @@ -903,7 +903,7 @@ class task extends control if(!is_array($taskIDList)) return print(js::locate($this->createLink('execution', 'task', "executionID=$execution"), 'parent')); $taskIDList = array_unique($taskIDList); - $muletipleTasks = $this->dao->select('root , account')->from(TABLE_TEAM)->where('type')->eq('task')->andWhere('root')->in($taskIDList)->fetchGroup('root', 'account'); + $muletipleTasks = $this->dao->select('task, account')->from(TABLE_TASKTEAM)->where('task')->in($taskIDList)->fetchGroup('task', 'account'); $tasks = $this->task->getByList($taskIDList); $this->loadModel('action'); foreach($tasks as $taskID => $task) @@ -1098,12 +1098,15 @@ class task extends control return print(js::locate($this->createLink('task', 'view', "taskID=$taskID"), 'parent')); } + $assignedTo = empty($task->assignedTo) ? $this->app->user->account : $task->assignedTo; + if(!empty($task->team)) $assignedTo = $this->task->getAssignedTo4Multi($task->team, $task); + $this->view->title = $this->view->execution->name . $this->lang->colon .$this->lang->task->start; $this->view->position[] = $this->lang->task->start; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->members = $this->loadModel('user')->getTeamMemberPairs($task->execution, 'execution', 'nodeleted'); - $this->view->assignedTo = $task->assignedTo == '' ? $this->app->user->account : $task->assignedTo; + $this->view->assignedTo = $assignedTo; $this->display(); } @@ -1177,6 +1180,7 @@ class task extends control $this->view->task = $this->task->getById($taskID); $this->view->estimates = $this->task->getTaskEstimate($taskID); $this->view->title = $this->lang->task->record; + $this->view->users = $this->loadModel('user')->getPairs('noclosed|noletter'); $this->display(); } @@ -1202,11 +1206,10 @@ class task extends control return print(js::locate($url, 'parent')); } - $estimate = $this->task->getEstimateById($estimateID); - $this->view->title = $this->lang->task->editEstimate; $this->view->position[] = $this->lang->task->editEstimate; $this->view->estimate = $estimate; + $this->view->task = $this->task->getById($estimate->task); $this->display(); } @@ -1220,35 +1223,21 @@ class task extends control */ public function deleteEstimate($estimateID, $confirm = 'no') { - $estimate = $this->task->getEstimateById($estimateID); - $task = $this->task->getById($estimate->task); - if($confirm == 'no' and $task->consumed - $estimate->consumed != 0) + if($confirm == 'no') { return print(js::confirm($this->lang->task->confirmDeleteEstimate, $this->createLink('task', 'deleteEstimate', "estimateID=$estimateID&confirm=yes"))); } - elseif($confirm == 'no' and $task->consumed - $estimate->consumed == 0) - { - return print(js::confirm($this->lang->task->confirmDeleteLastEstimate, $this->createLink('task', 'deleteEstimate', "estimateID=$estimateID&confirm=yes"))); - } else { + $estimate = $this->task->getEstimateById($estimateID); + $task = $this->task->getById($estimate->task); $changes = $this->task->deleteEstimate($estimateID); if(dao::isError()) return print(js::error(dao::getError())); $actionID = $this->loadModel('action')->create('task', $estimate->task, 'DeleteEstimate'); $this->action->logHistory($actionID, $changes); - if($task->consumed - $estimate->consumed == 0) - { - $actionID = $this->loadModel('action')->create('task', $estimate->task, 'Adjusttasktowait'); - } - if($task->consumed - $estimate->consumed == 0) - { - return print(js::reload('parent.parent')); - } - else - { - return print(js::reload('parent')); - } + + return print(js::reload('parent')); } } @@ -1346,21 +1335,10 @@ class task extends control if(!empty($task->team)) { - $teams = array_keys($task->team); - - $task->nextBy = $this->task->getNextUser($teams, $this->app->user->account); - $task->myConsumed = isset($task->team[$this->app->user->account]) ? $task->team[$this->app->user->account]->consumed : 0; - - $lastAccount = end($teams); - $finishedUsers = $this->task->getFinishedUsers($taskID, $teams); - if(($lastAccount != $task->assignedTo and $task->mode == 'linear') or ($task->mode == 'multi' and count($teams) != count($finishedUsers))) - { - $members = $this->task->getMemberPairs($task); - } - else - { - $task->nextBy = $task->openedBy; - } + $task->nextBy = $this->task->getAssignedTo4Multi($task->team, $task, 'next'); + $task->myConsumed = 0; + $currentTeam = $this->task->getTeamByAccount($task->team); + if($currentTeam) $task->myConsumed = $currentTeam->consumed; } $this->view->title = $this->view->execution->name . $this->lang->colon .$this->lang->task->finish; @@ -1817,14 +1795,8 @@ class task extends control return print(js::locate($this->createLink('task', 'view', "taskID=$taskID"), 'parent')); } - if(!empty($this->view->task->team)) - { - $members = array(); - foreach($this->view->task->team as $account => $member) $members[$account] = zget($this->view->members, $account); - $this->view->members = $members; - } - if(!isset($this->view->members[$this->view->task->finishedBy])) $this->view->members[$this->view->task->finishedBy] = $this->view->task->finishedBy; + $this->view->title = $this->view->execution->name . $this->lang->colon . $this->lang->task->activate; $this->view->position[] = $this->lang->task->activate; $this->view->users = $this->loadModel('user')->getPairs('noletter'); @@ -2107,10 +2079,7 @@ class task extends control } /* Get team for multiple task. */ - $taskTeam = $this->dao->select('*')->from(TABLE_TEAM) - ->where('root')->in(array_keys($tasks)) - ->andWhere('type')->eq('task') - ->fetchGroup('root'); + $taskTeam = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task'); /* Process multiple task info. */ if(!empty($taskTeam)) @@ -2332,14 +2301,15 @@ class task extends control * Update assign of multi task. * * @param int $requestID - * @param object $task + * @param object $taskID * @param string $kanbanGroup + * @param string $from * @access public * @return void */ - public function editTeam($executionID, $task, $kanbanGroup = 'default') + public function editTeam($executionID, $taskID, $kanbanGroup = 'default', $from = '') { - $taskID = $task->id; + $task = $this->task->getById($taskID); $this->commonAction($taskID); if(!empty($_POST)) @@ -2361,9 +2331,18 @@ class task extends control if($this->viewType == 'json' or (defined('RUN_MODE') && RUN_MODE == 'api')) return $this->send(array('result' => 'success')); if(isonlybody()) { - $task = $this->task->getById($taskID); $execution = $this->execution->getByID($task->execution); - if(($this->app->tab == 'execution' or $this->config->vision == 'lite') and $this->app->tab == 'execution') + + if(($this->app->tab == 'execution' or ($this->config->vision == 'lite' and $this->app->tab == 'project' and $this->session->kanbanview == 'kanban')) and $execution->type == 'kanban') + { + $rdSearchValue = $this->session->rdSearchValue ? $this->session->rdSearchValue : ''; + $kanbanData = $this->loadModel('kanban')->getRDKanban($task->execution, $execLaneType, 'id_desc', 0, $execGroupBy, $rdSearchValue); + $kanbanData = json_encode($kanbanData); + + return print(js::closeModal('parent.parent', '', "parent.parent.updateKanban($kanbanData)")); + } + + if($from == 'taskkanban') { $rdSearchValue = $this->session->rdSearchValue ? $this->session->rdSearchValue : ''; $kanbanData = $this->loadModel('kanban')->getRDKanban($task->execution, $this->session->execLaneType ? $this->session->execLaneType : 'all', 'id_desc', 0, $kanbanGroup, $rdSearchValue); @@ -2371,10 +2350,8 @@ class task extends control return print(js::closeModal('parent.parent', '', "parent.parent.updateKanban($kanbanData)")); } - else - { - return print(js::closeModal('parent.parent', 'this')); - } + + return print(js::closeModal('parent.parent', 'this')); } return print(js::locate($this->createLink('task', 'view', "taskID=$taskID"), 'parent')); } diff --git a/module/task/css/recordestimate.css b/module/task/css/recordestimate.css index e9786a1ccf..1bfcfe1a18 100644 --- a/module/task/css/recordestimate.css +++ b/module/task/css/recordestimate.css @@ -1 +1,7 @@ +.table-recorded thead{background: rgb(245 245 245);} .form-condensed .table-form {margin-bottom: 20px;} +.hours {display: flex; float: left; transform: translate(0, 50%); margin: 0 0 0 10px;} +.estimateTotally, .consumedTotally {border-radius: 9px; padding: 3px 7px; margin: 0 5px 0 5px; min-width: 18px;} +.consumedTotally {background: #EAF3FF; color: #2B80FF;} +.estimateTotally {background: #FFEFEF; color: #FF6262;} +.nav > li.divider {margin: 0 10px 0 5px; background-color: #000; opacity: 0.06; width: 1px ; height: 15px; padding: 0 !important;} diff --git a/module/task/js/activate.js b/module/task/js/activate.js new file mode 100644 index 0000000000..c4aacfa326 --- /dev/null +++ b/module/task/js/activate.js @@ -0,0 +1,109 @@ +$('#confirmButton').click(function() +{ + var memberCount = ''; + var totalEstimate = 0; + var totalConsumed = oldConsumed; + var totalLeft = 0; + var error = false; + $('select[name^=team]').each(function() + { + if($(this).val() == '') return; + + memberCount++; + + var $tr = $(this).closest('tr'); + var account = $(this).find('option:selected').text(); + + estimate = parseFloat($tr.find('[name^=teamEstimate]').val()); + if(!isNaN(estimate)) totalEstimate += estimate; + if(isNaN(estimate) || estimate == 0) + { + bootbox.alert(account + ' ' + estimateNotEmpty); + error = true; + return false; + } + + consumed = parseFloat($tr.find('[name^=teamConsumed]').val()); + if(!isNaN(consumed)) totalConsumed += consumed; + + left = parseFloat($tr.find('[name^=teamLeft]').val()); + if(!isNaN(left)) totalLeft += left; + if(!$tr.hasClass('member-done') && (isNaN(left) || left == 0)) + { + bootbox.alert(account + ' ' + leftNotEmpty); + error = true; + return false; + } + + if(estimate == 0 || isNaN(estimate)) + { + $(this).val('').trigger("chosen:updated"); + bootbox.alert(estimateNotEmpty); + error = true; + return false; + } + }) + + if(error) return false; + + if(memberCount < 2) + { + bootbox.alert(teamMemberError); + return false; + } + + $('#left').val(totalLeft); + updateAssignedTo(); + + $('.close').click(); +}); + +/** + * Update assignedTo. + * + * @access public + * @return void + */ +function updateAssignedTo() +{ + var html = ''; + var multiple = $('#multiple').prop('checked'); + var assignedTo = $('#assignedTo').val(); + if(multiple) + { + var isTeamMember = false; + var mode = $('#mode').val(); + $('select[name^=team]').each(function() + { + if($(this).find('option:selected').text() == '') return; + if($(this).val() == currentUser) isTeamMember = true; + + var account = $(this).find('option:selected').val(); + var realName = $(this).find('option:selected').text(); + var selected = account == assignedTo ? 'selected' : ''; + + html += ""; + }); + + if(mode == 'multi' && isTeamMember && mode != 'linear') + { + $('[name=assignedTo]').removeAttr('disabled').trigger('chosen:updated'); + } + else + { + $('[name=assignedTo]').attr('disabled', 'disabled').trigger('chosen:updated'); + } + } + else + { + for(key in members) + { + var selected = key == assignedTo ? 'selected' : ''; + html += ""; + } + } + + $('#assignedTo').html(html); + if(multiple && mode == 'linear' && $('#modalTeam tr.member-doing').length == 0 && $('#modalTeam tr.member-wait').length >= 1) $('[name=assignedTo]').val($('#modalTeam tr.member-wait:first').find('select[name^=team]:first').val()); + $('#assignedTo').trigger('chosen:updated'); +} diff --git a/module/task/js/create.js b/module/task/js/create.js index 6239804906..0a65fc34c3 100644 --- a/module/task/js/create.js +++ b/module/task/js/create.js @@ -536,44 +536,6 @@ $(document).ready(function() $('#dataPlanGroup').fixInputGroup(); }); - /* Init task team manage dialog */ - var $taskTeamEditor = $('#taskTeamEditor').batchActionForm( - { - idStart: 0, - idEnd: 5, - chosen: true, - datetimepicker: false, - colorPicker: false, - }); - var taskTeamEditor = $taskTeamEditor.data('zui.batchActionForm'); - - var adjustButtons = function() - { - var $deleteBtn = $taskTeamEditor.find('.btn-delete'); - if ($deleteBtn.length == 1) $deleteBtn.addClass('disabled').attr('disabled', 'disabled'); - }; - - $taskTeamEditor.on('click', '.btn-add', function() - { - var $newRow = taskTeamEditor.createRow(null, $(this).closest('tr')); - $newRow.addClass('highlight'); - setTimeout(function() - { - $newRow.removeClass('highlight'); - }, 1600); - adjustButtons(); - }).on('click', '.btn-delete', function() - { - var $row = $(this).closest('tr'); - $row.addClass('highlight').fadeOut(700, function() - { - $row.remove(); - adjustButtons(); - }); - }); - - adjustButtons(); - $('#showAllModule').change(function() { var executionID = $('#execution').val(); @@ -604,45 +566,28 @@ $(document).on('click', '#testStory_chosen', function() $obj.trigger("chosen:updated"); }) -$('#modalTeam .btn').click(function() +$('#modalTeam tfoot .btn').click(function() { var team = ''; var time = 0; var error = false; - - /* Unique team. */ - $('select[name^=team]').each(function(i) - { - value = $(this).val(); - if(value == '') return; - $('select[name^=team]').each(function(j) - { - if(i <= j) return; - if(value == $(this).val()) $(this).closest('tr').addClass('hidden'); - }) - }) - $('select[name^=team]').closest('tr.hidden').remove(); + var mode = $('#mode').val(); $('select[name^=team]').each(function() { + if($(this).val() == '') return; + + var tr = $(this).closest('tr'); var account = $(this).find('option:selected').text(); - if(account != '') - { - team += ' ' + account; - } + team += ' ' + account; estimate = parseFloat($(this).parents('td').next('td').find('[name^=teamEstimate]').val()); - if(!isNaN(estimate)) + if(!isNaN(estimate) && estimate > 0) time += estimate; + if(account != '' && (isNaN(estimate) || estimate == 0)) { - time += estimate; - } - - var requiredFieldList = ',' + requiredFields + ','; - if(account && requiredFieldList.indexOf(',estimate,') >= 0 && (estimate == 0 || isNaN(estimate))) - { - alert(estimateNotEmpty); - error = true; - return false; + bootbox.alert(account + ' ' + estimateNotEmpty); + error = true; + return false; } }); @@ -650,7 +595,7 @@ $('#modalTeam .btn').click(function() var teamList = team.split(" "); if(teamList.length <= 2) { - alert(teamMemberError); + bootbox.alert(teamMemberError); return false; } else diff --git a/module/task/js/edit.js b/module/task/js/edit.js index d0571552fa..f59094b8fd 100644 --- a/module/task/js/edit.js +++ b/module/task/js/edit.js @@ -70,76 +70,6 @@ function setPreview(){} $(document).ready(function() { - /* show team menu. */ - $('[name=multiple]').change(function() - { - var checked = $(this).prop('checked'); - if(checked) - { - $('#teamTr').removeClass('hidden'); - $('.modeBox').removeClass('hidden'); - $('#mode').removeAttr('disabled').trigger('chosen:updated'); - $('#parent').val(''); - $('#parent').trigger('chosen:updated'); - $('#parent').closest('tr').addClass('hidden'); - $('#estimate').attr('disabled', 'disabled'); - $('#left').attr('disabled', 'disabled'); - - var mode = $('#mode').val(); - if((mode == 'linear' && currentUser != oldAssignedTo) || !team[currentUser]) $('[name=assignedTo]').attr('disabled', 'disabled').trigger('chosen:updated'); - } - else - { - $('#teamTr').addClass('hidden'); - $('.modeBox').addClass('hidden'); - $('#mode').attr('disabled', 'disabled').trigger('chosen:updated'); - $('#parent').closest('tr').removeClass('hidden'); - $('#estimate').removeAttr('disabled'); - $('#left').removeAttr('disabled'); - $('[name=assignedTo]').removeAttr('disabled').trigger('chosen:updated'); - } - - updateAssignedTo(); - }); - - /* Init task team manage dialog */ - var $taskTeamEditor = $('#taskTeamEditor').batchActionForm( - { - idStart: 0, - idEnd: newRowCount - 1, - chosen: true, - datetimepicker: false, - colorPicker: false, - }); - var taskTeamEditor = $taskTeamEditor.data('zui.batchActionForm'); - - var adjustButtons = function() - { - var $deleteBtn = $taskTeamEditor.find('.btn-delete'); - if ($deleteBtn.length == 1) $deleteBtn.addClass('disabled').attr('disabled', 'disabled'); - }; - - $taskTeamEditor.on('click', '.btn-add', function() - { - var $newRow = taskTeamEditor.createRow(null, $(this).closest('tr')); - $newRow.addClass('highlight'); - setTimeout(function() - { - $newRow.removeClass('highlight'); - }, 1600); - adjustButtons(); - }).on('click', '.btn-delete', function() - { - var $row = $(this).closest('tr'); - $row.addClass('highlight').fadeOut(700, function() - { - $row.remove(); - adjustButtons(); - }); - }); - - adjustButtons(); - $('#showAllModule').change(function() { var moduleID = $('#moduleIdBox #module').val(); @@ -153,20 +83,6 @@ $(document).ready(function() $('#confirmButton').click(function() { - /* Unique team. */ - $('select[name^=team]').each(function(i) - { - value = $(this).val(); - if(value == '') return; - $('select[name^=team]').each(function(j) - { - if(i <= j) return; - if(value == $(this).val()) $(this).closest('tr').addClass('hidden'); - }) - }); - - $('select[name^=team]').closest('tr.hidden').remove(); - var memberCount = ''; var totalEstimate = 0; var totalConsumed = oldConsumed; @@ -174,24 +90,38 @@ $('#confirmButton').click(function() var error = false; $('select[name^=team]').each(function() { - if($(this).find('option:selected').text() == '') return; + if($(this).val() == '') return; memberCount++; - estimate = parseFloat($(this).parents('td').next('td').find('[name^=teamEstimate]').val()); - if(!isNaN(estimate)) totalEstimate += estimate; + var $tr = $(this).closest('tr'); + var account = $(this).find('option:selected').text(); - consumed = parseFloat($(this).parents('td').next('td').find('[name^=teamConsumed]').val()); + estimate = parseFloat($tr.find('[name^=teamEstimate]').val()); + if(!isNaN(estimate)) totalEstimate += estimate; + if(isNaN(estimate) || estimate == 0) + { + bootbox.alert(account + ' ' + estimateNotEmpty); + error = true; + return false; + } + + consumed = parseFloat($tr.find('[name^=teamConsumed]').val()); if(!isNaN(consumed)) totalConsumed += consumed; - left = parseFloat($(this).parents('td').next('td').find('[name^=teamLeft]').val()); + left = parseFloat($tr.find('[name^=teamLeft]').val()); if(!isNaN(left)) totalLeft += left; + if(!$tr.hasClass('member-done') && (isNaN(left) || left == 0)) + { + bootbox.alert(account + ' ' + leftNotEmpty); + error = true; + return false; + } - var requiredFieldList = ',' + requiredFields + ','; - if(requiredFieldList.indexOf(',estimate,') >= 0 && (estimate == 0 || isNaN(estimate))) + if(estimate == 0 || isNaN(estimate)) { $(this).val('').trigger("chosen:updated"); - alert(estimateNotEmpty); + bootbox.alert(estimateNotEmpty); error = true; return false; } @@ -201,13 +131,13 @@ $('#confirmButton').click(function() if(memberCount < 2) { - alert(teamMemberError); + bootbox.alert(teamMemberError); return false; } if(totalLeft == 0 && (taskStatus == 'doing' || taskStatus == 'pause')) { - alert(totalLeftError); + bootbox.alert(totalLeftError); return false; } @@ -246,13 +176,13 @@ function updateAssignedTo() html += ""; }); - if(mode == 'multi' && isTeamMember) + if(mode == 'multi' && isTeamMember && mode != 'linear') { $('[name=assignedTo]').removeAttr('disabled').trigger('chosen:updated'); } else { - if(currentUser != oldAssignedTo || !isTeamMember) $('[name=assignedTo]').attr('disabled', 'disabled').trigger('chosen:updated'); + $('[name=assignedTo]').attr('disabled', 'disabled').trigger('chosen:updated'); } } else @@ -265,5 +195,6 @@ function updateAssignedTo() } $('#assignedTo').html(html); + if(multiple && mode == 'linear' && $('#modalTeam tr.member-doing').length == 0 && $('#modalTeam tr.member-wait').length >= 1) $('[name=assignedTo]').val($('#modalTeam tr.member-wait:first').find('select[name^=team]:first').val()); $('#assignedTo').trigger('chosen:updated'); } diff --git a/module/task/js/editestimate.js b/module/task/js/editestimate.js index ddaf03ed54..d9cdb02abe 100644 --- a/module/task/js/editestimate.js +++ b/module/task/js/editestimate.js @@ -6,7 +6,8 @@ */ function confirmLeft() { - if($('#left').val() === '0') return confirm(confirmRecord); + $left = $('#left'); + if(!$left.prop('readonly') && $left.val() === '0') return confirm(confirmRecord); } $(function() diff --git a/module/task/js/editteam.js b/module/task/js/editteam.js index b41bb1b4c2..bc08f7bfbd 100644 --- a/module/task/js/editteam.js +++ b/module/task/js/editteam.js @@ -1,124 +1,18 @@ $(document).ready(function() { - /* show team menu. */ - $('[name=multiple]').change(function() + $('#submit').click(function() { - var checked = $(this).prop('checked'); - if(checked) + var memberCount = 0; + $('select[name^=team]').each(function() { - $('#teamTr').removeClass('hidden'); - $('.modeBox').removeClass('hidden'); - $('#mode').removeAttr('disabled').trigger('chosen:updated'); - $('#parent').val(''); - $('#parent').trigger('chosen:updated'); - $('#parent').closest('tr').addClass('hidden'); - $('#estimate').attr('disabled', 'disabled'); - $('#left').attr('disabled', 'disabled'); - - var mode = $('#mode').val(); - if((mode == 'linear' && currentUser != oldAssignedTo) || !team[currentUser]) $('[name=assignedTo]').attr('disabled', 'disabled').trigger('chosen:updated'); - } - else - { - $('#teamTr').addClass('hidden'); - $('.modeBox').addClass('hidden'); - $('#mode').attr('disabled', 'disabled').trigger('chosen:updated'); - $('#parent').closest('tr').removeClass('hidden'); - $('#estimate').removeAttr('disabled'); - $('#left').removeAttr('disabled'); - $('[name=assignedTo]').removeAttr('disabled').trigger('chosen:updated'); - } - - updateAssignedTo(); - }); - - /* Init task team manage dialog */ - var $taskTeamEditor = $('#taskTeamEditor').batchActionForm( - { - idStart: 0, - idEnd: newRowCount - 1, - chosen: true, - datetimepicker: false, - colorPicker: false, - }); - var taskTeamEditor = $taskTeamEditor.data('zui.batchActionForm'); - - var adjustButtons = function() - { - var $deleteBtn = $taskTeamEditor.find('.btn-delete'); - if ($deleteBtn.length == 1) $deleteBtn.addClass('disabled').attr('disabled', 'disabled'); - }; - - $taskTeamEditor.on('click', '.btn-add', function() - { - var $newRow = taskTeamEditor.createRow(null, $(this).closest('tr')); - $newRow.addClass('highlight'); - setTimeout(function() - { - $newRow.removeClass('highlight'); - }, 1600); - adjustButtons(); - }).on('click', '.btn-delete', function() - { - var $row = $(this).closest('tr'); - $row.addClass('highlight').fadeOut(700, function() - { - $row.remove(); - adjustButtons(); - }); - }); - - adjustButtons(); -}); - -$('#confirmButton').click(function() -{ - /* Unique team. */ - $('select[name^=team]').each(function(i) - { - value = $(this).val(); - if(value == '') return; - $('select[name^=team]').each(function(j) - { - if(i <= j) return; - if(value == $(this).val()) $(this).closest('tr').addClass('hidden'); + if($(this).find('option:selected').text() == '') return; + memberCount++; }) + + if(memberCount < 2) + { + alert(teamMemberError); + return false; + } }); - - $('select[name^=team]').closest('tr.hidden').remove(); - - var memberCount = ''; - var totalEstimate = 0; - var totalConsumed = oldConsumed; - var totalLeft = 0; - $('select[name^=team]').each(function() - { - if($(this).find('option:selected').text() == '') return; - - memberCount++; - - estimate = parseFloat($(this).parents('td').next('td').find('[name^=teamEstimate]').val()); - if(!isNaN(estimate)) totalEstimate += estimate; - - consumed = parseFloat($(this).parents('td').next('td').find('[name^=teamConsumed]').val()); - if(!isNaN(consumed)) totalConsumed += consumed; - - left = parseFloat($(this).parents('td').next('td').find('[name^=teamLeft]').val()); - if(!isNaN(left)) totalLeft += left; - }) - $('#estimate').val(totalEstimate); - $('#consumedSpan').html(totalConsumed); - $('#left').val(totalLeft); - - if(memberCount < 2) - { - alert(teamMemberError); - return false; - } - if(totalLeft == 0 && (taskStatus == 'doing' || taskStatus == 'pause')) - { - alert(totalLeftError); - return false; - } - $('.close').click(); }); diff --git a/module/task/lang/de.php b/module/task/lang/de.php index bf5d6c16dc..1cbd6b7491 100644 --- a/module/task/lang/de.php +++ b/module/task/lang/de.php @@ -25,6 +25,7 @@ $lang->task->delayed = 'Verzögert'; $lang->task->view = "Übersicht"; $lang->task->logEfforts = "Stunden"; $lang->task->record = "Schätzung"; +$lang->task->recordedBy = "Recorded By"; $lang->task->teamConsumed = "Tean Consumed"; $lang->task->start = "Start"; $lang->task->startAction = "Start Task"; @@ -54,6 +55,7 @@ $lang->task->progressTips = 'Genutzt/(Genutzt+Rest)'; $lang->task->copy = 'Aufgabe kopieren'; $lang->task->waitTask = 'Waiting Task'; $lang->task->allModule = 'All Module'; +$lang->task->replace = 'Replace'; $lang->task->common = 'Aufgabe'; $lang->task->id = 'ID'; @@ -216,7 +218,6 @@ $lang->task->legendMisc = 'Misc.'; $lang->task->confirmDelete = "Möchten Sie diese Aufgabe löschen?"; $lang->task->confirmDeleteEstimate = "Do you want to delete it?"; -$lang->task->confirmDeleteLastEstimate = "Do you want to delete the log? After deleting the last work log, the task status will be adjusted to Not Started."; $lang->task->copyStoryTitle = "Story kopieren"; $lang->task->afterSubmit = "Nach der Erstellung,"; $lang->task->successSaved = "Erstellt!"; @@ -232,6 +233,8 @@ $lang->task->noticeLinkStory = "Es wurde keine Story verknüpft. Sie k $lang->task->noticeSaveRecord = 'Ihre Stunden wurden nicht gespeichrt. Bitte erst speichern.'; $lang->task->commentActions = '%s. %s, kommentiert von %s.'; $lang->task->deniedNotice = 'Nur %s kann die Aufgabe %s.'; +$lang->task->deniedStatusNotice = 'The task status is %s, the effort cannot be maintained.'; +$lang->task->transferNotice = 'Linear task cannot be transferred.'; $lang->task->noTask = 'Keine Aufagben. '; $lang->task->createDenied = 'Aufgben erstellen it in diesem Projekt gesperrt'; $lang->task->cannotDeleteParent = 'Cannot delete parent task'; diff --git a/module/task/lang/en.php b/module/task/lang/en.php index 2058e72cad..aedaa34ef6 100755 --- a/module/task/lang/en.php +++ b/module/task/lang/en.php @@ -25,6 +25,7 @@ $lang->task->delayed = 'Delayed'; $lang->task->view = "Task Detail"; $lang->task->logEfforts = "Effort"; $lang->task->record = "Estimates"; +$lang->task->recordedBy = "Recorded By"; $lang->task->teamConsumed = "Tean Consumed"; $lang->task->start = "Start"; $lang->task->startAction = "Start Task"; @@ -54,6 +55,7 @@ $lang->task->progressTips = 'Cost/(Cost+Left)'; $lang->task->copy = 'Copy Task'; $lang->task->waitTask = 'Waiting Task'; $lang->task->allModule = 'All Module'; +$lang->task->replace = 'Replace'; $lang->task->common = 'Task'; $lang->task->id = 'ID'; @@ -216,7 +218,6 @@ $lang->task->legendMisc = 'Misc.'; $lang->task->confirmDelete = "Do you want to delete this task?"; $lang->task->confirmDeleteEstimate = "Do you want to delete it?"; -$lang->task->confirmDeleteLastEstimate = "Do you want to delete the log? After deleting the last work log, the task status will be adjusted to Not Started."; $lang->task->copyStoryTitle = "Copy Story"; $lang->task->afterSubmit = "Next "; $lang->task->successSaved = "Created!"; @@ -232,6 +233,8 @@ $lang->task->noticeLinkStory = "No story has been linked. You can %s f $lang->task->noticeSaveRecord = 'Your Hour is not saved. Please save it first.'; $lang->task->commentActions = '%s. %s, commented by %s.'; $lang->task->deniedNotice = 'Only the %s can %s the task.'; +$lang->task->deniedStatusNotice = 'The task status is %s, the effort cannot be maintained.'; +$lang->task->transferNotice = 'Linear task cannot be transferred.'; $lang->task->noTask = 'No tasks yet. '; $lang->task->createDenied = 'Create Task is denied in this project'; $lang->task->cannotDeleteParent = 'Cannot delete parent task'; diff --git a/module/task/lang/fr.php b/module/task/lang/fr.php index d676f327a7..2eab1df9e2 100644 --- a/module/task/lang/fr.php +++ b/module/task/lang/fr.php @@ -25,6 +25,7 @@ $lang->task->delayed = 'Ajournées'; $lang->task->view = "Détail Tâche"; $lang->task->logEfforts = "Effort"; $lang->task->record = "Estimés"; +$lang->task->recordedBy = "Recorded By"; $lang->task->teamConsumed = "Tean Consumed"; $lang->task->start = "Démarrer"; $lang->task->startAction = "Démarrer Tâche"; @@ -54,6 +55,7 @@ $lang->task->progressTips = 'Coût/(Coût+Reste)'; $lang->task->copy = 'Copier Tâche'; $lang->task->waitTask = 'Tâche en attente'; $lang->task->allModule = 'Tous Modules'; +$lang->task->replace = 'Replace'; $lang->task->common = 'Tâche'; $lang->task->id = 'ID'; @@ -216,7 +218,6 @@ $lang->task->legendMisc = 'Misc.'; $lang->task->confirmDelete = "Voulez-vous réellement supprimer cette tâche ?"; $lang->task->confirmDeleteEstimate = "Voulez-vous la supprimer ?"; -$lang->task->confirmDeleteLastEstimate = "Do you want to delete the log? After deleting the last work log, the task status will be adjusted to Not Started."; $lang->task->copyStoryTitle = 'StoryModel'; $lang->task->afterSubmit = "Suiv. "; $lang->task->successSaved = "Créée !"; @@ -232,6 +233,8 @@ $lang->task->noticeLinkStory = "Aucune Story n'est associée. Vous pou $lang->task->noticeSaveRecord = "Votre temps n'a pas été sauvé. Enregistrez-le d'abord."; $lang->task->commentActions = '%s. %s, commenté par %s.'; $lang->task->deniedNotice = 'Seulement le %s peut %s la tâche.'; +$lang->task->deniedStatusNotice = 'The task status is %s, the effort cannot be maintained.'; +$lang->task->transferNotice = 'Linear task cannot be transferred.'; $lang->task->noTask = "Pas de tâche pour l'instant. "; $lang->task->createDenied = 'La création de tâches est interdite dans ce projet'; $lang->task->cannotDeleteParent = 'Impossible de supprimer la tâche parente'; diff --git a/module/task/lang/vi.php b/module/task/lang/vi.php index 42cccd53b3..10e3cdbab6 100644 --- a/module/task/lang/vi.php +++ b/module/task/lang/vi.php @@ -25,6 +25,7 @@ $lang->task->delayed = 'Tạm ngưng'; $lang->task->view = "Chi tiết nhiệm vụ"; $lang->task->logEfforts = "Chấm công"; $lang->task->record = "Dự tính"; +$lang->task->recordedBy = "Recorded By"; $lang->task->start = "Bắt đầu"; $lang->task->startAction = "Bắt đầu nhiệm vụ"; $lang->task->restart = "Tiếp tục"; @@ -52,6 +53,7 @@ $lang->task->progressTips = 'Đã làm/(Đã làm + Còn lại)'; $lang->task->copy = 'Copy nhiệm vụ'; $lang->task->waitTask = 'Đang đợi nhiệm vụ'; $lang->task->allModule = 'Tất cả Module'; +$lang->task->replace = 'Replace'; $lang->task->common = 'Nhiệm vụ'; $lang->task->id = 'ID'; diff --git a/module/task/lang/zh-cn.php b/module/task/lang/zh-cn.php index 6e0cd4e8bd..943a5b1862 100755 --- a/module/task/lang/zh-cn.php +++ b/module/task/lang/zh-cn.php @@ -25,6 +25,7 @@ $lang->task->delayed = '延期'; $lang->task->view = "查看任务"; $lang->task->logEfforts = "记录工时"; $lang->task->record = "工时"; +$lang->task->recordedBy = "记录人"; $lang->task->teamConsumed = "团队工时"; $lang->task->start = "开始"; $lang->task->startAction = "开始任务"; @@ -54,6 +55,7 @@ $lang->task->progressTips = '已消耗/(已消耗+剩余)'; $lang->task->copy = '复制任务'; $lang->task->waitTask = '未开始的任务'; $lang->task->allModule = '所有模块'; +$lang->task->replace = '替换'; $lang->task->common = '任务'; $lang->task->id = '编号'; @@ -216,7 +218,6 @@ $lang->task->legendMisc = '其他相关'; $lang->task->confirmDelete = "您确定要删除这个任务吗?"; $lang->task->confirmDeleteEstimate = "您确定要删除这个记录吗?"; -$lang->task->confirmDeleteLastEstimate = "您确定要删除该条日志吗?删除最后一条工时日志后,该任务状态将调整为未开始。"; $lang->task->copyStoryTitle = "同{$lang->SRCommon}"; $lang->task->afterSubmit = "添加之后"; $lang->task->successSaved = "成功添加,"; @@ -232,6 +233,8 @@ $lang->task->noticeLinkStory = "没有可关联的相关{$lang->SRComm $lang->task->noticeSaveRecord = '您有尚未保存的工时记录,请先将其保存。'; $lang->task->commentActions = '%s. %s, 由 %s 添加备注。'; $lang->task->deniedNotice = '当前任务只有%s才可以%s。'; +$lang->task->deniedStatusNotice = '当前任务状态是%s,不能维护日志。'; +$lang->task->transferNotice = '串行多人任务不能转交。'; $lang->task->noTask = '暂时没有任务。'; $lang->task->createDenied = '你不能在该项目添加任务'; $lang->task->cannotDeleteParent = '不能删除父任务。'; @@ -354,4 +357,4 @@ $lang->taskestimate = new stdclass(); $lang->taskestimate->consumed = '工时'; $lang->task->overEsStartDate = '已超出%s计划开始时间,请先修改%s计划开始时间'; -$lang->task->overEsEndDate = '已超出%s计划结束时间,请先修改%s计划结束时间'; \ No newline at end of file +$lang->task->overEsEndDate = '已超出%s计划结束时间,请先修改%s计划结束时间'; diff --git a/module/task/model.php b/module/task/model.php old mode 100644 new mode 100755 index 54cb9ada59..0605a0af01 --- a/module/task/model.php +++ b/module/task/model.php @@ -89,7 +89,7 @@ class taskModel extends model ->cleanINT('execution,story,module') ->stripTags($this->config->task->editor->create['id'], $this->config->allowedTags) ->join('mailto', ',') - ->remove('after,files,labels,assignedTo,uid,storyEstimate,storyDesc,storyPri,team,teamEstimate,teamMember,multiple,teams,contactListMenu,selectTestStory,testStory,testPri,testEstStarted,testDeadline,testAssignedTo,testEstimate,sync,otherLane,region,lane,estStartedDitto,deadlineDitto') + ->remove('after,files,labels,assignedTo,uid,storyEstimate,storyDesc,storyPri,team,teamSource,teamEstimate,teamMember,multiple,teams,contactListMenu,selectTestStory,testStory,testPri,testEstStarted,testDeadline,testAssignedTo,testEstimate,sync,otherLane,region,lane,estStartedDitto,deadlineDitto') ->add('version', 1) ->get(); @@ -232,37 +232,16 @@ class taskModel extends model foreach($taskFiles as $fileID => $taskFile) unset($taskFiles[$fileID]->id); } - $teams = array(); if($this->post->multiple and count(array_filter($this->post->team)) > 1) { - foreach($this->post->team as $row => $account) + $teams = $this->manageTaskTeam($task->mode, $taskID, 'wait'); + if($teams) { - if(empty($account) or isset($team[$account])) continue; - $member = new stdClass(); - $member->root = 0; - $member->account = $account; - $member->role = $assignedTo; - $member->join = helper::today(); - $member->estimate = $this->post->teamEstimate[$row] ? (float)$this->post->teamEstimate[$row] : 0; - $member->left = $member->estimate; - $member->order = $row; - $teams[$account] = $member; + $task->id = $taskID; + $this->computeHours4Multiple($task); } } - if(!empty($teams)) - { - foreach($teams as $team) - { - $team->root = $taskID; - $team->type = 'task'; - $this->dao->insert(TABLE_TEAM)->data($team)->autoCheck()->exec(); - } - - $task->id = $taskID; - $this->computeHours4Multiple($task); - } - if(!dao::isError()) $this->loadModel('score')->create('task', 'create', $taskID); $taskIdList[$assignedTo] = array('status' => 'created', 'id' => $taskID); } @@ -800,6 +779,7 @@ class taskModel extends model * * @param object $oldTask * @param object $task + * @param array $team * @param bool $autoStatus * @access public * @return object|bool @@ -808,13 +788,15 @@ class taskModel extends model { if(!$oldTask) return false; - if(empty($team)) $team = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->eq($oldTask->id)->andWhere('type')->eq('task')->orderBy('order')->fetchAll('account'); + if(empty($team)) $team = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->eq($oldTask->id)->orderBy('order')->fetchAll(); if(!empty($team)) { $now = helper::now(); - $teams = array_keys($team); + $oldTeam = zget($oldTask, 'team', array()); + $members = array_map(function($member){ return $member->account; }, $team); $currentTask = !empty($task) ? $task : new stdclass(); if(!isset($currentTask->status)) $currentTask->status = $oldTask->status; + $oldTask->team = $team; $currentTask->assignedTo = $oldTask->assignedTo; if(!empty($_POST['assignedTo']) and is_string($_POST['assignedTo'])) @@ -823,40 +805,23 @@ class taskModel extends model } else { - if(!$oldTask->assignedTo) - { - $firstMember = reset($team); - $currentTask->assignedTo = $firstMember->account; - $currentTask->assignedDate = $now; - } - else - { - if($team[$oldTask->assignedTo]->left == 0 and $team[$oldTask->assignedTo]->consumed != 0 and $this->app->rawMethod != 'deleteestimate') - { - if($oldTask->assignedTo != $teams[count($teams) - 1]) - { - $currentTask->assignedTo = $this->getNextUser(array_keys($team), $oldTask->assignedTo); - } - else - { - $currentTask->assignedTo = $oldTask->openedBy; - } - $currentTask->assignedDate = $now; - } - } + $currentTask->assignedTo = $this->getAssignedTo4Multi($members, $oldTask); + if($oldTask->assignedTo != $currentTask->assignedTo) $currentTask->assignedDate = $now; } $currentTask->estimate = 0; - $currentTask->consumed = 0; $currentTask->left = 0; foreach($team as $member) { $currentTask->estimate += (float)$member->estimate; - $currentTask->consumed += (float)$member->consumed; $currentTask->left += (float)$member->left; } - if($this->app->rawMethod == 'edit' and empty($oldTask->team) and isset($oldTask->consumed)) $currentTask->consumed += (float)$oldTask->consumed; + $efforts = $this->getTaskEstimate($oldTask->id); + $currentTask->consumed = 0; + foreach($efforts as $effort) $currentTask->consumed += (float)$effort->consumed; + + $oldTask->team = $oldTeam; if(!empty($task)) { @@ -878,38 +843,196 @@ class taskModel extends model if($currentTask->consumed > 0 and $currentTask->left == 0) { - $finisedUsers = $this->getFinishedUsers($oldTask->id, $teams); - if(($oldTask->mode == 'linear' and isset($team[$currentTask->assignedTo]) and $oldTask->assignedTo != $teams[count($teams) - 1]) or ($oldTask->mode == 'multi' and count($finisedUsers) != count($teams))) + $finisedUsers = $this->getFinishedUsers($oldTask->id, $members); + if(count($finisedUsers) != count($team)) { - $currentTask->status = 'doing'; - $currentTask->finishedBy = ''; - $currentTask->finishedDate = ''; + if(strpos('cancel,pause', $oldTask->status) === false or ($oldTask->status == 'closed' and $oldTask->reason == 'done')) + { + $currentTask->status = 'doing'; + $currentTask->finishedBy = ''; + $currentTask->finishedDate = ''; + } } - elseif(($oldTask->mode == 'linear' and $oldTask->assignedTo == $teams[count($teams) - 1]) or $oldTask->mode == 'multi') + elseif(strpos('wait,doing,pause', $oldTask->status) !== false) { $currentTask->status = 'done'; + $currentTask->assignedTo = $oldTask->openedBy; + $currentTask->assignedDate = $now; $currentTask->finishedBy = $this->app->user->account; $currentTask->finishedDate = $task->finishedDate; } } - if(($oldTask->assignedTo != $currentTask->assignedTo or $currentTask->status == 'done') - and isset($team[$this->app->user->account]) and $team[$this->app->user->account]->left == 0 - and strpos($oldTask->finishedList, ",{$this->app->user->account},") === false) - { - $currentTask->finishedList = ',' . trim(trim($oldTask->finishedList, ',') . ",{$this->app->user->account}", ',') . ','; - } - if(($oldTask->status == 'done' or $oldTask->status == 'closed') and $currentTask->status == 'doing' and $this->post->assignedTo) - { - $currentTask->finishedList = ',' . trim(substr($oldTask->finishedList, 0, strpos($oldTask->finishedList, ",{$this->post->assignedTo},")), ',') . ','; - } - return $currentTask; } $this->dao->update(TABLE_TASK)->data($currentTask)->autoCheck()->where('id')->eq($oldTask->id)->exec(); } } + /** + * Manage multi task team members. + * + * @param string $mode + * @param int $taskID + * @param string $taskStatus + * @access public + * @return array + */ + public function manageTaskTeam($mode, $taskID, $taskStatus) + { + $oldTeams = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->eq($taskID)->fetchAll(); + $oldMembers = array_map(function($team){return $team->account;}, $oldTeams); + + $this->dao->delete()->from(TABLE_TASKTEAM)->where('task')->eq($taskID)->exec(); + + if($taskStatus == 'doing') + { + $efforts = $this->getTaskEstimate($taskID); + $doingUsers = array(); + foreach($efforts as $i => $effort) + { + if($effort->left != 0) $doingUsers[$effort->account] = $effort->account; + if($effort->left == 0) unset($doingUsers[$effort->account]); + } + } + + $teams = array(); + $minStatus = 'done'; + $changeUsers = array(); + foreach($this->post->team as $row => $account) + { + if(empty($account)) continue; + + $teamSource = $this->post->teamSource[$row]; + $member = new stdClass(); + $member->task = $taskID; + $member->order = $row; + $member->account = $account; + $member->estimate = zget($this->post->teamEstimate, $row, 0); + $member->consumed = zget($this->post->teamConsumed, $row, 0); + $member->left = zget($this->post->teamLeft, $row, 0); + $member->status = 'wait'; + if($taskStatus == 'wait' and $member->estimate > 0 and $member->left == 0) $member->left = $member->estimate; + if($taskStatus == 'done') $member->left = 0; + + if($member->left == 0 and $member->consumed > 0) + { + $member->status = 'done'; + } + elseif($taskStatus == 'doing') + { + if(!empty($teamSource) and $teamSource != $account and isset($doingUsers[$teamSource])) $member->transfer = $teamSource; + if(isset($doingUsers[$account]) and ($mode == 'multi' or ($mode == 'linear' and $minStatus != 'wait'))) $member->status = 'doing'; + } + if($minStatus != 'wait' and $member->status == 'doing') $minStatus = 'doing'; + if($member->status == 'wait') $minStatus = 'wait'; + + /* Doing status is only one in linear task. */ + if($mode == 'linear' and $member->status == 'doing') $minStatus = 'wait'; + if($member->status == 'wait') $minStatus = 'wait'; + if($minStatus != 'wait' and $member->status == 'doing') $minStatus = 'doing'; + + /* Insert or update team. */ + if($mode == 'multi' and isset($teams[$account])) + { + $this->dao->update(TABLE_TASKTEAM)->set("estimate= estimate + {$member->estimate}") + ->set("`left` = `left` + {$member->left}") + ->set("`consumed` = `consumed` + {$member->consumed}") + ->where('task')->eq($member->task) + ->andWhere('account')->eq($member->account) + ->exec(); + } + else + { + $this->dao->insert(TABLE_TASKTEAM)->data($member)->autoCheck()->exec(); + } + + /* Set effort left = 0 when linear task members be changed. */ + if($mode == 'linear' and isset($oldTeams[$row]) and $oldTeams[$row]->account != $account) $changeUsers[] = $oldTeams[$row]->account; + + $teams[$account] = $account; + } + + /* Set effort left = 0 when multi task members be removed. */ + if($mode == 'multi' and $oldMembers) + { + $removedMembers = array_diff($oldMembers, $teams); + $changeUsers = array_merge($changeUsers, $removedMembers); + } + if($changeUsers) $this->resetEffortLeft($taskID, $changeUsers); + + return $teams; + } + + /** + * Get team by account from task teams. + * + * @param array $teams + * @param string $account + * @param array $extra + * @access public + * @return object + */ + public function getTeamByAccount($teams, $account = '', $extra = array('filter' => 'done')) + { + if(empty($account)) $account = $this->app->user->account; + + $filter = zget($extra, 'filter', ''); + $effortID = zget($extra, 'effortID', ''); + + $duplicates = array(); + $members = array(); + $taskID = 0; + foreach($teams as $team) + { + if(empty($taskID)) $taskID = $team->task; + if(isset($members[$team->account])) $duplicates[$team->account] = $team->account; + if(!isset($members[$team->account])) $members[$team->account] = 0; + $members[$team->account] += 1; + } + + /* + * 1. No duplicate account; + * 2. Account is not duplicate account; + * 3. Not by effort; + * Then direct get team by account. + */ + if(empty($duplicates) or (!isset($duplicates[$account]))) + { + foreach($teams as $team) + { + if($team->account == $account) return $team; + } + } + elseif(empty($effortID)) + { + foreach($teams as $team) + { + if($filter and $team->status == $filter) continue; + if($team->account == $account) return $team; + } + } + elseif($effortID) + { + $efforts = $this->getTaskEstimate($taskID, '', $effortID); + + $prevTeam = null; + $thisTeam = null; + foreach($efforts as $effort) + { + $thisTeam = reset($teams); + if($effort->id == $effortID) + { + if($effort->account == $thisTeam->account) return $thisTeam; + if($effort->account == $prevTeam->account) return $prevTeam; + return false; + } + + if($effort->left == 0 and $thisTeam->account == $effort->account) $prevTeam = array_shift($teams); + } + } + } + /** * Update a task. * @@ -935,10 +1058,7 @@ class taskModel extends model } /* If a multiple task is assigned to a team member who is not the task, assign to the team member instead. */ - if(!$this->post->assignedTo and !empty($_POST['team']) and !in_array($oldTask->assignedTo, $this->post->team)) - { - $_POST['assignedTo'] = reset($_POST['team']); - } + if(!$this->post->assignedTo and !empty($_POST['team'])) $_POST['assignedTo'] = $this->getAssignedTo4Multi($_POST['team'], $oldTask); /* When the selected parent task is a common task and has consumption, select other parent tasks. */ if($this->post->parent > 0) @@ -993,7 +1113,7 @@ class taskModel extends model ->stripTags($this->config->task->editor->edit['id'], $this->config->allowedTags) ->cleanINT('execution,story,module') ->join('mailto', ',') - ->remove('comment,files,labels,uid,multiple,team,teamEstimate,teamConsumed,teamLeft,contactListMenu') + ->remove('comment,files,labels,uid,multiple,team,teamEstimate,teamConsumed,teamLeft,teamSource,contactListMenu') ->get(); if($task->consumed < $oldTask->consumed) return print(js::error($this->lang->task->error->consumedSmall)); @@ -1008,55 +1128,12 @@ class taskModel extends model $task = $this->loadModel('file')->processImgURL($task, $this->config->task->editor->edit['id'], $this->post->uid); - $teams = array(); - if($this->post->multiple and count(array_unique(array_filter($this->post->team))) > 1) + if(count(array_filter($this->post->team)) > 1) { - if(strpos(',done,closed,cancel,', ",{$task->status},") === false && $this->post->assignedTo && !in_array($this->post->assignedTo, $this->post->team)) - { - dao::$errors[] = $this->lang->task->error->assignedTo; - return false; - } - - foreach($this->post->team as $row => $account) - { - if(empty($account) or isset($team[$account])) continue; - - $member = new stdClass(); - $member->account = $account; - $member->role = $task->assignedTo; - $member->join = helper::today(); - $member->root = $taskID; - $member->type = 'task'; - $member->estimate = $this->post->teamEstimate[$row] ? $this->post->teamEstimate[$row] : 0; - $member->consumed = $this->post->teamConsumed[$row] ? $this->post->teamConsumed[$row] : 0; - $member->left = $this->post->teamLeft[$row] === '' ? 0 : $this->post->teamLeft[$row]; - $member->order = $row; - $teams[$account] = $member; - if($task->status == 'done') $member->left = 0; - } - } - - /* Save team. */ - $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq($taskID)->andWhere('type')->eq('task')->exec(); - if(!empty($teams)) - { - foreach($teams as $member) $this->dao->insert(TABLE_TEAM)->data($member)->autoCheck()->exec(); - - /* Assign the left hours to zero who will be skipped. */ - $skipMembers = $this->loadModel('execution')->getTeamSkip($oldTask->team, $oldTask->assignedTo, isset($task->assignedTo) ? $task->assignedTo : $oldTask->assignedTo); - foreach($skipMembers as $account => $team) $this->dao->update(TABLE_TEAM)->set('left')->eq(0)->where('root')->eq($taskID)->andWhere('type')->eq('task')->andWhere('account')->eq($account)->exec(); - - $task = $this->computeHours4Multiple($oldTask, $task, array(), $autoStatus = false); - if($task->status == 'wait') - { - reset($teams); - $task->assignedTo = key($teams); - } - } - else - { - $task->mode = ''; + $teams = $this->manageTaskTeam($oldTask->mode, $taskID, $task->status); + if(!empty($teams)) $task = $this->computeHours4Multiple($oldTask, $task, array(), $autoStatus = false); } + if(empty($teams)) $task->mode = ''; $execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($task->execution)->fetch(); $requiredFields = "," . $this->config->task->edit->requiredFields . ","; @@ -1521,20 +1598,6 @@ class taskModel extends model return false; } - if(!empty($oldTask->team)) - { - $skipMembers = $this->loadModel('execution')->getTeamSkip($oldTask->team, $oldTask->assignedTo, $task->assignedTo); - foreach($skipMembers as $account => $team) $this->dao->update(TABLE_TEAM)->set('left')->eq(0)->where('root')->eq($taskID)->andWhere('type')->eq('task')->andWhere('account')->eq($account)->exec(); - - $this->dao->update(TABLE_TEAM)->set('left')->eq($task->left) - ->where('root')->eq($taskID) - ->andWhere('type')->eq('task') - ->andWhere('account')->eq($task->assignedTo) - ->exec(); - - $task = $this->computeHours4Multiple($oldTask, $task); - } - if($oldTask->parent > 0) $this->updateParentStatus($taskID); $task = $this->loadModel('file')->processImgURL($task, $this->config->task->editor->assignto['id'], $this->post->uid); @@ -1559,10 +1622,15 @@ class taskModel extends model public function updateTeam($taskID) { $oldTask = $this->getById($taskID); - if($this->post->estimate < 0 or $this->post->left < 0 or $this->post->consumed < 0) + foreach($this->post->team as $i => $account) { - dao::$errors[] = $this->lang->task->error->recordMinus; - return false; + if(!$account) continue; + + if($this->post->teamConsumed[$i] == 0 and $this->post->teamLeft[$i] == 0) + { + dao::$errors[] = $this->lang->task->noticeTaskStart; + return false; + } } $now = helper::now(); @@ -1576,53 +1644,15 @@ class taskModel extends model ->setDefault('lastEditedDate', $now) ->setDefault('assignedDate', $now) ->stripTags($this->config->task->editor->assignto['id'], $this->config->allowedTags) - ->remove('comment,showModule,team,teamEstimate,teamConsumed,teamLeft') + ->remove('comment,showModule,team,teamEstimate,teamConsumed,teamLeft,teamSource') ->get(); - if($task->consumed < $oldTask->consumed) return print(js::error($this->lang->task->error->consumedSmall)); - - $teams = array(); - if(count(array_unique(array_filter($this->post->team))) > 1) + if(count(array_filter($this->post->team)) > 1) { - foreach($this->post->team as $row => $account) - { - if(empty($account) or isset($team[$account])) continue; - - $member = new stdClass(); - $member->account = $account; - $member->join = helper::today(); - $member->root = $taskID; - $member->type = 'task'; - $member->estimate = $this->post->teamEstimate[$row] ? $this->post->teamEstimate[$row] : 0; - $member->consumed = $this->post->teamConsumed[$row] ? $this->post->teamConsumed[$row] : 0; - $member->left = $this->post->teamLeft[$row] === '' ? 0 : $this->post->teamLeft[$row]; - $member->order = $row; - $teams[$account] = $member; - if($oldTask->status == 'done') $member->left = 0; - } - } - - /* Save team. */ - $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq($taskID)->andWhere('type')->eq('task')->exec(); - if(!empty($teams)) - { - foreach($teams as $member) $this->dao->insert(TABLE_TEAM)->data($member)->autoCheck()->exec(); - - /* Assign the left hours to zero who will be skipped. */ - $skipMembers = $this->loadModel('execution')->getTeamSkip($oldTask->team, $oldTask->assignedTo, isset($task->assignedTo) ? $task->assignedTo : $oldTask->assignedTo); - foreach($skipMembers as $account => $team) $this->dao->update(TABLE_TEAM)->set('left')->eq(0)->where('root')->eq($taskID)->andWhere('type')->eq('task')->andWhere('account')->eq($account)->exec(); - - $task = $this->computeHours4Multiple($oldTask, $task, array(), $autoStatus = false); - if($task->status == 'wait') - { - reset($teams); - $task->assignedTo = key($teams); - } - } - else - { - $task->mode = ''; + $teams = $this->manageTaskTeam($oldTask->mode, $taskID, $task->status); + if(!empty($teams)) $task = $this->computeHours4Multiple($oldTask, $task); } + if(empty($teams)) $task->mode = ''; if($oldTask->parent > 0) $this->updateParentStatus($taskID); @@ -1654,14 +1684,16 @@ class taskModel extends model parse_str($extra, $output); $oldTask = $this->getById($taskID); - if($oldTask->status == 'doing') dao::$errors[] = $this->lang->task->error->alreadyStarted; if(!empty($oldTask->team)) { - if($this->post->consumed < $oldTask->team[$this->app->user->account]->consumed) dao::$errors['consumed'] = $this->lang->task->error->consumedSmall; + $currentTeam = $this->getTeamByAccount($oldTask->team); + if($currentTeam and $this->post->consumed < $currentTeam->consumed) dao::$errors['consumed'] = $this->lang->task->error->consumedSmall; + if($currentTeam and $currentTeam->status == 'doing' and $oldTask->status == 'doing') dao::$errors[] = $this->lang->task->error->alreadyStarted; } else { if($this->post->consumed < $oldTask->consumed) dao::$errors['consumed'] = $this->lang->task->error->consumedSmall; + if($oldTask->status == 'doing') dao::$errors[] = $this->lang->task->error->alreadyStarted; } if(dao::isError()) return false; @@ -1681,15 +1713,14 @@ class taskModel extends model $task = $this->loadModel('file')->processImgURL($task, $editorIdList, $this->post->uid); if($this->post->left == 0) { - if(isset($task->consumed) and $task->consumed == 0) + if(isset($task->consumed) and $task->consumed == 0) return dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->task->consumed); + if(empty($oldTask->team)) { - dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->task->consumed); - return false; + $task->status = 'done'; + $task->finishedBy = $this->app->user->account; + $task->finishedDate = helper::now(); + $task->assignedTo = $oldTask->openedBy; } - $task->status = 'done'; - $task->finishedBy = $this->app->user->account; - $task->finishedDate = helper::now(); - $task->assignedTo = $oldTask->openedBy; } /* Record consumed and left. */ @@ -1700,30 +1731,31 @@ class taskModel extends model $estimate->left = zget($_POST, 'left', 0); $estimate->work = zget($task, 'work', ''); $estimate->account = $this->app->user->account; - $estimate->consumed = !empty($oldTask->team) ? $estimate->consumed - $oldTask->team[$this->app->user->account]->consumed : $estimate->consumed - $oldTask->consumed; + $estimate->consumed = (!empty($oldTask->team) and $currentTeam) ? $estimate->consumed - $currentTeam->consumed : $estimate->consumed - $oldTask->consumed; if($this->post->comment) $estimate->work = $this->post->comment; $this->addTaskEstimate($estimate); - if(!empty($oldTask->team)) + if(!empty($oldTask->team) and $currentTeam) { - $teams = array_keys($oldTask->team); - $assignedTo = empty($oldTask->assignedTo) ? $teams[0] : $oldTask->assignedTo; - $data = new stdclass(); $data->consumed = $this->post->consumed; $data->left = $this->post->left; + $data->status = 'doing'; - $this->dao->update(TABLE_TEAM)->data($data) - ->where('root')->eq($taskID) - ->andWhere('type')->eq('task') - ->andWhere('account')->eq($assignedTo) - ->exec(); + $this->dao->update(TABLE_TASKTEAM)->data($data)->where('id')->eq($currentTeam->id)->exec(); $task = $this->computeHours4Multiple($oldTask, $task); + + $finishedUsers = $this->getFinishedUsers($oldTask->id, array_keys($oldTask->members)); + if(count($finishedUsers) == count($oldTask->team)) + { + $task->status = 'done'; + $task->finishedBy = $this->app->user->account; + $task->finishedDate = $task->finishedDate; + } } - $this->dao->update(TABLE_TASK)->data($task) - ->autoCheck() + $this->dao->update(TABLE_TASK)->data($task)->autoCheck() ->check('consumed,left', 'float') ->checkFlow() ->where('id')->eq((int)$taskID)->exec(); @@ -1763,9 +1795,10 @@ class taskModel extends model if(dao::isError()) return false; $estimates = array(); - $task = $this->getById($taskID); + $task = $this->dao->select('*')->from(TABLE_TASK)->where('id')->eq($taskID)->fetch();; + $task->team = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->eq($taskID)->orderBy('order')->fetchAll('id'); $earliestTime = ''; - foreach(array_keys($record->id) as $id) + foreach(array_keys($record->dates) as $id) { if($earliestTime == '') { @@ -1795,86 +1828,117 @@ class taskModel extends model $this->loadModel('action'); - $consumed = 0; - $left = $task->left; - $now = helper::now(); - $lastDate = $this->dao->select('*')->from(TABLE_TASKESTIMATE)->where('task')->eq($taskID)->orderBy('date_desc')->limit(1)->fetch('date'); + $allChanges = array();; + $left = $task->left; + $now = helper::now(); + $oldStatus = $task->status; + $lastDate = $this->dao->select('*')->from(TABLE_TASKESTIMATE)->where('task')->eq($taskID)->orderBy('date_desc,id_desc')->limit(1)->fetch('date'); foreach($estimates as $estimate) { $this->addTaskEstimate($estimate); - $consumed += $estimate->consumed; $work = $estimate->work; $estimateID = $this->dao->lastInsertID(); - $actionID = $this->action->create('task', $taskID, 'RecordEstimate', $work, (float)$estimate->consumed); + + $newTask = clone $task; + $newTask->consumed += $estimate->consumed; + $newTask->lastEditedBy = $this->app->user->account; + $newTask->lastEditedDate = $now; + if(helper::isZeroDate($task->realStarted)) $newTask->realStarted = $now; if(empty($lastDate) or $lastDate <= $estimate->date) { - $left = $estimate->left; - $lastDate = $estimate->date; + $newTask->left = $estimate->left; + $lastDate = $estimate->date; } + + if($newTask->left == 0 and strpos('done,cancel,closed', $task->status) === false) + { + $newTask->status = 'done'; + $newTask->assignedTo = $task->openedBy; + $newTask->assignedDate = $now; + $newTask->finishedBy = $this->app->user->account; + $newTask->finishedDate = $now; + $actionID = $this->action->create('task', $taskID, 'Finished', $work); + } + elseif($newTask->status == 'wait') + { + $newTask->status = 'doing'; + $newTask->assignedTo = $this->app->user->account; + $newTask->assignedDate = $now; + $actionID = $this->action->create('task', $taskID, 'Started', $work); + } + elseif($newTask->left != 0 and strpos('done,pause,cancel,closed,pause', $task->status) !== false) + { + $newTask->status = 'doing'; + $newTask->assignedTo = $this->app->user->account; + $newTask->finishedBy = ''; + $newTask->canceledBy = ''; + $newTask->closedBy = ''; + $newTask->closedReason = ''; + $newTask->finishedDate = '0000-00-00 00:00:00'; + $newTask->canceledDate = '0000-00-00 00:00:00'; + $newTask->closedDate = '0000-00-00 00:00:00'; + $actionID = $this->action->create('task', $taskID, 'Activated', $work); + } + else + { + $actionID = $this->action->create('task', $taskID, 'RecordEstimate', $work, (float)$estimate->consumed); + } + + /* Process multi-person task. Update consumed on team table. */ + if(!empty($task->team)) + { + $currentTeam = $this->getTeamByAccount($task->team, $this->app->user->account); + if($currentTeam) + { + $teamStatus = $estimate->left == 0 ? 'done' : 'doing'; + $this->dao->update(TABLE_TASKTEAM)->set('left')->eq($estimate->left)->set("consumed = consumed + {$estimate->consumed}")->set('status')->eq($teamStatus)->where('id')->eq($currentTeam->id)->exec(); + $currentTeam->consumed += $estimate->consumed; + $currentTeam->left = $estimate->left; + $currentTeam->status = $teamStatus; + } + + $newTask = $this->computeHours4Multiple($task, $newTask, $task->team); + } + + $changes = common::createChanges($task, $newTask, 'task'); + if($changes and !empty($actionID)) $this->action->logHistory($actionID, $changes); + if($changes) $allChanges = array_merge($allChanges, $changes); + $task = $newTask; } - $data = new stdClass(); - $data->consumed = $task->consumed + $consumed; - $data->left = $left; - $data->status = $task->status; - $data->lastEditedBy = $this->app->user->account; - $data->lastEditedDate = $now; - if(helper::isZeroDate($task->realStarted)) $data->realStarted = $now; - - if($left == 0 and strpos('done,cancel,closed', $task->status) === false) + if($allChanges) { - $data->status = 'done'; - $data->assignedTo = $task->openedBy; - $data->assignedDate = $now; - $data->finishedBy = $this->app->user->account; - $data->finishedDate = $now; + $this->dao->update(TABLE_TASK)->data($task, 'team')->where('id')->eq($taskID)->exec(); - $this->action->create('task', $taskID, 'Finished'); - } - elseif($task->status == 'wait') - { - $data->status = 'doing'; - $data->assignedTo = $this->app->user->account; - $data->assignedDate = $now; - $data->realStarted = $earliestTime; - } - elseif($left != 0 and strpos('done,cancel,closed,pause', $task->status) !== false) - { - $data->status = 'doing'; - $data->assignedTo = $this->app->user->account; - $data->assignedDate = $now; + if($task->parent > 0) $this->updateParentStatus($task->id); + if($task->story) $this->loadModel('story')->setStage($task->story); + if($task->status != $oldStatus) $this->loadModel('kanban')->updateLane($task->execution, 'task', $taskID); + if($task->status == 'done' and !dao::isError()) $this->loadModel('score')->create('task', 'finish', $taskID); } - if(!empty($task->team)) - { - $myConsumed = $task->team[$this->app->user->account]->consumed; + return $allChanges; + } - $newTeamInfo = new stdClass(); - $newTeamInfo->consumed = $myConsumed + $consumed; - $newTeamInfo->left = $left; - $this->dao->update(TABLE_TEAM)->data($newTeamInfo) - ->where('root')->eq($taskID) - ->andWhere('type')->eq('task') - ->andWhere('account')->eq($this->app->user->account) - ->exec(); - - $data = $this->computeHours4Multiple($task, $data); - } - - $this->dao->update(TABLE_TASK)->data($data)->where('id')->eq($taskID)->exec(); - - $changes = common::createChanges($task, $data); - if(!empty($actionID)) $this->action->logHistory($actionID, $changes); - - if($task->parent > 0) $this->updateParentStatus($task->id); - if($task->story) $this->loadModel('story')->setStage($task->story); - if(isset($data->status) and $task->status != $data->status) $this->loadModel('kanban')->updateLane($task->execution, 'task', $taskID); - if($task->status == 'done' and !dao::isError()) $this->loadModel('score')->create('task', 'finish', $taskID); - - return $changes; + /** + * Set effort left to 0. + * + * @param int $taskID + * @param array $members + * @access public + * @return void + */ + public function resetEffortLeft($taskID, $members) + { + $table = $this->config->edition == 'open' ? TABLE_TASKESTIMATE : TABLE_EFFORT; + $this->dao->update($table)->set('`left`')->eq(0)->where('account')->in($members) + ->beginIF($this->config->edition == 'open')->andWhere('task')->eq($taskID)->fi() + ->beginIF($this->config->edition != 'open')->andWhere('objectID')->eq($taskID)->andWhere('objectType')->eq('task')->fi() + ->orderBy('date_desc,id_desc') + ->limit('1') + ->exec(); } /** @@ -1916,54 +1980,23 @@ class taskModel extends model ->get(); $currentConsumed = trim($this->post->currentConsumed); - if(!is_numeric($currentConsumed)) - { - dao::$errors[] = $this->lang->task->error->consumedNumber; - return false; - } - - if(empty($currentConsumed) and $oldTask->consumed == '0') - { - dao::$errors[] = $this->lang->task->error->consumedEmpty; - return false; - } - - if(!$this->post->realStarted) - { - dao::$errors[] = $this->lang->task->error->realStartedEmpty; - return false; - } - - if(!$this->post->finishedDate) - { - dao::$errors[] = $this->lang->task->error->finishedDateEmpty; - return false; - } - - if($this->post->realStarted > $this->post->finishedDate) - { - dao::$errors[] = $this->lang->task->error->finishedDateSmall; - return false; - } + if(!is_numeric($currentConsumed)) return dao::$errors[] = $this->lang->task->error->consumedNumber; + if(empty($currentConsumed) and $oldTask->consumed == '0') return dao::$errors[] = $this->lang->task->error->consumedEmpty; + if(!$this->post->realStarted) return dao::$errors[] = $this->lang->task->error->realStartedEmpty; + if(!$this->post->finishedDate) return dao::$errors[] = $this->lang->task->error->finishedDateEmpty; + if($this->post->realStarted > $this->post->finishedDate) return dao::$errors[] = $this->lang->task->error->finishedDateSmall; /* Record consumed and left. */ if(empty($oldTask->team)) { $consumed = $task->consumed - $oldTask->consumed; - if($consumed < 0) - { - dao::$errors[] = $this->lang->task->error->consumedSmall; - return false; - } + if($consumed < 0) return dao::$errors[] = $this->lang->task->error->consumedSmall; } else { - $consumed = $task->consumed - $oldTask->team[$this->app->user->account]->consumed; - if($consumed < 0) - { - dao::$errors[] = $this->lang->task->error->consumedSmall; - return false; - } + $currentTeam = $this->getTeamByAccount($oldTask->team); + $consumed = $currentTeam ? $task->consumed - $currentTeam->consumed : $task->consumed; + if($consumed < 0) return dao::$errors[] = $this->lang->task->error->consumedSmall; } $estimate = new stdclass(); @@ -1974,41 +2007,18 @@ class taskModel extends model $estimate->account = $this->app->user->account; $estimate->consumed = $consumed; if($this->post->comment) $estimate->work = $this->post->comment; - if(!empty($oldTask->team)) - { - foreach($oldTask->team as $teamAccount => $team) - { - if($teamAccount == $this->app->user->account) continue; - $estimate->left += $team->left; - } - } if($estimate->consumed) $this->addTaskEstimate($estimate); - if(!empty($oldTask->team)) + if(!empty($oldTask->team) and $currentTeam) { - $this->dao->update(TABLE_TEAM)->set('left')->eq(0)->set('consumed')->eq($task->consumed) - ->where('root')->eq((int)$taskID) - ->andWhere('type')->eq('task') - ->andWhere('account')->eq($this->app->user->account)->exec(); - - $skipMembers = $oldTask->mode == 'linear' ? $this->loadModel('execution')->getTeamSkip($oldTask->team, $oldTask->assignedTo, $task->assignedTo) : $this->getFinishedUsers($oldTask->id, array_keys($oldTask->team)); - foreach($skipMembers as $account => $team) $this->dao->update(TABLE_TEAM)->set('left')->eq(0)->where('root')->eq($taskID)->andWhere('type')->eq('task')->andWhere('account')->eq($account)->exec(); - + $this->dao->update(TABLE_TASKTEAM)->set('left')->eq(0)->set('consumed')->eq($task->consumed)->set('status')->eq('done')->where('id')->eq($currentTeam->id)->exec(); $task = $this->computeHours4Multiple($oldTask, $task); - if($oldTask->mode == 'multi' and count($skipMembers) == (count($oldTask->team) - 1) and !isset($skipMembers[$this->app->user->account])) - { - $task->status = 'done'; - $task->finishedBy = $this->app->user->account; - $task->finishedDate = $task->finishedDate; - } } if($task->finishedDate == substr($now, 0, 10)) $task->finishedDate = $now; $task = $this->loadModel('file')->processImgURL($task, $this->config->task->editor->finish['id'], $this->post->uid); - $this->dao->update(TABLE_TASK)->data($task) - ->autoCheck() - ->checkFlow() + $this->dao->update(TABLE_TASK)->data($task)->autoCheck()->checkFlow() ->where('id')->eq((int)$taskID) ->exec(); @@ -2218,7 +2228,7 @@ class taskModel extends model ->setDefault('assignedDate', helper::now()) ->setDefault('activatedDate', helper::now()) ->stripTags($this->config->task->editor->activate['id'], $this->config->allowedTags) - ->remove('comment') + ->remove('comment,uid,multiple,team,teamEstimate,teamConsumed,teamLeft,teamSource') ->get(); if(!is_numeric($task->left)) @@ -2229,12 +2239,7 @@ class taskModel extends model if(!empty($oldTask->team)) { - $this->dao->update(TABLE_TEAM)->set('left')->eq($this->post->left) - ->where('root')->eq($taskID) - ->andWhere('type')->eq('task') - ->andWhere('account')->eq($this->post->assignedTo) - ->exec(); - + $this->manageTaskTeam($oldTask->mode, $oldTask->id, $task->status); $task = $this->computeHours4Multiple($oldTask, $task); } @@ -2300,8 +2305,15 @@ class taskModel extends model /* Check parent Task. */ if($task->parent > 0) $task->parentName = $this->dao->findById($task->parent)->from(TABLE_TASK)->fetch('name'); - $task->team = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->eq($taskID)->andWhere('type')->eq('task')->orderBy('order')->fetchAll('account'); - foreach($children as $child) $child->team = array(); + $task->members = array(); + $task->team = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->eq($taskID)->orderBy('order')->fetchAll('id'); + foreach($task->team as $member) $task->members[$member->account] = $member->account; + + foreach($children as $child) + { + $child->team = array(); + $child->members = array(); + } $task = $this->loadModel('file')->replaceImgURL($task, 'desc'); if($setImgSize) $task->desc = $this->file->setImgSize($task->desc); @@ -2403,12 +2415,12 @@ class taskModel extends model ->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->leftJoin(TABLE_USER)->alias('t3')->on('t1.assignedTo = t3.account') - ->leftJoin(TABLE_TEAM)->alias('t4')->on('t4.root = t1.id') + ->leftJoin(TABLE_TASKTEAM)->alias('t4')->on('t4.task = t1.id') ->leftJoin(TABLE_MODULE)->alias('t5')->on('t1.module = t5.id') ->beginIF($this->config->edition == 'max')->leftJoin(TABLE_DESIGN)->alias('t6')->on('t1.design= t6.id')->fi() ->where('t1.execution')->eq((int)$executionID) ->beginIF($type == 'myinvolved') - ->andWhere("((t4.`account` = '{$this->app->user->account}' AND t4.`type` = 'task') OR t1.`assignedTo` = '{$this->app->user->account}' OR t1.`finishedby` = '{$this->app->user->account}')") + ->andWhere("((t4.`account` = '{$this->app->user->account}') OR t1.`assignedTo` = '{$this->app->user->account}' OR t1.`finishedby` = '{$this->app->user->account}')") ->fi() ->beginIF($productID)->andWhere("((t5.root=" . (int)$productID . " and t5.type='story') OR t2.product=" . (int)$productID . ")")->fi() ->beginIF($type == 'undone')->andWhere('t1.status')->notIN('done,closed')->fi() @@ -2416,7 +2428,7 @@ class taskModel extends model ->beginIF($type == 'assignedtome')->andWhere("(t1.assignedTo = '{$this->app->user->account}' or (t1.mode = 'multi' and t4.`account` = '{$this->app->user->account}') )")->fi() ->beginIF($type == 'finishedbyme') ->andWhere('t1.finishedby', 1)->eq($this->app->user->account) - ->orWhere('t1.finishedList')->like("%,{$this->app->user->account},%") + ->orWhere('t5.status')->eq("done") ->markRight(1) ->fi() ->beginIF($type == 'delayed')->andWhere('t1.deadline')->gt('1970-1-1')->andWhere('t1.deadline')->lt(date(DT_DATE1))->andWhere('t1.status')->in('wait,doing')->fi() @@ -2437,7 +2449,7 @@ class taskModel extends model if(empty($tasks)) return array(); $taskList = array_keys($tasks); - $taskTeam = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->in($taskList)->andWhere('type')->eq('task')->fetchGroup('root'); + $taskTeam = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in($taskList)->fetchGroup('task'); if(!empty($taskTeam)) { foreach($taskTeam as $taskID => $team) $tasks[$taskID]->team = $team; @@ -2513,10 +2525,10 @@ class taskModel extends model ->beginIF($append)->orWhere('id')->in($append)->fi() ->fetchPairs(); + $taskTeams = $this->dao->select('task, count(*) as count')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->groupBy('task')->fetchPairs('task', 'count'); foreach($tasks as $id => $name) { - $taskTeam = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->eq($id)->andWhere('type')->eq('task')->fetch(); - if(!empty($taskTeam)) unset($tasks[$id]); + if(!empty($taskTeams[$id])) unset($tasks[$id]); } return array('' => '') + $tasks ; } @@ -2542,7 +2554,7 @@ class taskModel extends model ->leftJoin(TABLE_EXECUTION)->alias('t2')->on("t1.execution = t2.id") ->leftJoin(TABLE_STORY)->alias('t3')->on('t1.story = t3.id') ->leftJoin(TABLE_PROJECT)->alias('t4')->on("t1.project = t4.id") - ->leftJoin(TABLE_TEAM)->alias('t5')->on("t5.root = t1.id and t5.type = 'task' and t5.account = '{$account}'") + ->leftJoin(TABLE_TASKTEAM)->alias('t5')->on("t5.task = t1.id and t5.account = '{$account}'") ->where('t1.deleted')->eq(0) ->andWhere('t2.deleted')->eq(0) ->beginIF($this->config->vision)->andWhere('t1.vision')->eq($this->config->vision)->fi() @@ -2552,7 +2564,7 @@ class taskModel extends model ->beginIF(!$this->app->user->admin)->andWhere('t1.execution')->in($this->app->user->view->sprints)->fi() ->beginIF($type == 'finishedBy') ->andWhere('t1.finishedby', 1)->eq($account) - ->orWhere('t1.finishedList')->like("%,{$account},%") + ->orWhere('t5.status')->eq("done") ->markRight(1) ->fi() ->beginIF($type == 'assignedTo' and ($this->app->rawModule == 'my' or $this->app->rawModule == 'block'))->andWhere('t2.status', true)->ne('suspended')->orWhere('t4.status')->ne('suspended')->markRight(1)->fi() @@ -2566,7 +2578,7 @@ class taskModel extends model $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'task', false); - $taskTeam = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->in(array_keys($tasks))->andWhere('type')->eq('task')->fetchGroup('root'); + $taskTeam = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task'); if(!empty($taskTeam)) { foreach($taskTeam as $taskID => $team) $tasks[$taskID]->team = $team; @@ -2750,13 +2762,27 @@ class taskModel extends model * Get task estimate. * * @param int $taskID + * @param string $account + * @param string $append * @access public - * @return object + * @return array */ - public function getTaskEstimate($taskID) + public function getTaskEstimate($taskID, $account = '', $append = '') { - return $this->dao->select('*')->from(TABLE_TASKESTIMATE) - ->where('task')->eq($taskID) + if($this->config->edition == 'open') + { + return $this->dao->select('*')->from(TABLE_TASKESTIMATE) + ->where('task')->eq($taskID) + ->beginIF($account)->andWhere('account')->eq($account)->fi() + ->beginIF($append)->orWhere('id')->eq($append)->fi() + ->orderBy('date,id') + ->fetchAll(); + } + return $this->dao->select('*')->from(TABLE_EFFORT)->where('objectID')->eq($taskID) + ->andWhere('objectType')->eq('task') + ->andWhere('deleted')->eq('0') + ->beginIF($account)->andWhere('account')->eq($account)->fi() + ->beginIF($append)->orWhere('id')->eq($append)->fi() ->orderBy('date,id') ->fetchAll(); } @@ -2775,14 +2801,43 @@ class taskModel extends model ->fetch(); /* If the estimate is the last of its task, status of task will be checked. */ - $lastID = $this->dao->select('id')->from(TABLE_TASKESTIMATE) - ->where('task')->eq($estimate->task) - ->andWhere('id')->gt($estimate->id) - ->fetch('id'); - $estimate->isLast = $lastID ? false : true; + $lastID = $this->dao->select('id')->from(TABLE_TASKESTIMATE)->where('task')->eq($estimate->task)->orderBy('date_desc,id_desc')->limit(1)->fetch('id'); + $estimate->isLast = $lastID == $estimate->id; return $estimate; } + /** + * Check operate effort. + * + * @param object $task + * @param object $effort + * @access public + * @return bool + */ + public function canOperateEffort($task, $effort = null) + { + if(empty($task->team)) return true; + + /* Check for add effort. */ + if(empty($effort)) + { + if($task->mode == 'linear' and strpos('|done|closed|cancel|pause|', "|{$task->status}|") !== false) return false; + $members = array_map(function($member){ return $member->account; }, $task->team); + if(!in_array($this->app->user->account, $members)) return false; + if($task->mode == 'linear' and $this->app->user->account != $task->assignedTo) return false; + return true; + } + + /* Check for edit and delete effort. */ + if($task->mode == 'linear') + { + if(strpos('|closed|cancel|pause|', "|{$task->status}|") !== false) return false; + if($task->status == 'doing') return $effort->account == $this->app->user->account; + } + if($this->app->user->account == $effort->account) return true; + return false; + } + /** * Update estimate. * @@ -2793,31 +2848,33 @@ class taskModel extends model public function updateEstimate($estimateID) { $oldEstimate = $this->getEstimateById($estimateID); - $estimate = fixer::input('post')->get(); + $estimate = fixer::input('post')->cleanINT('consumed,left')->get(); $today = helper::today(); if($estimate->date > $today) return dao::$errors[] = $this->lang->task->error->date; + if($estimate->consumed < 0) return dao::$errors[] = sprintf($this->lang->error->ge, $this->lang->task->record, '0'); + if($estimate->left < 0) return dao::$errors[] = sprintf($this->lang->error->ge, $this->lang->task->left, '0'); $task = $this->getById($oldEstimate->task); $this->dao->update(TABLE_TASKESTIMATE)->data($estimate) ->autoCheck() - ->check('consumed', 'notempty') ->where('id')->eq((int)$estimateID) ->exec(); $consumed = $task->consumed + $estimate->consumed - $oldEstimate->consumed; - $lastEstimate = $this->dao->select('*')->from(TABLE_TASKESTIMATE)->where('task')->eq($task->id)->orderBy('id desc')->fetch(); + $lastEstimate = $this->dao->select('*')->from(TABLE_TASKESTIMATE)->where('task')->eq($task->id)->orderBy('date_desc,id_desc')->limit(1)->fetch(); $left = ($lastEstimate and $estimateID == $lastEstimate->id) ? $estimate->left : $task->left; $now = helper::now(); $data = new stdClass(); $data->consumed = $consumed; $data->left = $left; - $data->status = $left == 0 ? 'done' : $task->status; + $data->status = $task->status; $data->lastEditedBy = $this->app->user->account; $data->lastEditedDate = $now; - if(!$left) + if(empty($left) and strpos('wait,doing,pause', $task->status) !== false) { + $data->status = 'done'; $data->finishedBy = $this->app->user->account; $data->finishedDate = $now; $data->assignedTo = $task->openedBy; @@ -2825,18 +2882,17 @@ class taskModel extends model if(!empty($task->team)) { - $oldConsumed = $task->team[$oldEstimate->account]->consumed; + $currentTeam = $this->getTeamByAccount($task->team, $oldEstimate->account, array('effortID' => $estimateID)); + if($currentTeam) + { + $newTeamInfo = new stdClass(); + $newTeamInfo->consumed = $currentTeam->consumed + $estimate->consumed - $oldEstimate->consumed; + if($currentTeam->status != 'done') $newTeamInfo->left = $left; + if($currentTeam->status != 'done' and $newTeamInfo->consumed > 0 and $left == 0) $newTeamInfo->status = 'done'; + $this->dao->update(TABLE_TASKTEAM)->data($newTeamInfo)->where('id')->eq($currentTeam->id)->exec(); - $newTeamInfo = new stdClass(); - $newTeamInfo->consumed = $oldConsumed + $estimate->consumed - $oldEstimate->consumed; - $newTeamInfo->left = $left; - $this->dao->update(TABLE_TEAM)->data($newTeamInfo) - ->where('root')->eq($oldEstimate->task) - ->andWhere('type')->eq('task') - ->andWhere('account')->eq($oldEstimate->account) - ->exec(); - - $data = $this->computeHours4Multiple($task, $data); + $data = $this->computeHours4Multiple($task, $data); + } } $this->dao->update(TABLE_TASK)->data($data)->where('id')->eq($task->id)->exec(); @@ -2870,45 +2926,82 @@ class taskModel extends model { $estimate = $this->getEstimateById($estimateID); $task = $this->getById($estimate->task); - $this->dao->delete()->from(TABLE_TASKESTIMATE)->where('id')->eq($estimateID)->exec(); + $now = helper::now(); - $lastEstimate = $this->dao->select('*')->from(TABLE_TASKESTIMATE)->where('task')->eq($estimate->task)->orderBy('date desc,id desc')->limit(1)->fetch(); - $consumed = $task->consumed - $estimate->consumed; - $left = isset($lastEstimate->left) ? $lastEstimate->left : $estimate->left; + $consumed = $task->consumed - $estimate->consumed; + $left = $task->left; + if($estimate->isLast) + { + $lastTwoEstimates = $this->dao->select('*')->from(TABLE_TASKESTIMATE)->where('task')->eq($estimate->task)->orderBy('date desc,id desc')->limit(2)->fetchAll(); + $lastTwoEstimate = isset($lastTwoEstimates[1]) ? $lastTwoEstimates[1] : ''; + if($lastTwoEstimate) $left = $lastTwoEstimate->left; + if(empty($lastTwoEstimate) and $left == 0) $left = $task->estimate; + } $data = new stdclass(); $data->consumed = $consumed; $data->left = $left; $data->status = ($left == 0 && $consumed != 0) ? 'done' : $task->status; - if($left == 0 and $consumed != 0) + if($consumed != 0 and $left == 0 and strpos('done,pause,cancel,closed', $task->status) === false) { - $data->status = 'done'; + $data->status = 'done'; + $data->assignedTo = $task->openedBy; + $data->assignedDate = $now; + $data->finishedBy = $this->app->user->account; + $data->finishedDate = $now; } - elseif($consumed == 0) + elseif($estimate->isLast and $left != 0 and strpos('done,pause,cancel,closed', $task->status) !== false) { - $data->status = 'wait'; - $data->left = $task->estimate; + $data->status = 'doing'; + $data->finishedBy = ''; + $data->canceledBy = ''; + $data->closedBy = ''; + $data->closedReason = ''; + $data->finishedDate = '0000-00-00 00:00:00'; + $data->canceledDate = '0000-00-00 00:00:00'; + $data->closedDate = '0000-00-00 00:00:00'; } else { $data->status = $task->status; } + if(!empty($task->team)) { - $oldConsumed = $task->team[$estimate->account]->consumed; + $currentTeam = $this->getTeamByAccount($task->team, $estimate->account, array('effortID' => $estimateID)); + if($currentTeam) + { + $left = $currentTeam->left; + if($task->mode == 'multi') + { + $accountEstimates = $this->getTaskEstimate($currentTeam->task, $estimate->account, $estimateID); + $lastEstimate = array_pop($accountEstimates); + if($lastEstimate->id == $estimateID) + { + $lastTwoEstimate = array_pop($accountEstimates); + if($lastTwoEstimate) $left = $lastTwoEstimate->left; + } + } - $newTeamInfo = new stdClass(); - $newTeamInfo->consumed = $oldConsumed - $estimate->consumed; - $newTeamInfo->left = $left; - $this->dao->update(TABLE_TEAM)->data($newTeamInfo) - ->where('root')->eq($estimate->task) - ->andWhere('type')->eq('task') - ->andWhere('account')->eq($estimate->account) - ->exec(); + $newTeamInfo = new stdClass(); + $newTeamInfo->consumed = $currentTeam->consumed - $estimate->consumed; + if($currentTeam->status != 'done') $newTeamInfo->left = $left; + if($currentTeam->status == 'done' and $left > 0 and $task->mode == 'multi') $newTeamInfo->left = $left; - $data = $this->computeHours4Multiple($task, $data); + if($currentTeam->status != 'done' and $newTeamInfo->consumed > 0 and $left == 0) $newTeamInfo->status = 'done'; + if($task->mode == 'multi' and $currentTeam->status == 'done' and $left > 0) $newTeamInfo->status = 'doing'; + if($task->mode == 'multi' and $currentTeam->status == 'done' and ($newTeamInfo->consumed == 0 and $left == 0)) + { + $newTeamInfo->status = 'doing'; + $newTeamInfo->left = $currentTeam->estimate; + } + $this->dao->update(TABLE_TASKTEAM)->data($newTeamInfo)->where('id')->eq($currentTeam->id)->exec(); + } } + $this->dao->delete()->from(TABLE_TASKESTIMATE)->where('id')->eq($estimateID)->exec(); + if(!empty($task->team)) $data = $this->computeHours4Multiple($task, $data); + $this->dao->update(TABLE_TASK)->data($data) ->where('id')->eq($estimate->task)->exec(); if($task->parent > 0) $this->updateParentStatus($task->id); if($task->story) $this->loadModel('story')->setStage($task->story); @@ -3423,6 +3516,29 @@ class taskModel extends model if($action == 'recordestimate' and $task->parent == -1) return false; if($action == 'delete' and $task->parent < 0) return false; + if(!empty($task->team)) + { + global $app; + if($task->mode == 'linear') + { + if($action == 'assignto' and strpos('done,cencel,closed', $task->status) === false) return false; + if($action == 'start' and strpos('wait,doing', $task->status) !== false) + { + if($task->assignedTo != $app->user->account) return false; + + $currentTeam = (new self())->getTeamByAccount($task->team, $app->user->account); + if($currentTeam and $currentTeam->status == 'wait') return true; + } + if($action == 'finish' and $task->assignedTo != $app->user->account) return false; + } + elseif($task->mode == 'multi') + { + $currentTeam = (new self())->getTeamByAccount($task->team, $app->user->account); + if($action == 'start' and strpos('wait,doing', $task->status) !== false and $currentTeam and $currentTeam->status == 'wait') return true; + if($action == 'finish' and (empty($currentTeam) or $currentTeam->status == 'done')) return false; + } + } + if($action == 'start') return $task->status == 'wait'; if($action == 'restart') return $task->status == 'pause'; if($action == 'pause') return $task->status == 'doing'; @@ -3666,21 +3782,26 @@ class taskModel extends model public function printAssignedHtml($task, $users) { $btnTextClass = ''; + $assignedToText = $assignedToTitle = zget($users, $task->assignedTo); if(!empty($task->team) and $task->mode == 'multi' and $task->status != 'closed') { $assignedToText = $this->lang->task->team; $teamMembers = array(); - foreach($task->team as $teamMember) $teamMembers[] = zget($users, $teamMember->account); + foreach($task->team as $teamMember) + { + $realname = zget($users, $teamMember->account); + if($this->app->user->account == $teamMember->account and $teamMember->status != 'done') + { + $task->assignedTo = $this->app->user->account; + $assignedToText = $realname; + } + $teamMembers[] = $realname; + } + $assignedToTitle = implode($this->lang->comma, $teamMembers); } - else - { - $assignedToText = $assignedToTitle = zget($users, $task->assignedTo); - } - $assignedToText = (!empty($task->team) and $task->mode == 'multi' and $task->status != 'closed') ? $this->lang->task->team : zget($users, $task->assignedTo); - - if(empty($task->assignedTo)) + elseif(empty($task->assignedTo)) { $btnTextClass = 'text-primary'; $assignedToText = $this->lang->task->noAssigned; @@ -3735,34 +3856,32 @@ class taskModel extends model * Get next user. * * @param string $users - * @param string $current + * @param object $task + * @param string $type current|next * * @access public - * @return void + * @return string */ - public function getNextUser($users, $current) + public function getAssignedTo4Multi($users, $task, $type = 'current') { + if(empty($task->team) or $task->mode != 'linear') return $task->assignedTo; + + $teamHours = array_values($task->team); + /* Process user */ if(!is_array($users)) $users = explode(',', trim($users, ',')); - if(!$current || !in_array($current, $users) || array_search($current, $users) == max(array_keys($users))) - { - return reset($users); - } + $users = array_values($users); + if(is_object($users[0])) $users = array_map(function($member){ return $member->account; }, $users); - $next = ''; - while(true) + foreach($users as $i => $account) { - if(current($users) == $current) - { - $next = next($users); - break; - } - else - { - next($users); - } + if(isset($teamHours[$i]) and $teamHours[$i]->status == 'done') continue; + if($type == 'current') return $account; + break; } - return $next; + if($type == 'next' and isset($users[$i + 1])) return $users[$i + 1]; + + return $task->openedBy; } /** @@ -3794,15 +3913,11 @@ class taskModel extends model */ public function getFinishedUsers($taskID = 0, $team = array()) { - $task = $this->getById($taskID); - if($task->activatedDate == '') $task->activatedDate = "0000-00-00"; - return $this->dao->select('actor')->from(TABLE_ACTION) - ->where('objectType')->eq('task') - ->andWhere('objectID')->eq($taskID) - ->andWhere('actor')->in($team) - ->andWhere('action')->eq('finished') - ->andWhere('date')->ge($task->activatedDate) - ->fetchPairs('actor'); + return $this->dao->select('id,account')->from(TABLE_TASKTEAM) + ->where('task')->eq($taskID) + ->andWhere('status')->eq('done') + ->beginIF($team)->andWhere('account')->in($team)->fi() + ->fetchPairs('id', 'account'); } /** @@ -3905,8 +4020,7 @@ class taskModel extends model $menu .= $this->buildMenu('task', 'batchCreate', "execution=$task->execution&storyID=$task->story&moduleID=$task->module&taskID=$task->id", $task, 'view', 'split', '', '', '', "title='{$this->lang->task->children}'", $this->lang->task->children); } - $assignToLang = (!empty($task->team) and $task->mode == 'linear') ? $this->lang->task->transfer : $this->lang->task->assignTo; - $menu .= $this->buildMenu('task', 'assignTo', "executionID=$task->execution&taskID=$task->id", $task, 'button', '', '', 'iframe', true, '', $assignToLang); + $menu .= $this->buildMenu('task', 'assignTo', "executionID=$task->execution&taskID=$task->id", $task, 'button', '', '', 'iframe', true, '', $this->lang->task->assignTo); $menu .= $this->buildMenu('task', 'start', $params, $task, 'view', '', '', 'iframe showinonlybody', true); $menu .= $this->buildMenu('task', 'restart', $params, $task, 'view', '', '', 'iframe showinonlybody', true); @@ -3922,7 +4036,7 @@ class taskModel extends model $menu .= "
"; $menu .= $this->buildMenu('task', 'edit', $params, $task, 'view', '', '', 'showinonlybody'); - $menu .= $this->buildMenu('task', 'create', "projctID={$task->execution}&storyID=0&moduleID=0&taskID=$task->id", $task, 'view', 'copy'); + if(empty($task->team)) $menu .= $this->buildMenu('task', 'create', "projctID={$task->execution}&storyID=0&moduleID=0&taskID=$task->id", $task, 'view', 'copy'); $menu .= $this->buildMenu('task', 'delete', "executionID=$task->execution&taskID=$task->id", $task, 'view', 'trash', 'hiddenwin', 'showinonlybody', true); if($task->parent > 0) $menu .= $this->buildMenu('task', 'view', "taskID=$task->parent", $task, 'view', 'chevron-double-up', '', '', '', '', $this->lang->task->parent); diff --git a/module/task/view/activate.html.php b/module/task/view/activate.html.php index d23301ad33..302e5e1c66 100644 --- a/module/task/view/activate.html.php +++ b/module/task/view/activate.html.php @@ -12,6 +12,14 @@ ?> + +consumed);?> +user->account);?> + +task->error->teamMember);?> +lang->task->error->leftEmptyAB, $this->lang->task->statusList[$task->status]));?> +error->notempty, $lang->task->estimate))?> +error->notempty, $lang->task->left))?>
@@ -23,20 +31,41 @@
- +
+ team);?> + + + + + + + - - + + parent != '-1'):?> @@ -53,16 +82,40 @@
task->mode;?>task->modeList, $task->mode) . html::hidden('mode', $task->mode);?>
task->assignedTo;?>finishedBy, "class='form-control chosen'");?> +
+ finishedBy, "class='form-control chosen'" . ($isMultiple ? ' disabled' : ''));?> + + task->team;?> + +
+
+ +
+ +
+ +
task->left;?>
- + task->hour;?>
- task->activate); echo html::linkButton($lang->goback, $this->session->taskList); ?>
+ +

+team) and count($task->team) < 6) ? 6 - count($task->team) : 1);?> diff --git a/module/task/view/assignto.html.php b/module/task/view/assignto.html.php index 50f162d9d9..a04bc937ea 100644 --- a/module/task/view/assignto.html.php +++ b/module/task/view/assignto.html.php @@ -14,12 +14,12 @@
- team) and (!isset($task->team[$app->user->account]) or ($task->assignedTo != $app->user->account and $task->mode == 'linear'))):?> + members) and strpos('wait,doing,pause', $task->status) !== false and (!isset($task->members[$app->user->account]) or $task->mode == 'linear')):?>
- assignedTo != $app->user->account and $task->mode == 'linear'):?> -

task->deniedNotice, '' . $task->assignedToRealName . '', $lang->task->transfer);?>

+ mode == 'linear'):?> +

task->transferNotice;?>

task->deniedNotice, '' . $lang->task->teamMember . '', $lang->task->transfer);?>

@@ -38,14 +38,14 @@
- - - + + + status != 'done' and $task->status != 'closed' and $task->parent >= 0):?> - + diff --git a/module/task/view/create.html.php b/module/task/view/create.html.php index 428c5e4f5d..ca23448ac5 100644 --- a/module/task/view/create.html.php +++ b/module/task/view/create.html.php @@ -78,7 +78,7 @@ foreach(explode(',', $config->task->create->requiredFields) as $field) @@ -293,24 +293,11 @@ foreach(explode(',', $config->task->create->requiredFields) as $field)
team) ? $lang->task->assign : $lang->task->transferTo;?>team) ? $task->assignedTo : $task->nextUser, "class='form-control chosen'");?>
team) or strpos('done,cencel,closed', $task->status) !== false) ? $lang->task->assign : $lang->task->transferTo;?>team) or strpos('done,cencel,closed', $task->status) !== false) ? $task->assignedTo : $task->nextUser, "class='form-control chosen'");?>
task->left;?>
left, "class='form-control'");?> task->hour;?>
task->status;?>
- +
- - - - - + - +
-
- task->estimateAB}'") ?> - task->hour;?> -
-
- - - -
confirm, '', "class='btn btn-primary'");?>confirm, '', "id='confirmButton' class='btn btn-primary'");?>
@@ -347,6 +334,7 @@ foreach(explode(',', $config->task->create->requiredFields) as $field) id);?> type);?> + diff --git a/module/task/view/edit.html.php b/module/task/view/edit.html.php index a391a88a98..fb56d27a51 100644 --- a/module/task/view/edit.html.php +++ b/module/task/view/edit.html.php @@ -20,13 +20,14 @@ consumed);?> status);?> user->account);?> -team);?> +members);?> task->confirmChangeExecution);?> -team) < 6 ? 6 - count($task->team) : 1);?> + task->error->teamMember);?> lang->task->error->leftEmptyAB, $this->lang->task->statusList[$task->status]));?> error->notempty, $lang->task->estimate))?> +error->notempty, $lang->task->left))?> task->edit->requiredFields);?> task->edit->requiredFields) as $field) children) and empty($task->parent) and $task->type != 'affair'):?>
- team) ? '' : 'checked';?> /> + team) ? '' : 'checked';?> />
@@ -138,12 +139,12 @@ foreach(explode(',', $config->task->edit->requiredFields) as $field) task->assignedTo;?> - team) and (($task->assignedTo != $this->app->user->account and $task->mode == 'linear') or !isset($task->team[$app->user->account]))) ? "disabled='disabled'" :'';?> + team) and $task->mode == 'linear') ? "disabled='disabled'" : '';?> team)) { - $teamAccounts = array_keys($task->team); + $teamAccounts = $task->members; foreach($teamAccounts as $teamAccount) { if(!isset($members[$teamAccount])) continue; @@ -159,8 +160,7 @@ foreach(explode(',', $config->task->edit->requiredFields) as $field) task->mode;?> - team[$app->user->account]) ? '' : "disabled='disabled'"?> - task->modeList, $task->mode, "class='form-control chosen' $disabledMode onchange='updateAssignedTo()'");?> + task->modeList, $task->mode) . html::hidden('mode', $task->mode);?> ' id='teamTr'> task->team;?> @@ -205,8 +205,8 @@ foreach(explode(',', $config->task->edit->requiredFields) as $field) task->estimate;?> - team) or $task->parent < 0) ? "disabled='disabled'" : '';?> - estimate, "class='form-control' {$disabled}");?> + team) or $task->parent < 0) ? "readonly" : '';?> + estimate, "class='form-control' {$readonly}");?> @@ -215,10 +215,7 @@ foreach(explode(',', $config->task->edit->requiredFields) as $field) task->left;?> - - team) or $task->parent < 0) ? "disabled='disabled'" : '';?> - left, "class='form-control' {$disabled}");?> - + left, "class='form-control' {$readonly}");?>
@@ -278,44 +275,7 @@ foreach(explode(',', $config->task->edit->requiredFields) as $field)