model.php 146 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951
  1. <?php
  2. /**
  3. * The model file of kanban module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  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@easycorp.ltd>
  8. * @package kanban
  9. * @version $Id: model.php 5118 2021-10-22 10:18:41Z $
  10. * @link https://www.zentao.net
  11. */
  12. class kanbanModel extends model
  13. {
  14. /**
  15. * 创建看板分组。
  16. * Create a kanban group.
  17. *
  18. * @param int $kanbanID
  19. * @param int $regionID
  20. * @access public
  21. * @return int|false
  22. */
  23. public function createGroup($kanbanID, $regionID)
  24. {
  25. $maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBANGROUP)
  26. ->where('region')->eq($regionID)
  27. ->fetch('maxOrder');
  28. $order = $maxOrder ? $maxOrder + 1 : 1;
  29. $group = new stdclass();
  30. $group->kanban = $kanbanID;
  31. $group->region = $regionID;
  32. $group->order = $order;
  33. $this->dao->insert(TABLE_KANBANGROUP)->data($group)->autoCheck()->exec();
  34. if(dao::isError()) return false;
  35. return $this->dao->lastInsertID();
  36. }
  37. /**
  38. * 创建默认看板区域。
  39. * Create a default kanban region.
  40. *
  41. * @param object $kanban
  42. * @access public
  43. * @return int|bool
  44. */
  45. public function createDefaultRegion($kanban)
  46. {
  47. $region = new stdclass();
  48. $region->name = $this->lang->kanbanregion->default;
  49. $region->kanban = $kanban->id;
  50. $region->space = $kanban->space;
  51. $region->createdBy = $this->app->user->account;
  52. $region->createdDate = helper::now();
  53. return $this->createRegion($kanban, $region);
  54. }
  55. /**
  56. * 复制看板区域。
  57. * Copy kanban regions.
  58. *
  59. * @param object $kanban
  60. * @param int $copyKanbanID
  61. * @param string $from kanban|execution
  62. * @param string $param
  63. * @access public
  64. * @return void
  65. */
  66. public function copyRegions($kanban, $copyKanbanID, $from = 'kanban', $param = 'withArchived')
  67. {
  68. if(empty($kanban) || empty($copyKanbanID)) return;
  69. $regions = $this->getRegionPairs($copyKanbanID, 0, $from);
  70. $order = 1;
  71. foreach($regions as $copyID => $copyName)
  72. {
  73. $region = new stdclass();
  74. $region->name = $copyName;
  75. $region->kanban = $kanban->id;
  76. $region->space = $kanban->space;
  77. $region->createdBy = $this->app->user->account;
  78. $region->createdDate = helper::now();
  79. $region->order = $order;
  80. $this->createRegion($kanban, $region, $copyID, $from, $param);
  81. $order ++;
  82. }
  83. }
  84. /**
  85. * 创建区域。
  86. * Create a new region.
  87. *
  88. * @param object $kanban
  89. * @param object $fromRegion
  90. * @param int $copyRegionID
  91. * @param string $from kanban|execution
  92. * @param string $param
  93. * @access public
  94. * @return int
  95. */
  96. public function createRegion($kanban, $fromRegion = null, $copyRegionID = 0, $from = 'kanban', $param = '')
  97. {
  98. $account = $this->app->user->account;
  99. $order = 1;
  100. $region = $fromRegion;
  101. if(empty($region))
  102. {
  103. $maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBANREGION)
  104. ->where('kanban')->eq($kanban->id)
  105. ->fetch('maxOrder');
  106. $order = $maxOrder ? $maxOrder + 1 : 1;
  107. $region = new stdclass();
  108. $region->kanban = $kanban->id;
  109. $region->space = $from == 'kanban' ? $kanban->space : 0;
  110. $region->name = $this->post->name;
  111. $region->createdBy = $account;
  112. $region->createdDate = helper::now();
  113. }
  114. $region->order = isset($region->order) ? $region->order : $order;
  115. $this->dao->insert(TABLE_KANBANREGION)->data($region)
  116. ->batchCheck($this->config->kanban->require->createregion, 'notempty')
  117. ->check('name', 'unique', "kanban = {$kanban->id} AND deleted = '0' AND space='$region->space'")
  118. ->autoCheck()
  119. ->exec();
  120. $regionID = $this->dao->lastInsertID();
  121. if(dao::isError()) return false;
  122. $this->loadModel('action')->create('kanbanRegion', $regionID, 'Created');
  123. if($copyRegionID)
  124. {
  125. $this->copyRegion($kanban, $regionID, $copyRegionID, $from, $param);
  126. }
  127. elseif($from == 'kanban')
  128. {
  129. $groupID = $this->createGroup((int)$kanban->id, $regionID);
  130. if(dao::isError()) return false;
  131. $this->createDefaultLane($regionID, $groupID);
  132. if(dao::isError()) return false;
  133. $this->createDefaultColumns($regionID, $groupID);
  134. if(dao::isError()) return false;
  135. }
  136. return $regionID;
  137. }
  138. /**
  139. * 复制区域。
  140. * Copy a region.
  141. *
  142. * @param object $kanban
  143. * @param int $regionID
  144. * @param int $copyRegionID
  145. * @param string $from
  146. * @param string $param
  147. * @access public
  148. * @return void
  149. */
  150. public function copyRegion($kanban, $regionID, $copyRegionID, $from = 'kanban', $param = '')
  151. {
  152. /* Gets the groups, lanes and columns of the replication region. */
  153. $copyGroups = $this->getGroupGroupByRegions((array)$copyRegionID);
  154. $copyLaneGroup = $this->getLaneGroupByRegions((array)$copyRegionID);
  155. $copyColumnGroup = $this->getColumnGroupByRegions((array)$copyRegionID, 'id_asc', $param);
  156. /* Create groups, lanes, and columns. */
  157. if(empty($copyGroups)) return $regionID;
  158. foreach($copyGroups[$copyRegionID] as $copyGroupID => $copyGroup)
  159. {
  160. $newGroupID = $this->createGroup((int)$kanban->id, $regionID);
  161. if(dao::isError()) return false;
  162. $copyLanes = isset($copyLaneGroup[$copyGroupID]) ? $copyLaneGroup[$copyGroupID] : array();
  163. $copyColumns = isset($copyColumnGroup[$copyGroupID]) ? $copyColumnGroup[$copyGroupID] : array();
  164. $lanePairs = $this->copyLanes($kanban, $copyLanes, $regionID, $newGroupID);
  165. $this->copyColumns($copyColumns, $regionID, $newGroupID, $from);
  166. if(dao::isError()) return false;
  167. if($param == 'updateTaskCell')
  168. {
  169. foreach($lanePairs as $oldLaneID => $newLaneID)
  170. {
  171. $cards = $this->dao->select('id,cards')->from(TABLE_KANBANCELL)->where('lane')->eq($oldLaneID)->andWhere('type')->eq('task')->fetchPairs();
  172. $cards = implode(',', $cards);
  173. $cards = preg_replace('/[,]+/', ',',$cards);
  174. $cards = trim($cards, ',');
  175. $group = $this->dao->select('`group`')->from(TABLE_KANBANLANE)->where('id')->eq($newLaneID)->fetch();
  176. $waitColumn = $this->dao->select('id')->from(TABLE_KANBANCOLUMN)->where('type')->eq('wait')->andWhere('`group`')->eq($group->group)->fetch();
  177. if(!empty($waitColumn)) $this->addKanbanCell($kanban->id, $newLaneID, $waitColumn->id, 'task', $cards);
  178. }
  179. }
  180. }
  181. }
  182. /**
  183. * 复制区域中的泳道。
  184. * Copy lanes.
  185. *
  186. * @param object $kanban
  187. * @param array $copyLanes
  188. * @param int $regionID
  189. * @param int $newGroupID
  190. * @access public
  191. * @return array|false
  192. */
  193. public function copyLanes($kanban, $copyLanes, $regionID, $newGroupID)
  194. {
  195. $lanePairs = array();
  196. foreach($copyLanes as $copyLane)
  197. {
  198. if(is_array($copyLane)) $copyLane = (object)$copyLane;
  199. $laneID = $copyLane->id;
  200. $copyLane->name = $copyLane->title;
  201. unset($copyLane->id);
  202. unset($copyLane->actionList);
  203. unset($copyLane->title);
  204. $copyLane->region = $regionID;
  205. $copyLane->group = $newGroupID;
  206. $copyLane->lastEditedTime = helper::now();
  207. $lanePairs[$laneID] = $this->createLane((int)$kanban->id, $regionID, $copyLane, 'copy');
  208. if(dao::isError()) return false;
  209. }
  210. return $lanePairs;
  211. }
  212. /**
  213. * 复制区域中的列。
  214. * Copy columns.
  215. *
  216. * @param array $copyColumns
  217. * @param int $regionID
  218. * @param int $newGroupID
  219. * @param string $from
  220. * @access public
  221. * @return void
  222. */
  223. public function copyColumns($copyColumns, $regionID, $newGroupID, $from = 'kanban')
  224. {
  225. $parentColumns = array();
  226. foreach($copyColumns as $copyColumn)
  227. {
  228. if(is_array($copyColumn)) $copyColumn = (object)$copyColumn;
  229. $copyColumnID = $copyColumn->id;
  230. $copyColumn->name = $copyColumn->title;
  231. unset($copyColumn->id);
  232. unset($copyColumn->title);
  233. unset($copyColumn->actionList);
  234. unset($copyColumn->asParent);
  235. unset($copyColumn->parentName);
  236. $copyColumn->region = $regionID;
  237. $copyColumn->group = $newGroupID;
  238. if($copyColumn->parent > 0 && isset($parentColumns[$copyColumn->parent]))
  239. {
  240. $copyColumn->parent = $parentColumns[$copyColumn->parent];
  241. }
  242. $parentColumnID = $this->createColumn($regionID, $copyColumn, $from, 'copy');
  243. if($copyColumn->parent < 0) $parentColumns[$copyColumnID] = $parentColumnID;
  244. if(dao::isError()) return false;
  245. }
  246. }
  247. /**
  248. * 创建默认泳道。
  249. * Create default lane.
  250. *
  251. * @param int $regionID
  252. * @param int $groupID
  253. * @access public
  254. * @return int
  255. */
  256. public function createDefaultLane($regionID, $groupID)
  257. {
  258. $lane = new stdclass();
  259. $lane->name = $this->lang->kanbanlane->default;
  260. $lane->group = $groupID;
  261. $lane->region = $regionID;
  262. $lane->type = 'common';
  263. $lane->lastEditedTime = helper::now();
  264. $lane->color = '#7ec5ff';
  265. $lane->order = 1;
  266. $lane->groupby = '';
  267. $lane->extra = '';
  268. $this->dao->insert(TABLE_KANBANLANE)->data($lane)->exec();
  269. $laneID = $this->dao->lastInsertId();
  270. return $laneID;
  271. }
  272. /**
  273. * 创建默认看板列。
  274. * Create default kanban columns.
  275. *
  276. * @param int $regionID
  277. * @param int $groupID
  278. * @access public
  279. * @return void
  280. */
  281. public function createDefaultColumns($regionID, $groupID)
  282. {
  283. $order = 1;
  284. foreach($this->lang->kanban->defaultColumn as $columnName)
  285. {
  286. $column = new stdclass();
  287. $column->region = $regionID;
  288. $column->group = $groupID;
  289. $column->name = $columnName;
  290. $column->order = $order;
  291. $column->limit = -1;
  292. $column->color = '#333';
  293. $column->type = '';
  294. $this->createColumn($regionID, $column, 'kanban', 'copy');
  295. $order ++;
  296. }
  297. return !dao::isError();
  298. }
  299. /**
  300. * 创建看板列。
  301. * Create a column.
  302. *
  303. * @param int $regionID
  304. * @param object $column
  305. * @param string $from kanban|execution
  306. * @param string $mode new|copy
  307. * @access public
  308. * @return int|false
  309. */
  310. public function createColumn($regionID, $column = null, $from = 'kanban', $mode = 'new')
  311. {
  312. if($mode == 'new')
  313. {
  314. if(!$column->limit && empty($column->noLimit)) dao::$errors['limit'][] = sprintf($this->lang->error->notempty, $this->lang->kanban->WIP);
  315. if(!preg_match("/^-?\d+$/", (string)$column->limit) || (!isset($column->noLimit) && $column->limit <= 0)) dao::$errors['limit'] = $this->lang->kanban->error->mustBeInt;
  316. if(dao::isError()) return false;
  317. }
  318. $limit = $column->limit;
  319. if(isset($column->parent) && $column->parent > 0)
  320. {
  321. /* Create a child column. */
  322. $parentColumn = $this->getColumnByID((int)$column->parent);
  323. if($parentColumn->limit != -1)
  324. {
  325. /* The WIP of the child column is infinite or greater than the WIP of the parent column. */
  326. $sumChildLimit = $this->dao->select('SUM(`limit`) AS sumChildLimit')->from(TABLE_KANBANCOLUMN)->where('parent')->eq($column->parent)->andWhere('deleted')->eq(0)->fetch('sumChildLimit');
  327. if($limit == -1 || (((int)$limit + (int)$sumChildLimit) > $parentColumn->limit)) dao::$errors['limit'][] = $this->lang->kanban->error->parentLimitNote;
  328. if(dao::isError()) return false;
  329. }
  330. }
  331. if($mode == 'new' && $column->order)
  332. {
  333. $this->dao->update(TABLE_KANBANCOLUMN)
  334. ->set('`order` = `order` + 1')
  335. ->where('`group`')->eq($column->group)
  336. ->andWhere('`order`')->ge($column->order)
  337. ->exec();
  338. }
  339. $this->dao->insert(TABLE_KANBANCOLUMN)->data($column, 'noLimit,position,copyItems')
  340. ->batchCheck($this->config->kanban->require->createcolumn, 'notempty')
  341. ->autoCheck()
  342. ->exec();
  343. if(dao::isError()) return false;
  344. $columnID = $this->dao->lastInsertID();
  345. $this->loadModel('action')->create('kanbanColumn', $columnID, 'Created');
  346. if($from == 'kanban') $this->dao->update(TABLE_KANBANCOLUMN)->set('type')->eq("column{$columnID}")->where('id')->eq($columnID)->exec();
  347. /* Add kanban cell. */
  348. $lanes = $this->dao->select('id,type')->from(TABLE_KANBANLANE)->where('`group`')->eq($column->group)->fetchPairs();
  349. $kanbanID = $this->dao->select('kanban')->from(TABLE_KANBANREGION)->where('id')->eq($regionID)->fetch('kanban');
  350. foreach($lanes as $laneID => $laneType) $this->addKanbanCell((int)$kanbanID, $laneID, $columnID, $laneType);
  351. return $columnID;
  352. }
  353. /**
  354. * 拆分看板列。
  355. * Split column.
  356. *
  357. * @param int $columnID
  358. * @param array $columns
  359. * @access public
  360. * @return void
  361. */
  362. public function splitColumn($columnID, $columns)
  363. {
  364. $this->loadModel('action');
  365. $column = $this->getColumnByID($columnID);
  366. $maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBANCOLUMN)->where('`group`')->eq($column->group)->fetch('maxOrder');
  367. $order = $maxOrder ? $maxOrder + 1 : 1;
  368. $sumChildLimit = 0;
  369. $childrenColumn = array();
  370. foreach($columns as $childColumn)
  371. {
  372. $childColumn->parent = $column->id;
  373. $childColumn->region = $column->region;
  374. $childColumn->group = $column->group;
  375. $childColumn->limit = $childColumn->noLimit == -1 ? -1 : $childColumn->limit;
  376. $childColumn->order = $order;
  377. $sumChildLimit += $childColumn->limit == -1 ? 0 : $childColumn->limit;
  378. if(!$this->checkChildColumn($column, $childColumn, $sumChildLimit)) return false;
  379. $order ++;
  380. $childrenColumn[] = $childColumn;
  381. }
  382. foreach($childrenColumn as $i => $childColumn)
  383. {
  384. $this->dao->insert(TABLE_KANBANCOLUMN)->data($childColumn, 'noLimit')
  385. ->autoCheck()
  386. ->batchCheck($this->config->kanban->splitcolumn->requiredFields, 'notempty')
  387. ->exec();
  388. if(dao::isError()) return false;
  389. $childColumnID = $this->dao->lastInsertID();
  390. $this->dao->update(TABLE_KANBANCOLUMN)->set('type')->eq("column{$childColumnID}")->where('id')->eq($childColumnID)->exec();
  391. $this->action->create('kanbanColumn', $childColumnID, 'created');
  392. $this->kanbanTao->addChildColumnCell($columnID, $childColumnID, $i);
  393. }
  394. $this->dao->update(TABLE_KANBANCOLUMN)->set('parent')->eq(-1)->where('id')->eq($columnID)->exec();
  395. }
  396. /**
  397. * 检查看板列信息是否合法。
  398. * Check the column information is legal.
  399. *
  400. * @param object $column
  401. * @param object $childColumn
  402. * @param int $sumChildLimit
  403. * @access public
  404. * @return bool
  405. */
  406. public function checkChildColumn($column, $childColumn, $sumChildLimit)
  407. {
  408. if(empty($childColumn->name))
  409. {
  410. dao::$errors['name'] = sprintf($this->lang->error->notempty, $this->lang->kanbancolumn->name);
  411. return false;
  412. }
  413. if(!preg_match("/^-?\d+$/", (string)$childColumn->limit) || (!$childColumn->noLimit && $childColumn->limit <= 0))
  414. {
  415. dao::$errors['limit'] = $this->lang->kanban->error->mustBeInt;
  416. return false;
  417. }
  418. if($column->limit != -1 && ($childColumn->limit == -1 || ($column->limit < $sumChildLimit)))
  419. {
  420. dao::$errors['limit'] = $this->lang->kanban->error->parentLimitNote;
  421. return false;
  422. }
  423. return true;
  424. }
  425. /**
  426. * 创建看板卡片。
  427. * Create a kanban card.
  428. *
  429. * @param int $columnID
  430. * @param object $card
  431. * @access public
  432. * @return bool|int
  433. */
  434. public function createCard($columnID, $card)
  435. {
  436. if($card->estimate < 0)
  437. {
  438. dao::$errors['estimate'] = $this->lang->kanbancard->error->recordMinus;
  439. return false;
  440. }
  441. if($card->end && $card->begin > $card->end)
  442. {
  443. dao::$errors['end'] = $this->lang->kanbancard->error->endSmall;
  444. return false;
  445. }
  446. $card = $this->loadModel('file')->processImgURL($card, $this->config->kanban->editor->createcard['id'], $this->post->uid);
  447. $this->dao->insert(TABLE_KANBANCARD)->data($card)->autoCheck()
  448. ->checkIF($card->estimate != '', 'estimate', 'float')
  449. ->batchCheck($this->config->kanban->createcard->requiredFields, 'notempty')
  450. ->exec();
  451. if(dao::isError()) return false;
  452. $cardID = $this->dao->lastInsertID();
  453. $this->loadModel('action')->create('kanbanCard', $cardID, 'created');
  454. $this->file->saveUpload('kanbancard', $cardID);
  455. $this->file->updateObjectID($this->post->uid, $cardID, 'kanbancard');
  456. $this->addKanbanCell((int)$card->kanban, (int)$this->post->lane, $columnID, 'common', (string)$cardID);
  457. return $cardID;
  458. }
  459. /**
  460. * 转入其它看板的卡片。
  461. * Import card.
  462. *
  463. * @param int $kanbanID
  464. * @param int $regionID
  465. * @param int $groupID
  466. * @param int $columnID
  467. * @access public
  468. * @return array|false
  469. */
  470. public function importCard($kanbanID, $regionID, $groupID, $columnID)
  471. {
  472. $importIDList = $this->post->cards;
  473. $targetLaneID = $this->post->targetLane;
  474. if(!$importIDList || !$targetLaneID) return false;
  475. $updateData = new stdclass();
  476. $updateData->kanban = $kanbanID;
  477. $updateData->region = $regionID;
  478. $updateData->group = $groupID;
  479. $this->dao->update(TABLE_KANBANCARD)->data($updateData)->where('id')->in($importIDList)->exec();
  480. $kanban = $this->getByID($kanbanID);
  481. $oldCardsKanban = array();
  482. $kanbanUsers = trim($kanban->owner) . ',' . trim($kanban->team);
  483. $users = $this->loadModel('user')->getPairs('noclosed|nodeleted', '', 0, $kanbanUsers);
  484. $cardList = $this->dao->select('*')->from(TABLE_KANBANCARD)->where('id')->in($importIDList)->fetchAll('id');
  485. foreach($cardList as $cardID => $card)
  486. {
  487. $oldCardsKanban[$cardID] = $card->kanban;
  488. if(empty($card->assignedTo)) continue;
  489. $this->kanbanTao->updateCardAssignedTo($cardID, $card->assignedTo, $users);
  490. }
  491. if(!dao::isError())
  492. {
  493. $this->removeKanbanCell('common', $importIDList, $oldCardsKanban);
  494. $cards = implode(',', $importIDList);
  495. $this->addKanbanCell($kanbanID, (int)$targetLaneID, $columnID, 'common', $cards);
  496. return $importIDList;
  497. }
  498. return false;
  499. }
  500. /**
  501. * 转入其它对象作为看板卡片
  502. * Import object.
  503. *
  504. * @param int $kanbanID
  505. * @param int $regionID
  506. * @param int $groupID
  507. * @param int $columnID
  508. * @param string $objectType
  509. * @access public
  510. * @return array|false
  511. */
  512. public function importObject($kanbanID, $regionID, $groupID, $columnID, $objectType)
  513. {
  514. $objectIDList = $this->post->{$objectType . 's'};
  515. $targetLaneID = $this->post->targetLane;
  516. $objectCards = array();
  517. $now = helper::now();
  518. foreach($objectIDList as $objectID)
  519. {
  520. $cardData = new stdclass();
  521. $cardData->kanban = $kanbanID;
  522. $cardData->region = $regionID;
  523. $cardData->group = $groupID;
  524. $cardData->fromID = $objectID;
  525. $cardData->fromType = $objectType;
  526. $cardData->createdBy = $this->app->user->account;
  527. $cardData->createdDate = $now;
  528. $this->dao->insert(TABLE_KANBANCARD)->data($cardData)->exec();
  529. $cardID = $this->dao->lastInsertID();
  530. $objectCards[$cardID] = $objectID;
  531. }
  532. if(!dao::isError())
  533. {
  534. $cards = implode(',', array_keys($objectCards));
  535. $this->addKanbanCell($kanbanID, (int)$targetLaneID, $columnID, 'common', $cards);
  536. return $objectCards;
  537. }
  538. return false;
  539. }
  540. /**
  541. * 批量创建卡片。
  542. * Batch create kanban cards.
  543. *
  544. * @param int $kanbanID
  545. * @param int $regionID
  546. * @param int $groupID
  547. * @param int $columnID
  548. * @param array $cards
  549. * @access public
  550. * @return void
  551. */
  552. public function batchCreateCard($kanbanID, $regionID, $groupID, $columnID, $cards)
  553. {
  554. $now = helper::now();
  555. foreach($cards as $i => $card)
  556. {
  557. if($card->estimate < 0)
  558. {
  559. dao::$errors["estimate[$i]"] = $this->lang->kanbancard->error->recordMinus;
  560. return false;
  561. }
  562. if($card->end && $card->begin > $card->end)
  563. {
  564. dao::$errors["end[$i]"] = $this->lang->kanbancard->error->endSmall;
  565. return false;
  566. }
  567. }
  568. foreach($cards as $card)
  569. {
  570. $card->kanban = $kanbanID;
  571. $card->region = $regionID;
  572. $card->group = $groupID;
  573. $card->createdBy = $this->app->user->account;
  574. $card->createdDate = $now;
  575. $card->assignedDate = $now;
  576. $card->color = '#fff';
  577. $this->dao->insert(TABLE_KANBANCARD)->data($card, 'lane')->autoCheck()
  578. ->checkIF($card->estimate != '', 'estimate', 'float')
  579. ->batchCheck($this->config->kanban->createcard->requiredFields, 'notempty')
  580. ->exec();
  581. if(!dao::isError())
  582. {
  583. $cardID = $this->dao->lastInsertID();
  584. $this->addKanbanCell($kanbanID, $card->lane, $columnID, 'common', (string)$cardID);
  585. $this->loadModel('action')->create('kanbancard', $cardID, 'created');
  586. }
  587. }
  588. }
  589. /**
  590. * 获取看板数据。
  591. * Get kanban by id.
  592. *
  593. * @param int $kanbanID
  594. * @access public
  595. * @return object
  596. */
  597. public function getByID($kanbanID)
  598. {
  599. $kanban = $this->dao->findByID($kanbanID)->from(TABLE_KANBAN)->fetch();
  600. $kanban = $this->loadModel('file')->replaceImgURL($kanban, 'desc');
  601. return $kanban;
  602. }
  603. /**
  604. * 获取看板键值对。
  605. * Get kanban pairs.
  606. *
  607. * @access public
  608. * @return array
  609. */
  610. public function getPairs()
  611. {
  612. $idList = $this->getCanViewObjects();
  613. return $this->dao->select('id,name')->from(TABLE_KANBAN)
  614. ->where('id')->in($idList)
  615. ->andWhere('deleted')->eq('0')
  616. ->fetchPairs();
  617. }
  618. /**
  619. * 获取看板所有数据。
  620. * Get kanban data.
  621. *
  622. * @param int $kanbanID
  623. * @param mixed $regionIDList
  624. * @access public
  625. * @return array
  626. */
  627. public function getKanbanData($kanbanID, $regionIDList = '')
  628. {
  629. $regions = $this->getRegionPairs($kanbanID);
  630. if(empty($regionIDList))
  631. {
  632. $regionIDList = array_keys($regions);
  633. }
  634. elseif(!is_array($regionIDList))
  635. {
  636. $regionIDList = array($regionIDList);
  637. }
  638. $groupGroup = $this->getGroupGroupByRegions($regionIDList);
  639. $laneGroup = $this->getLaneGroupByRegions($regionIDList);
  640. $columnGroup = $this->getColumnGroupByRegions($regionIDList);
  641. $cardGroup = $this->getCardGroupByKanban($kanbanID);
  642. $kanbanList = array();
  643. foreach($regionIDList as $regionID)
  644. {
  645. $regionData = array();
  646. $heading = new stdclass();
  647. $heading->title = new stdClass();
  648. $heading->title->html = zget($regions, $regionID, '') . '<span class="icon icon-angle-top"></span>';
  649. $heading->actions = $this->getRegionActions($kanbanID, $regionID, count($regions));
  650. $regionData['key'] = "region{$regionID}";
  651. $regionData['id'] = $regionID;
  652. $regionData['heading'] = $heading;
  653. $regionData['toggleFromHeading'] = true;
  654. $groups = zget($groupGroup, $regionID, array());
  655. $kanbanList[] = $this->kanbanTao->buildRegionData($regionData, $groups, $laneGroup, $columnGroup, $cardGroup);
  656. }
  657. return $kanbanList;
  658. }
  659. /**
  660. * 获取看板区域上的操作按钮。
  661. * Get region actions.
  662. *
  663. * @param int $kanbanID
  664. * @param int|string $regionID
  665. * @param int $regionCount
  666. * @access public
  667. * @return array
  668. */
  669. public function getRegionActions($kanbanID, $regionID, $regionCount)
  670. {
  671. $action = array();
  672. $actions = array();
  673. $action['type'] = 'dropdown';
  674. $action['icon'] = 'ellipsis-v';
  675. $action['caret'] = false;
  676. $action['items'] = array();
  677. if(common::hasPriv('kanban', 'createRegion')) $action['items'][] = array('text' => $this->lang->kanban->createRegion, 'url' => helper::createLink('kanban', 'createRegion', "kanbanID=$kanbanID"), 'data-toggle' => 'modal', 'icon' => 'plus');
  678. if(common::hasPriv('kanban', 'editRegion')) $action['items'][] = array('text' => $this->lang->kanban->editRegion, 'url' => helper::createLink('kanban', 'editRegion', "regionID=$regionID"), 'data-toggle' => 'modal', 'icon' => 'edit');
  679. if(common::hasPriv('kanban', 'sortRegion')) $action['items'][] = array('text' => $this->lang->kanban->sortRegion, 'url' => 'javascript:;', 'icon' => 'move', 'data-on' => 'click', 'data-call' => 'sortItems', 'data-params' => 'event', 'data-type' => 'region', 'data-id' => $regionID);
  680. if(common::hasPriv('kanban', 'createLane')) $action['items'][] = array('text' => $this->lang->kanban->createLane, 'url' => helper::createLink('kanban', 'createLane', "kanbanID=$kanbanID&regionID=$regionID"), 'data-toggle' => 'modal', 'icon' => 'plus');
  681. if(common::hasPriv('kanban', 'deleteRegion') and ($regionCount > 1)) $action['items'][] = array('text' => $this->lang->kanban->deleteRegion, 'url' => helper::createLink('kanban', 'deleteRegion', "regionID=$regionID"), 'data-confirm' => $this->lang->kanbanregion->confirmDelete, 'icon' => 'trash', 'innerClass' => 'ajax-submit');
  682. $action['items'][] = array('type' => 'divider');
  683. if(commonModel::hasPriv('kanban', 'viewArchivedCard')) $action['items'][] = array('text' => $this->lang->kanban->viewArchivedCard, 'url' => "javascript:loadMore(\"Card\", $regionID)", 'icon' => 'card-archive');
  684. if(commonModel::hasPriv('kanban', 'viewArchivedColumn')) $action['items'][] = array('text' => $this->lang->kanban->viewArchivedColumn, 'url' => "javascript:loadMore(\"Column\", $regionID)", 'icon' => 'col-archive');
  685. $actions[] = $action;
  686. return $actions;
  687. }
  688. /**
  689. * 获取专业研发看板区域上的操作按钮。
  690. * Get RD region actions.
  691. *
  692. * @param int $kanbanID
  693. * @param int $regionID
  694. * @param int $regionCount
  695. * @access public
  696. * @return array
  697. */
  698. public function getRDRegionActions($kanbanID, $regionID, $regionCount = 1)
  699. {
  700. $action = array();
  701. $actions = array();
  702. $action['type'] = 'dropdown';
  703. $action['icon'] = 'ellipsis-v';
  704. $action['caret'] = false;
  705. $action['items'] = array();
  706. if(common::hasPriv('kanban', 'createRegion')) $action['items'][] = array('text' => $this->lang->kanban->createRegion, 'url' => helper::createLink('kanban', 'createRegion', "kanbanID=$kanbanID&from=execution"), 'data-toggle' => 'modal', 'icon' => 'plus', 'class' => 'kanban-createRegion-btn');
  707. if(common::hasPriv('kanban', 'editRegion')) $action['items'][] = array('text' => $this->lang->kanban->editRegion, 'url' => helper::createLink('kanban', 'editRegion', "regionID=$regionID"), 'data-toggle' => 'modal', 'icon' => 'edit');
  708. if(common::hasPriv('kanban', 'createLane')) $action['items'][] = array('text' => $this->lang->kanban->createLane, 'url' => helper::createLink('kanban', 'createLane', "kanbanID=$kanbanID&regionID=$regionID&from=execution"), 'data-toggle' => 'modal', 'icon' => 'plus');
  709. if(common::hasPriv('kanban', 'deleteRegion') && ($regionCount > 1)) $action['items'][] = array('text' => $this->lang->kanban->deleteRegion, 'url' => helper::createLink('kanban', 'deleteRegion', "regionID=$regionID"), 'data-confirm' => $this->lang->kanbanregion->confirmDelete, 'icon' => 'trash', 'innerClass' => 'ajax-submit');
  710. $actions[] = $action;
  711. return $actions;
  712. }
  713. /**
  714. * 获取计划看板。
  715. * Get plan kanban.
  716. *
  717. * @param object $product
  718. * @param string $branchID
  719. * @param array $planGroup
  720. * @access public
  721. * @return array
  722. */
  723. public function getPlanKanban($product, $branchID = '0', $planGroup = array())
  724. {
  725. $this->loadModel('productplan');
  726. $lanes = $columns = $kanbanData = $planList = $lanes = $columns = $columnCards = array();
  727. $colorIndex = 0;
  728. $laneOrder = 1;
  729. $cardActions = array('view', 'createExecution', 'linkStory', 'linkBug', 'edit', 'start', 'finish', 'close', 'activate', 'delete');
  730. $branches = $this->kanbanTao->getBranchesForPlanKanban($product, $branchID);
  731. foreach($branches as $id => $name)
  732. {
  733. if($product->type != 'normal') $plans = isset($planGroup[$product->id][$id]) ? array_filter($planGroup[$product->id][$id]) : array();
  734. if($product->type == 'normal') $plans = $planGroup;
  735. foreach($plans as $plan)
  736. {
  737. if(empty($plan) || $plan->parent == -1) continue;
  738. $plan->isParent = false;
  739. $planBegin = $plan->begin == $this->config->productplan->future ? $this->lang->productplan->future : date('m-d', strtotime($plan->begin));
  740. $planEnd = $plan->end == $this->config->productplan->future ? $this->lang->productplan->future : date('m-d', strtotime($plan->end));
  741. $item = array('id' => $plan->id, 'name' => $plan->id, 'title' => htmlspecialchars_decode($plan->title), 'status' => $plan->status);
  742. $item['statusLabel'] = zget($this->lang->productplan->statusList, $plan->status);
  743. $item['delay'] = helper::today() > $plan->end ? true : false;
  744. $item['desc'] = strip_tags(htmlspecialchars_decode($plan->desc));
  745. $item['dateLine'] = in_array($this->lang->productplan->future, array($planBegin, $planEnd)) ? $this->lang->productplan->future : $planBegin . ' ' . $this->lang->productplan->to . ' ' . $planEnd;
  746. $item['actionList'] = array();
  747. foreach($cardActions as $action)
  748. {
  749. if($this->productplan->isClickable($plan, $action)) $item['actionList'][] = $action;
  750. }
  751. $planList['lane' . $id][$plan->status][] = $item;
  752. if(!isset($columnCards[$plan->status])) $columnCards[$plan->status] = 0;
  753. $columnCards[$plan->status] ++;
  754. }
  755. $lanes[] = array('id' => $id, 'name' => 'lane' . $id, 'title' => $name, 'color' => $this->config->productplan->laneColorList[$colorIndex], 'order' => $laneOrder);
  756. $laneOrder ++;
  757. $colorIndex ++;
  758. if($colorIndex == count($this->config->productplan->laneColorList)) $colorIndex = 0;
  759. }
  760. foreach($this->lang->kanban->defaultColumn as $columnType => $columnName) $columns[] = array('id' => $columnType, 'name' => $columnType, 'title' => $columnName);
  761. foreach($columns as $key => $column) $columns[$key]['cards'] = !empty($columnCards[$column['name']]) ? $columnCards[$column['name']] : 0;
  762. $groupData['key'] = 'planKanban';
  763. $groupData['data'] = array('lanes' => $lanes, 'cols' => $columns, 'items' => $planList);
  764. $kanbanData[] = array('items' => array($groupData), 'key' => 'planKanban');
  765. return $kanbanData;
  766. }
  767. /**
  768. * 获取专业研发看板。
  769. * Get a RD kanban data.
  770. *
  771. * @param int $executionID
  772. * @param string $browseType all|story|task|bug
  773. * @param string $orderBy
  774. * @param int $regionID
  775. * @param string $groupBy
  776. * @param string $searchValue
  777. * @access public
  778. * @return array
  779. */
  780. public function getRDKanban($executionID, $browseType = 'all', $orderBy = 'id_desc', $regionID = 0, $groupBy = 'default', $searchValue = '')
  781. {
  782. $kanbanList = array();
  783. $execution = $this->loadModel('execution')->getByID($executionID);
  784. if($groupBy != 'default' && $groupBy != '') return $this->getRDKanbanByGroup($execution, $browseType, $orderBy, $regionID, $groupBy, $searchValue);
  785. $regions = $this->getRegionPairs($executionID, $regionID, 'execution');
  786. $regionIDList = $regionID == 0 ? array_keys($regions) : array(0 => $regionID);
  787. $groupGroup = $this->getGroupGroupByRegions($regionIDList);
  788. $laneGroup = $this->getLaneGroupByRegions($regionIDList, $browseType);
  789. foreach($laneGroup as $lanes)
  790. {
  791. foreach($lanes as $lane)
  792. {
  793. $lane['execution'] = $executionID;
  794. if(in_array($execution->attribute, array('request', 'design', 'review')) && $lane['type'] == 'bug') continue 2;
  795. if(in_array($execution->attribute, array('request', 'review')) && $lane['type'] == 'story') continue 2;
  796. $this->refreshCards((array)$lane);
  797. $lane['defaultCardType'] = $lane['type'];
  798. }
  799. }
  800. $columnGroup = $this->getRDColumnGroupByRegions($regionIDList, array_keys($laneGroup));
  801. $cardGroup = $this->getCardGroupByExecution($executionID, $browseType, $orderBy, $searchValue);
  802. foreach($regions as $regionID => $regionName)
  803. {
  804. $regionData = array();
  805. $heading = new stdclass();
  806. $heading->title = new stdClass();
  807. $heading->title->html = $regionName . '<span class="icon icon-angle-top"></span>';
  808. $heading->actions = $this->getRDRegionActions($executionID, $regionID, count($regions));
  809. $regionData['key'] = "region{$regionID}";
  810. $regionData['id'] = $regionID;
  811. $regionData['heading'] = $heading;
  812. $regionData['toggleFromHeading'] = true;
  813. $regionData['items'] = array();
  814. $groups = zget($groupGroup, $regionID, array());
  815. $regionData = $this->kanbanTao->buildRDRegionData($regionData, $groups, $laneGroup, $columnGroup, $cardGroup, $searchValue);
  816. $kanbanList[] = $regionData;
  817. }
  818. return $kanbanList;
  819. }
  820. /**
  821. * 获取专业研发看板分组视图下的看板数据。
  822. * Get kanban data for group view of RD kanban.
  823. *
  824. * @param object $execution
  825. * @param string $browseType
  826. * @param string $groupBy
  827. * @param string $searchValue
  828. * @param string $orderBy
  829. * @access private
  830. * @return array
  831. * @param int $regionID
  832. */
  833. private function getRDKanbanByGroup($execution, $browseType, $orderBy, $regionID, $groupBy, $searchValue)
  834. {
  835. $regionData = array();
  836. $heading = new stdclass();
  837. $heading->title = $execution->name;
  838. $heading->actions = $this->getRDRegionActions($execution->id, $regionID, 1);
  839. $regionData['key'] = "region{$execution->id}";
  840. $regionData['id'] = $execution->id;
  841. $regionData['heading'] = $heading;
  842. $regionData['toggleFromHeading'] = true;
  843. $regionData['items'] = $this->getKanban4Group($execution->id, $browseType, $groupBy, $searchValue, $orderBy);
  844. $kanbanList[] = $regionData;
  845. return $kanbanList;
  846. }
  847. /**
  848. * 获取看板区域。
  849. * Get region by id.
  850. *
  851. * @param int $regionID
  852. * @access public
  853. * @return object
  854. */
  855. public function getRegionByID($regionID)
  856. {
  857. return $this->dao->findByID($regionID)->from(TABLE_KANBANREGION)->fetch();
  858. }
  859. /**
  860. * 获取看板区域的键值对。
  861. * Get ordered region pairs.
  862. *
  863. * @param int $kanbanID
  864. * @param int $regionID
  865. * @param string $from kanban|execution
  866. * @access public
  867. * @return array
  868. */
  869. public function getRegionPairs($kanbanID, $regionID = 0, $from = 'kanban')
  870. {
  871. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getRegionPairs();
  872. return $this->dao->select('id,name')->from(TABLE_KANBANREGION)
  873. ->where('kanban')->eq($kanbanID)
  874. ->andWhere('deleted')->eq('0')
  875. ->beginIF($regionID)->andWhere('id')->eq($regionID)->fi()
  876. ->beginIF($from == 'execution')->andWhere('space')->eq(0)->fi()
  877. ->beginIF($from == 'kanban')->andWhere('space')->ne(0)->fi()
  878. ->orderBy('order_asc')
  879. ->fetchPairs();
  880. }
  881. /**
  882. * 根据区域获取看板ID。
  883. * Get kanban id by region id.
  884. *
  885. * @param int $regionID
  886. * @access public
  887. * @return int
  888. */
  889. public function getKanbanIDByRegion($regionID)
  890. {
  891. return (int)$this->dao->select('kanban')->from(TABLE_KANBANREGION)->where('id')->eq($regionID)->fetch('kanban');
  892. }
  893. /**
  894. * 分组获取看板组。
  895. * Get kanban group by regions.
  896. *
  897. * @param array $regions
  898. * @access public
  899. * @return array
  900. */
  901. public function getGroupGroupByRegions($regions)
  902. {
  903. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getGroups();
  904. return $this->dao->select('*')->from(TABLE_KANBANGROUP)
  905. ->where('region')->in($regions)
  906. ->orderBy('order')
  907. ->fetchGroup('region', 'id');
  908. }
  909. /**
  910. * 获取区域中的泳道分组。
  911. * Get lane group by regions.
  912. *
  913. * @param array $regions
  914. * @param string $browseType
  915. * @access public
  916. * @return array
  917. */
  918. public function getLaneGroupByRegions($regions, $browseType = 'all')
  919. {
  920. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getLaneGroup();
  921. $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE)
  922. ->where('deleted')->eq('0')
  923. ->andWhere('region')->in($regions)
  924. ->beginIf($browseType != 'all')->andWhere('type')->eq($browseType)->fi()
  925. ->orderBy('order_asc')
  926. ->fetchAll();
  927. $actions = array('sortLane', 'deleteLane', 'editLaneName', 'editLaneColor');
  928. $laneGroup = array();
  929. foreach($lanes as $lane)
  930. {
  931. $item = array();
  932. $item['id'] = $lane->id;
  933. $item['execution'] = $lane->execution;
  934. $item['type'] = $lane->type;
  935. $item['name'] = $lane->id;
  936. $item['region'] = $lane->region;
  937. $item['title'] = htmlspecialchars_decode($lane->name);
  938. $item['color'] = $lane->color;
  939. $item['order'] = $lane->order;
  940. foreach($actions as $action)
  941. {
  942. if($this->isClickable($lane, $action)) $item['actionList'][] = $action;
  943. }
  944. $laneGroup[$lane->group][] = $item;
  945. }
  946. return $laneGroup;
  947. }
  948. /**
  949. * 根据分组ID获取泳道键值对。
  950. * Get kanban lane pairs by group id.
  951. *
  952. * @param int $groupID
  953. * @param string $orderBy
  954. * @access public
  955. * @return array
  956. */
  957. public function getLanePairsByGroup($groupID, $orderBy = '`order`_asc')
  958. {
  959. return $this->dao->select('id,name')->from(TABLE_KANBANLANE)
  960. ->where('deleted')->eq(0)
  961. ->andWhere('`group`')->eq($groupID)
  962. ->orderBy($orderBy)
  963. ->fetchPairs();
  964. }
  965. /**
  966. * 根据分组ID获取看板列键值对。
  967. * Get kanban column pairs by group id.
  968. *
  969. * @param int $groupID
  970. * @param int $parentID
  971. * @param string $orderBy
  972. * @access public
  973. * @return array
  974. */
  975. public function getColumnPairsByGroup($groupID, $parentID = 0, $orderBy = '`order`_asc')
  976. {
  977. return $this->dao->select('id,name')->from(TABLE_KANBANCOLUMN)
  978. ->where('deleted')->eq(0)
  979. ->andWhere('`group`')->eq($groupID)
  980. ->beginIf($parentID > 0)->andWhere('parent')->eq($parentID)->fi()
  981. ->beginIf($parentID <= 0)->andWhere('parent')->le(0)->fi()
  982. ->orderBy($orderBy)
  983. ->fetchPairs();
  984. }
  985. /**
  986. * Get column group by regions.
  987. *
  988. * @param array $regions
  989. * @param string $order order|id_asc
  990. * @param string $param
  991. * @access public
  992. * @return array
  993. */
  994. public function getColumnGroupByRegions($regions, $order = '`order`', $param = '')
  995. {
  996. $columns = $this->dao->select("*")->from(TABLE_KANBANCOLUMN)
  997. ->where('deleted')->eq('0')
  998. ->andWhere('region')->in($regions)
  999. ->beginIF(strpos(",$param,", ',withArchived,') === false)->andWhere('archived')->eq('0')->fi()
  1000. ->orderBy($order)
  1001. ->fetchAll();
  1002. $actions = array('createColumn', 'setColumn', 'setWIP', 'sortColumn', 'archiveColumn', 'restoreColumn', 'deleteColumn', 'createCard', 'batchCreateCard', 'splitColumn', 'sortColumn');
  1003. /* Group by parent. */
  1004. $columnGroup = array();
  1005. foreach($columns as $column)
  1006. {
  1007. $item = array();
  1008. $item['title'] = htmlspecialchars_decode($column->name);
  1009. $item['name'] = $column->id;
  1010. $item['id'] = $column->id;
  1011. $item['type'] = $column->type;
  1012. $item['limit'] = $column->limit;
  1013. $item['region'] = $column->region;
  1014. $item['group'] = $column->group;
  1015. $item['parent'] = $column->parent;
  1016. $item['color'] = $column->color;
  1017. $item['order'] = (int)$column->order;
  1018. if($column->parent > 0) $item['parentName'] = $column->parent;
  1019. /* Judge column action priv. */
  1020. foreach($actions as $action)
  1021. {
  1022. if($this->isClickable($column, $action)) $item['actionList'][] = $action;
  1023. }
  1024. $columnGroup[$column->group][] = $item;
  1025. }
  1026. return $columnGroup;
  1027. }
  1028. /**
  1029. * 获取看板下的卡片。
  1030. * Get card group by kanban id.
  1031. *
  1032. * @param int $kanbanID
  1033. * @access public
  1034. * @return array
  1035. */
  1036. public function getCardGroupByKanban($kanbanID)
  1037. {
  1038. /* Get card data.*/
  1039. $cards = $this->dao->select('*')->from(TABLE_KANBANCARD)
  1040. ->where('deleted')->eq(0)
  1041. ->andWhere('kanban')->eq($kanbanID)
  1042. ->andWhere('archived')->eq(0)
  1043. ->andWhere('fromID')->eq(0)
  1044. ->fetchAll('id', false);
  1045. foreach($this->config->kanban->fromType as $fromType) $cards = $this->getImportedCards($kanbanID, $cards, $fromType);
  1046. $cellList = $this->dao->select('*')->from(TABLE_KANBANCELL)
  1047. ->where('kanban')->eq($kanbanID)
  1048. ->andWhere('type')->eq('common')
  1049. ->fetchAll('id', false);
  1050. $actions = array('editCard', 'archiveCard', 'deleteCard', 'moveCard', 'setCardColor', 'viewCard', 'sortCard', 'viewExecution', 'viewPlan', 'viewRelease', 'viewBuild', 'viewTicket', 'activateCard', 'finishCard');
  1051. $cardGroup = array();
  1052. $avatarPairs = $this->loadModel('user')->getAvatarPairs();
  1053. $users = $this->loadModel('user')->getPairs('noletter');
  1054. foreach($cellList as $cell)
  1055. {
  1056. $cardIdList = array_filter(explode(',', $cell->cards));
  1057. if(empty($cardIdList)) continue;
  1058. $order = 0;
  1059. foreach($cardIdList as $cardID)
  1060. {
  1061. if(!isset($cards[$cardID])) continue;
  1062. $card = zget($cards, $cardID);
  1063. $item = $this->kanbanTao->initCardItem($card, $cell, $order, $avatarPairs, $users);
  1064. $order ++;
  1065. foreach($actions as $action)
  1066. {
  1067. if(in_array($action, array('viewExecution', 'viewPlan', 'viewRelease', 'viewBuild', 'viewTicket')))
  1068. {
  1069. if($card->fromType == 'execution')
  1070. {
  1071. if($card->execType == 'kanban' && common::hasPriv('execution', 'kanban')) $item['actionList'][] = $action;
  1072. if($card->execType != 'kanban' && common::hasPriv('execution', 'view')) $item['actionList'][] = $action;
  1073. }
  1074. else
  1075. {
  1076. if(common::hasPriv($fromType, 'view')) $item['actionList'][] = $action;
  1077. }
  1078. continue;
  1079. }
  1080. if(common::hasPriv('kanban', $action)) $item['actionList'][] = $action;
  1081. }
  1082. $cardGroup[$card->group][$cell->lane][$cell->column][] = $item;
  1083. }
  1084. }
  1085. return $cardGroup;
  1086. }
  1087. /**
  1088. * 获取已经导入的卡片。
  1089. * Get imported cards.
  1090. *
  1091. * @param int $kanbanID
  1092. * @param array $cards
  1093. * @param string $fromType
  1094. * @param int $archived
  1095. * @param int $regionID
  1096. * @access public
  1097. * @return array
  1098. */
  1099. public function getImportedCards($kanbanID, $cards, $fromType, $archived = 0, $regionID = 0)
  1100. {
  1101. /* Get imported cards based on imported object type. */
  1102. $objectCards = $this->dao->select('*')->from(TABLE_KANBANCARD)
  1103. ->where('deleted')->eq(0)
  1104. ->andWhere('kanban')->eq($kanbanID)
  1105. ->andWhere('archived')->eq($archived)
  1106. ->andWhere('fromType')->eq($fromType)
  1107. ->beginIF($regionID)->andWhere('region')->eq($regionID)->fi()
  1108. ->fetchGroup('fromID', 'id');
  1109. if(!empty($objectCards))
  1110. {
  1111. /* Get imported objects. */
  1112. $table = $this->config->objectTables[$fromType];
  1113. $objects = $this->dao->select('*')->from($table)
  1114. ->where('id')->in(array_keys($objectCards))
  1115. ->fetchAll('id', false);
  1116. $creators = array();
  1117. if($fromType == 'productplan' || $fromType == 'release')
  1118. {
  1119. $creators = $this->dao->select('objectID, actor')->from(TABLE_ACTION)
  1120. ->where('objectID')->in(array_keys($objectCards))
  1121. ->andWhere('objectType')->eq($fromType)
  1122. ->andWhere('action')->eq('opened')
  1123. ->fetchPairs();
  1124. }
  1125. /* Data for constructing the card. */
  1126. foreach($objectCards as $objectID => $cardsInfo)
  1127. {
  1128. foreach($cardsInfo as $cardID => $objectCard)
  1129. {
  1130. $object = $objects[$objectID];
  1131. $fieldType = $fromType . 'Field';
  1132. if($fromType == 'execution') $objectCard->progress = $object->progress;
  1133. foreach($this->config->kanban->$fieldType as $field) $objectCard->$field = $object->$field;
  1134. $objectCard = $this->kanbanTao->buildObjectCard($objectCard, $object, $fromType, $creators);
  1135. $cards[$cardID] = $objectCard;
  1136. }
  1137. }
  1138. }
  1139. return $cards;
  1140. }
  1141. /**
  1142. * 获取专业研发看板的列。
  1143. * Get RD column group by regions.
  1144. *
  1145. * @param array $regions
  1146. * @param array $groupIDList
  1147. * @access public
  1148. * @return array
  1149. */
  1150. public function getRDColumnGroupByRegions($regions, $groupIDList = array())
  1151. {
  1152. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getColumns();
  1153. $columnGroup = $this->dao->select("*")->from(TABLE_KANBANCOLUMN)
  1154. ->where('deleted')->eq('0')
  1155. ->andWhere('region')->in($regions)
  1156. ->beginIF(!empty($groupIDList))->andWhere('`group`')->in($groupIDList)->fi()
  1157. ->orderBy('id_asc')
  1158. ->fetchGroup('group');
  1159. $actions = array('setColumn', 'setWIP', 'deleteColumn');
  1160. /* Group by parent. */
  1161. $columnData = array();
  1162. foreach($columnGroup as $group => $columns)
  1163. {
  1164. foreach($columns as $column)
  1165. {
  1166. $item = array();
  1167. $item['title'] = htmlspecialchars_decode($column->name);
  1168. $item['name'] = $column->id;
  1169. $item['id'] = $column->id;
  1170. $item['type'] = $column->type;
  1171. $item['limit'] = $column->limit;
  1172. $item['region'] = $column->region;
  1173. $item['group'] = $column->group;
  1174. $item['parent'] = $column->parent;
  1175. $item['color'] = $column->color;
  1176. if($column->parent > 0) $item['parentName'] = $column->parent;
  1177. /* Judge column action priv. */
  1178. foreach($actions as $action)
  1179. {
  1180. if($this->isClickable($column, $action)) $item['actionList'][] = $action;
  1181. }
  1182. $columnData[$group][] = $item;
  1183. }
  1184. }
  1185. return $columnData;
  1186. }
  1187. /**
  1188. * 获取执行中的看板卡片分组。
  1189. * Get card group by execution id.
  1190. *
  1191. * @param int $kanbanID
  1192. * @param string $browseType all|task|bug|story
  1193. * @param string $orderBy
  1194. * @param string $searchValue
  1195. *
  1196. * @access public
  1197. * @return array
  1198. * @param int $executionID
  1199. */
  1200. public function getCardGroupByExecution($executionID, $browseType = 'all', $orderBy = 'id_asc', $searchValue = '')
  1201. {
  1202. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getCardGroup();
  1203. $cards = $this->dao->select('t1.*, t2.type as columnType, t2.group')
  1204. ->from(TABLE_KANBANCELL)->alias('t1')
  1205. ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column=t2.id')
  1206. ->where('t1.kanban')->eq($executionID)
  1207. ->beginIF($browseType != 'all')->andWhere('t1.type')->eq($browseType)->fi()
  1208. ->orderby($orderBy)
  1209. ->fetchgroup('lane', 'column');
  1210. /* Get group objects. */
  1211. if($browseType == 'all' || $browseType == 'parentStory') $objectGroup['parentStory'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
  1212. if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
  1213. if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID);
  1214. if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id");
  1215. $cardGroup = array();
  1216. $avatarPairs = $this->loadModel('user')->getAvatarPairs();
  1217. $users = $this->loadModel('user')->getPairs('noletter');
  1218. foreach($cards as $laneID => $cells)
  1219. {
  1220. foreach($cells as $cell)
  1221. {
  1222. $order = 0;
  1223. $cardIdList = array_filter(explode(',', $cell->cards));
  1224. foreach($cardIdList as $cardID)
  1225. {
  1226. $cardData = array();
  1227. $objects = zget($objectGroup, $cell->type, array());
  1228. $object = zget($objects, $cardID, array());
  1229. if(empty($object)) continue;
  1230. if($cell->type == 'parentStory' && $object->isParent != '1') continue;
  1231. if($cell->type == 'story' && $object->isParent == '1') continue;
  1232. $cardData = $this->kanbanTao->initCardItem($object, $cell, $order, $avatarPairs, $users);
  1233. $cardData['acl'] = 'open';
  1234. $order ++;
  1235. if($searchValue != '' && strpos($cardData['title'], $searchValue) === false) continue;
  1236. $cardGroup[$cell->group][$laneID][$cell->column][] = $cardData;
  1237. }
  1238. }
  1239. }
  1240. return $cardGroup;
  1241. }
  1242. /**
  1243. * 获取执行看板的数据。
  1244. * Get Kanban by execution id.
  1245. *
  1246. * @param int $executionID
  1247. * @param string $browseType all|story|bug|task
  1248. * @param string $groupBy default|pri|category|module|source|assignedTo|type|story|severity
  1249. * @param string $searchValue
  1250. * @param string $orderBy
  1251. * @access public
  1252. * @return array
  1253. */
  1254. public function getExecutionKanban($executionID, $browseType = 'all', $groupBy = 'default', $searchValue = '', $orderBy = 'id_asc')
  1255. {
  1256. if($groupBy != 'default') return array($this->getKanban4Group($executionID, $browseType, $groupBy, $searchValue, $orderBy), array());
  1257. $execution = $this->loadModel('execution')->fetchByID($executionID);
  1258. $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE)
  1259. ->where('execution')->eq($executionID)
  1260. ->andWhere('deleted')->eq(0)
  1261. ->beginIF($browseType != 'all')->andWhere('type')->eq($browseType)->fi()
  1262. ->beginIF(!empty($execution->attribute) && !in_array($execution->attribute, array('mix', 'request', 'design')))->andWhere('type')->notIn('epic,requirement')->fi()
  1263. ->orderBy('order_asc')
  1264. ->fetchAll('id');
  1265. if(empty($lanes)) return array();
  1266. foreach($lanes as $lane) $this->refreshCards((array)$lane);
  1267. $columns = $this->dao->select('t1.cards, t1.lane, t2.id, t2.type, t2.name, t2.color, t2.limit, t2.parent')->from(TABLE_KANBANCELL)->alias('t1')
  1268. ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column = t2.id')
  1269. ->where('t2.deleted')->eq(0)
  1270. ->andWhere('t1.lane')->in(array_keys($lanes))
  1271. ->orderBy('id_asc')
  1272. ->fetchGroup('lane', 'id');
  1273. $geMax = in_array($this->config->edition, array('max', 'ipd'));
  1274. /* Get group objects. */
  1275. if($browseType == 'all' || $browseType == 'epic') $objectGroup['epic'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'epic');
  1276. if($browseType == 'all' || $browseType == 'requirement') $objectGroup['requirement'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'requirement');
  1277. if($browseType == 'all' || $browseType == 'parentStory') $objectGroup['parentStory'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
  1278. if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
  1279. if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID);
  1280. if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id");
  1281. if($geMax && ($browseType == 'all' || $browseType == 'risk')) $objectGroup['risk'] = $this->loadModel('risk')->getKanbanRisks($executionID);
  1282. /* Get objects cards menus. */
  1283. $menus = array();
  1284. $menus['epic'] = $browseType == 'all' || $browseType == 'epic' ? $this->getKanbanCardMenu($executionID, $objectGroup['epic'], 'epic') : array();
  1285. $menus['requirement'] = $browseType == 'all' || $browseType == 'requirement' ? $this->getKanbanCardMenu($executionID, $objectGroup['requirement'], 'requirement') : array();
  1286. $menus['parentStory'] = $browseType == 'all' || $browseType == 'parentStory' ? $this->getKanbanCardMenu($executionID, $objectGroup['parentStory'], 'parentStory') : array();
  1287. $menus['story'] = $browseType == 'all' || $browseType == 'story' ? $this->getKanbanCardMenu($executionID, $objectGroup['story'], 'story') : array();
  1288. $menus['bug'] = $browseType == 'all' || $browseType == 'bug' ? $this->getKanbanCardMenu($executionID, $objectGroup['bug'], 'bug') : array();
  1289. $menus['task'] = $browseType == 'all' || $browseType == 'task' ? $this->getKanbanCardMenu($executionID, $objectGroup['task'], 'task') : array();
  1290. $menus['risk'] = ($geMax && ($browseType == 'all' || $browseType == 'risk')) ? $this->getKanbanCardMenu($executionID, $objectGroup['risk'], 'risk') : array();
  1291. /* 获取看板连线的fromKanbanID. */
  1292. $storyFromKanbanID = '';
  1293. $taskFromKanbanID = '';
  1294. foreach($lanes as $lane)
  1295. {
  1296. if($lane->type == 'parentStory') $storyFromKanbanID = 'group' . $lane->id;
  1297. if($lane->type == 'task') $taskFromKanbanID = 'group' . $lane->id;
  1298. }
  1299. /* Build kanban group data. */
  1300. $kanbanGroup = array();
  1301. $links = array();
  1302. $execution = $this->loadModel('execution')->fetchById($executionID);
  1303. $project = $this->loadModel('project')->fetchById($execution->project);
  1304. foreach($lanes as $lane)
  1305. {
  1306. $laneType = $lane->type;
  1307. if(in_array($laneType, array('epic', 'requirement')) && strpos($project->storyType, $laneType) === false) continue;
  1308. list($laneData, $columnData, $cardsData) = $this->buildExecutionGroup($lane, $columns, $objectGroup, $searchValue, $menus);
  1309. if($lane->type == 'risk' && in_array($this->config->edition, array('max', 'ipd'))) $cardsData = $this->appendRiskField($cardsData, $objectGroup['risk']);
  1310. $kanbanID = 'group' . $lane->id;
  1311. $kanbanGroup[$laneType]['id'] = $lane->id;
  1312. $kanbanGroup[$laneType]['key'] = $kanbanID;
  1313. $kanbanGroup[$laneType]['data'] = array();
  1314. $kanbanGroup[$laneType]['data']['lanes'] = array($laneData);
  1315. $kanbanGroup[$laneType]['data']['cols'] = $columnData;
  1316. $kanbanGroup[$laneType]['data']['items'] = $cardsData;
  1317. foreach($cardsData as $columnCard)
  1318. {
  1319. foreach($columnCard as $cards)
  1320. {
  1321. foreach($cards as $card)
  1322. {
  1323. if($card['parent'] == 0) continue;
  1324. /* 获取看板卡片的连线关系,业需、用需和父需求共用同一组看板列,所以fromKanban和toKanban是一样的。 */
  1325. $link = array('from' => $card['parent'], 'to' => $card['id']);
  1326. if($laneType == 'task')
  1327. {
  1328. $link['fromKanban'] = $taskFromKanbanID;
  1329. $link['toKanban'] = $taskFromKanbanID;
  1330. }
  1331. else
  1332. {
  1333. $link['fromKanban'] = $storyFromKanbanID;
  1334. $link['toKanban'] = in_array($laneType, array('epic', 'requirement')) ? $storyFromKanbanID : $kanbanID;
  1335. }
  1336. $links[] = $link;
  1337. }
  1338. }
  1339. }
  1340. }
  1341. /* 展示所有泳道的时候,业务、用需与父需求泳道共用看板列。 */
  1342. if($browseType == 'all')
  1343. {
  1344. foreach(array('requirement', 'epic') as $laneType)
  1345. {
  1346. if(!isset($kanbanGroup[$laneType])) continue;
  1347. array_unshift($kanbanGroup['parentStory']['data']['lanes'], $kanbanGroup[$laneType]['data']['lanes'][0]);
  1348. $kanbanGroup['parentStory']['data']['items'] += $kanbanGroup[$laneType]['data']['items'];
  1349. unset($kanbanGroup[$laneType]);
  1350. }
  1351. }
  1352. return array(array_values($kanbanGroup), $links);
  1353. }
  1354. /**
  1355. * 构建迭代看板的泳道组数据。
  1356. * Build the laneGroup data for the execution Kanban.
  1357. *
  1358. * @param object $lane
  1359. * @param array $columns
  1360. * @param array $objectGroup
  1361. * @param string $searchValue
  1362. * @param array $menus array(story => array, bug => array, task => array)
  1363. * @access public
  1364. * @return array
  1365. */
  1366. public function buildExecutionGroup($lane, $columns, $objectGroup, $searchValue = '', $menus = array())
  1367. {
  1368. $laneData = array();
  1369. $columnsData = array();
  1370. $cardsData = array();
  1371. $laneData['id'] = $lane->id;
  1372. $laneData['type'] = $lane->type;
  1373. $laneData['name'] = $lane->id;
  1374. $laneData['region'] = $lane->region;
  1375. $laneData['title'] = $lane->name;
  1376. $laneData['color'] = $lane->color;
  1377. foreach($columns[$lane->id] as $columnID => $column)
  1378. {
  1379. $cardIdList = array_unique(array_filter(explode(',', $column->cards)));
  1380. $columnsData[$column->id]['id'] = $columnID;
  1381. $columnsData[$column->id]['type'] = $column->type;
  1382. $columnsData[$column->id]['name'] = $columnID;
  1383. $columnsData[$column->id]['title'] = $column->name;
  1384. $columnsData[$column->id]['color'] = $column->color;
  1385. $columnsData[$column->id]['limit'] = $column->limit;
  1386. $columnsData[$column->id]['region'] = $lane->region;
  1387. $columnsData[$column->id]['laneName'] = $column->lane;
  1388. $columnsData[$column->id]['group'] = $lane->type;
  1389. $columnsData[$column->id]['cards'] = 0;
  1390. $columnsData[$column->id]['actionList'] = array('setColumn', 'setWIP');
  1391. if($column->parent > 0) $columnsData[$column->id]['parentName'] = $column->parent;
  1392. if($cardIdList)
  1393. {
  1394. $cardsData = $this->buildExecutionCards($cardsData, $column, $lane->type, $cardIdList, $objectGroup, $searchValue, $menus);
  1395. $columnsData[$column->id]['cards'] = empty($cardsData[$column->lane][$column->id]) ? 0 : count($cardsData[$column->lane][$column->id]);
  1396. }
  1397. }
  1398. foreach($columnsData as $columnData)
  1399. {
  1400. if(isset($columnData['parentName'])) $columnsData[$columnData['parentName']]['cards'] += $columnData['cards'];
  1401. }
  1402. return array($laneData, array_values($columnsData), $cardsData);
  1403. }
  1404. /**
  1405. * 构建迭代看板的卡片数据。
  1406. * Build the card data for the execution Kanban.
  1407. *
  1408. * @param object $lane
  1409. * @param array $laneData
  1410. * @param string $columnType
  1411. * @param array $cardIdList
  1412. * @param array $objectGroup
  1413. * @param string $searchValue
  1414. * @param array $menus array(story => array, bug => array, task => array)
  1415. * @access public
  1416. * @return array
  1417. * @param object $column
  1418. * @param mixed[] $cardsData
  1419. * @param string $laneType
  1420. */
  1421. public function buildExecutionCards($cardsData, $column, $laneType, $cardIdList, $objectGroup, $searchValue = '', $menus = array())
  1422. {
  1423. foreach($cardIdList as $cardID)
  1424. {
  1425. $cardData = array();
  1426. $objects = zget($objectGroup, $laneType, array());
  1427. $object = zget($objects, $cardID, array());
  1428. if(empty($object)) continue;
  1429. if($laneType == 'story' && $object->isParent == '1') continue;
  1430. $cardData = $this->buildExecutionCard($object, $column, $laneType, $searchValue, $menus);
  1431. if(empty($cardData)) continue;
  1432. $cardsData[$column->lane][$column->id][] = $cardData;
  1433. }
  1434. return $cardsData;
  1435. }
  1436. /**
  1437. * 构造执行看板卡片数据。
  1438. * Build execution card.
  1439. *
  1440. * @param object $object
  1441. * @param object $column
  1442. * @param string $laneType
  1443. * @param string $searchValue
  1444. * @param array $menus array(story => array, bug => array, task => array)
  1445. * @access public
  1446. * @return array
  1447. */
  1448. public function buildExecutionCard($object, $column, $laneType, $searchValue = '', $menus = array())
  1449. {
  1450. if(empty($object)) return array();
  1451. $cardData = array();
  1452. $cardData['id'] = $object->id;
  1453. $cardData['lane'] = $column->lane;
  1454. $cardData['column'] = zget($column, 'column', $column->id);
  1455. $cardData['pri'] = zget($object, 'pri', 0);
  1456. $cardData['group'] = $laneType;
  1457. $cardData['parent'] = zget($object, 'originParent', 0);
  1458. $cardData['status'] = zget($object, 'status', '');
  1459. $cardData['estimate'] = zget($object, 'estimate', 0);
  1460. $cardData['assignedTo'] = $object->assignedTo;
  1461. $cardData['deadline'] = zget($object, 'deadline', '');
  1462. $cardData['severity'] = zget($object, 'severity', 0);
  1463. $cardData['actionList'] = zget(zget($menus, $laneType, array()), $object->id, array());
  1464. $cardData['canAssignTo'] = true;
  1465. $cardData['title'] = zget($object, 'title', '');
  1466. if($laneType == 'task')
  1467. {
  1468. $cardData['title'] = $object->name;
  1469. $cardData['status'] = $object->status;
  1470. $cardData['parent'] = $object->parent;
  1471. $cardData['isParent'] = $object->isParent;
  1472. $cardData['left'] = $object->left;
  1473. $cardData['estStarted'] = $object->estStarted;
  1474. $cardData['mode'] = $object->mode;
  1475. $cardData['canAssignTo'] = common::hasPriv('task', 'assignTo') && common::hasDBPriv($object, 'task', 'assignto');
  1476. if($object->mode == 'multi') $cardData['teamMembers'] = $object->teamMembers;
  1477. }
  1478. if($searchValue != '' && strpos($cardData['title'], $searchValue) === false) return array();
  1479. return $cardData;
  1480. }
  1481. /**
  1482. * 获取执行看板分组视图的数据。
  1483. * Get kanban for group view.
  1484. *
  1485. * @param int $executionID
  1486. * @param string $browseType
  1487. * @param string $groupBy
  1488. * @param string $searchValue
  1489. * @param string $orderBy
  1490. *
  1491. * @access public
  1492. * @return array
  1493. */
  1494. public function getKanban4Group($executionID, $browseType, $groupBy, $searchValue = '', $orderBy = 'id_asc')
  1495. {
  1496. /* Get card data. */
  1497. $cardList = array();
  1498. if(in_array($browseType, array('story', 'parentStory'))) $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory');
  1499. if($browseType == 'epic') $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'epic');
  1500. if($browseType == 'requirement') $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'requirement');
  1501. if($browseType == 'bug') $cardList = $this->loadModel('bug')->getExecutionBugs($executionID);
  1502. if($browseType == 'task') $cardList = $this->loadModel('execution')->getKanbanTasks($executionID);
  1503. if($browseType == 'task' && $groupBy == 'assignedTo') $cardList = $this->appendTeamMember($cardList);
  1504. /* Get objects cards menus. */
  1505. $menus = array();
  1506. if($browseType == 'story') $menus['story'] = $this->getKanbanCardMenu($executionID, $cardList, 'story');
  1507. if($browseType == 'bug') $menus['bug'] = $this->getKanbanCardMenu($executionID, $cardList, 'bug');
  1508. if($browseType == 'task') $menus['task'] = $this->getKanbanCardMenu($executionID, $cardList, 'task');
  1509. if($groupBy == 'story' && $browseType == 'task' && !isset($this->lang->kanban->orderList[$orderBy])) $orderBy = 'id_asc';
  1510. $lanes = $this->getLanes4Group($executionID, $browseType, $groupBy, $cardList, $orderBy);
  1511. if(empty($lanes)) return array();
  1512. $columns = $this->getCols4Group($executionID, $browseType);
  1513. $cardGroup = array();
  1514. foreach($columns as $column)
  1515. {
  1516. if(empty($column->cards)) continue;
  1517. foreach($cardList as $card)
  1518. {
  1519. if(strpos($column->cards, ",$card->id,") !== false) $cardGroup[$column->columnType][$card->id] = $card;
  1520. }
  1521. }
  1522. /* Build kanban group data. */
  1523. $kanbanGroup = array();
  1524. list($laneData, $columnData, $cardData) = $this->buildGroupKanban($lanes, $columns, $cardGroup, $searchValue, $groupBy, $browseType, $menus);
  1525. $kanbanGroup['id'] = $executionID;
  1526. $kanbanGroup['key'] = 'group' . $executionID;
  1527. $kanbanGroup['data'] = array();
  1528. $kanbanGroup['data']['lanes'] = $laneData;
  1529. $kanbanGroup['data']['cols'] = array_values($columnData);
  1530. $kanbanGroup['data']['items'] = $cardData;
  1531. return array($kanbanGroup);
  1532. }
  1533. /**
  1534. * 构建分组视图的看板泳道。
  1535. * Build lanes for group kanban.
  1536. *
  1537. * @access public
  1538. * @param int $executionID
  1539. * @param string $browseType
  1540. * @param string $groupBy
  1541. * @param array $cardList
  1542. * @param string $orderBy
  1543. *
  1544. * @return array
  1545. */
  1546. public function getLanes4Group($executionID, $browseType, $groupBy, $cardList, $orderBy = 'id_asc')
  1547. {
  1548. $lanes = array();
  1549. $groupByList = array();
  1550. foreach($cardList as $item)
  1551. {
  1552. if(!isset($groupByList[$item->$groupBy])) $groupByList[$item->$groupBy] = $item->$groupBy;
  1553. if($groupBy == 'assignedTo' && !empty($item->teamMember))
  1554. {
  1555. foreach($item->teamMember as $account => $name)
  1556. {
  1557. if(!isset($groupByList[$account])) $groupByList[$account] = $account;
  1558. }
  1559. }
  1560. }
  1561. $objectPairs = $this->getObjectPairs($groupBy, $groupByList, $browseType, $orderBy);
  1562. if($groupBy == 'story') $objects = $this->dao->select('*')->from(TABLE_STORY)->where('deleted')->eq(0)->andWhere('id')->in($groupByList)->orderBy($orderBy)->fetchAll('id');
  1563. $laneColor = 0;
  1564. $order = 1;
  1565. foreach($objectPairs as $objectType => $objectName)
  1566. {
  1567. if(!isset($groupByList[$objectType]) && $objectType && !in_array($objectType, array('feature', 'design'))) continue;
  1568. $lane = new stdclass();
  1569. $lane->id = $groupBy . $objectType;
  1570. $lane->type = $browseType;
  1571. $lane->execution = $executionID;
  1572. $lane->name = $objectName;
  1573. $lane->order = (int)$order;
  1574. $lane->color = $this->config->kanban->laneColorList[$laneColor];
  1575. $lane->pri = (isset($objects) && isset($objects[$objectType]->pri)) ? $objects[$objectType]->pri : '';
  1576. $lane->assignedTo = (isset($objects) && isset($objects[$objectType]->assignedTo)) ? $objects[$objectType]->assignedTo : '';
  1577. $order += 1;
  1578. $laneColor += 1;
  1579. if($laneColor == count($this->config->kanban->laneColorList)) $laneColor = 0;
  1580. $lanes[$objectType] = $lane;
  1581. }
  1582. return $lanes;
  1583. }
  1584. /**
  1585. * 获取分组视图的看板列数据。
  1586. * Get columns for group kanban.
  1587. *
  1588. * @param int $executionID
  1589. * @param string $browseType
  1590. * @access public
  1591. * @return array
  1592. */
  1593. public function getCols4Group($executionID, $browseType)
  1594. {
  1595. $execution = $this->loadModel('execution')->getByID($executionID);
  1596. $cards = $this->dao->select('t1.*, t1.cards, t2.`type` as columnType, t2.parent, t2.limit, t2.name as columnName, t2.color')->from(TABLE_KANBANCELL)->alias('t1')
  1597. ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id')
  1598. ->leftJoin(TABLE_KANBANLANE)->alias('t3')->on('t1.lane = t3.id')
  1599. ->leftJoin(TABLE_KANBANREGION)->alias('t4')->on('t1.kanban = t4.kanban')
  1600. ->where('t1.kanban')->eq($executionID)
  1601. ->andWhere('t1.`type`')->eq($browseType)
  1602. ->beginIF(isset($execution->type) && $execution->type == 'kanban')
  1603. ->andWhere('t3.deleted')->eq(0)
  1604. ->andWhere('t4.deleted')->eq(0)
  1605. ->fi()
  1606. ->orderBy('column_asc')
  1607. ->fetchAll();
  1608. $columns = array();
  1609. foreach($cards as $card)
  1610. {
  1611. if(!isset($columns[$card->columnType])) $columns[$card->columnType] = $card;
  1612. $columns[$card->columnType]->cards .= $card->cards;
  1613. $columns[$card->columnType]->cards = ',' . trim($columns[$card->columnType]->cards, ',') . ',';
  1614. }
  1615. return $columns;
  1616. }
  1617. /**
  1618. * 获取看板空间列表。
  1619. * Get space list.
  1620. *
  1621. * @param string $browseType private|cooperation|public|involved
  1622. * @param object $pager
  1623. * @access public
  1624. * @return array
  1625. */
  1626. public function getSpaceList($browseType, $pager = null)
  1627. {
  1628. $account = $this->app->user->account;
  1629. $spaceIdList = $this->getCanViewObjects('kanbanspace', $browseType);
  1630. $spaceList = $this->dao->select('*')->from(TABLE_KANBANSPACE)
  1631. ->where('deleted')->eq(0)
  1632. ->beginIF(in_array($browseType, array('private', 'cooperation', 'public')))->andWhere('type')->eq($browseType)->fi()
  1633. ->beginIF($browseType == 'private' && !$this->app->user->admin)->andWhere('owner')->eq($account)->fi()
  1634. ->beginIF($this->cookie->showClosed == 0)->andWhere('status')->ne('closed')->fi()
  1635. ->andWhere('id')->in($spaceIdList)
  1636. ->orderBy('id_desc')
  1637. ->page($pager)
  1638. ->fetchAll('id');
  1639. $kanbanIdList = $this->getCanViewObjects('kanban', $browseType);
  1640. $kanbanGroup = $this->getGroupBySpaceList(array_keys($spaceList), $kanbanIdList);
  1641. foreach($spaceList as $spaceID => $space)
  1642. {
  1643. if(isset($kanbanGroup[$spaceID])) $space->kanbans = $kanbanGroup[$spaceID];
  1644. }
  1645. return $spaceList;
  1646. }
  1647. /**
  1648. * 获取看板空间键值对。
  1649. * Get space pairs.
  1650. *
  1651. * @param string $browseType private|cooperation|public|involved
  1652. * @access public
  1653. * @return array
  1654. */
  1655. public function getSpacePairs($browseType = 'private')
  1656. {
  1657. $spaceIdList = $this->getCanViewObjects('kanbanspace', $browseType);
  1658. return $this->dao->select('id,name')->from(TABLE_KANBANSPACE)
  1659. ->where('deleted')->eq(0)
  1660. ->andWhere('id')->in($spaceIdList)
  1661. ->beginIF(in_array($browseType, array('private', 'cooperation', 'public')))->andWhere('type')->eq($browseType)->fi()
  1662. ->beginIF($this->cookie->showClosed == 0 && $browseType != 'showClosed')->andWhere('status')->ne('closed')->fi()
  1663. ->orderBy('id_desc')
  1664. ->fetchPairs('id');
  1665. }
  1666. /**
  1667. * 获取看板键值对。
  1668. * Get Kanban pairs.
  1669. *
  1670. * @access public
  1671. * @return array
  1672. */
  1673. public function getKanbanPairs()
  1674. {
  1675. $kanbanIdList = $this->getCanViewObjects('kanban');
  1676. return $this->dao->select('id,name')->from(TABLE_KANBAN)
  1677. ->where('deleted')->eq(0)
  1678. ->beginIF(!$this->app->user->admin)->andWhere('id')->in($kanbanIdList)->fi()
  1679. ->orderBy('id_desc')
  1680. ->fetchPairs('id');
  1681. }
  1682. /**
  1683. * 获取有权查看的对象。
  1684. * Get can view objects.
  1685. *
  1686. * @param string $objectType kanbanspace|kanban
  1687. * @param string $param all|noclosed|private|cooperation|public|involved
  1688. * @access public
  1689. * @return array
  1690. */
  1691. public function getCanViewObjects($objectType = 'kanban', $param = 'all')
  1692. {
  1693. $table = $this->config->objectTables[$objectType];
  1694. $objects = $this->dao->select('*')->from($table)
  1695. ->where('deleted')->eq(0)
  1696. ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi()
  1697. ->fetchAll('id', false);
  1698. $spaceList = $objectType == 'kanban' ? $this->dao->select('id,owner,type')->from(TABLE_KANBANSPACE)->fetchAll('id') : array();
  1699. if($param && $this->app->user->admin && strpos('private,involved', $param) === false) return array_keys($objects);
  1700. $account = $this->app->user->account;
  1701. foreach($objects as $objectID => $object)
  1702. {
  1703. if($objectType == 'kanbanspace' && $object->type == 'public' && $param != 'involved') continue;
  1704. $remove = true;
  1705. if($object->owner == $account) $remove = false;
  1706. if(strpos(",{$object->team},", ",$account,") !== false) $remove = false;
  1707. if(strpos(",{$object->whitelist},", ",$account,") !== false) $remove = false;
  1708. if($objectType == 'kanban')
  1709. {
  1710. $spaceOwner = isset($spaceList[$object->space]->owner) ? $spaceList[$object->space]->owner : '';
  1711. $spaceType = isset($spaceList[$object->space]->type) ? $spaceList[$object->space]->type : '';
  1712. if(strpos(",$spaceOwner,", ",$account,") !== false) $remove = false;
  1713. if($spaceType == 'public' && $param != 'involved') $remove = false;
  1714. }
  1715. if($remove) unset($objects[$objectID]);
  1716. }
  1717. return array_keys($objects);
  1718. }
  1719. /**
  1720. * 创建看板空间。
  1721. * Create a space.
  1722. *
  1723. * @param object $space
  1724. * @access public
  1725. * @return int|false
  1726. */
  1727. public function createSpace($space)
  1728. {
  1729. $account = $this->app->user->account;
  1730. if($space->type == 'private') $space->owner = $account;
  1731. if($space->type != 'private') $this->config->kanban->createspace->requiredFields .= ',owner';
  1732. if(strpos(",{$space->team},", ",$account,") === false) $space->team .= ",$account";
  1733. if(strpos(",{$space->team},", ",$space->owner,") === false) $space->team .= ",$space->owner";
  1734. $this->dao->insert(TABLE_KANBANSPACE)->data($space)
  1735. ->autoCheck()
  1736. ->batchCheck($this->config->kanban->createspace->requiredFields, 'notempty')
  1737. ->exec();
  1738. if(dao::isError()) return false;
  1739. $spaceID = $this->dao->lastInsertID();
  1740. $this->dao->update(TABLE_KANBANSPACE)->set('`order`')->eq($spaceID)->where('id')->eq($spaceID)->exec();
  1741. $this->loadModel('file')->saveUpload('kanbanspace', $spaceID);
  1742. $this->file->updateObjectID($this->post->uid, $spaceID, 'kanbanspace');
  1743. $this->loadModel('action')->create('kanbanSpace', $spaceID, 'created');
  1744. return $spaceID;
  1745. }
  1746. /**
  1747. * 编辑看板空间。
  1748. * Update a space.
  1749. *
  1750. * @param object $space
  1751. * @param int $spaceID
  1752. * @access public
  1753. * @return bool
  1754. */
  1755. public function updateSpace($space, $spaceID)
  1756. {
  1757. $oldSpace = $this->getSpaceById($spaceID);
  1758. if($space->type == 'cooperation' || $space->type == 'public') $space->whitelist = '';
  1759. if($space->type != 'private') $this->config->kanban->editspace->requiredFields .= ',owner';
  1760. $this->dao->update(TABLE_KANBANSPACE)->data($space)
  1761. ->autoCheck()
  1762. ->batchCheck($this->config->kanban->editspace->requiredFields, 'notempty')
  1763. ->where('id')->eq($spaceID)
  1764. ->exec();
  1765. if($oldSpace->type == 'private' && (in_array($space->type, array('cooperation', 'public'))))
  1766. {
  1767. $this->dao->update(TABLE_KANBAN)->set('team = whitelist')->set('whitelist')->eq('')->where('space')->eq($spaceID)->andWhere('deleted')->eq('0')->exec();
  1768. }
  1769. if(dao::isError()) return false;
  1770. $this->loadModel('file')->saveUpload('kanbanspace', $spaceID);
  1771. $this->file->updateObjectID($this->post->uid, $spaceID, 'kanbanspace');
  1772. $changes = common::createChanges($oldSpace, $space);
  1773. if($changes)
  1774. {
  1775. $actionID = $this->loadModel('action')->create('kanbanSpace', $spaceID, 'edited');
  1776. $this->action->logHistory($actionID, $changes);
  1777. }
  1778. return true;
  1779. }
  1780. /**
  1781. * 关闭看板空间。
  1782. * Close a space.
  1783. *
  1784. * @param int $spaceID
  1785. * @param object $space
  1786. * @access public
  1787. * @return bool
  1788. */
  1789. function closeSpace($spaceID, $space)
  1790. {
  1791. $oldSpace = $this->getSpaceById($spaceID);
  1792. $this->dao->update(TABLE_KANBANSPACE)->data($space)
  1793. ->autoCheck()
  1794. ->where('id')->eq($spaceID)
  1795. ->exec();
  1796. if(dao::isError()) return false;
  1797. $changes = common::createChanges($oldSpace, $space);
  1798. if($changes)
  1799. {
  1800. $actionID = $this->loadModel('action')->create('kanbanSpace', $spaceID, 'closed', $this->post->comment);
  1801. $this->action->logHistory($actionID, $changes);
  1802. }
  1803. return true;
  1804. }
  1805. /**
  1806. * 激活看板空间。
  1807. * Activate a space.
  1808. *
  1809. * @param int $spaceID
  1810. * @param object $space
  1811. * @access public
  1812. * @return bool
  1813. */
  1814. function activateSpace($spaceID, $space)
  1815. {
  1816. $oldSpace = $this->getSpaceById($spaceID);
  1817. $this->dao->update(TABLE_KANBANSPACE)->data($space)
  1818. ->autoCheck()
  1819. ->where('id')->eq($spaceID)
  1820. ->exec();
  1821. if(dao::isError()) return false;
  1822. $changes = common::createChanges($oldSpace, $space);
  1823. if($changes)
  1824. {
  1825. $actionID = $this->loadModel('action')->create('kanbanSpace', $spaceID, 'activated', $this->post->comment);
  1826. $this->action->logHistory($actionID, $changes);
  1827. }
  1828. return true;
  1829. }
  1830. /**
  1831. * 获取看板泳道键值对。
  1832. * Get lane pairs by region id.
  1833. *
  1834. * @param int $regionID
  1835. * @param string $type all|story|task|bug|common
  1836. * @access public
  1837. * @return array
  1838. */
  1839. public function getLanePairsByRegion($regionID, $type = 'all')
  1840. {
  1841. return $this->dao->select('id, name')->from(TABLE_KANBANLANE)
  1842. ->where('deleted')->eq('0')
  1843. ->andWhere('region')->in($regionID)
  1844. ->beginIF($type != 'all')->andWhere('type')->eq($type)->fi()
  1845. ->fetchPairs();
  1846. }
  1847. /**
  1848. * 获取看板泳道分组。
  1849. * Get lane by region id.
  1850. *
  1851. * @param array|int $regionID
  1852. * @param string $type all|story|task|bug|common
  1853. * @access public
  1854. * @return array
  1855. */
  1856. public function getLaneGroupByRegion($regionID, $type = 'all')
  1857. {
  1858. return $this->dao->select('*')->from(TABLE_KANBANLANE)
  1859. ->where('deleted')->eq('0')
  1860. ->andWhere('region')->in($regionID)
  1861. ->beginIF($type != 'all')->andWhere('type')->eq($type)->fi()
  1862. ->fetchGroup('region');
  1863. }
  1864. /**
  1865. * 创建看板泳道。
  1866. * Create a lane.
  1867. *
  1868. * @param int $kanbanID
  1869. * @param int $regionID
  1870. * @param object $lane
  1871. * @param string $mode new|copy
  1872. * @access public
  1873. * @return int|bool
  1874. */
  1875. public function createLane($kanbanID, $regionID, $lane = null, $mode = 'new')
  1876. {
  1877. if($mode == 'new')
  1878. {
  1879. $laneType = isset($_POST['laneType']) ? $_POST['laneType'] : 'common';
  1880. if($laneType == 'common')
  1881. {
  1882. $sameNameLane = $this->dao->select('id')->from(TABLE_KANBANLANE)->where('region')->eq($regionID)->andWhere('name')->eq($lane->name)->andWhere('deleted')->eq('0')->limit(1)->fetch();
  1883. if($sameNameLane)
  1884. {
  1885. dao::$errors['name'][] = $this->lang->kanbanlane->error->hasExist;
  1886. return false;
  1887. }
  1888. }
  1889. /* 共享看板列不能为空。 */
  1890. if($lane->mode == 'sameAsOther' && $lane->otherLane == 0)
  1891. {
  1892. dao::$errors['otherLane'][] = $this->lang->kanbanlane->error->emptyOtherLane;
  1893. return false;
  1894. }
  1895. $maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBANLANE)
  1896. ->where('region')->eq($regionID)
  1897. ->fetch('maxOrder');
  1898. $lane->order = $maxOrder ? $maxOrder + 1 : 1;
  1899. $lane->type = $laneType;
  1900. $lane->execution = isset($_POST['laneType']) ? $kanbanID : 0;
  1901. if($lane->mode == 'sameAsOther')
  1902. {
  1903. if($lane->otherLane) $lane->group = $this->dao->select('`group`')->from(TABLE_KANBANLANE)->where('id')->eq($lane->otherLane)->fetch('group');
  1904. }
  1905. elseif($lane->mode == 'independent')
  1906. {
  1907. $lane->group = $this->createGroup($kanbanID, $regionID);
  1908. if($lane->type == 'common') $this->createDefaultColumns($regionID, $lane->group);
  1909. }
  1910. }
  1911. $this->dao->insert(TABLE_KANBANLANE)->data($lane, $skip = 'mode,otherLane')
  1912. ->batchCheck($this->config->kanban->require->createlane, 'notempty')
  1913. ->autoCheck()
  1914. ->exec();
  1915. if(dao::isError()) return false;
  1916. $laneID = $this->dao->lastInsertID();
  1917. if($mode == 'new')
  1918. {
  1919. if($lane->type != 'common' && isset($lane->mode) && $lane->mode == 'independent') $this->createRDColumn($regionID, $lane->group, $laneID, $lane->type, $kanbanID);
  1920. if($lane->mode == 'sameAsOther' || ($lane->type == 'common' && $lane->mode == 'independent'))
  1921. {
  1922. $columnIDList = $this->dao->select('id')->from(TABLE_KANBANCOLUMN)->where('deleted')->eq(0)->andWhere('archived')->eq(0)->andWhere('`group`')->eq($lane->group)->fetchPairs();
  1923. foreach($columnIDList as $columnID)
  1924. {
  1925. $this->addKanbanCell($kanbanID, $laneID, $columnID, $lane->type);
  1926. if(dao::isError()) return false;
  1927. }
  1928. }
  1929. }
  1930. $this->loadModel('action')->create('kanbanLane', $laneID, 'created');
  1931. return $laneID;
  1932. }
  1933. /*
  1934. * 创建看板。
  1935. * Create a kanban.
  1936. *
  1937. * @param object $kanban
  1938. * @access public
  1939. * @return int|false
  1940. */
  1941. /**
  1942. * @return int|false
  1943. * @param object $kanban
  1944. */
  1945. public function create($kanban)
  1946. {
  1947. $account = $this->app->user->account;
  1948. if($this->post->import == 'on') $kanban->object = implode(',', $this->post->importObjectList);
  1949. if(strpos(",{$kanban->team},", ",$account,") === false) $kanban->team .= ",$account";
  1950. if(strpos(",{$kanban->team},", ",$kanban->owner,") === false) $kanban->team .= ",$kanban->owner";
  1951. if(!empty($kanban->space))
  1952. {
  1953. $maxOrder = $this->dao->select('MAX(`order`) AS maxOrder')->from(TABLE_KANBAN)
  1954. ->where('space')->eq($kanban->space)
  1955. ->fetch('maxOrder');
  1956. $kanban->order = $maxOrder ? $maxOrder + 1 : 1;
  1957. $space = $this->getSpaceById($kanban->space);
  1958. if(!empty($space) && $space->type == 'private') $kanban->owner = $account;
  1959. }
  1960. $this->kanbanTao->createKanban($kanban);
  1961. if(dao::isError()) return false;
  1962. $kanbanID = $this->dao->lastInsertID();
  1963. $kanbanInfo = $this->getByID($kanbanID);
  1964. $this->loadModel('action')->create('kanban', $kanbanID, 'created');
  1965. $this->loadModel('file')->saveUpload('kanban', $kanbanID);
  1966. $this->file->updateObjectID($this->post->uid, $kanbanID, 'kanban');
  1967. !empty($kanban->copyRegion) ? $this->copyRegions($kanbanInfo, $kanban->copyKanbanID): $this->createDefaultRegion($kanbanInfo);
  1968. if(!empty($kanbanInfo->team) || !empty($kanbanInfo->whitelist))
  1969. {
  1970. $type = !empty($kanbanInfo->team) ? 'team' : 'whitelist';
  1971. $kanbanMembers = empty($kanbanInfo->{$type}) ? array() : explode(',', $kanbanInfo->{$type});
  1972. $this->addSpaceMembers($kanbanInfo->space, $type, $kanbanMembers);
  1973. }
  1974. return $kanbanID;
  1975. }
  1976. /**
  1977. * 编辑看板。
  1978. * Update a kanban.
  1979. *
  1980. * @param int $kanbanID
  1981. * @param object $kanban
  1982. * @access public
  1983. * @return bool
  1984. */
  1985. public function update($kanbanID, $kanban)
  1986. {
  1987. $oldKanban = $this->getByID($kanbanID);
  1988. if(strpos(",{$kanban->team},", ",$kanban->owner,") === false) $kanban->team .= ",$kanban->owner";
  1989. $this->dao->update(TABLE_KANBAN)->data($kanban)
  1990. ->autoCheck()
  1991. ->batchCheck($this->config->kanban->edit->requiredFields, 'notempty')
  1992. ->where('id')->eq($kanbanID)
  1993. ->exec();
  1994. if(dao::isError()) return false;
  1995. $this->loadModel('file')->saveUpload('kanban', $kanbanID);
  1996. $this->file->updateObjectID($this->post->uid, $kanbanID, 'kanban');
  1997. if(!empty($kanban->team) || !empty($kanban->whitelist))
  1998. {
  1999. $type = !empty($kanban->team) ? 'team' : 'whitelist';
  2000. $kanbanMembers = empty($kanban->{$type}) ? array() : explode(',', $kanban->{$type});
  2001. $this->addSpaceMembers($kanban->space, $type, $kanbanMembers);
  2002. }
  2003. $changes = common::createChanges($oldKanban, $kanban);
  2004. $actionID = $this->loadModel('action')->create('kanban', $kanbanID, 'edited');
  2005. $this->action->logHistory($actionID, $changes);
  2006. return true;
  2007. }
  2008. /**
  2009. * 设置看板。
  2010. * Setting kanban.
  2011. *
  2012. * @param int $kanbanID
  2013. * @param object $kanban
  2014. * @access public
  2015. * @return bool
  2016. */
  2017. public function setting($kanbanID, $kanban)
  2018. {
  2019. $oldKanban = $this->getByID($kanbanID);
  2020. if($this->post->import == 'off') $kanban->object = '';
  2021. if($this->post->heightType == 'auto') $kanban->displayCards = 0;
  2022. if(empty($kanban->displayCards)) $kanban->displayCards = 0;
  2023. if($this->post->import == 'on') $kanban->object = $this->post->importObjectList ? implode(',', $this->post->importObjectList) : '';
  2024. if(isset($_POST['heightType']) && $this->post->heightType == 'custom' && !$this->checkDisplayCards($kanban->displayCards)) return false;
  2025. $this->dao->update(TABLE_KANBAN)->data($kanban)
  2026. ->autoCheck()
  2027. ->batchCheck($this->config->kanban->edit->requiredFields, 'notempty')
  2028. ->checkIF(!$kanban->fluidBoard, 'colWidth', 'ge', $this->config->minColWidth)
  2029. ->batchCheckIF($kanban->fluidBoard, 'minColWidth', 'ge', $this->config->minColWidth)
  2030. ->checkIF($kanban->minColWidth >= $this->config->minColWidth && $kanban->fluidBoard, 'maxColWidth', 'gt', $kanban->minColWidth)
  2031. ->where('id')->eq($kanbanID)
  2032. ->exec();
  2033. if(dao::isError()) return false;
  2034. $changes = common::createChanges($oldKanban, $kanban);
  2035. if($changes)
  2036. {
  2037. $actionID = $this->loadModel('action')->create('kanban', $kanbanID, 'edited');
  2038. $this->action->logHistory($actionID, $changes);
  2039. }
  2040. return true;
  2041. }
  2042. /**
  2043. * 激活看板。
  2044. * Activate a kanban.
  2045. *
  2046. * @param int $kanbanID
  2047. * @param object $kanban
  2048. * @access public
  2049. * @return bool
  2050. */
  2051. function activate($kanbanID, $kanban)
  2052. {
  2053. $oldKanban = $this->getByID($kanbanID);
  2054. $this->dao->update(TABLE_KANBAN)->data($kanban)
  2055. ->autoCheck()
  2056. ->where('id')->eq($kanbanID)
  2057. ->exec();
  2058. if(dao::isError()) return false;
  2059. $changes = common::createChanges($oldKanban, $kanban);
  2060. $actionID = $this->loadModel('action')->create('kanban', $kanbanID, 'activated', $this->post->comment);
  2061. $this->action->logHistory($actionID, $changes);
  2062. return true;
  2063. }
  2064. /**
  2065. * 关闭看板。
  2066. * Close a kanban.
  2067. *
  2068. * @param int $kanbanID
  2069. * @param object $kanban
  2070. * @access public
  2071. * @return bool
  2072. */
  2073. function close($kanbanID, $kanban)
  2074. {
  2075. $oldKanban = $this->getByID($kanbanID);
  2076. $this->dao->update(TABLE_KANBAN)->data($kanban)
  2077. ->autoCheck()
  2078. ->where('id')->eq($kanbanID)
  2079. ->exec();
  2080. if(dao::isError()) return false;
  2081. $changes = common::createChanges($oldKanban, $kanban);
  2082. if($changes)
  2083. {
  2084. $actionID = $this->loadModel('action')->create('kanban', $kanbanID, 'closed', $this->post->comment);
  2085. $this->action->logHistory($actionID, $changes);
  2086. }
  2087. return true;
  2088. }
  2089. /* 执行看板新增列和泳道。
  2090. * Check whether kanban lane exists.
  2091. *
  2092. * @param object $execution
  2093. * @access public
  2094. * @return void
  2095. */
  2096. /**
  2097. * @param object $execution
  2098. */
  2099. public function createLaneIfNotExist($execution)
  2100. {
  2101. $kanbanColumns = $this->dao->select('*')->from(TABLE_KANBANLANE)
  2102. ->where('execution')->eq($execution->id)
  2103. ->andWhere('deleted')->eq(0)
  2104. ->fetchAll('type');
  2105. $types = array_keys($kanbanColumns);
  2106. /* 如果没有任意一种类型的 kanban, 生成一份新的 */
  2107. if(empty($types))
  2108. {
  2109. $this->createExecutionLane($execution, 'all');
  2110. }
  2111. else
  2112. {
  2113. /* 如果缺少某种类型的 kanban,单独生成缺少的 */
  2114. $kanbanTypes = $this->lang->kanban->type;
  2115. unset($kanbanTypes['all']);
  2116. unset($kanbanTypes['epic']);
  2117. unset($kanbanTypes['requirement']);
  2118. foreach($kanbanTypes as $type => $typeName)
  2119. {
  2120. if(!in_array($type, $types)) $this->createExecutionLane($execution, $type);
  2121. }
  2122. }
  2123. }
  2124. /**
  2125. * 执行看板新增列和泳道。
  2126. * Add execution Kanban lanes and columns.
  2127. *
  2128. * @param object $execution
  2129. * @param string $type all|parentStory|story|bug|task|risk
  2130. * @access public
  2131. * @return void
  2132. */
  2133. public function createExecutionLane($execution, $type = 'all')
  2134. {
  2135. if(!$execution) return false;
  2136. /* e.g $defaults = array('risk' => (object)array('name' => 'risk', 'color' => '#FF0000', 'order' => 20)); */
  2137. $defaults = ($type != 'all') ? array($type => $this->config->kanban->default->$type) : (array)$this->config->kanban->default;
  2138. $ERURLanes = array();
  2139. $parentStoryLaneID = 0;
  2140. foreach($defaults as $type => $lane)
  2141. {
  2142. /* 只有综合、需求、设计阶段,才可关联业需、用需。 */
  2143. if($execution->type != 'stage' && !in_array($execution->attribute, array('mix', 'request', 'design')) && in_array($type, array('epic', 'requirement'))) continue;
  2144. $lane->type = $type;
  2145. $lane->execution = $execution->id;
  2146. $lane->region = 0;
  2147. $lane->group = 0;
  2148. $lane->groupby = '';
  2149. $lane->extra = '';
  2150. $this->dao->insert(TABLE_KANBANLANE)->data($lane)->exec();
  2151. $laneID = $this->dao->lastInsertId();
  2152. if($type == 'parentStory') $parentStoryLaneID = $laneID;
  2153. if(in_array($type, array('epic', 'requirement')))
  2154. {
  2155. $ERURLanes[$type] = $laneID;
  2156. }
  2157. else
  2158. {
  2159. if($type != 'risk') $this->createExecutionColumns($laneID, $type, $execution->id);
  2160. else $this->createExecutionRiskColumns($laneID, $type, $execution->id);
  2161. }
  2162. }
  2163. if($ERURLanes)
  2164. {
  2165. $columnIDList = $this->dao->select('`column`')->from(TABLE_KANBANCELL)->where('lane')->eq($parentStoryLaneID)->fetchPairs();
  2166. foreach($ERURLanes as $type => $laneID)
  2167. {
  2168. foreach($columnIDList as $columnID)
  2169. {
  2170. $this->addKanbanCell($execution->id, $laneID, $columnID, $type);
  2171. if(dao::isError()) return false;
  2172. }
  2173. }
  2174. }
  2175. return true;
  2176. }
  2177. /**
  2178. * 创建执行看板列。
  2179. * Create execution columns.
  2180. *
  2181. * @param int|array $laneID
  2182. * @param string $type story|bug|task
  2183. * @param int $executionID
  2184. * @access public
  2185. * @return void
  2186. */
  2187. public function createExecutionColumns($laneID, $type, $executionID)
  2188. {
  2189. $designColumnID = $devColumnID = $testColumnID = $resolvingColumnID = 0;
  2190. $columns = array();
  2191. if(in_array($type, array('epic', 'requirement', 'parentStory'))) $columns = $this->lang->kanban->ERURColumn;
  2192. if($type == 'story') $columns = $this->lang->kanban->storyColumn;
  2193. if($type == 'bug') $columns = $this->lang->kanban->bugColumn;
  2194. if($type == 'task') $columns = $this->lang->kanban->taskColumn;
  2195. if(empty($columns)) return;
  2196. foreach($columns as $colType => $name)
  2197. {
  2198. $data = new stdclass();
  2199. $data->name = $name;
  2200. $data->color = '#333';
  2201. $data->type = $colType;
  2202. $data->region = 0;
  2203. if(strpos(',designing,designed,', ",{$colType},") !== false) $data->parent = $designColumnID;
  2204. if(strpos(',developing,developed,', ",{$colType},") !== false) $data->parent = $devColumnID;
  2205. if(strpos(',testing,tested,', ",{$colType},") !== false) $data->parent = $testColumnID;
  2206. if(strpos(',fixing,fixed,', ",{$colType},") !== false) $data->parent = $resolvingColumnID;
  2207. if(strpos(',resolving,develop,test,', ",{$colType},") !== false) $data->parent = -1;
  2208. $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec();
  2209. $colID = $this->dao->lastInsertId();
  2210. if($colType == 'design') $designColumnID = $colID;
  2211. if($colType == 'develop') $devColumnID = $colID;
  2212. if($colType == 'test') $testColumnID = $colID;
  2213. if($colType == 'resolving') $resolvingColumnID = $colID;
  2214. if(is_array($laneID))
  2215. {
  2216. foreach($laneID as $id) $this->addKanbanCell($executionID, $id, $colID, $type);
  2217. }
  2218. else
  2219. {
  2220. $this->addKanbanCell($executionID, $laneID, $colID, $type);
  2221. }
  2222. }
  2223. }
  2224. /**
  2225. * 新增看板Cell。
  2226. * Add kanban cell for new lane.
  2227. *
  2228. * @param int $kanbanID
  2229. * @param int $laneID
  2230. * @param int $colID
  2231. * @param string $type story|task|bug|card
  2232. * @param string $cardID
  2233. * @access public
  2234. * @return void
  2235. */
  2236. public function addKanbanCell($kanbanID, $laneID, $colID, $type, $cardID = '')
  2237. {
  2238. $cell = $this->dao->select('id, cards')->from(TABLE_KANBANCELL)
  2239. ->where('kanban')->eq($kanbanID)
  2240. ->andWhere('lane')->eq($laneID)
  2241. ->andWhere('`column`')->eq($colID)
  2242. ->andWhere('type')->eq($type)
  2243. ->fetch();
  2244. if(empty($cell))
  2245. {
  2246. $cell = new stdclass();
  2247. $cell->kanban = $kanbanID;
  2248. $cell->lane = $laneID;
  2249. $cell->column = $colID;
  2250. $cell->type = $type;
  2251. $cell->cards = $cardID ? ",$cardID," : '';
  2252. $this->dao->insert(TABLE_KANBANCELL)->data($cell)->exec();
  2253. }
  2254. else
  2255. {
  2256. $cell->cards = $cell->cards ? ",$cardID" . $cell->cards : ",$cardID,";
  2257. $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cell->cards)->where('id')->eq($cell->id)->exec();
  2258. }
  2259. }
  2260. /**
  2261. * 向看板空间添加成员。
  2262. * Add space members.
  2263. *
  2264. * @param int $spaceID
  2265. * @param string $type team|whitelist
  2266. * @param array $kanbanMembers
  2267. * @access public
  2268. * @return void
  2269. */
  2270. public function addSpaceMembers($spaceID, $type, $kanbanMembers = array())
  2271. {
  2272. $space = $this->getSpaceById($spaceID);
  2273. if(empty($space)) return;
  2274. $spaceMembers = empty($space->{$type}) ? array() : explode(',', $space->{$type});
  2275. $members = $space->{$type};
  2276. $addMembers = array_diff($kanbanMembers, $spaceMembers);
  2277. if(!empty($addMembers))
  2278. {
  2279. $addMembers = implode(',', $addMembers);
  2280. $members .= ',' . trim($addMembers, ',');
  2281. $this->dao->update(TABLE_KANBANSPACE)->set($type)->eq($members)->where('id')->eq($spaceID)->exec();
  2282. }
  2283. }
  2284. /**
  2285. * 从看板格子中移除卡片。
  2286. * Remove kanban cell.
  2287. *
  2288. * @param string $type
  2289. * @param int|array $removeCardID
  2290. * @param array $kanbanList
  2291. * @access public
  2292. * @return void
  2293. */
  2294. public function removeKanbanCell($type, $removeCardID, $kanbanList)
  2295. {
  2296. $removeIDList = is_array($removeCardID) ? $removeCardID : array($removeCardID);
  2297. foreach($removeIDList as $cardID)
  2298. {
  2299. if(empty($cardID)) continue;
  2300. $this->dao->update(TABLE_KANBANCELL)->set("cards = REPLACE(cards, ',$cardID,', ',')")->where('type')->eq($type)->andWhere('kanban')->eq($kanbanList[$cardID])->exec();
  2301. }
  2302. $this->dao->update(TABLE_KANBANCELL)
  2303. ->set('cards')->eq('')
  2304. ->where('cards')->eq(',')
  2305. ->andWhere('type')->eq($type)
  2306. ->andWhere('kanban')->in($kanbanList)
  2307. ->exec();
  2308. }
  2309. /**
  2310. * 创建默认的专业研发看板。
  2311. * Create a default RD kanban.
  2312. *
  2313. * @param object $execution
  2314. * @access public
  2315. * @return bool
  2316. */
  2317. public function createRDKanban($execution)
  2318. {
  2319. $regionID = $this->createRDRegion($execution);
  2320. if(dao::isError()) return false;
  2321. $this->createGroup((int)$execution->id, $regionID);
  2322. if(dao::isError()) return false;
  2323. $this->createRDLane((int)$execution->id, $regionID);
  2324. if(dao::isError()) return false;
  2325. return true;
  2326. }
  2327. /**
  2328. * 创建默认的专业研发看板区域。
  2329. * Create a default RD region.
  2330. *
  2331. * @param object $execution
  2332. *
  2333. * @access public
  2334. * @return int|bool
  2335. */
  2336. public function createRDRegion($execution)
  2337. {
  2338. $region = new stdclass();
  2339. $region->name = $this->lang->kanbanregion->default;
  2340. $region->kanban = $execution->id;
  2341. $region->createdBy = $this->app->user->account;
  2342. $region->createdDate = helper::now();
  2343. $region->order = 1;
  2344. $this->dao->insert(TABLE_KANBANREGION)->data($region)
  2345. ->check('name', 'unique', "kanban={$execution->id} AND deleted='0' AND space = '0'")
  2346. ->autoCheck()
  2347. ->exec();
  2348. if(dao::isError()) return false;
  2349. return $this->dao->lastInsertId();
  2350. }
  2351. /**
  2352. * 创建默认的专业研发看板泳道。
  2353. * Create default RD lanes.
  2354. *
  2355. * @param int $executionID
  2356. * @param int $regionID
  2357. *
  2358. * @access public
  2359. * @return bool
  2360. */
  2361. public function createRDLane($executionID, $regionID)
  2362. {
  2363. $execution = $this->loadModel('execution')->fetchByID($executionID);
  2364. $project = $this->loadModel('project')->fetchByID($execution->project);
  2365. $index = 0;
  2366. foreach($this->lang->kanban->laneTypeList as $type => $name)
  2367. {
  2368. $groupID = $this->createGroup($executionID, $regionID);
  2369. if(dao::isError()) return false;
  2370. $lane = new stdclass();
  2371. $lane->execution = $executionID;
  2372. $lane->type = $type;
  2373. $lane->region = $regionID;
  2374. $lane->group = $groupID;
  2375. $lane->name = $name;
  2376. $lane->color = $this->config->kanban->laneColorList[$index];
  2377. $lane->order = $this->config->kanban->RDLaneOrder[$type];
  2378. $this->dao->insert(TABLE_KANBANLANE)->data($lane)->autoCheck()->exec();
  2379. if(dao::isError()) return false;
  2380. $laneID = $this->dao->lastInsertId();
  2381. $this->createRDColumn($regionID, $groupID, $laneID, $type, $executionID);
  2382. $index ++;
  2383. }
  2384. return true;
  2385. }
  2386. /**
  2387. * 创建默认值的专业研发看板列。
  2388. * Create default RD columns.
  2389. *
  2390. * @param int $regionID
  2391. * @param int $groupID
  2392. * @param int $laneID
  2393. * @param string $laneType
  2394. * @param int $executionID
  2395. *
  2396. * @access public
  2397. * @return bool
  2398. */
  2399. public function createRDColumn($regionID, $groupID, $laneID, $laneType, $executionID)
  2400. {
  2401. $designColumnID = $devColumnID = $testColumnID = $resolvingColumnID = 0;
  2402. if($laneType == 'parentStory') $columnList = $this->lang->kanban->ERURColumn;
  2403. if($laneType == 'story') $columnList = $this->lang->kanban->storyColumn;
  2404. if($laneType == 'bug') $columnList = $this->lang->kanban->bugColumn;
  2405. if($laneType == 'task') $columnList = $this->lang->kanban->taskColumn;
  2406. foreach($columnList as $type => $name)
  2407. {
  2408. $data = new stdclass();
  2409. $data->name = $name;
  2410. $data->color = '#333';
  2411. $data->type = $type;
  2412. $data->group = $groupID;
  2413. $data->region = $regionID;
  2414. if(strpos(',designing,designed,', $type) !== false) $data->parent = $designColumnID;
  2415. if(strpos(',developing,developed,', $type) !== false) $data->parent = $devColumnID;
  2416. if(strpos(',testing,tested,', $type) !== false) $data->parent = $testColumnID;
  2417. if(strpos(',fixing,fixed,', $type) !== false) $data->parent = $resolvingColumnID;
  2418. if(strpos(',design,develop,test,resolving,', $type) !== false) $data->parent = -1;
  2419. $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec();
  2420. if(dao::isError()) return false;
  2421. $columnID = $this->dao->lastInsertId();
  2422. if($type == 'design') $designColumnID = $columnID;
  2423. if($type == 'develop') $devColumnID = $columnID;
  2424. if($type == 'test') $testColumnID = $columnID;
  2425. if($type == 'resolving') $resolvingColumnID = $columnID;
  2426. $this->addKanbanCell($executionID, $laneID, $columnID, $laneType);
  2427. }
  2428. return true;
  2429. }
  2430. /**
  2431. * 编辑区域。
  2432. * Update a region.
  2433. *
  2434. * @param int $regionID
  2435. * @access public
  2436. * @return bool
  2437. */
  2438. public function updateRegion($regionID)
  2439. {
  2440. $region = new stdclass();
  2441. $region->lastEditedBy = $this->app->user->account;
  2442. $region->lastEditedDate = helper::now();
  2443. $region->name = trim($this->post->name);
  2444. $oldRegion = $this->getRegionById($regionID);
  2445. $this->dao->update(TABLE_KANBANREGION)->data($region)
  2446. ->autoCheck()
  2447. ->batchcheck($this->config->kanban->editregion->requiredFields, 'notempty')
  2448. ->where('id')->eq($regionID)
  2449. ->exec();
  2450. if(dao::isError()) return false;
  2451. $changes = common::createChanges($oldRegion, $region);
  2452. if($changes)
  2453. {
  2454. $actionID = $this->loadModel('action')->create('kanbanregion', $regionID, 'edited');
  2455. $this->action->logHistory($actionID, $changes);
  2456. }
  2457. return true;
  2458. }
  2459. /**
  2460. * 编辑看板泳道。
  2461. * Update kanban lane.
  2462. *
  2463. * @param int $executionID
  2464. * @param string $laneType
  2465. * @param int $cardID
  2466. * @access public
  2467. * @return void
  2468. */
  2469. public function updateLane($executionID, $laneType = '', $cardID = 0)
  2470. {
  2471. $execution = $this->loadModel('execution')->getByID($executionID);
  2472. if($execution->type == 'kanban')
  2473. {
  2474. $lanes = $this->dao->select('t2.*')->from(TABLE_KANBANREGION)->alias('t1')
  2475. ->leftJoin(TABLE_KANBANLANE)->alias('t2')->on('t1.id=t2.region')
  2476. ->leftJoin(TABLE_KANBANCELL)->alias('t3')->on('t2.id=t3.lane')
  2477. ->where('t1.deleted')->eq(0)
  2478. ->andWhere('t2.deleted')->eq(0)
  2479. ->andWhere('t1.kanban')->eq($executionID)
  2480. ->andWhere('t2.execution')->eq($executionID)
  2481. ->beginIF($laneType)->andWhere('t2.type')->eq($laneType)->fi()
  2482. ->beginIF(!empty($cardID))->andWhere('t3.cards')->like("%,$cardID,%")->fi()
  2483. ->orderBy('t1.`order` asc, t2.`order` asc')
  2484. ->fetchAll('id');
  2485. if(count($lanes) > 1) $lanes = array_slice($lanes, 0, 2);
  2486. }
  2487. else
  2488. {
  2489. $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE)
  2490. ->where('execution')->eq($executionID)
  2491. ->beginIF($laneType)->andWhere('type')->eq($laneType)->fi()
  2492. ->fetchAll('id');
  2493. }
  2494. foreach($lanes as $lane) $this->refreshCards((array)$lane);
  2495. }
  2496. /**
  2497. * 更新看板泳道上的卡片。
  2498. * Refresh column cards.
  2499. *
  2500. * @param array $lane
  2501. * @access public
  2502. * @return void
  2503. */
  2504. public function refreshCards($lane)
  2505. {
  2506. if(common::isTutorialMode()) return;
  2507. $laneID = zget($lane, 'id');
  2508. $laneType = zget($lane, 'type');
  2509. $executionID = zget($lane, 'execution');
  2510. $otherCardList = '';
  2511. $otherLanes = $this->dao->select('t2.id, t2.cards')->from(TABLE_KANBANLANE)->alias('t1')
  2512. ->leftJoin(TABLE_KANBANCELL)->alias('t2')->on('t1.id=t2.lane')
  2513. ->where('t1.id')->ne($laneID)
  2514. ->andWhere('t1.execution')->eq($executionID)
  2515. ->andWhere('t2.`type`')->eq($laneType)
  2516. ->fetchPairs();
  2517. foreach($otherLanes as $cardIDList)
  2518. {
  2519. $cardIDList = trim($cardIDList, ',');
  2520. if(!empty($cardIDList)) $otherCardList .= ',' . $cardIDList;
  2521. }
  2522. $cardPairs = $this->dao->select('t2.type, t1.cards')->from(TABLE_KANBANCELL)->alias('t1')
  2523. ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id')
  2524. ->where('t1.kanban')->eq($executionID)
  2525. ->andWhere('t1.lane')->eq($laneID)
  2526. ->fetchPairs();
  2527. if(empty($cardPairs)) return;
  2528. $sourceCards = $cardPairs;
  2529. if(in_array($laneType, array('epic', 'requirement', 'parentStory'))) $cardPairs = $this->kanbanTao->refreshERURCards($cardPairs, $executionID, $otherCardList, $laneType);
  2530. if($laneType == 'story') $cardPairs = $this->kanbanTao->refreshStoryCards($cardPairs, $executionID, $otherCardList);
  2531. if($laneType == 'bug') $cardPairs = $this->kanbanTao->refreshBugCards($cardPairs, $executionID, $otherCardList);
  2532. if($laneType == 'task') $cardPairs = $this->kanbanTao->refreshTaskCards($cardPairs, $executionID, $otherCardList);
  2533. if($laneType == 'risk' && in_array($this->config->edition, array('max', 'ipd'))) $cardPairs = $this->refreshRiskCards($cardPairs, $executionID, $otherCardList);
  2534. $colPairs = $this->dao->select('t2.type, t2.id')->from(TABLE_KANBANCELL)->alias('t1')
  2535. ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id')
  2536. ->where('t1.kanban')->eq($executionID)
  2537. ->andWhere('t1.lane')->eq($laneID)
  2538. ->fetchPairs();
  2539. $updated = false;
  2540. foreach($cardPairs as $colType => $cards)
  2541. {
  2542. if(!isset($colPairs[$colType])) continue;
  2543. if($sourceCards[$colType] == $cards) continue;
  2544. $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cards)->where('lane')->eq($laneID)->andWhere('`column`')->eq($colPairs[$colType])->exec();
  2545. if(!$updated) $updated = true;
  2546. }
  2547. if($updated) $this->dao->update(TABLE_KANBANLANE)->set('lastEditedTime')->eq(helper::now())->where('id')->eq($laneID)->exec();
  2548. }
  2549. /**
  2550. * 编辑看板列。
  2551. * Update column.
  2552. *
  2553. * @param int $columnID
  2554. * @param object $column
  2555. * @access public
  2556. * @return array|false
  2557. */
  2558. public function updateColumn($columnID, $column)
  2559. {
  2560. $oldColumn = $this->getColumnById($columnID);
  2561. $this->dao->update(TABLE_KANBANCOLUMN)->data($column)
  2562. ->autoCheck()
  2563. ->batchcheck($this->config->kanban->setColumn->requiredFields, 'notempty')
  2564. ->where('id')->eq($columnID)
  2565. ->exec();
  2566. if(dao::isError()) return false;
  2567. $changes = common::createChanges($oldColumn, $column);
  2568. return $changes;
  2569. }
  2570. /**
  2571. * 激活卡片。
  2572. * Activate a card.
  2573. *
  2574. * @param int $cardID
  2575. * @access public
  2576. * @return bool
  2577. */
  2578. public function activateCard($cardID)
  2579. {
  2580. if($this->post->progress >= 100 || $this->post->progress < 0)
  2581. {
  2582. dao::$errors['progress'][] = $this->lang->kanbancard->error->progressIllegal;
  2583. return false;
  2584. }
  2585. $oldCard = $this->getCardByID($cardID);
  2586. $this->dao->update(TABLE_KANBANCARD)->set('progress')->eq($this->post->progress ? $this->post->progress : 0)->set('status')->eq('doing')->where('id')->eq($cardID)->exec();
  2587. $card = $this->getCardByID($cardID);
  2588. $changes = common::createChanges($oldCard, $card);
  2589. $actionID = $this->loadModel('action')->create('kanbanCard', $cardID, 'activated');
  2590. $this->action->logHistory($actionID, $changes);
  2591. return true;
  2592. }
  2593. /**
  2594. * 编辑看板卡片。
  2595. * Update a card.
  2596. *
  2597. * @param int $cardID
  2598. * @param object $card
  2599. * @access public
  2600. * @return bool
  2601. */
  2602. public function updateCard($cardID, $card)
  2603. {
  2604. if($card->estimate < 0)
  2605. {
  2606. dao::$errors['estimate'] = $this->lang->kanbancard->error->recordMinus;
  2607. return false;
  2608. }
  2609. if($card->end && ($card->begin > $card->end))
  2610. {
  2611. dao::$errors['end'] = $this->lang->kanbancard->error->endSmall;
  2612. return false;
  2613. }
  2614. if($card->progress > 100 || $card->progress < 0)
  2615. {
  2616. dao::$errors['progress'] = $this->lang->kanbancard->error->progressIllegal;
  2617. return false;
  2618. }
  2619. $oldCard = $this->getCardByID($cardID);
  2620. if(!empty($this->post->assignedTo) && $oldCard->assignedTo != $this->post->assignedTo) $card->assignedDate = helper::now();
  2621. $card->status = $card->progress == 100 ? 'done' : 'doing';
  2622. $card = $this->loadModel('file')->processImgURL($card, $this->config->kanban->editor->editcard['id'], $this->post->uid);
  2623. $this->dao->update(TABLE_KANBANCARD)->data($card)
  2624. ->autoCheck()
  2625. ->checkIF($card->estimate != '', 'estimate', 'float')
  2626. ->batchcheck($this->config->kanban->editcard->requiredFields, 'notempty')
  2627. ->where('id')->eq($cardID)
  2628. ->exec();
  2629. if(dao::isError()) return false;
  2630. $this->file->saveUpload('kanbancard', $cardID);
  2631. $this->file->updateObjectID($this->post->uid, $cardID, 'kanbancard');
  2632. $changes = common::createChanges($oldCard, $card);
  2633. if($changes)
  2634. {
  2635. $actionID = $this->loadModel('action')->create('kanbanCard', $cardID, 'edited');
  2636. $this->action->logHistory($actionID, $changes);
  2637. }
  2638. return true;
  2639. }
  2640. /**
  2641. * 设置看板在制品限制。
  2642. * Set WIP limit.
  2643. *
  2644. * @param int $columnID
  2645. * @param object $WIP
  2646. * @access public
  2647. * @return bool
  2648. */
  2649. public function setWIP($columnID, $WIP)
  2650. {
  2651. $oldColumn = $this->getColumnById($columnID);
  2652. if(!preg_match("/^-?\d+$/", (string)$WIP->limit) || (!$WIP->noLimit && $WIP->limit <= 0))
  2653. {
  2654. dao::$errors['limit'] = $this->lang->kanban->error->mustBeInt;
  2655. return false;
  2656. }
  2657. /* Check column limit. */
  2658. $sumChildLimit = 0;
  2659. if($oldColumn->parent == -1 && $WIP->limit != -1)
  2660. {
  2661. $childColumns = $this->dao->select('id,`limit`')->from(TABLE_KANBANCOLUMN)->where('parent')->eq($columnID)->andWhere('deleted')->eq(0)->fetchAll();
  2662. foreach($childColumns as $childColumn) $sumChildLimit += $childColumn->limit;
  2663. if($sumChildLimit > $WIP->limit)
  2664. {
  2665. dao::$errors['limit'] = $this->lang->kanban->error->parentLimitNote;
  2666. }
  2667. }
  2668. elseif($oldColumn->parent > 0)
  2669. {
  2670. $parentColumn = $this->getColumnByID($oldColumn->parent);
  2671. if($parentColumn->limit != -1)
  2672. {
  2673. $siblingLimit = $this->dao->select('`limit`')->from(TABLE_KANBANCOLUMN)
  2674. ->where('`parent`')->eq($oldColumn->parent)
  2675. ->andWhere('id')->ne($columnID)
  2676. ->fetch('limit');
  2677. $sumChildLimit = (int)$siblingLimit + (int)$WIP->limit;
  2678. if($WIP->limit == -1 || $siblingLimit == -1 || $sumChildLimit > $parentColumn->limit)
  2679. {
  2680. dao::$errors['limit'] = $this->lang->kanban->error->childLimitNote;
  2681. }
  2682. }
  2683. }
  2684. if(dao::isError()) return false;
  2685. $this->dao->update(TABLE_KANBANCOLUMN)->data($WIP, 'noLimit')
  2686. ->autoCheck()
  2687. ->checkIF($WIP->limit != -1, 'limit', 'gt', 0)
  2688. ->where('id')->eq($columnID)
  2689. ->exec();
  2690. return !dao::isError();
  2691. }
  2692. /**
  2693. * 设置看板列。
  2694. * Set lane info.
  2695. *
  2696. * @param int $laneID
  2697. * @param object $lane
  2698. * @access public
  2699. * @return bool
  2700. */
  2701. public function setLane($laneID, $lane)
  2702. {
  2703. $this->dao->update(TABLE_KANBANLANE)->data($lane)
  2704. ->autoCheck()
  2705. ->batchcheck($this->config->kanban->setlane->requiredFields, 'notempty')
  2706. ->where('id')->eq($laneID)
  2707. ->exec();
  2708. return !dao::isError();
  2709. }
  2710. /**
  2711. * 获取看板右上角的工具栏。
  2712. * Get kanban pageToolBar.
  2713. *
  2714. * @param object $kanban
  2715. * @access public
  2716. * @return string
  2717. */
  2718. public function getPageToolBar($kanban)
  2719. {
  2720. $actions = '';
  2721. $actions .= "<a href='javascript:$(\"#kanbanList\").fullscreen();' id='fullScreenBtn' class='toolbar-item ghost btn btn-default'><i class='icon icon-fullscreen'></i> {$this->lang->kanban->fullScreen}</a>";
  2722. $printKanbanBtn = (common::hasPriv('kanban', 'edit') || ($kanban->status == 'active' && common::hasPriv('kanban', 'close')) || common::hasPriv('kanban', 'delete') || ($kanban->status == 'closed' && common::hasPriv('kanban', 'activate')));
  2723. if($printKanbanBtn)
  2724. {
  2725. $actions .= "<a class='toolbar-item ghost btn btn-default setting' type='button' data-toggle='dropdown' data-target='#kanbanActionMenu'>" . '<i class="icon icon-edit"></i> ' . $this->lang->edit . '</a>';
  2726. $actions .= "<menu id='kanbanActionMenu' class='dropdown-menu text-left'>";
  2727. $columnActions = '';
  2728. $actions .= $columnActions;
  2729. $commonActions = '';
  2730. if($columnActions && $commonActions)
  2731. {
  2732. $actions .= "<li class='divider'></li>";
  2733. }
  2734. $actions .= $commonActions;
  2735. $kanbanActions = '';
  2736. if(common::hasPriv('kanban', 'edit')) $kanbanActions .= '<li class="menu-item item">' . html::a(helper::createLink('kanban', 'edit', "kanbanID=$kanban->id", '', true), '<i class="icon icon-edit"></i>' . $this->lang->kanban->edit, '', "class='listitem item-inner menu-item-inner state' data-toggle='modal'") . '</li>';
  2737. if(common::hasPriv('kanban', 'close') && $kanban->status == 'active') $kanbanActions .= '<li class="menu-item item">' . html::a(helper::createLink('kanban', 'close', "kanbanID=$kanban->id", '', true), '<i class="icon icon-off"></i>' . $this->lang->kanban->close, '', "class='listitem item-inner menu-item-inner state' data-toggle='modal'") . '</li>';
  2738. if(common::hasPriv('kanban', 'activate') && $kanban->status == 'closed') $kanbanActions .= '<li class="menu-item item">' . html::a(helper::createLink('kanban', 'activate', "kanbanID=$kanban->id", '', true), '<i class="icon icon-magic"></i>' . $this->lang->kanban->activate, '', "class='listitem item-inner menu-item-inner state' data-toggle='modal'") . '</li>';
  2739. if(common::hasPriv('kanban', 'delete')) $kanbanActions .= '<li class="menu-item item">' . html::a(helper::createLink('kanban', 'delete', "kanbanID=$kanban->id"), '<i class="icon icon-trash"></i>' . $this->lang->kanban->delete, 'hiddenwin', "class='listitem item-inner menu-item-inner state ajax-submit' data-confirm={$this->lang->kanban->confirmDeleteKanban}") . '</li>';
  2740. if($commonActions && $kanbanActions)
  2741. {
  2742. $actions .= "<li class='divider'></li>";
  2743. }
  2744. $actions .= $kanbanActions;
  2745. $actions .= "</menu>";
  2746. }
  2747. if(common::hasPriv('kanban', 'setting'))
  2748. {
  2749. $actions .= html::a(helper::createLink('kanban', 'setting', "kanbanID=$kanban->id", '', true), '<i class="icon icon-cog-outline"></i> ' . $this->lang->kanban->setting, '', "class='toolbar-item ghost btn btn-default' data-toggle='modal'");
  2750. }
  2751. return $actions;
  2752. }
  2753. /**
  2754. * 排序看板分组。
  2755. * Sort kanban group.
  2756. *
  2757. * @param int $region
  2758. * @param array $groups
  2759. * @access public
  2760. * @return bool
  2761. */
  2762. public function sortGroup($region, $groups)
  2763. {
  2764. $groupList = $this->getGroupList($region);
  2765. $order = 1;
  2766. foreach($groups as $groupID)
  2767. {
  2768. if(!$groupID) continue;
  2769. if(!isset($groupList[$groupID])) continue;
  2770. $this->dao->update(TABLE_KANBANGROUP)->set('`order`')->eq($order)->where('id')->eq($groupID)->exec();
  2771. $order++;
  2772. }
  2773. return !dao::isError();
  2774. }
  2775. /**
  2776. * 移动看板卡片。
  2777. * Move a card.
  2778. *
  2779. * @param int $cardID
  2780. * @param int $fromColID
  2781. * @param int $toColID
  2782. * @param int $fromLaneID
  2783. * @param int $toLaneID
  2784. * @param int $kanbanID
  2785. * @access public
  2786. * @return void
  2787. */
  2788. public function moveCard($cardID, $fromColID, $toColID, $fromLaneID, $toLaneID, $kanbanID = 0)
  2789. {
  2790. $groupBy = ($this->session->execGroupBy && ($this->app->tab == 'execution' || $this->config->vision == 'lite')) ? $this->session->execGroupBy : '';
  2791. $fromCell = $this->dao->select('id,cards,lane')->from(TABLE_KANBANCELL)
  2792. ->where('`column`')->eq($fromColID)
  2793. ->beginIF((!$groupBy || $groupBy == 'default') && $fromLaneID)->andWhere('lane')->eq($fromLaneID)->fi()
  2794. ->beginIF($groupBy && $groupBy != 'default')
  2795. ->andWhere('type')->eq($this->session->execLaneType)
  2796. ->andWhere('cards')->like("%,$cardID,%")
  2797. ->fi()
  2798. ->fetch();
  2799. if($groupBy && $groupBy != 'default') $fromLaneID = $toLaneID = $fromCell->lane;
  2800. if(empty($fromLaneID) && $fromCell) $fromLaneID = $toLaneID = $fromCell->lane;
  2801. $fromCells[$fromCell->id] = $fromCell;
  2802. /* Remove all cells with cardID in fromCell. */
  2803. $fromLane = $this->getLaneById($fromLaneID);
  2804. $fromCells += $this->dao->select('t1.id as id,t1.cards,t1.lane')->from(TABLE_KANBANCELL)->alias('t1')
  2805. ->leftJoin(TABLE_KANBANLANE)->alias('t2')->on('t1.lane=t2.id')
  2806. ->where('t1.type')->eq($fromLane->type)
  2807. ->andWhere('t1.id')->ne($fromCell->id)
  2808. ->andWhere('t2.region')->eq($fromLane->region)
  2809. ->andWhere('t1.cards')->like("%,$cardID,%")
  2810. ->fetchAll('id');
  2811. foreach($fromCells as $fromCell)
  2812. {
  2813. $fromCellCards = explode(',', $fromCell->cards);
  2814. $fromCellCards = array_unique($fromCellCards);
  2815. $fromCellCards = array_filter($fromCellCards);
  2816. $fromCellCards = implode(',', $fromCellCards);
  2817. $fromCardList = str_replace(",$cardID,", ',', ",$fromCellCards,");
  2818. if($fromCardList == ',') $fromCardList = '';
  2819. $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($fromCardList)->where('id')->eq($fromCell->id)->exec();
  2820. }
  2821. /* Add cardID to toCell. */
  2822. $toCell = $this->dao->select('*')->from(TABLE_KANBANCELL)->where('lane')->eq($toLaneID)->andWhere('`column`')->eq($toColID)->fetch();
  2823. $toCellCards = $this->dao->select('cards')->from(TABLE_KANBANCELL)->where('lane')->eq($toLaneID)->andWhere('`column`')->eq($toColID)->fetch('cards');
  2824. $kanbanID = $kanbanID == 0 ? $toCell->kanban : $kanbanID;
  2825. if(!$toCell) $this->addKanbanCell($kanbanID, $toLaneID, $toColID, 'common');
  2826. $toCardList = rtrim($toCellCards, ',') . ",$cardID,";
  2827. $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($toCardList)->where('`column`')->eq($toColID)->andWhere('lane')->eq($toLaneID)->exec();
  2828. $toLane = $this->getLaneById($toLaneID);
  2829. $this->dao->update(TABLE_KANBANCARD)->set('group')->eq($toLane->group)->where('id')->eq($cardID)->exec();
  2830. }
  2831. /**
  2832. * 更新卡片的颜色。
  2833. * Update a card's color.
  2834. *
  2835. * @param int $cardID
  2836. * @param string $color
  2837. * @access public
  2838. * @return void
  2839. */
  2840. public function updateCardColor($cardID, $color)
  2841. {
  2842. $this->dao->update(TABLE_KANBANCARD)->set('`color`')->eq('#' . $color)->where('id')->eq($cardID)->exec();
  2843. }
  2844. /**
  2845. * 归档看板列。
  2846. * Archive a column.
  2847. *
  2848. * @param int $columnID
  2849. * @access public
  2850. * @return void
  2851. */
  2852. public function archiveColumn($columnID)
  2853. {
  2854. $this->dao->update(TABLE_KANBANCOLUMN)
  2855. ->set('archived')->eq(1)
  2856. ->where('id')->eq($columnID)
  2857. ->exec();
  2858. if(!dao::isError()) $this->loadModel('action')->create('kanbancolumn', $columnID, 'archived');
  2859. }
  2860. /**
  2861. * 还原看板列。
  2862. * Restore a column.
  2863. *
  2864. * @param int $columnID
  2865. * @access public
  2866. * @return void
  2867. */
  2868. public function restoreColumn($columnID)
  2869. {
  2870. $column = $this->getColumnByID($columnID);
  2871. if($column->parent)
  2872. {
  2873. $parent = $this->getColumnByID($column->parent);
  2874. /* If the parent column is normal now, put its card into child column. */
  2875. if($parent && $parent->parent != -1)
  2876. {
  2877. $parentCells = $this->dao->select('*')->from(TABLE_KANBANCELL)
  2878. ->where('`column`')->eq($column->parent)
  2879. ->andWhere('type')->eq('common')
  2880. ->fetchAll('id');
  2881. foreach($parentCells as $cell)
  2882. {
  2883. $cards = $this->dao->select('cards')->from(TABLE_KANBANCELL)
  2884. ->where('lane')->eq($cell->lane)
  2885. ->andWhere('`column`')->eq($columnID)
  2886. ->andWhere('type')->eq('common')
  2887. ->fetch('cards');
  2888. $cards = $cards ? $cards . ltrim($cell->cards, ',') : $cell->cards;
  2889. $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cards)
  2890. ->where('lane')->eq($cell->lane)
  2891. ->andWhere('`column`')->eq($columnID)
  2892. ->andWhere('type')->eq('common')
  2893. ->exec();
  2894. $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq('')
  2895. ->where('lane')->eq($cell->lane)
  2896. ->andWhere('`column`')->eq($cell->column)
  2897. ->andWhere('type')->eq('common')
  2898. ->exec();
  2899. }
  2900. $this->dao->update(TABLE_KANBANCOLUMN)->set('parent')->eq(-1)->where('id')->eq($column->parent)->exec();
  2901. }
  2902. }
  2903. $this->dao->update(TABLE_KANBANCOLUMN)
  2904. ->set('archived')->eq(0)
  2905. ->where('id')->eq($columnID)
  2906. ->exec();
  2907. if(!dao::isError()) $this->loadModel('action')->create('kanbancolumn', $columnID, 'restore');
  2908. }
  2909. /**
  2910. * 归档看板卡片。
  2911. * Archive a card.
  2912. *
  2913. * @param int $cardID
  2914. * @access public
  2915. * @return bool
  2916. */
  2917. public function archiveCard($cardID)
  2918. {
  2919. $oldCard = $this->getCardByID($cardID);
  2920. $this->dao->update(TABLE_KANBANCARD)
  2921. ->set('archived')->eq(1)
  2922. ->set('archivedBy')->eq($this->app->user->account)
  2923. ->set('archivedDate')->eq(helper::now())
  2924. ->where('id')->eq($cardID)
  2925. ->exec();
  2926. $card = $this->getCardByID($cardID);
  2927. if(dao::isError()) return false;
  2928. $changes = common::createChanges($oldCard, $card);
  2929. if($changes)
  2930. {
  2931. $actionID = $this->loadModel('action')->create('kanbancard', $cardID, 'archived');
  2932. $this->action->logHistory($actionID, $changes);
  2933. }
  2934. return true;
  2935. }
  2936. /**
  2937. * 还原看板卡片。
  2938. * Restore a card.
  2939. *
  2940. * @param int $cardID
  2941. * @access public
  2942. * @return bool
  2943. */
  2944. public function restoreCard($cardID)
  2945. {
  2946. $oldCard = $this->getCardByID($cardID);
  2947. $this->dao->update(TABLE_KANBANCARD)
  2948. ->set('archived')->eq(0)
  2949. ->set('archivedBy')->eq('')
  2950. ->set('archivedDate')->eq(null)
  2951. ->where('id')->eq($cardID)
  2952. ->exec();
  2953. if(dao::isError()) return false;
  2954. $card = $this->getCardByID($cardID);
  2955. $changes = common::createChanges($oldCard, $card);
  2956. if($changes)
  2957. {
  2958. $actionID = $this->loadModel('action')->create('kanbancard', $cardID, 'restore');
  2959. $this->action->logHistory($actionID, $changes);
  2960. }
  2961. return true;
  2962. }
  2963. /**
  2964. * 删除子列时,将子列的卡片移动到第一个子列,或者移动到父列。
  2965. * Process cards when delete a column.
  2966. *
  2967. * @param object $column
  2968. * @access public
  2969. * @return void
  2970. */
  2971. public function processCards($column)
  2972. {
  2973. $firstColumnID = $this->dao->select('id')->from(TABLE_KANBANCOLUMN)
  2974. ->where('parent')->eq($column->parent)
  2975. ->andWhere('id')->ne($column->id)
  2976. ->andWhere('deleted')->eq('0')
  2977. ->andWhere('archived')->eq('0')
  2978. ->orderBy('`order` asc')
  2979. ->fetch('id');
  2980. $extendID = $firstColumnID;
  2981. if(!$firstColumnID)
  2982. {
  2983. $extendID = $column->parent;
  2984. $this->dao->update(TABLE_KANBANCOLUMN)->set('parent')->eq(0)->where('id')->eq($column->parent)->exec();
  2985. }
  2986. $cells = $this->dao->select('*')->from(TABLE_KANBANCELL)
  2987. ->where('`column`')->eq($column->id)
  2988. ->andWhere('type')->eq('common')
  2989. ->fetchAll('id', false);
  2990. foreach($cells as $cell)
  2991. {
  2992. $extendCards = $this->dao->select('cards')->from(TABLE_KANBANCELL)
  2993. ->where('lane')->eq($cell->lane)
  2994. ->andWhere('`column`')->eq($extendID)
  2995. ->andWhere('type')->eq('common')
  2996. ->fetch('cards');
  2997. $extendCards = $extendCards ? $extendCards . ltrim($cell->cards, ',') : $cell->cards;
  2998. $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($extendCards)
  2999. ->where('lane')->eq($cell->lane)
  3000. ->andWhere('`column`')->eq($extendID)
  3001. ->andWhere('type')->eq('common')
  3002. ->exec();
  3003. }
  3004. }
  3005. /**
  3006. * 获取看板空间。
  3007. * Get space by id.
  3008. *
  3009. * @param int $spaceID
  3010. * @access public
  3011. * @return object
  3012. */
  3013. public function getSpaceById($spaceID)
  3014. {
  3015. $space = $this->dao->findById($spaceID)->from(TABLE_KANBANSPACE)->fetch();
  3016. if($space) $space = $this->loadModel('file')->replaceImgURL($space, 'desc');
  3017. return $space;
  3018. }
  3019. /**
  3020. * Get kanban group by space id list.
  3021. *
  3022. * @param array $spaceIdList
  3023. * @param array $kanbanIdList
  3024. * @access public
  3025. * @return array
  3026. */
  3027. public function getGroupBySpaceList($spaceIdList, $kanbanIdList = array())
  3028. {
  3029. $spaceList = $this->dao->select('*')->from(TABLE_KANBAN)
  3030. ->where('deleted')->eq(0)
  3031. ->andWhere('space')->in($spaceIdList)
  3032. ->beginIF($kanbanIdList)->andWhere('id')->in($kanbanIdList)->fi()
  3033. ->fetchGroup('space', 'id');
  3034. $kanbanIDList = array();
  3035. foreach($spaceList as $kanbanList) $kanbanIDList = array_merge_recursive($kanbanIDList, array_keys($kanbanList));
  3036. $cardsCount = $this->dao->select('kanban, COUNT(1) AS count')->from(TABLE_KANBANCARD)
  3037. ->where('deleted')->eq(0)
  3038. ->andWhere('kanban')->in($kanbanIDList)
  3039. ->groupBy('kanban')
  3040. ->fetchPairs('kanban');
  3041. foreach($spaceList as $kanbanList)
  3042. {
  3043. foreach($kanbanList as $kanban)
  3044. {
  3045. $kanban->cardsCount = zget($cardsCount, $kanban->id, 0);
  3046. }
  3047. }
  3048. return $spaceList;
  3049. }
  3050. /**
  3051. * 获取看板组。
  3052. * Get group list by region.
  3053. *
  3054. * @param int $regionID
  3055. * @access public
  3056. * @return array
  3057. */
  3058. public function getGroupList($regionID)
  3059. {
  3060. return $this->dao->select('*')->from(TABLE_KANBANGROUP)
  3061. ->where('region')->eq($regionID)
  3062. ->orderBy('order')
  3063. ->fetchAll('id');
  3064. }
  3065. /**
  3066. * 获取看板列。
  3067. * Get column by id.
  3068. *
  3069. * @param int $columnID
  3070. * @access public
  3071. * @return object
  3072. */
  3073. public function getColumnByID($columnID)
  3074. {
  3075. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getColumn();
  3076. $column = $this->dao->select('t1.*, t2.type as laneType')->from(TABLE_KANBANCOLUMN)->alias('t1')
  3077. ->leftJoin(TABLE_KANBANCELL)->alias('t2')->on('t1.id=t2.column')
  3078. ->where('t1.id')->eq($columnID)
  3079. ->fetch();
  3080. if(empty($column)) return false;
  3081. if($column->parent > 0) $column->parentName = $this->dao->findById($column->parent)->from(TABLE_KANBANCOLUMN)->fetch('name');
  3082. return $column;
  3083. }
  3084. /**
  3085. * 根据字段获取看板列。
  3086. * Get columns by field.
  3087. *
  3088. * @param string $field parent|region|group
  3089. * @param int $fieldID
  3090. * @param string $archived
  3091. * @param string $deleted
  3092. * @access public
  3093. * @return array
  3094. */
  3095. public function getColumnsByField($field = '', $fieldID = 0, $archived = '0', $deleted = '0')
  3096. {
  3097. return $this->dao->select('*')->from(TABLE_KANBANCOLUMN)
  3098. ->where('1 = 1')
  3099. ->beginIF($field)->andWhere($field)->eq($fieldID)->fi()
  3100. ->beginIF($archived != '')->andWhere('archived')->eq($archived)->fi()
  3101. ->beginIF($deleted != '')->andWhere('deleted')->eq($deleted)->fi()
  3102. ->orderBy('order')
  3103. ->fetchAll('id');
  3104. }
  3105. /**
  3106. * 获取泳道的看板列ID。
  3107. * Get column ID by lane ID.
  3108. *
  3109. * @param int $laneID
  3110. * @param string $columnType
  3111. * @access public
  3112. * @return string|int
  3113. */
  3114. public function getColumnIDByLaneID($laneID, $columnType)
  3115. {
  3116. return $this->dao->select('t1.column')->from(TABLE_KANBANCELL)->alias('t1')
  3117. ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column = t2.id')
  3118. ->where('t1.lane')->eq($laneID)
  3119. ->andWhere('t2.type')->eq($columnType)
  3120. ->fetch('column');
  3121. }
  3122. /**
  3123. * 根据ID获取看板泳道。
  3124. * Get lane by id.
  3125. *
  3126. * @param int $laneID
  3127. * @access public
  3128. * @return object
  3129. */
  3130. public function getLaneById($laneID)
  3131. {
  3132. return $this->dao->findById($laneID)->from(TABLE_KANBANLANE)->fetch();
  3133. }
  3134. /**
  3135. * 获取看板卡片。
  3136. * Get card by id.
  3137. *
  3138. * @param int $cardID
  3139. * @access public
  3140. * @return object|false
  3141. */
  3142. public function getCardByID($cardID)
  3143. {
  3144. $card = $this->dao->select('*')->from(TABLE_KANBANCARD)->where('id')->eq($cardID)->fetch();
  3145. if(!$card) return false;
  3146. $card = $this->loadModel('file')->replaceImgURL($card, 'desc');
  3147. return $card;
  3148. }
  3149. /**
  3150. * 获取某个对象的看板卡片。
  3151. * Get cards by object id.
  3152. *
  3153. * @param string $objectType kanban|region|group|lane|column
  3154. * @param int|string $objectID
  3155. * @param string $archived
  3156. * @param string $deleted
  3157. * @access public
  3158. * @return array
  3159. */
  3160. public function getCardsByObject($objectType = '', $objectID = 0, $archived = '0', $deleted = '0')
  3161. {
  3162. return $this->dao->select('*')->from(TABLE_KANBANCARD)
  3163. ->where('1 = 1')
  3164. ->beginIF($objectType)->andWhere($objectType)->eq($objectID)->fi()
  3165. ->beginIF($archived != '')->andWhere('archived')->eq($archived)->fi()
  3166. ->beginIF($deleted != '')->andWhere('deleted')->eq($deleted)->fi()
  3167. ->orderBy('order')
  3168. ->fetchAll('id', false);
  3169. }
  3170. /**
  3171. * 保存看板卡片到期提醒。
  3172. * Save kanban card due reminders.
  3173. *
  3174. * @param array $cards
  3175. * @access public
  3176. * @return void
  3177. */
  3178. public function saveCardNotice($cards)
  3179. {
  3180. if(empty($cards)) $cards = $this->loadModel('report')->getUserKanbanCards();
  3181. if(empty($cards)) return;
  3182. $today = helper::today();
  3183. foreach($cards as $user => $cardList)
  3184. {
  3185. foreach($cardList as $card)
  3186. {
  3187. $url = helper::createLink('kanban', 'view', "kanbanID={$card->kanban}") . "#app=kanban";
  3188. $data = $today > $card->deadline ? $this->lang->kanban->due : $this->lang->kanban->nearing;
  3189. $data .= html::a($url, "#{$card->id} {$card->name}");
  3190. $notify = new stdclass();
  3191. $notify->objectType = 'message';
  3192. $notify->action = 0;
  3193. $notify->toList = ",{$user},";
  3194. $notify->data = $data;
  3195. $notify->status = 'wait';
  3196. $notify->createdBy = 'admin';
  3197. $notify->createdDate = helper::now();
  3198. $notify->sendTime = null;
  3199. $this->dao->insert(TABLE_NOTIFY)->data($notify)->exec();
  3200. }
  3201. }
  3202. }
  3203. /**
  3204. * 获取可转入的看板卡片。
  3205. * Get cards to import.
  3206. *
  3207. * @param int $kanbanID
  3208. * @param int $excludedID
  3209. * @param object $pager
  3210. * @access public
  3211. * @return array
  3212. */
  3213. public function getCards2Import($kanbanID = 0, $excludedID = 0, $pager = null)
  3214. {
  3215. $kanbanIdList = $this->getCanViewObjects();
  3216. return $this->dao->select('*')->from(TABLE_KANBANCARD)
  3217. ->where('deleted')->eq(0)
  3218. ->andWhere('archived')->eq(0)
  3219. ->andWhere('fromID')->eq(0)
  3220. ->andWhere('kanban')->in($kanbanIdList)
  3221. ->beginIF($kanbanID)->andWhere('kanban')->eq($kanbanID)->fi()
  3222. ->beginIF($excludedID)->andWhere('kanban')->ne($excludedID)->fi()
  3223. ->orderBy('order')
  3224. ->page($pager)
  3225. ->fetchAll('id', false);
  3226. }
  3227. /**
  3228. * 获取看板卡片的操作按钮。
  3229. * Get Kanban cards menus by execution id.
  3230. *
  3231. * @param int $executionID
  3232. * @param array $objects
  3233. * @param string $objecType story|bug|task
  3234. * @access public
  3235. * @return array
  3236. */
  3237. public function getKanbanCardMenu($executionID, $objects, $objecType)
  3238. {
  3239. $this->app->loadLang('execution');
  3240. $execution = $this->loadModel('execution')->getByID($executionID);
  3241. if(!common::canModify('execution', $execution)) return array();
  3242. $menus = array();
  3243. switch ($objecType)
  3244. {
  3245. case 'epic':
  3246. $menus = $this->kanbanTao->getERURCardMenu($executionID, $objects);
  3247. break;
  3248. case 'requirement':
  3249. $menus = $this->kanbanTao->getERURCardMenu($executionID, $objects);
  3250. break;
  3251. case 'parentStory':
  3252. $menus = $this->kanbanTao->getERURCardMenu($executionID, $objects);
  3253. break;
  3254. case 'story':
  3255. if($execution) $menus = $this->kanbanTao->getStoryCardMenu($execution, $objects);
  3256. break;
  3257. case 'bug':
  3258. $menus = $this->kanbanTao->getBugCardMenu($objects);
  3259. break;
  3260. case 'task':
  3261. if(!isset($_SESSION['limitedExecutions'])) $this->loadModel('execution')->getLimitedExecution();
  3262. $menus = $this->kanbanTao->getTaskCardMenu($objects, $executionID);
  3263. break;
  3264. case 'risk':
  3265. $menus = $this->kanbanTao->getRiskCardMenu($objects);
  3266. break;
  3267. }
  3268. return $menus;
  3269. }
  3270. /**
  3271. * 获取卡片的发送人和抄送人。
  3272. * Get toList and ccList.
  3273. *
  3274. * @param object $card
  3275. * @access public
  3276. * @return bool|array
  3277. */
  3278. public function getToAndCcList($card)
  3279. {
  3280. /* Set toList and ccList. */
  3281. $toList = $card->createdBy;
  3282. $ccList = trim($card->assignedTo, ',');
  3283. if(empty($toList))
  3284. {
  3285. if(empty($ccList)) return false;
  3286. if(strpos($ccList, ',') === false)
  3287. {
  3288. $toList = $ccList;
  3289. $ccList = '';
  3290. }
  3291. else
  3292. {
  3293. $commaPos = strpos($ccList, ',');
  3294. $toList = substr($ccList, 0, $commaPos);
  3295. $ccList = substr($ccList, $commaPos + 1);
  3296. }
  3297. }
  3298. return array($toList, $ccList);
  3299. }
  3300. /**
  3301. * 检查操作按钮是否可点。
  3302. * Check if user can execute an action.
  3303. *
  3304. * @param object $object
  3305. * @param string $action
  3306. * @access public
  3307. * @return bool
  3308. */
  3309. public function isClickable($object, $action)
  3310. {
  3311. $action = strtolower($action);
  3312. $clickable = commonModel::hasPriv('kanban', $action);
  3313. if(!$clickable) return false;
  3314. switch($action)
  3315. {
  3316. case 'sortlane' :
  3317. case 'deletelane' :
  3318. if($object->deleted != '0') return false;
  3319. $count = $this->dao->select('COUNT(id) AS count')->from(TABLE_KANBANLANE)
  3320. ->where('deleted')->eq('0')
  3321. ->andWhere('region')->eq($object->region)
  3322. ->beginIF($action == 'sortlane')->andWhere('`group`')->eq($object->group)->fi()
  3323. ->fetch('count');
  3324. return $count > 1;
  3325. case 'splitcolumn' :
  3326. return $object->parent == 0;
  3327. case 'restorecolumn' :
  3328. if($object->parent > 0)
  3329. {
  3330. $parent = $this->getColumnByID($object->parent);
  3331. if(!empty($parent) && ($parent->deleted == '1' || $parent->archived == '1')) return false;
  3332. }
  3333. return $object->archived == '1';
  3334. case 'archivecolumn' :
  3335. return $object->archived == '0'; // The column has been archived.
  3336. case 'deletecolumn' :
  3337. return $object->deleted == '0';
  3338. case 'sortcolumn':
  3339. return $object->deleted == '0';
  3340. case 'editcard':
  3341. return $object->deleted == '0' && $object->archived == '0';
  3342. case 'finishcard':
  3343. return $object->status != 'done' && $object->deleted == '0' && $object->archived == '0';
  3344. case 'activatecard':
  3345. return $object->status == 'done' && $object->deleted == '0' && $object->archived == '0';
  3346. case 'deletecard':
  3347. return $object->deleted == '0';
  3348. case 'archivecard':
  3349. return $object->archived == '0';
  3350. case 'restorecard':
  3351. return $object->archived == '1';
  3352. }
  3353. return true;
  3354. }
  3355. /**
  3356. * 获取泳道的数量。
  3357. * Get kanban lane count.
  3358. *
  3359. * @param int $kanbanID
  3360. * @param string $type
  3361. * @access public
  3362. * @return int
  3363. */
  3364. public function getLaneCount($kanbanID, $type = 'common')
  3365. {
  3366. if($type == 'common' || $type == 'kanban')
  3367. {
  3368. return $this->dao->select('COUNT(t2.id) as count')->from(TABLE_KANBANREGION)->alias('t1')
  3369. ->leftJoin(TABLE_KANBANLANE)->alias('t2')->on('t1.id=t2.region')
  3370. ->where('t1.kanban')->eq($kanbanID)
  3371. ->andWhere('t1.deleted')->eq(0)
  3372. ->andWhere('t2.deleted')->eq(0)
  3373. ->beginIF($type == 'common')->andWhere('t2.type')->eq('common')->fi()
  3374. ->beginIF($type != 'common')->andWhere('t2.type')->ne('common')->fi()
  3375. ->fetch('count');
  3376. }
  3377. else
  3378. {
  3379. return $this->dao->select('COUNT(id) as count')->from(TABLE_KANBANLANE)
  3380. ->where('execution')->eq($kanbanID)
  3381. ->andWhere('deleted')->eq(0)
  3382. ->fetch('count');
  3383. }
  3384. }
  3385. /**
  3386. * 检查卡片数量是否合法。
  3387. * Check display card count.
  3388. *
  3389. * @param int $count
  3390. * @access public
  3391. * @return bool
  3392. */
  3393. public function checkDisplayCards($count)
  3394. {
  3395. if(!preg_match("/^-?\d+$/", (string)$count) || $count <= DEFAULT_CARDCOUNT || $count > MAX_CARDCOUNT) dao::$errors['displayCards'] = $this->lang->kanbanlane->error->mustBeInt;
  3396. return !dao::isError();
  3397. }
  3398. /**
  3399. * 获取看板动态刷新的回调函数数组。
  3400. * Get kanban callback data.
  3401. *
  3402. * @param int $kanbanID
  3403. * @param int $regionID
  3404. * @access public
  3405. * @return void
  3406. */
  3407. public function getKanbanCallback($kanbanID, $regionID)
  3408. {
  3409. $kanbanData = $this->getKanbanData($kanbanID, $regionID);
  3410. $kanbanData = reset($kanbanData);
  3411. return array('name' => 'updateKanbanRegion', 'params' => array('region' . $regionID, $kanbanData));
  3412. }
  3413. /**
  3414. * 获取卡片所在的单元格。
  3415. * Get card cell.
  3416. *
  3417. * @param int $cardID
  3418. * @param int $kanbanID
  3419. * @access public
  3420. * @return object
  3421. */
  3422. public function getCellByCard($cardID, $kanbanID)
  3423. {
  3424. return $this->dao->select('id,cards,lane,`column`')->from(TABLE_KANBANCELL)
  3425. ->where('kanban')->eq($kanbanID)
  3426. ->andWhere('type')->eq('common')
  3427. ->andWhere('cards')->like("%,$cardID,%")
  3428. ->fetch();
  3429. }
  3430. }