tao.php 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197
  1. <?php
  2. /**
  3. * The tao file of task module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Shujie Tian <tianshujie@easysoft.ltd>
  8. * @package task
  9. * @link https://www.zentao.net
  10. */
  11. class taskTao extends taskModel
  12. {
  13. /**
  14. * 计算任务列表中每个任务的进度,包括子任务。
  15. * Compute progress of task list, include its' children.
  16. *
  17. * @param object[] $tasks
  18. * @access protected
  19. * @return object[]
  20. */
  21. protected function batchComputeProgress($tasks)
  22. {
  23. foreach($tasks as $task)
  24. {
  25. $task->progress = $this->computeTaskProgress($task);
  26. if(empty($task->children)) continue;
  27. $task->children = $this->batchComputeProgress($task->children);
  28. }
  29. return $tasks;
  30. }
  31. /**
  32. * 根据填写的日志,记录历史记录、改变任务的状态、消耗工时等信息并返回。
  33. * According to the effort, record the history, change the status of the task, consumed hours and other information and return.
  34. *
  35. * @param object $record
  36. * @param object $task
  37. * @param string $lastDate
  38. * @param bool $isFinishTask
  39. * @access protected
  40. * @return array
  41. */
  42. protected function buildTaskForEffort($record, $task, $lastDate, $isFinishTask)
  43. {
  44. $this->loadModel('action');
  45. $now = helper::now();
  46. $actionID = 0;
  47. $newTask = clone $task;
  48. $newTask->consumed += $record->consumed;
  49. $newTask->lastEditedBy = $this->app->user->account;
  50. $newTask->lastEditedDate = $now;
  51. if(!$task->realStarted) $newTask->realStarted = $now;
  52. if($lastDate <= $record->date) $newTask->left = $record->left;
  53. /* Finish task by effort. */
  54. if(!$newTask->left && $isFinishTask)
  55. {
  56. $newTask->status = 'done';
  57. $newTask->assignedTo = $task->openedBy;
  58. $newTask->assignedDate = $now;
  59. $newTask->finishedBy = $this->app->user->account;
  60. $newTask->finishedDate = $now;
  61. $actionID = $this->action->create('task', $task->id, 'Finished', $record->work);
  62. }
  63. /* Start task by effort. */
  64. elseif($newTask->status == 'wait')
  65. {
  66. $newTask->status = 'doing';
  67. $newTask->assignedTo = $this->app->user->account;
  68. $newTask->assignedDate = $now;
  69. $actionID = $this->action->create('task', $task->id, 'Started', $record->work);
  70. }
  71. /* Activate task by effort. */
  72. elseif($newTask->left != 0 && strpos('done,pause,cancel,closed,pause', $task->status) !== false)
  73. {
  74. $newTask->status = 'doing';
  75. $newTask->assignedTo = $this->app->user->account;
  76. $newTask->finishedBy = '';
  77. $newTask->canceledBy = '';
  78. $newTask->closedBy = '';
  79. $newTask->closedReason = '';
  80. $newTask->finishedDate = null;
  81. $newTask->canceledDate = null;
  82. $newTask->closedDate = null;
  83. $actionID = $this->action->create('task', $task->id, 'Activated', $record->work);
  84. }
  85. else
  86. {
  87. $actionID = $this->action->create('task', $task->id, 'RecordWorkhour', $record->work, (float)$record->consumed);
  88. }
  89. return array($newTask, $actionID);
  90. }
  91. /**
  92. * 编辑日志后,构造待更新的任务数据。
  93. * After editing the effort, build the task data to be updated.
  94. *
  95. * @param object $task
  96. * @access protected
  97. * @return array|false
  98. * @param object $oldEffort
  99. * @param object $effort
  100. */
  101. protected function buildTaskForUpdateEffort($task, $oldEffort, $effort)
  102. {
  103. $lastEffort = $this->dao->select('*')->from(TABLE_EFFORT)
  104. ->where('objectID')->eq($task->id)
  105. ->andWhere('objectType')->eq('task')
  106. ->orderBy('date_desc,id_desc')->limit(1)->fetch();
  107. $consumed = $task->consumed + $effort->consumed - $oldEffort->consumed;
  108. $left = ($lastEffort && $effort->id == $lastEffort->id) ? $effort->left : $lastEffort->left;
  109. $now = helper::now();
  110. $data = new stdclass();
  111. $data->consumed = $consumed;
  112. $data->left = $left;
  113. $data->status = $task->status;
  114. $data->lastEditedBy = $this->app->user->account;
  115. $data->lastEditedDate = $now;
  116. if(empty($left) && strpos('wait,doing,pause', $task->status) !== false)
  117. {
  118. $data->status = 'done';
  119. $data->finishedBy = $this->app->user->account;
  120. $data->finishedDate = $now;
  121. $data->assignedTo = $task->openedBy;
  122. }
  123. return $data;
  124. }
  125. /**
  126. * 将任务的层级改为父子结构。
  127. * Change the hierarchy of tasks to a parent-child structure.
  128. *
  129. * @param object[] $tasks
  130. * @param object[] $parentTasks
  131. * @access protected
  132. * @return object[]
  133. */
  134. protected function buildTaskTree($tasks, $parentTasks)
  135. {
  136. foreach($tasks as $task)
  137. {
  138. /* 如果不是父任务则跳过。*/
  139. if($task->parent <= 0) continue;
  140. if(isset($tasks[$task->parent]))
  141. {
  142. /* 如果任务列表里有这个任务的父任务,则将子任务放到父任务里,并删除子任务。*/
  143. if(!isset($tasks[$task->parent]->children)) $tasks[$task->parent]->children = array();
  144. $tasks[$task->parent]->children[$task->id] = $task;
  145. unset($tasks[$task->id]);
  146. }
  147. else
  148. {
  149. /* 如果任务列表里没有这个任务的父任务,则从父任务列表获取到父任务名称并附加到子任务上。*/
  150. $parent = $parentTasks[$task->parent];
  151. $task->parentName = $parent->name;
  152. }
  153. }
  154. return $tasks;
  155. }
  156. /**
  157. * 构造日志数据,加上任务ID、记录人字段。
  158. * Add fields to workhour.
  159. *
  160. * @param int $taskID
  161. * @param array $workhour
  162. * @access protected
  163. * @return array
  164. */
  165. protected function buildWorkhour($taskID, $workhour)
  166. {
  167. foreach($workhour as $record)
  168. {
  169. $record->task = $taskID;
  170. $record->account = $this->app->user->account;
  171. }
  172. return $workhour;
  173. }
  174. /**
  175. * 编辑日志时,检查输入是否合法。
  176. * When editing a effort, check that the input is legal.
  177. *
  178. * @param object $effort
  179. * @access protected
  180. * @return bool
  181. */
  182. protected function checkEffort($effort)
  183. {
  184. $today = helper::today();
  185. if(helper::isZeroDate($effort->date)) dao::$errors['date'] = $this->lang->task->error->dateEmpty;
  186. if($effort->date > $today) dao::$errors['date'] = $this->lang->task->error->date;
  187. if($effort->consumed <= 0) dao::$errors['comsumed'] = sprintf($this->lang->error->gt, $this->lang->task->record, '0');
  188. if($effort->left < 0) dao::$errors['left'] = sprintf($this->lang->error->ge, $this->lang->task->left, '0');
  189. return !dao::isError();
  190. }
  191. /**
  192. * 检查一个任务是否有子任务。
  193. * Check if a task has children.
  194. *
  195. * @param int $taskID
  196. * @access protected
  197. * @return bool
  198. */
  199. protected function checkHasChildren($taskID)
  200. {
  201. $childrenCount = $this->dao->select('COUNT(1) AS count')->from(TABLE_TASK)->where('parent')->eq($taskID)->fetch('count');
  202. if(!$childrenCount) return false;
  203. return true;
  204. }
  205. /**
  206. * 获取任务的进度,通过任务的消耗和剩余工时计算,结果以百分比的数字部分显示。
  207. * Compute progress of a task.
  208. *
  209. * @param object $task
  210. * @access protected
  211. * @return float
  212. */
  213. protected function computeTaskProgress($task)
  214. {
  215. if($task->left > 0) return round($task->consumed / ((float)$task->consumed + (float)$task->left), 2) * 100;
  216. if($task->consumed == 0) return 0;
  217. return 100;
  218. }
  219. /**
  220. * 计算当前任务的状态。
  221. * Compute the status of the current task.
  222. *
  223. * @param object $currentTask
  224. * @param object $oldTask
  225. * @param object $task
  226. * @param bool $autoStatus true|false
  227. * @param bool $hasEfforts true|false
  228. * @param array $members
  229. * @access protected
  230. * @return object
  231. */
  232. protected function computeTaskStatus($currentTask, $oldTask, $task, $autoStatus, $hasEfforts, $members)
  233. {
  234. /* If the status is not automatic, return the current task. */
  235. if(!$autoStatus) return $currentTask;
  236. /* If consumed of the current task is empty and current task has no efforts, the current task status should be wait. */
  237. if($currentTask->consumed == 0 && !$hasEfforts)
  238. {
  239. if(!isset($task->status)) $currentTask->status = 'wait';
  240. $currentTask->finishedBy = '';
  241. $currentTask->finishedDate = null;
  242. }
  243. /* If neither consumed nor left of the current task is empty, the current task status should be doing. */
  244. if($currentTask->consumed > 0 && $currentTask->left > 0)
  245. {
  246. $currentTask->status = 'doing';
  247. $currentTask->finishedBy = '';
  248. $currentTask->finishedDate = null;
  249. }
  250. /* If consumed of the current task is not empty and left of the current task is empty, the current task status should be done or doing. */
  251. if($currentTask->consumed > 0 && $currentTask->left == 0)
  252. {
  253. $finishedUsers = $this->getFinishedUsers($oldTask->id, $members);
  254. /* If the number of finisher is less than the number of team members , the current task status should be doing. */
  255. if(count($finishedUsers) != count($members))
  256. {
  257. if(strpos('cancel,pause', $oldTask->status) === false || ($oldTask->status == 'closed' && $oldTask->reason == 'done'))
  258. {
  259. $currentTask->status = 'doing';
  260. $currentTask->finishedBy = '';
  261. $currentTask->finishedDate = null;
  262. }
  263. }
  264. /* If status of old task is wait or doing or pause, the current task status should be done. */
  265. elseif(strpos('wait,doing,pause', $oldTask->status) !== false)
  266. {
  267. $currentTask->status = 'done';
  268. $currentTask->assignedTo = $oldTask->openedBy;
  269. $currentTask->assignedDate = helper::now();
  270. $currentTask->finishedBy = $this->app->user->account;
  271. $currentTask->finishedDate = zget($task, 'finishedDate', null);
  272. }
  273. }
  274. return $currentTask;
  275. }
  276. /**
  277. * 拼接团队成员信息,包括账号、预计、消耗、剩余,用来创建历史记录。例如:团队成员: admin, 预计: 2, 消耗: 0, 剩余: 3。
  278. * Concat team info for create history.
  279. *
  280. * @param array $teamInfoList
  281. * @param array $userPairs
  282. * @access protected
  283. * @return string
  284. */
  285. protected function concatTeamInfo($teamInfoList, $userPairs)
  286. {
  287. $teamInfo = '';
  288. foreach($teamInfoList as $info) $teamInfo .= "{$this->lang->task->teamMember}: " . zget($userPairs, $info->account) . ", {$this->lang->task->estimateAB}: " . (float)$info->estimate . ", {$this->lang->task->consumedAB}: " . (float)$info->consumed . ", {$this->lang->task->leftAB}: " . (float)$info->left . "\n";
  289. return $teamInfo;
  290. }
  291. /**
  292. * 复制任务数据。
  293. * Copy the task data and update the effort to the new task.
  294. *
  295. * @param object $parentTask
  296. * @access public
  297. * @return bool|int
  298. * @param object $task
  299. */
  300. public function copyTaskData($task)
  301. {
  302. /* 复制当前任务信息。 */
  303. /* Copy the current task to child task, and change the parent field value. */
  304. $copyTask = clone $task;
  305. $copyTask->parent = $task->id;
  306. $copyTask->feedback = 0;
  307. unset($copyTask->id);
  308. if($this->config->edition != 'open')
  309. {
  310. $fieldList = $this->loadModel('workflowfield')->getList('task');
  311. foreach($fieldList as $field)
  312. {
  313. if($field->type == 'date' || $field->type == 'datetime') $this->config->task->dateFields[] = $field->field;
  314. }
  315. }
  316. foreach($this->config->task->dateFields as $dateField)
  317. {
  318. if(empty($copyTask->$dateField)) unset($copyTask->$dateField);
  319. }
  320. $this->dao->insert(TABLE_TASK)->data($copyTask)->autoCheck()->exec();
  321. $copyTaskID = $this->dao->lastInsertID();
  322. $copyTaskPath = $task->path . "{$copyTaskID},";
  323. $this->dao->update(TABLE_TASK)->set('path')->eq($copyTaskPath)->where('id')->eq($copyTaskID)->exec();
  324. if(dao::isError()) return false;
  325. $path = $copyTask->path . "{$copyTaskID},";
  326. $this->dao->update(TABLE_TASK)->set('path')->eq($path)->where('id')->eq($copyTaskID)->exec();
  327. if(dao::isError()) return false;
  328. /* 将父任务的日志记录更新到子任务下。 */
  329. /* Update the logs of the parent task under the subtask. */
  330. $this->dao->update(TABLE_EFFORT)->set('objectID')->eq($copyTaskID)
  331. ->where('objectID')->eq($task->id)
  332. ->andWhere('objectType')->eq('task')
  333. ->exec();
  334. return !dao::isError() ? $copyTaskID : false;
  335. }
  336. /**
  337. * 多人任务的团队变更后,记录对比历史记录。
  338. * Update a task.
  339. *
  340. * @param object $oldTask
  341. * @param object $task
  342. * @access protected
  343. * @return bool
  344. */
  345. protected function createChangesForTeam($oldTask, $task)
  346. {
  347. $users = $this->loadModel('user')->getPairs('noletter|noempty');
  348. $oldTeams = $oldTask->team;
  349. $oldTeamInfo = array();
  350. foreach($oldTeams as $team) $oldTeamInfo[$team->account][] = "{$this->lang->task->teamMember}: " . zget($users, $team->account) . ", {$this->lang->task->estimateAB}: " . (float)$team->estimate . ", {$this->lang->task->consumedAB}: " . (float)$team->consumed . ", {$this->lang->task->leftAB}: " . (float)$team->left;
  351. $newTeamInfo = array();
  352. foreach($this->post->team as $i => $account)
  353. {
  354. if(empty($account)) continue;
  355. $newTeamInfo[$account][] = "{$this->lang->task->teamMember}: " . zget($users, $account) . ", {$this->lang->task->estimateAB}: " . zget($this->post->teamEstimate, $i, 0) . ", {$this->lang->task->consumedAB}: " . zget($this->post->teamConsumed, $i, 0) . ", {$this->lang->task->leftAB}: " . zget($this->post->teamLeft, $i, 0);
  356. }
  357. /* 只保留有变化的部分, 方便对比。*/
  358. $oldTask->team = '';
  359. $task->team = '';
  360. foreach($oldTeamInfo as $account => $infos)
  361. {
  362. foreach($infos as $index => $info)
  363. {
  364. if(isset($newTeamInfo[$account][$index]) && $newTeamInfo[$account][$index] == $info)
  365. {
  366. unset($newTeamInfo[$account][$index]);
  367. }
  368. else
  369. {
  370. $oldTask->team .= $info . "\n";
  371. }
  372. }
  373. }
  374. foreach($newTeamInfo as $account => $infos)
  375. {
  376. foreach($infos as $info)
  377. {
  378. $task->team .= $info . "\n";
  379. }
  380. }
  381. return array($oldTask, $task);
  382. }
  383. /**
  384. * 更新自动更新任务状态时记录修改日志。
  385. * Create action record when auto update task.
  386. *
  387. * @param object $oldTask
  388. * @param string $source parent|child
  389. * @access protected
  390. * @return void
  391. */
  392. protected function createAutoUpdateTaskAction($oldTask, $source = 'parent')
  393. {
  394. $newTask = $this->dao->select('*')->from(TABLE_TASK)->where('id')->eq($oldTask->id)->fetch();
  395. unset($oldTask->subStatus);
  396. unset($newTask->subStatus);
  397. $changes = common::createChanges($oldTask, $newTask);
  398. $action = '';
  399. if($newTask->status == 'done' && $oldTask->status != 'done') $action = 'Finished';
  400. if($newTask->status == 'closed' && $oldTask->status != 'closed') $action = 'Closed';
  401. if($newTask->status == 'pause' && $oldTask->status != 'pause') $action = 'Paused';
  402. if($newTask->status == 'cancel' && $oldTask->status != 'cancel') $action = 'Canceled';
  403. if($newTask->status == 'doing' && $oldTask->status == 'wait') $action = 'Started';
  404. if($newTask->status == 'doing' && $oldTask->status == 'pause') $action = 'Restarted';
  405. if($newTask->status == 'wait' && $oldTask->status != 'wait') $action = 'Adjusttasktowait';
  406. if($newTask->status == 'doing' && $oldTask->status != 'wait' && $oldTask->status != 'pause') $action = 'Activated';
  407. if(!$action) return;
  408. $actionID = $this->loadModel('action')->create('task', $oldTask->id, $action, '', 'autoby' . $source, '', false);
  409. $this->action->logHistory($actionID, $changes);
  410. }
  411. /**
  412. * 更新一个任务。
  413. * Update a task.
  414. *
  415. * @param object $task
  416. * @param object $oldTask
  417. * @param string $requiredFields
  418. * @access protected
  419. * @return bool
  420. */
  421. protected function doUpdate($task, $oldTask, $requiredFields)
  422. {
  423. /* Task link design. */
  424. if(!empty($task->design))
  425. {
  426. $design = $this->dao->select('version')->from(TABLE_DESIGN)->where('id')->eq($task->design)->fetch();
  427. $task->designVersion = $design->version;
  428. }
  429. $execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($task->execution)->fetch();
  430. if($this->isNoStoryExecution($execution)) $task->story = 0;
  431. /* Set the datetime and operator when the task is modified. */
  432. if(empty($task->lastEditedDate) || empty($task->lastEditedBy))
  433. {
  434. $task->lastEditedBy = $this->app->user->account;
  435. $task->lastEditedDate = helper::now();
  436. }
  437. if(isset($task->estimate)) $task->estimate = round((float)$task->estimate, 2);
  438. if(isset($task->consumed)) $task->consumed = round((float)$task->consumed, 2);
  439. if(isset($task->left)) $task->left = round((float)$task->left, 2);
  440. $this->dao->update(TABLE_TASK)->data($task, 'deleteFiles,renameFiles,files,docVersions,oldDocs,docs')
  441. ->autoCheck()
  442. ->batchCheckIF($task->status != 'cancel', $requiredFields, 'notempty')
  443. ->checkIF(!helper::isZeroDate($task->deadline), 'deadline', 'ge', $task->estStarted)
  444. ->batchCheckIF($task->status == 'wait' || $task->status == 'doing', 'finishedBy,finishedDate,canceledBy,canceledDate,closedBy,closedDate,closedReason', 'empty')
  445. ->checkIF($task->status == 'done', 'consumed', 'notempty')
  446. ->batchCheckIF($task->status == 'done', 'canceledBy,canceledDate', 'empty')
  447. ->batchCheckIF($task->closedReason == 'cancel', 'finishedBy,finishedDate,closedBy,closedDate,closedReason', 'empty')
  448. ->checkFlow()
  449. ->where('id')->eq($task->id)
  450. ->exec();
  451. return !dao::isError();
  452. }
  453. /**
  454. * 获取执行下的任务。
  455. * Fetch tasks under execution by executionID(Todo).
  456. *
  457. * @param int|array $executionID
  458. * @param int $productID
  459. * @param string|array $type all|assignedbyme|myinvolved|undone|needconfirm|assignedtome|finishedbyme|delayed|review|wait|doing|done|pause|cancel|closed|array('wait','doing','done','pause','cancel','closed')
  460. * @param array $modules
  461. * @param string $orderBy
  462. * @param object $pager
  463. * @access protected
  464. * @return object[]
  465. */
  466. protected function fetchExecutionTasks($executionID, $productID = 0, $type = 'all', $modules = array(), $orderBy = 'status_asc, id_desc', $pager = null)
  467. {
  468. if(is_string($type)) $type = strtolower($type);
  469. $orderBy = str_replace('pri_', 'priOrder_', $orderBy);
  470. $orderBy = str_replace('status', 'statusOrder', $orderBy);
  471. $fields = "DISTINCT t1.*, t2.id AS storyID, t2.title AS storyTitle, t2.product, t2.branch, t2.version AS latestStoryVersion, t2.status AS storyStatus, IF(t1.`pri` = 0, {$this->config->maxPriValue}, t1.`pri`) as priOrder, INSTR('wait,doing,done,pause,cancel,closed,', t1.status) as statusOrder";
  472. ($this->config->edition == 'max' or $this->config->edition == 'ipd') && $fields .= ', t5.name as designName, t5.version as latestDesignVersion';
  473. $actionIDList = array();
  474. if($type == 'assignedbyme') $actionIDList = $this->dao->select('objectID')->from(TABLE_ACTION)->where('objectType')->eq('task')->andWhere('action')->eq('assigned')->andWhere('actor')->eq($this->app->user->account)->fetchPairs('objectID', 'objectID');
  475. if(is_numeric($executionID) && $executionID) $execution = $this->fetchByID($executionID, 'project');
  476. $tasks = $this->dao->select($fields)
  477. ->from(TABLE_TASK)->alias('t1')
  478. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
  479. ->leftJoin(TABLE_TASKTEAM)->alias('t3')->on('t3.task = t1.id')
  480. ->beginIF($productID)->leftJoin(TABLE_MODULE)->alias('t4')->on('t1.module = t4.id')->fi()
  481. ->beginIF($this->config->edition == 'max' or $this->config->edition == 'ipd')->leftJoin(TABLE_DESIGN)->alias('t5')->on('t1.design= t5.id')->fi()
  482. ->where('t1.execution')->in($executionID)
  483. ->beginIF(is_numeric($executionID) && !empty($execution->isTpl))->andWhere('t1.isTpl')->eq('1')->fi()
  484. ->beginIF($type == 'myinvolved')
  485. ->andWhere("((t3.`account` = '{$this->app->user->account}') OR t1.`assignedTo` = '{$this->app->user->account}' OR t1.`finishedby` = '{$this->app->user->account}')")
  486. ->fi()
  487. ->beginIF($productID)->andWhere("((t4.root=" . (int)$productID . " and t4.type='story') OR t2.product=" . (int)$productID . ")")->fi()
  488. ->beginIF($type == 'undone')->andWhere('t1.status')->notIN('done,closed')->fi()
  489. ->beginIF($type == 'needconfirm')->andWhere('t2.version > t1.storyVersion')->andWhere("t2.status = 'active'")->fi()
  490. ->beginIF($type == 'assignedtome')->andWhere("(t1.assignedTo = '{$this->app->user->account}' or (t1.mode = 'multi' and t3.`account` = '{$this->app->user->account}' and t1.status != 'closed' and t3.status != 'done') )")->fi()
  491. ->beginIF($type == 'finishedbyme')
  492. ->andWhere('t1.finishedby', 1)->eq($this->app->user->account)
  493. ->orWhere('t3.status')->eq("done")
  494. ->markRight(1)
  495. ->fi()
  496. ->beginIF($type == 'delayed')->andWhere('t1.deadline')->gt('1970-1-1')->andWhere('t1.deadline')->lt(date(DT_DATE1))->andWhere('t1.status')->in('wait,doing')->fi()
  497. ->beginIF(is_array($type) or strpos(',all,undone,needconfirm,assignedtome,delayed,finishedbyme,myinvolved,assignedbyme,review,', ",$type,") === false)->andWhere('t1.status')->in($type)->fi()
  498. ->beginIF($modules)->andWhere('t1.module')->in($modules)->fi()
  499. ->beginIF($type == 'assignedbyme')->andWhere('t1.id')->in($actionIDList)->andWhere('t1.status')->ne('closed')->fi()
  500. ->beginIF($type == 'review')
  501. ->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")
  502. ->andWhere('t1.reviewStatus')->eq('doing')
  503. ->fi()
  504. ->andWhere('t1.deleted')->eq(0)
  505. ->orderBy($orderBy)
  506. ->page($pager, 't1.id')
  507. ->fetchAll('id', false);
  508. $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'task', !($productID || in_array($type, array('myinvolved', 'needconfirm', 'assignedtome', 'finishedbyme'))));
  509. return $tasks;
  510. }
  511. /**
  512. * 通过任务类型查找用户的任务。
  513. * Fetch user tasks by type.
  514. *
  515. * @param string $account
  516. * @param string $type assignedTo|finishedBy|closedBy
  517. * @param string $orderBy
  518. * @param int $projectID
  519. * @param int $limit
  520. * @param object|null $pager
  521. * @access protected
  522. * @return object[]
  523. */
  524. protected function fetchUserTasksByType($account, $type, $orderBy, $projectID, $limit, $pager)
  525. {
  526. $orderBy = str_replace('pri_', 'priOrder_', $orderBy);
  527. $orderBy = str_replace('project_', 't1.project_', $orderBy);
  528. return $this->dao->select("t1.*, t4.id AS project, t2.id AS executionID, t2.name AS executionName, t4.name AS projectName, t2.multiple AS executionMultiple, t2.type AS executionType, t3.id AS storyID, t3.title AS storyTitle, t3.status AS storyStatus, t3.version AS latestStoryVersion, IF(t1.`pri` = 0, {$this->config->maxPriValue}, t1.`pri`) AS priOrder")
  529. ->from(TABLE_TASK)->alias('t1')
  530. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.execution = t2.id')
  531. ->leftJoin(TABLE_STORY)->alias('t3')->on('t1.story = t3.id')
  532. ->leftJoin(TABLE_PROJECT)->alias('t4')->on('t2.project = t4.id')
  533. ->leftJoin(TABLE_TASKTEAM)->alias('t5')->on("t5.task = t1.id and t5.account = '{$account}'")
  534. ->where('t1.deleted')->eq(0)
  535. ->andWhere('t2.deleted')->eq(0)
  536. ->andWhere('t4.deleted')->eq(0)
  537. ->beginIF($this->config->vision)->andWhere('t1.vision')->eq($this->config->vision)->fi()
  538. ->beginIF($this->config->vision)->andWhere('t2.vision')->eq($this->config->vision)->fi()
  539. ->beginIF($type != 'closedBy' && $this->app->moduleName == 'block')->andWhere('t1.status')->ne('closed')->fi()
  540. ->beginIF($projectID)->andWhere('t1.project')->eq($projectID)->fi()
  541. ->beginIF(!$this->app->user->admin)->andWhere('t1.execution')->in($this->app->user->view->sprints)->fi()
  542. ->beginIF($type == 'finishedBy')
  543. ->andWhere('t1.finishedby', 1)->eq($account)
  544. ->orWhere('t5.status')->eq('done')
  545. ->markRight(1)
  546. ->fi()
  547. ->beginIF($type == 'assignedTo' && ($this->app->rawModule == 'my' || $this->app->rawModule == 'block'))->andWhere('t2.status', true)->ne('suspended')->orWhere('t4.status')->ne('suspended')->markRight(1)->fi()
  548. ->beginIF(!in_array($type, array('all', 'finishedBy', 'assignedTo', 'myInvolved')))->andWhere("t1.`$type`")->eq($account)->fi()
  549. ->beginIF($type == 'assignedTo')->andWhere("((t1.assignedTo = '{$account}') or (t1.mode = 'multi' and t5.`account` = '{$account}' and t1.status != 'closed' and t5.status != 'done') )")->fi()
  550. ->beginIF($type == 'assignedTo' && $this->app->rawModule == 'my' && $this->app->rawMethod == 'work')->andWhere('t1.status')->notin('closed,cancel')->fi()
  551. ->beginIF($type == 'myInvolved')
  552. ->andWhere("((t5.`account` = '{$this->app->user->account}') OR t1.`assignedTo` = '{$this->app->user->account}' OR t1.`finishedby` = '{$this->app->user->account}')")
  553. ->fi()
  554. ->orderBy($orderBy)
  555. ->beginIF($limit > 0)->limit($limit)->fi()
  556. ->page($pager, 't1.id')
  557. ->fetchAll('id', false);
  558. }
  559. /**
  560. * Format the task with valid datetime that set the zero date or datetime to null.
  561. *
  562. * @param object $task
  563. * @return object
  564. */
  565. protected function formatDatetime($task)
  566. {
  567. if(empty($task)) return $task;
  568. if($this->config->edition != 'open')
  569. {
  570. $fieldList = $this->loadModel('workflowfield')->getList('task');
  571. foreach($fieldList as $field)
  572. {
  573. if($field->type == 'date' || $field->type == 'datetime') $this->config->task->dateFields[] = $field->field;
  574. }
  575. }
  576. foreach($task as $key => $value)
  577. {
  578. if(!in_array($key, $this->config->task->dateFields)) continue;
  579. if(!empty($value) && is_string($value) && helper::isZeroDate($value)) $task->$key = null;
  580. if(isset($task->$key) && empty($value)) $task->$key = null;
  581. }
  582. return $task;
  583. }
  584. /**
  585. * 获取多人任务的完成者。
  586. * Get the users who finished the multiple task.
  587. *
  588. * @param int $taskID
  589. * @param array $team
  590. * @access protected
  591. * @return array
  592. */
  593. protected function getFinishedUsers($taskID = 0, $team = array())
  594. {
  595. return $this->dao->select('id,account')->from(TABLE_TASKTEAM)
  596. ->where('task')->eq($taskID)
  597. ->andWhere('status')->in('done,closed')
  598. ->beginIF($team)->andWhere('account')->in($team)->fi()
  599. ->fetchPairs('id', 'account');
  600. }
  601. /**
  602. * Get left workhour of task after deleting a workhour of task.
  603. *
  604. * @param object $effort
  605. * @param object $task
  606. * @return float
  607. */
  608. protected function getLeftAfterDeleteWorkhour($effort, $task)
  609. {
  610. $left = $task->left;
  611. if($effort->isLast)
  612. {
  613. $lastTwoEfforts = $this->dao->select('`left`')->from(TABLE_EFFORT)
  614. ->where('objectID')->eq($effort->objectID)
  615. ->andWhere('objectType')->eq('task')
  616. ->andWhere('deleted')->eq('0')
  617. ->orderBy('date desc,id desc')->limit(2)->fetchAll();
  618. $lastTwoEfforts = isset($lastTwoEfforts[1]) ? $lastTwoEfforts[1] : '';
  619. if($lastTwoEfforts) $left = $lastTwoEfforts->left;
  620. if(empty($lastTwoEfforts) && $left == 0) $left = $task->estimate;
  621. }
  622. /* 如果该任务是多人团队任务则做一些额外的处理。*/
  623. if(empty($task->team)) return (float)$left;
  624. /* 获取要删除的工时的团队,如果要删除的工时的用户不是团队成员则不做任何处理。*/
  625. $currentTeam = $this->getTeamByAccount($task->team, $effort->account, array('effortID' => $effort->id, 'order' => $effort->order));
  626. if(!$currentTeam) return $left;
  627. $left = $currentTeam->left;
  628. if($task->mode == 'multi') /* 如果任务是多人并行任务。注:多人串行任务的mode是linear。*/
  629. {
  630. /* 获取要删除的工时对应的用户的工时信息列表。*/
  631. $accountEfforts = $this->getTaskEfforts($currentTeam->task, $effort->account, $effort->id);
  632. $lastEffort = array_pop($accountEfforts);
  633. if($lastEffort->id == $effort->id)
  634. {
  635. $lastTwoEfforts = array_pop($accountEfforts);
  636. if($lastTwoEfforts) $left = $lastTwoEfforts->left;
  637. }
  638. }
  639. /* 更新要删除的工时对应的用户的任务信息。*/
  640. $newTeamInfo = new stdclass();
  641. $newTeamInfo->consumed = $currentTeam->consumed - $effort->consumed;
  642. if($currentTeam->status != 'done') $newTeamInfo->left = $left;
  643. if($currentTeam->status == 'done' && $left > 0 && $task->mode == 'multi')
  644. {
  645. $newTeamInfo->status = 'doing';
  646. $newTeamInfo->left = $left;
  647. }
  648. if($currentTeam->status != 'done' && $newTeamInfo->consumed > 0 && $left == 0) $newTeamInfo->status = 'done';
  649. if($task->mode == 'multi' && $currentTeam->status == 'done' && ($newTeamInfo->consumed == 0 && $left == 0))
  650. {
  651. $newTeamInfo->status = 'doing';
  652. $newTeamInfo->left = $currentTeam->estimate;
  653. }
  654. $this->dao->update(TABLE_TASKTEAM)->data($newTeamInfo)->where('id')->eq($currentTeam->id)->exec();
  655. return (float)$left;
  656. }
  657. /**
  658. * 根据报表条件查询任务.
  659. * Get task list by report.
  660. *
  661. * @param string $field
  662. * @param string $condition
  663. * @access protected
  664. * @return object[]
  665. */
  666. protected function getListByReportCondition($field, $condition)
  667. {
  668. return $this->dao->select("id,{$field}")->from(TABLE_TASK)->where($condition)->fetchAll('id');
  669. }
  670. /**
  671. * 查询当前任务作为父任务时的状态。
  672. * Get the status of the taskID as a parent task.
  673. *
  674. * @param int $taskID
  675. * @access protected
  676. * @return string
  677. */
  678. protected function getParentStatusById($taskID)
  679. {
  680. $childrenStatus = $this->dao->select('id,status,parent')->from(TABLE_TASK)->where('parent')->eq($taskID)->andWhere('deleted')->eq('0')->fetchPairs('status', 'status');
  681. if(empty($childrenStatus)) return '';
  682. unset($childrenStatus['closed']);
  683. if(count($childrenStatus) == 0) return 'closed';
  684. unset($childrenStatus['cancel']);
  685. if(count($childrenStatus) == 0) return 'cancel';
  686. if(count($childrenStatus) == 1) return current($childrenStatus);
  687. if(isset($childrenStatus['doing']) || isset($childrenStatus['pause']) || isset($childrenStatus['wait'])) return 'doing';
  688. if(isset($childrenStatus['done'])) return 'done';
  689. return '';
  690. }
  691. /**
  692. * 如果任务是从Bug转来的,并且已经完成了,则获取提醒bug的链接。
  693. * Get a link to locate the bug if the task was transferred from the bug and it has already been finished.
  694. *
  695. * @param object $task
  696. * @param array $changes
  697. * @access protected
  698. * @return array
  699. */
  700. protected function getRemindBugLink($task, $changes)
  701. {
  702. foreach($changes as $change)
  703. {
  704. if($change['field'] == 'status' && $change['new'] == 'done')
  705. {
  706. $confirmURL = helper::createLink('bug', 'view', "id={$task->fromBug}");
  707. return array('result' => 'success', 'load' => true, 'callback' => "zui.Modal.confirm('" . sprintf($this->lang->task->remindBug, $task->fromBug) . "').then((res) => {if(res) loadModal('{$confirmURL}')});", 'closeModal' => true);
  708. }
  709. }
  710. return array();
  711. }
  712. /**
  713. * 获取edit方法的必填项。
  714. * Get required fields for edit method.
  715. *
  716. * @param object $task
  717. * @access protected
  718. * @return string
  719. */
  720. protected function getRequiredFields4Edit($task)
  721. {
  722. $execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($task->execution)->fetch();
  723. $requiredFields = ',' . $this->config->task->edit->requiredFields . ',';
  724. if($this->isNoStoryExecution($execution)) $requiredFields = str_replace(',story,', ',', $requiredFields);
  725. if(strpos(',doing,pause,', $task->status) && empty($task->left))
  726. {
  727. dao::$errors['left'] = sprintf($this->lang->task->error->leftEmptyAB, $this->lang->task->statusList[$task->status]);
  728. return false;
  729. }
  730. return trim($requiredFields, ',');
  731. }
  732. /**
  733. * 获取团队成员以及他的预计、消耗、剩余工时。
  734. * Get team account,estimate,consumed and left info.
  735. *
  736. * @param array $teamList
  737. * @param array $teamSourceList
  738. * @param array $teamEstimateList
  739. * @param array $teamConsumedList
  740. * @param array $teamLeftList
  741. * @access protected
  742. * @return object[]
  743. */
  744. protected function getTeamInfoList($teamList, $teamSourceList, $teamEstimateList, $teamConsumedList, $teamLeftList)
  745. {
  746. $teamInfoList = array();
  747. foreach($teamList as $index => $account)
  748. {
  749. if(empty($account)) continue;
  750. $teamInfo = new stdclass();
  751. $teamInfo->account = $account;
  752. $teamInfo->source = $teamSourceList[$index];
  753. $teamInfo->estimate = $teamEstimateList[$index];
  754. $teamInfo->consumed = $teamConsumedList[$index];
  755. $teamInfo->left = $teamLeftList[$index];
  756. $teamInfoList[$index] = $teamInfo;
  757. }
  758. return $teamInfoList;
  759. }
  760. /**
  761. * 维护团队成员信息。
  762. * Maintain team member information.
  763. *
  764. * @param object $member
  765. * @param string $mode multi|linear
  766. * @param bool $inTeam
  767. * @access protected
  768. * @return bool
  769. */
  770. protected function setTeamMember($member, $mode, $inTeam)
  771. {
  772. if($mode == 'multi' && $inTeam)
  773. {
  774. $this->dao->update(TABLE_TASKTEAM)
  775. ->beginIF($member->estimate)->set("estimate= estimate + {$member->estimate}")->fi()
  776. ->beginIF($member->left)->set("`left` = `left` + {$member->left}")->fi()
  777. ->beginIF($member->consumed)->set("`consumed` = `consumed` + {$member->consumed}")->fi()
  778. ->where('task')->eq($member->task)
  779. ->andWhere('account')->eq($member->account)
  780. ->exec();
  781. }
  782. else
  783. {
  784. $this->dao->insert(TABLE_TASKTEAM)->data($member)->autoCheck()->exec();
  785. }
  786. return !dao::isError();
  787. }
  788. /**
  789. * 通过任务ID列表查询任务团队信息。
  790. * Get task team by id list.
  791. *
  792. * @param array $taskIdList
  793. * @access protected
  794. * @return array[]
  795. */
  796. protected function getTeamMembersByIdList($taskIdList)
  797. {
  798. return $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in($taskIdList)->fetchGroup('task');
  799. }
  800. /**
  801. * Get a new task after deleting a workhour of task.
  802. *
  803. * @param object $effort
  804. * @param object $task
  805. * @return object
  806. */
  807. protected function getTaskAfterDeleteWorkhour($effort, $task)
  808. {
  809. /* Compute the left and consumed workhour of the task. */
  810. $consumed = $task->consumed - $effort->consumed;
  811. $left = $this->getLeftAfterDeleteWorkhour($effort, $task);
  812. /* Define and prepare one new task object to update the task. */
  813. $data = new stdclass();
  814. $data->consumed = $consumed;
  815. $data->left = $left;
  816. $data->status = ($left == 0 && $consumed != 0) ? 'done' : $task->status;
  817. if($effort->isLast && $consumed == 0 && $task->status != 'wait')
  818. {
  819. $data->status = 'wait';
  820. $data->left = $task->estimate;
  821. $data->finishedBy = '';
  822. $data->canceledBy = '';
  823. $data->closedBy = '';
  824. $data->closedReason = '';
  825. $data->finishedDate = null;
  826. $data->canceledDate = null;
  827. $data->closedDate = null;
  828. if($task->assignedTo == 'closed') $data->assignedTo = $this->app->user->account;
  829. }
  830. elseif($effort->isLast && $left != 0 && strpos('done,pause,cancel,closed', $task->status) !== false)
  831. {
  832. $data->status = 'doing';
  833. $data->finishedBy = '';
  834. $data->canceledBy = '';
  835. $data->closedBy = '';
  836. $data->closedReason = '';
  837. $data->finishedDate = null;
  838. $data->canceledDate = null;
  839. $data->closedDate = null;
  840. }
  841. elseif($consumed != 0 && $left == 0 && strpos('done,pause,cancel,closed', $task->status) === false)
  842. {
  843. $now = helper::now();
  844. $data->status = 'done';
  845. $data->assignedTo = $task->openedBy;
  846. $data->assignedDate = $now;
  847. $data->finishedBy = $this->app->user->account;
  848. $data->finishedDate = $now;
  849. }
  850. else
  851. {
  852. $data->status = $task->status;
  853. }
  854. return $data;
  855. }
  856. /**
  857. * 记录任务的版本。
  858. * Record task version.
  859. *
  860. * @param object $task
  861. * @access protected
  862. * @return bool
  863. */
  864. protected function recordTaskVersion($task)
  865. {
  866. $taskSpec = new stdclass();
  867. $taskSpec->task = $task->id;
  868. $taskSpec->version = $task->version;
  869. $taskSpec->name = $task->name;
  870. $taskSpec->estStarted = $task->estStarted;
  871. $taskSpec->deadline = $task->deadline;
  872. $this->dao->insert(TABLE_TASKSPEC)->data($taskSpec)->autoCheck()->exec();
  873. return !dao::isError();
  874. }
  875. /**
  876. * 将日志表中剩余设置为0。
  877. * Set effort left to 0.
  878. *
  879. * @param int $taskID
  880. * @param array $members
  881. * @access protected
  882. * @return bool
  883. */
  884. protected function resetEffortLeft($taskID, $members)
  885. {
  886. foreach($members as $account)
  887. {
  888. $this->dao->update(TABLE_EFFORT)->set('`left`')->eq(0)->where('account')->eq($account)
  889. ->andWhere('objectID')->eq($taskID)
  890. ->andWhere('objectType')->eq('task')
  891. ->orderBy('date_desc,id_desc')
  892. ->limit('1')
  893. ->exec();
  894. }
  895. return !dao::isError();
  896. }
  897. /**
  898. * 根据子任务以及父任务的状态更新父任务。
  899. * Update parent task status by child and parent status.
  900. *
  901. * @param object $task
  902. * @param object $childTask
  903. * @param string $status
  904. * @access protected
  905. * @return void
  906. */
  907. protected function autoUpdateTaskByStatus($task, $childTask, $status)
  908. {
  909. $now = helper::now();
  910. $account = $this->app->user->account;
  911. $data = new stdclass();
  912. $data->status = $status;
  913. if($status == 'done')
  914. {
  915. $data->assignedTo = $task->openedBy;
  916. $data->assignedDate = $now;
  917. $data->finishedBy = $account;
  918. $data->finishedDate = $now;
  919. $data->canceledBy = '';
  920. $data->canceledDate = null;
  921. }
  922. if($status == 'cancel')
  923. {
  924. $data->assignedTo = $task->openedBy;
  925. $data->assignedDate = $now;
  926. $data->finishedBy = '';
  927. $data->finishedDate = null;
  928. $data->canceledBy = $account;
  929. $data->canceledDate = $now;
  930. }
  931. if($status == 'closed')
  932. {
  933. $data->assignedTo = 'closed';
  934. $data->assignedDate = $now;
  935. $data->closedBy = $account;
  936. $data->closedDate = $now;
  937. $data->closedReason = $task->status == 'cancel' ? 'cancel' : 'done';
  938. }
  939. if($status == 'doing' || $status == 'wait')
  940. {
  941. if($task->assignedTo == 'closed')
  942. {
  943. $data->assignedTo = !empty($childTask) ? $childTask->assignedTo : $task->openedBy;
  944. $data->assignedDate = $now;
  945. }
  946. if($status == 'doing' && !empty($childTask->realStarted)) $data->realStarted = $childTask->realStarted;
  947. $data->finishedBy = '';
  948. $data->finishedDate = null;
  949. $data->canceledBy = '';
  950. $data->canceledDate = null;
  951. $data->closedBy = '';
  952. $data->closedDate = null;
  953. $data->closedReason = '';
  954. }
  955. $data->lastEditedBy = $account;
  956. $data->lastEditedDate = $now;
  957. $this->dao->update(TABLE_TASK)->data($data)->where('id')->eq($task->id)->exec();
  958. }
  959. /**
  960. * 通过拖动甘特图修改任务的预计开始日期和截止日期。
  961. * Update task estimate date and deadline through gantt.
  962. *
  963. * @param object $postData
  964. * @access protected
  965. * @return bool
  966. */
  967. protected function updateTaskEsDateByGantt($postData)
  968. {
  969. $task = $this->dao->select('*')->from(TABLE_TASK)->where('id')->eq($postData->id)->fetch();
  970. $isChildTask = $task->parent > 0;
  971. if($isChildTask) $parentTask = $this->dao->select('*')->from(TABLE_TASK)->where('id')->eq($task->parent)->fetch();
  972. $stage = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($task->execution)->andWhere('project')->eq($task->project)->fetch();
  973. $start = $isChildTask ? $parentTask->estStarted : $stage->begin;
  974. $end = $isChildTask ? $parentTask->deadline : $stage->end;
  975. $typeLang = $isChildTask ? $this->lang->task->parent : $this->lang->project->stage;
  976. if(helper::diffDate($start, $postData->startDate) > 0) dao::$errors[] = sprintf($this->lang->task->overEsStartDate, $typeLang, $typeLang);
  977. if(helper::diffDate($end, $postData->endDate) < 0) dao::$errors[] = sprintf($this->lang->task->overEsEndDate, $typeLang, $typeLang);
  978. if(dao::isError()) return false;
  979. /* Update estimate started and deadline of a task. */
  980. $this->dao->update(TABLE_TASK)
  981. ->set('estStarted')->eq($postData->startDate)
  982. ->set('deadline')->eq($postData->endDate)
  983. ->set('lastEditedBy')->eq($this->app->user->account)
  984. ->where('id')->eq($postData->id)
  985. ->exec();
  986. return !dao::isError();
  987. }
  988. /**
  989. * 通过填写的日志更新多人任务的团队表,计算多人任务的工时。
  990. * Update team of multi-task by effort.
  991. *
  992. * @param int $effortID
  993. * @param object $record
  994. * @param object $currentTeam
  995. * @param object $task
  996. * @access protected
  997. * @return void
  998. * @param string $lastDate
  999. */
  1000. protected function updateTeamByEffort($effortID, $record, $currentTeam, $task, $lastDate)
  1001. {
  1002. $this->dao->update(TABLE_TASKTEAM)
  1003. ->set("consumed = consumed + {$record->consumed}")
  1004. ->set('status')->eq($currentTeam->status)
  1005. ->beginIF($record->date >= $lastDate)->set('left')->eq($record->left)->fi()
  1006. ->where('id')->eq($currentTeam->id)
  1007. ->exec();
  1008. if($task->mode == 'linear' && empty($record->order)) $this->updateEffortOrder($effortID, $currentTeam->order);
  1009. }
  1010. /**
  1011. * 更新父子任务关系。
  1012. * Update relation of parent and child.
  1013. *
  1014. * @param int $childID
  1015. * @param int $parentID
  1016. * @access protected
  1017. * @return void
  1018. */
  1019. protected function updateRelation($childID, $parentID = 0)
  1020. {
  1021. if(empty($childID)) return;
  1022. $relation = $this->dao->select('*')->from(TABLE_RELATION)->where('AID')->eq($childID)->andWhere('relation')->eq('subdividefrom')->andWhere('AType')->eq('task')->andWhere('BType')->eq('task')->fetch();
  1023. if($relation)
  1024. {
  1025. if($parentID && $relation->BID == $parentID) return;
  1026. $this->dao->delete()->from(TABLE_RELATION)->where('BID')->eq($childID)->andWhere('relation')->eq('subdivideinto')->andWhere('AType')->eq('task')->andWhere('BType')->eq('task')->exec();
  1027. $this->dao->delete()->from(TABLE_RELATION)->where('AID')->eq($childID)->andWhere('relation')->eq('subdividefrom')->andWhere('AType')->eq('task')->andWhere('BType')->eq('task')->exec();
  1028. }
  1029. if(empty($parentID)) return;
  1030. $data = new stdclass();
  1031. $data->AType = 'task';
  1032. $data->BType = 'task';
  1033. $data->AID = $childID;
  1034. $data->BID = $parentID;
  1035. $data->relation = 'subdividefrom';
  1036. $this->dao->insert(TABLE_RELATION)->data($data)->exec();
  1037. $data->AID = $parentID;
  1038. $data->BID = $childID;
  1039. $data->relation = 'subdivideinto';
  1040. $this->dao->insert(TABLE_RELATION)->data($data)->exec();
  1041. return;
  1042. }
  1043. /**
  1044. * 归并子任务到父任务下。
  1045. * Merge child into parent
  1046. *
  1047. * @param array $tasks
  1048. * @access public
  1049. * @return array
  1050. */
  1051. public function mergeChildIntoParent($tasks)
  1052. {
  1053. $mergeTasks = function($parents, $parentID = 0) use(&$mergeTasks)
  1054. {
  1055. $tasks = array();
  1056. if(!isset($parents[$parentID])) return $tasks;
  1057. foreach($parents[$parentID] as $childTask)
  1058. {
  1059. $tasks[$childTask->id] = $childTask;
  1060. if(isset($parents[$childTask->id])) $tasks += $mergeTasks($parents, $childTask->id);
  1061. }
  1062. return $tasks;
  1063. };
  1064. $parents = array();
  1065. foreach($tasks as $task) $parents[$task->parent][$task->id] = $task;
  1066. $mergedTasks = array();
  1067. foreach($tasks as $task)
  1068. {
  1069. if(isset($mergedTasks[$task->id])) continue;
  1070. if($task->parent && isset($tasks[$task->parent])) continue;
  1071. $mergedTasks[$task->id] = $task;
  1072. $mergedTasks += $mergeTasks($parents, $task->id);
  1073. }
  1074. return $mergedTasks;
  1075. }
  1076. }