model.php 100 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094
  1. <?php
  2. /**
  3. * The model file of pivot 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 Chunsheng Wang <chunsheng@cnezsoft.com>
  8. * @package pivot
  9. * @version $Id: model.php 4726 2013-05-03 05:51:27Z chencongzhi520@gmail.com $
  10. * @link https://www.zentao.net
  11. */
  12. class pivotModel extends model
  13. {
  14. /**
  15. * 初始化,加载BI相关类。
  16. * Construct,load BI related classes.
  17. *
  18. * @access public
  19. * @return void
  20. */
  21. public function __construct()
  22. {
  23. parent::__construct();
  24. $this->loadBIDAO();
  25. $this->loadModel('bi');
  26. }
  27. /**
  28. * 判断是否有权限访问。
  29. * Check pivot access.
  30. *
  31. * @param int $pivotID
  32. * @access public
  33. * @return array
  34. */
  35. public function checkAccess($pivotID, $method = 'preview')
  36. {
  37. $viewableObjects = $this->bi->getViewableObject('pivot');
  38. if(!in_array($pivotID, $viewableObjects))
  39. {
  40. return $this->app->control->sendError($this->lang->pivot->accessDenied, helper::createLink('pivot', $method));
  41. }
  42. }
  43. /**
  44. * 过滤不可见的透视表。
  45. * Filter invisible pivot.
  46. *
  47. * @param array $pivots
  48. * @access public
  49. * @return array
  50. */
  51. public function filterInvisiblePivot($pivots)
  52. {
  53. $viewableObjects = $this->bi->getViewableObject('pivot');
  54. foreach($pivots as $index => $pivot)
  55. {
  56. if(!in_array($pivot->id, $viewableObjects)) unset($pivots[$index]);
  57. }
  58. return array_values($pivots);
  59. }
  60. /*
  61. * 获取透视表数据。
  62. * Get pivot data by id.
  63. *
  64. * @param int $id
  65. * @access public
  66. * @return object|bool
  67. */
  68. public function getPivotDataByID($id)
  69. {
  70. $pivot = $this->pivotTao->fetchPivot($id);
  71. if(!$pivot) return false;
  72. return $pivot;
  73. }
  74. /*
  75. * 获取透视表。
  76. * Get pivot.
  77. *
  78. * @param int $pivotID
  79. * @param bool $processDateVar
  80. * @param string $filterStatus
  81. * @param bool $addDrills
  82. * @access public
  83. * @return object|bool
  84. */
  85. /**
  86. * @return object|bool
  87. * @param int $pivotID
  88. * @param bool $processDateVar
  89. * @param string $filterStatus
  90. * @param bool $addDrills
  91. */
  92. public function getByID($pivotID, $processDateVar = false, $filterStatus = 'published', $addDrills = true)
  93. {
  94. $pivot = $this->pivotTao->fetchPivot($pivotID);
  95. if(!$pivot) return false;
  96. $pivot->fieldSettings = array();
  97. if(!empty($pivot->fields) && $pivot->fields != 'null')
  98. {
  99. $pivot->fieldSettings = json_decode($pivot->fields);
  100. $pivot->fields = array_keys(get_object_vars($pivot->fieldSettings));
  101. }
  102. if(!empty($pivot->filters))
  103. {
  104. $filters = json_decode($pivot->filters, true);
  105. $pivot->filters = $this->setFilterDefault($filters, $processDateVar);
  106. }
  107. else
  108. {
  109. $pivot->filters = array();
  110. }
  111. $this->completePivot($pivot);
  112. if($addDrills) $this->addDrills($pivot);
  113. // if(isset($pivot->stage) && $pivot->stage == 'published' && $this->app->methodName == 'preview') $this->processFieldSettings($pivot);
  114. return $pivot;
  115. }
  116. /*
  117. * 获取透视表某版本。
  118. * Get pivot by id and version.
  119. *
  120. * @param int $pivotID
  121. * @param string $version
  122. * @param bool $processDateVar
  123. * @param bool $addDrills
  124. * @access public
  125. * @return object|bool
  126. */
  127. /**
  128. * @param int $pivotID
  129. * @param string $version
  130. * @param bool $processDateVar
  131. * @param bool $addDrills
  132. */
  133. public function getPivotSpec($pivotID, $version, $processDateVar = false, $addDrills = true)
  134. {
  135. $pivot = $this->pivotTao->fetchPivot($pivotID, $version);
  136. if(!$pivot) return false;
  137. $pivot->fieldSettings = array();
  138. if(!empty($pivot->fields) && $pivot->fields != 'null')
  139. {
  140. $pivot->fieldSettings = json_decode($pivot->fields);
  141. $pivot->fields = array_keys(get_object_vars($pivot->fieldSettings));
  142. }
  143. if(!empty($pivot->filters))
  144. {
  145. $filters = json_decode($pivot->filters, true);
  146. $pivot->filters = $this->setFilterDefault($filters, $processDateVar);
  147. }
  148. else
  149. {
  150. $pivot->filters = array();
  151. }
  152. $this->completePivot($pivot);
  153. if($addDrills) $this->addDrills($pivot);
  154. return $pivot;
  155. }
  156. /**
  157. * 时间占位符替换为实际的时间。
  158. * Replace time placeholder with actual time.
  159. *
  160. * @param mixed $var
  161. * @param string $type
  162. * @access public
  163. * @return string
  164. */
  165. public function processDateVar($var, $type = 'date')
  166. {
  167. if(!is_string($var) || $var === '') return '';
  168. $format = $type == 'datetime' ? 'Y-m-d H:i:s' : 'Y-m-d';
  169. switch($var)
  170. {
  171. case '$MONDAY': $var = date($format, time() - (date('N') - 1) * 24 * 3600); break;
  172. case '$SUNDAY': $var = date($format, time() + (7 - date('N')) * 24 * 3600); break;
  173. case '$MONTHBEGIN': $var = date($format, time() - (date('j') - 1) * 24 * 3600); break;
  174. case '$MONTHEND': $var = date($format, time() + (date('t') - date('j')) * 24 * 3600); break;
  175. }
  176. return $var;
  177. }
  178. /**
  179. * 构建透视表的信息。
  180. * Process pivot information.
  181. *
  182. * @param object|array $pivots
  183. * @param bool $isObject
  184. * @access public
  185. * @return object|array
  186. */
  187. public function processPivot($pivots, $isObject = true)
  188. {
  189. if($isObject) $pivots = array($pivots);
  190. foreach($pivots as $pivot)
  191. {
  192. $this->completePivot($pivot);
  193. if($isObject) $this->addDrills($pivot);
  194. }
  195. return $isObject ? $pivot : $pivots;
  196. }
  197. /**
  198. * Process name and desc of pivot.
  199. *
  200. * @param object $pivot
  201. * @access private
  202. * @return void
  203. */
  204. public function processNameDesc($pivot)
  205. {
  206. if(!empty($pivot->type)) return;
  207. $pivot->names = array('zh-cn' => '', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => '');
  208. $pivot->descs = array('zh-cn' => '', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => '');
  209. $clientLang = $this->app->getClientLang();
  210. if(!empty($pivot->name))
  211. {
  212. $pivot->names = json_decode($pivot->name, true);
  213. $langNames = empty($pivot->names) ? array() : array_filter($pivot->names);
  214. $firstName = empty($langNames) ? '' : reset($langNames);
  215. $clientLangName = zget($pivot->names, $clientLang, '');
  216. $pivot->name = empty($clientLangName) ? $firstName : $clientLangName;
  217. }
  218. if(!empty($pivot->desc))
  219. {
  220. $pivot->descs = json_decode($pivot->desc, true);
  221. $langDescs = empty($pivot->descs) ? array() : array_filter($pivot->descs);
  222. $firstDesc = empty($langDescs) ? '' : reset($langDescs);
  223. $clientLangDesc = zget($pivot->descs, $clientLang, '');
  224. $pivot->desc = empty($clientLangDesc) ? $firstDesc : $clientLangDesc;
  225. }
  226. }
  227. /**
  228. * 完善透视表。
  229. * Complete pivot.
  230. *
  231. * @param object $pivot
  232. * @access public
  233. * @return void
  234. */
  235. private function completePivot($pivot)
  236. {
  237. if(!empty($pivot->settings)) $pivot->settings = json_decode($pivot->settings, true);
  238. $this->processNameDesc($pivot);
  239. }
  240. /**
  241. * 添加下钻信息到透视表。
  242. * Add drills to pivot.
  243. *
  244. * @param object $pivot
  245. * @access public
  246. * @return void
  247. */
  248. public function addDrills($pivot)
  249. {
  250. $settings = $pivot->settings;
  251. if(!is_array($settings) || !isset($settings['columns'])) return;
  252. $columns = $settings['columns'];
  253. $drillFields = array_column($columns, 'field');
  254. $drills = $this->pivotTao->fetchPivotDrills($pivot->id, $pivot->version, $drillFields);
  255. foreach($columns as $index => $column) $pivot->settings['columns'][$index]['drill'] = zget($drills, $column['field']);
  256. }
  257. /**
  258. * 检测图表是否在使用。
  259. * Check if the Chart is in use.
  260. *
  261. * @param int $chartID
  262. * @param string $type
  263. * @access public
  264. * @return bool
  265. * @param mixed[] $screens
  266. */
  267. public function checkIFChartInUse($chartID, $type = 'chart', $screens = array())
  268. {
  269. static $screenList = array();
  270. if($screens) $screenList = $screens;
  271. if(empty($screenList)) $screenList = $this->dao->select('scheme')->from(TABLE_SCREEN)->where('deleted')->eq(0)->andWhere('status')->eq('published')->fetchAll();
  272. foreach($screenList as $screen)
  273. {
  274. $scheme = json_decode($screen->scheme);
  275. if(empty($scheme->componentList)) continue;
  276. foreach($scheme->componentList as $component)
  277. {
  278. $list = !empty($component->isGroup) ? $component->groupList : array($component);
  279. foreach($list as $groupComponent)
  280. {
  281. if(!isset($groupComponent->chartConfig)) continue;
  282. $sourceID = zget($groupComponent->chartConfig, 'sourceID', '');
  283. $sourceType = zget($groupComponent->chartConfig, 'package', '') == 'Tables' ? 'pivot' : 'chart';
  284. if($chartID == $sourceID && $type == $sourceType) return true;
  285. }
  286. }
  287. }
  288. return false;
  289. }
  290. /**
  291. * 构建透视表字段的配置信息,类似于dataview/js/basequery.js getFieldSettings()。
  292. * Process pivot field settings, function like dataview/js/basequery.js getFieldSettings().
  293. *
  294. * @param object $pivot
  295. * @access public
  296. * @return void
  297. */
  298. public function processFieldSettings($pivot)
  299. {
  300. $this->loadModel('dataview');
  301. $fieldSettings = $pivot->fieldSettings;
  302. if(empty($fieldSettings)) return;
  303. $sql = isset($pivot->sql) ? $pivot->sql : '';
  304. $filters = $this->getFieldsFromPivot($pivot, 'filters', array(), !is_array($pivot->filters), true);
  305. if(!empty($filters)) $filters = $this->setFilterDefault($filters);
  306. /* 检测sql是否有效。 */
  307. /* Check if the sql is valid. */
  308. $querySQL = $this->loadModel('bi')->parseSqlVars($sql, $filters);
  309. $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
  310. $stmt = $this->dbh->query($querySQL);
  311. if(!$stmt) return;
  312. $columns = $this->bi->getColumnsType($querySQL);
  313. $columnFields = array();
  314. foreach(array_keys(get_object_vars($columns)) as $type) $columnFields[$type] = $type;
  315. extract($this->bi->getTableAndFields($querySQL));
  316. /* 获取field的键值对以及相关联的对象。 */
  317. /* Get field key value pairs and related objects. */
  318. $this->loadModel('dataview');
  319. if($tables)
  320. {
  321. $statement = $this->bi->sql2Statement($sql);
  322. $moduleNames = $this->dataview->getModuleNames($tables);
  323. $aliasNames = $this->dataview->getAliasNames($statement, $moduleNames);
  324. }
  325. list($fieldPairs, $relatedObjects) = $this->dataview->mergeFields($columnFields, $fields, $moduleNames, $aliasNames);
  326. $objectFields = $this->loadModel('dataview')->getObjectFields();
  327. /* 重建fieldSettings字段。 */
  328. /* Rebuild fieldSettings field. */
  329. $pivot->fieldSettings = $this->bi->rebuildFieldSettings($fieldPairs, $columns, $relatedObjects, $fieldSettings, $objectFields);
  330. }
  331. /**
  332. * 获取执行。
  333. * Get executions.
  334. *
  335. * @param string $begin
  336. * @param string $end
  337. * @access public
  338. * @return array
  339. */
  340. public function getExecutions($begin = '', $end = '')
  341. {
  342. $permission = common::hasPriv('pivot', 'showProject') || $this->app->user->admin;
  343. $IDList = !$permission ? $this->app->user->view->sprints : array();
  344. $executions = $this->pivotTao->getExecutionList($begin, $end, $IDList);
  345. foreach($executions as $execution)
  346. {
  347. $execution->deviation = round($execution->consumed - $execution->estimate, 2);
  348. $execution->deviationRate = $execution->estimate ? round($execution->deviation / $execution->estimate * 100, 2) : 'n/a';
  349. }
  350. return $executions;
  351. }
  352. /**
  353. * 获取产品。
  354. * Get products.
  355. *
  356. * @param string $conditions
  357. * @param string $storyType
  358. * @param array $filters
  359. * @access public
  360. * @return array
  361. */
  362. public function getProducts($conditions, $storyType = 'story', $filters = array())
  363. {
  364. $permission = common::hasPriv('pivot', 'showProduct') || $this->app->user->admin;
  365. $IDList = !$permission ? $this->app->user->view->products : array();
  366. $products = $this->pivotTao->getProductList($conditions, $IDList, $filters);
  367. /* 为产品生成计划数据和相关的需求数据。 */
  368. /* Generate plan data and related story data for products. */
  369. $this->pivotTao->processPlanStories($products, $storyType, $this->pivotTao->processProductPlan($products, $conditions));
  370. unset($products['']);
  371. return $products;
  372. }
  373. /**
  374. * 获取Bug创建表的数据。
  375. * Get bug related pivot information.
  376. *
  377. * @param string $begin
  378. * @param string $end
  379. * @param int $product
  380. * @param int $execution
  381. * @access public
  382. * @return array
  383. */
  384. public function getBugs($begin, $end, $product = 0, $execution = 0)
  385. {
  386. $end = date('Y-m-d', strtotime("{$end} +1 day"));
  387. $bugGroups = $this->pivotTao->getBugGroup($begin, $end, $product, $execution);
  388. /* 为bug生成统计数据。 */
  389. /* Generate statistics data for bugs. */
  390. $bugs = array();
  391. foreach($bugGroups as $account => $userBugs)
  392. {
  393. $bug = array();
  394. $bug['openedBy'] = $account;
  395. $bug['unResolved'] = 0;
  396. $bug['validRate'] = 0;
  397. $bug['total'] = 0;
  398. /* Bug已解决状态数据初始化。 */
  399. /* Bug resolved status data initialization. */
  400. foreach(array_keys($this->lang->bug->resolutionList) as $resolution)
  401. {
  402. if($resolution) $bug[$resolution] = 0;
  403. }
  404. /* 获取bug各个状态的统计数据。 */
  405. /* Get statistics data for each status of bugs. */
  406. $resolvedCount = 0;
  407. $validCount = 0;
  408. foreach($userBugs as $userBug)
  409. {
  410. if(!isset($bug[$userBug->resolution])) continue;
  411. $bug[$userBug->resolution]++;
  412. $bug['total']++;
  413. if($userBug->status == 'resolved' || $userBug->status == 'closed') $resolvedCount++;
  414. if($userBug->resolution == 'fixed' || $userBug->resolution == 'postponed') $validCount++;
  415. }
  416. if(!$bug['total']) continue;
  417. /* 获取有效率。*/
  418. /* Get valid rate. */
  419. $bug['validRate'] = $resolvedCount ? round($validCount / $resolvedCount * 100, 2) . '%' : '0%';
  420. $bugs[] = $bug;
  421. }
  422. uasort($bugs, 'sortSummary');
  423. return $bugs;
  424. }
  425. /**
  426. * 获取员工负载表的数据。
  427. * Get workload.
  428. *
  429. * @param int $dept
  430. * @param string $assign assign|noassign
  431. * @param array $users
  432. * @param float $allHour
  433. * @access public
  434. * @return array
  435. */
  436. public function getWorkload($dept, $assign, $users, $allHour)
  437. {
  438. /* 判断是否需要查询部门用户。 */
  439. /* Check if need to query department users. */
  440. $deptUsers = array();
  441. if($dept)
  442. {
  443. $deptUsers = $this->loadModel('dept')->getDeptUserPairs($dept);
  444. if(!$deptUsers) return array();
  445. }
  446. $canViewExecution = common::hasPriv('execution', 'view');
  447. return $assign == 'noassign' ? $this->getWorkloadNoAssign($deptUsers, $users, $canViewExecution) : $this->getWorkLoadAssign($deptUsers, $users, $canViewExecution, $allHour);
  448. }
  449. /**
  450. * 获取未指派的员工负载表。
  451. * Get workload no assign.
  452. *
  453. * @param array $deptUsers
  454. * @param array $users
  455. * @param bool $canViewExecution
  456. * @access public
  457. * @return array
  458. */
  459. public function getWorkloadNoAssign($deptUsers, $users, $canViewExecution)
  460. {
  461. $executions = $this->pivotTao->getNoAssignExecution(array_keys($deptUsers));
  462. if(empty($executions)) return array();
  463. /* 构建用户-项目-执行数据结构。 */
  464. /* Build user-project-execution data structure. */
  465. $executionGroups = array();
  466. foreach($executions as $execution)
  467. {
  468. if(!isset($users[$execution->user])) continue;
  469. $executionGroups[$execution->user][$execution->projectID][$execution->executionID] = $execution;
  470. }
  471. /* 计算未指派的执行统计数据。 */
  472. /* Calculate statistics data for no assign execution. */
  473. $workload = array();
  474. foreach($executionGroups as $account => $projects)
  475. {
  476. if(!isset($users[$account])) continue;
  477. $totalExecutions = 0;
  478. foreach($projects as $executions) $totalExecutions += count($executions);
  479. $userFirstRow = true;
  480. foreach($projects as $executions)
  481. {
  482. $projectFirstRow = true;
  483. foreach($executions as $execution)
  484. {
  485. $execution->executionTasks = 0;
  486. $execution->executionHours = 0;
  487. $execution->totalTasks = 0;
  488. $execution->totalHours = 0;
  489. $execution->workload = '0%';
  490. $this->setExecutionName($execution, $canViewExecution);
  491. if($userFirstRow) $execution->userRowspan = $totalExecutions;
  492. if($projectFirstRow) $execution->projectRowspan = count($executions);
  493. $workload[] = $execution;
  494. $userFirstRow = false;
  495. $projectFirstRow = false;
  496. }
  497. }
  498. }
  499. return $workload;
  500. }
  501. /**
  502. * 获取指派的员工负载表。
  503. * Get workload assign.
  504. *
  505. * @param array $deptUsers
  506. * @param array $users
  507. * @param bool $canViewExecution
  508. * @param float $allHour
  509. * @access public
  510. * @return array
  511. */
  512. public function getWorkLoadAssign($deptUsers, $users, $canViewExecution, $allHour)
  513. {
  514. $tasks = $this->pivotTao->getAssignTask(array_keys($deptUsers));
  515. if(empty($tasks)) return array();
  516. /* 构建用户-项目-执行-任务数据结构。 */
  517. /* Build user-project-execution-task data structure. */
  518. $taskGroups = array();
  519. foreach($tasks as $task)
  520. {
  521. if(!isset($users[$task->user])) continue;
  522. $user = $task->user;
  523. $project = $task->projectID;
  524. $execution = $task->executionID;
  525. $id = $task->id;
  526. if(isset($taskGroups[$user][$project][$execution][$id]))
  527. {
  528. $taskGroups[$user][$project][$execution][$id]->left = round($taskGroups[$user][$project][$execution][$id]->left + $task->left, 2);
  529. }
  530. else
  531. {
  532. $taskGroups[$user][$project][$execution][$id] = $task;
  533. }
  534. }
  535. /* 获取团队任务的剩余工时。 */
  536. /* Get team task left hours. */
  537. $teamTasks = $this->pivotTao->getTeamTasks(array_keys($deptUsers));
  538. $workload = array();
  539. foreach($taskGroups as $projects)
  540. {
  541. list($totalTasks, $totalHours, $totalExecutions, $userWorkload) = $this->getUserWorkLoad($projects, $teamTasks, $allHour);
  542. /* 计算用户的执行统计数据。 */
  543. /* Calculate user's execution statistics data. */
  544. $userFirstRow = true;
  545. foreach($projects as $executions)
  546. {
  547. $projectFirstRow = true;
  548. foreach($executions as $tasks)
  549. {
  550. $execution = current($tasks);
  551. $execution->executionTasks = count($tasks);
  552. $execution->executionHours = array_sum(array_map(function($task){return $task->isParent ? 0 : $task->left;}, $tasks));
  553. $execution->totalTasks = $totalTasks;
  554. $execution->totalHours = $totalHours;
  555. $execution->workload = $userWorkload;
  556. $this->setExecutionName($execution, $canViewExecution);
  557. if($userFirstRow) $execution->userRowspan = $totalExecutions;
  558. if($projectFirstRow) $execution->projectRowspan = count($executions);
  559. $workload[] = $execution;
  560. $userFirstRow = $projectFirstRow = false;
  561. }
  562. }
  563. }
  564. return $workload;
  565. }
  566. /**
  567. * 设置执行名称。
  568. * Set execution name.
  569. *
  570. * @param object $execution
  571. * @param bool $canViewExecution
  572. * @access public
  573. * @return void
  574. */
  575. public function setExecutionName($execution, $canViewExecution)
  576. {
  577. if($execution->multiple)
  578. {
  579. $execution->executionName = $canViewExecution ? html::a(helper::createLink('execution', 'view', "executionID={$execution->executionID}"), $execution->executionName, '', "title='{$execution->executionName}'") : "<span title='{$execution->executionName}'>{$execution->executionName}</span>";
  580. }
  581. else
  582. {
  583. $execution->executionName = $this->lang->null;
  584. }
  585. }
  586. /**
  587. * 获取用户的工作负载相关信息。
  588. * Get user's workload related information.
  589. *
  590. * @param array $projects
  591. * @param array $teamTasks
  592. * @param float $allHour
  593. * @access private
  594. * @return array
  595. */
  596. public function getUserWorkLoad($projects, $teamTasks, $allHour)
  597. {
  598. /* 计算员工的任务数,剩余工时和总任务数。 */
  599. /* Calculate user's task count, left hours and total task count. */
  600. $totalTasks = $totalHours = $totalExecutions = 0;
  601. foreach($projects as $executions)
  602. {
  603. $totalExecutions += count($executions);
  604. foreach($executions as $tasks)
  605. {
  606. $totalTasks += count($tasks);
  607. foreach($tasks as $task)
  608. {
  609. if($task->isParent) continue;
  610. if(isset($teamTasks[$task->id])) $task->left = $teamTasks[$task->id]->left;
  611. $totalHours = round($totalHours + $task->left, 2);
  612. }
  613. }
  614. }
  615. /* 计算员工的工作负载。 */
  616. /* Calculate user's workload. */
  617. $userWorkload = $allHour ? round($totalHours / $allHour * 100, 2) . '%' : '0%';
  618. return array($totalTasks, $totalHours, $totalExecutions, $userWorkload);
  619. }
  620. /**
  621. * 获取未解决Bug指派表相关数据。
  622. * Get bug assign.
  623. *
  624. * @access public
  625. * @return array
  626. */
  627. public function getBugAssign()
  628. {
  629. $bugGroups = $this->pivotTao->getAssignBugGroup();
  630. $products = $this->pivotTao->getAllProductsIDAndName();
  631. $productProjects = $this->pivotTao->getProductProjects();
  632. $canViewProduct = common::hasPriv('product', 'view');
  633. $canViewProject = common::hasPriv('project', 'view');
  634. $bugs = array();
  635. foreach($bugGroups as $userBugs)
  636. {
  637. $totalBugs = array_sum(array_map(function($bug){return $bug->bugCount;}, $userBugs));
  638. $first = true;
  639. foreach($userBugs as $bug)
  640. {
  641. if(!isset($products[$bug->product])) continue;
  642. $bug->productName = $products[$bug->product];
  643. if($bug->productName)
  644. {
  645. /* 用户有访问权限的情况下,允许用户点击。 */
  646. /* Allow users to click if they have access. */
  647. if($canViewProject && !empty($productProjects[$bug->product]))
  648. {
  649. $bug->productName = html::a(helper::createLink('project', 'view', "projectID={$productProjects[$bug->product]}"), $bug->productName);
  650. }
  651. elseif($canViewProduct)
  652. {
  653. $bug->productName = html::a(helper::createLink('product', 'view', "product={$bug->product}"), $bug->productName);
  654. }
  655. }
  656. $bug->total = $totalBugs;
  657. if($first) $bug->rowspan = count($userBugs);
  658. $bugs[] = $bug;
  659. $first = false;
  660. }
  661. }
  662. return $bugs;
  663. }
  664. /**
  665. * 获取执行的下拉菜单相关数据。
  666. * Get execution dropdown menu related data.
  667. *
  668. * @access public
  669. * @return array
  670. */
  671. public function getProjectExecutions()
  672. {
  673. $executions = $this->pivotTao->getProjectAndExecutionNameQuery();
  674. $pairs = array();
  675. foreach($executions as $execution)
  676. {
  677. if($execution->multiple) $pairs[$execution->id] = $execution->projectname . '/' . $execution->name;
  678. if(!$execution->multiple) $pairs[$execution->id] = $execution->projectname;
  679. }
  680. return $pairs;
  681. }
  682. /**
  683. * 格式化sql和过滤条件。
  684. * Format sql and filter.
  685. *
  686. * @param string $sql
  687. * @param array $filters
  688. * @access public
  689. * @return array
  690. */
  691. public function getFilterFormat($sql, $filters)
  692. {
  693. if(empty($filters)) return array($sql, false);
  694. $filters = $this->processQueryFilterDefaults($filters);
  695. $currentFilter = current($filters);
  696. $isQueryFilter = (isset($currentFilter['from']) && $currentFilter['from'] == 'query');
  697. $filterFormat = $isQueryFilter ? array_values($filters) : array();
  698. foreach($filters as $filter)
  699. {
  700. $field = $filter['field'];
  701. if($isQueryFilter)
  702. {
  703. $queryDefault = '';
  704. if(isset($filter['default']))
  705. {
  706. $queryDefault = $filter['default'];
  707. if($filter['type'] == 'date' || $filter['type'] == 'datetime') $queryDefault = $this->processDateVar($filter['default']);
  708. if($filter['type'] == 'datetime') $queryDefault .= ':00.000000000';
  709. if($filter['type'] == 'multipleselect' && is_array($filter['default'])) $queryDefault = implode("','", $filter['default']);
  710. }
  711. if(strpos($sql, $filter['field'] . 'Condition') === false)
  712. {
  713. $sql = str_replace('$' . $filter['field'], "'{$queryDefault}'", $sql);
  714. }
  715. else
  716. {
  717. $sql = str_replace('$' . $filter['field'] . 'Condition', "{$filter['relatedField']}='{$queryDefault}'", $sql);
  718. }
  719. }
  720. else
  721. {
  722. if(!isset($filter['default'])) continue;
  723. $default = $filter['default'];
  724. switch($filter['type'])
  725. {
  726. case 'select':
  727. if(is_string($default) && strpos($default, ',') !== false) $default = explode(',', $default);
  728. if(is_array($default)) $default = implode("', '", array_filter($default, function($val){return trim($val) != '';}));
  729. if(empty($default)) break;
  730. $value = "('" . $default . "')";
  731. $filterFormat[$field] = array('operator' => 'IN', 'value' => $value);
  732. break;
  733. case 'input':
  734. $filterFormat[$field] = array('operator' => 'LIKE', 'value' => "'%$default%'");
  735. break;
  736. case 'date':
  737. case 'datetime':
  738. $begin = $default['begin'];
  739. $end = $default['end'];
  740. if(!empty($begin)) $begin = date('Y-m-d 00:00:00', strtotime($begin));
  741. if(!empty($end)) $end = date('Y-m-d 23:59:59', strtotime($end));
  742. if(!empty($begin) && empty($end)) $filterFormat[$field] = array('operator' => '>=', 'value' => "'{$begin}'");
  743. if( empty($begin) && !empty($end)) $filterFormat[$field] = array('operator' => '<=', 'value' => "'{$end}'");
  744. if(!empty($begin) && !empty($end)) $filterFormat[$field] = array('operator' => 'BETWEEN', 'value' => "'{$begin}' AND '{$end}'");
  745. break;
  746. }
  747. }
  748. }
  749. return array($sql, $filterFormat);
  750. }
  751. /**
  752. * Get groups from settings.
  753. *
  754. * @param array $settings
  755. * @access public
  756. * @return array
  757. */
  758. public function getGroupsFromSettings($settings)
  759. {
  760. $groups = array();
  761. foreach($settings as $key => $value)
  762. {
  763. if(strpos($key, 'group') !== false && $value) $groups[] = $value;
  764. }
  765. return array_unique($groups);
  766. }
  767. /**
  768. * Trim semicolon of sql.
  769. *
  770. * @param string $sql
  771. * @access public
  772. * @return string
  773. */
  774. public function trimSemicolon($sql)
  775. {
  776. return trim($sql, " ;");
  777. }
  778. /**
  779. * Append where filter to sql from filters.
  780. *
  781. * @param string $sql
  782. * @param array|false $filters
  783. * @param string $driver
  784. * @access public
  785. * @return string
  786. */
  787. public function appendWhereFilterToSql($sql, $filters, $driver)
  788. {
  789. $connectSQL = '';
  790. if(!isset($filters[0]['from']) && $filters !== false)
  791. {
  792. if(!empty($filters))
  793. {
  794. $wheres = array();
  795. foreach($filters as $field => $filter)
  796. {
  797. $fieldSQL = $this->getFilterFieldSQL($filter, $field, $driver);
  798. if($filter['operator'] == 'LIKE' && $filter['value'] == "'%%'")
  799. {
  800. $wheres[] = "1=1"; // if empty filter then ignore this filter.
  801. }
  802. else
  803. {
  804. $wheres[] = "$fieldSQL {$filter['operator']} {$filter['value']}";
  805. }
  806. }
  807. $whereStr = implode(' and ', $wheres);
  808. $connectSQL .= " where $whereStr";
  809. }
  810. else
  811. {
  812. $connectSQL .= " where 1=0";
  813. }
  814. }
  815. if($connectSQL) $sql = "select * from ( $sql ) tt" . $connectSQL;
  816. return $sql;
  817. }
  818. /**
  819. * @param mixed[] $filter
  820. * @param string $field
  821. * @param string $driver
  822. */
  823. public function getFilterFieldSQL($filter, $field, $driver)
  824. {
  825. $fieldSql = "tt.`{$field}`";
  826. if($driver == 'duckdb')
  827. {
  828. $type = $filter['type'];
  829. if($type == 'input')
  830. {
  831. $fieldSql = " cast($fieldSql as varchar) ";
  832. }
  833. }
  834. return $fieldSql;
  835. }
  836. /**
  837. * Filter fields with settings.
  838. *
  839. * @param array $fields
  840. * @param array $groups
  841. * @param array $columns
  842. * @access public
  843. * @return array
  844. */
  845. public function filterFieldsWithSettings($fields, $groups, $columns)
  846. {
  847. $filteredFields = array();
  848. $settingFields = $groups;
  849. foreach($columns as $column)
  850. {
  851. $slice = zget($column, 'slice', 'noSlice');
  852. $settingFields[] = $column['field'];
  853. if($slice != 'noSlice') $settingFields[] = $slice;
  854. }
  855. $settingFields = array_unique($settingFields);
  856. foreach($settingFields as $field)
  857. {
  858. if(!isset($filteredFields[$field]) && isset($fields[$field])) $filteredFields[$field] = $fields[$field];
  859. }
  860. return $filteredFields;
  861. }
  862. /**
  863. * Map record value with field options.
  864. *
  865. * @param array $records
  866. * @param array $fields
  867. * @access public
  868. * @return array
  869. * @param string $driver
  870. */
  871. public function mapRecordValueWithFieldOptions($records, $fields, $driver)
  872. {
  873. $this->app->loadConfig('dataview');
  874. $fieldOptions = $this->getFieldsOptions($fields, $records, $driver);
  875. $records = json_decode(json_encode($records), true);
  876. foreach($records as $index => $record)
  877. {
  878. foreach($record as $field => $value)
  879. {
  880. if(!isset($fields[$field])) continue;
  881. $value = is_string($value) ? str_replace('"', '', htmlspecialchars_decode($value)) : $value;
  882. $record["{$field}_origin"] = $value;
  883. $tableField = !isset($fields[$field]) ? '' : $fields[$field]['object'] . '-' . $fields[$field]['field'];
  884. $withComma = in_array($tableField, $this->config->dataview->multipleMappingFields);
  885. $optionList = isset($fieldOptions[$field]) ? $fieldOptions[$field] : array();
  886. if($withComma)
  887. {
  888. $valueArr = array_filter(explode(',', $value));
  889. $resultArr = array();
  890. foreach($valueArr as $val)
  891. {
  892. $resultArr[] = isset($optionList[$val]) ? $optionList[$val] : $val;
  893. }
  894. $record[$field] = implode(',', $resultArr);
  895. }
  896. else
  897. {
  898. $valueKey = "$value";
  899. $record[$field] = isset($optionList[$valueKey]) ? $optionList[$valueKey] : $value;
  900. }
  901. $record[$field] = is_string($record[$field]) ? str_replace('"', '', htmlspecialchars_decode($record[$field])) : $record[$field];
  902. }
  903. $records[$index] = (object)$record;
  904. }
  905. return $records;
  906. }
  907. /**
  908. * Genereate table cols config.
  909. *
  910. * @param array $fields
  911. * @param array $groups
  912. * @param array $langs
  913. * @access public
  914. * @return array
  915. */
  916. public function generateTableCols($fields, $groups, $langs)
  917. {
  918. $cols = array();
  919. $clientLang = $this->app->getClientLang();
  920. /* Build cols. */
  921. foreach($groups as $group)
  922. {
  923. $fieldObject = $fields[$group]['object'];
  924. $relatedField = $fields[$group]['field'];
  925. $col = new stdclass();
  926. $col->name = $group;
  927. $col->field = $relatedField;
  928. $col->isGroup = true;
  929. $colLabel = $group;
  930. if($fieldObject)
  931. {
  932. $this->app->loadLang($fieldObject);
  933. if(isset($this->lang->$fieldObject->$relatedField)) $colLabel = $this->lang->$fieldObject->$relatedField;
  934. if($this->config->edition != 'open')
  935. {
  936. $workflowFields = $this->loadModel('workflowfield')->getFieldPairs($fieldObject);
  937. if(isset($workflowFields[$relatedField])) $colLabel = $workflowFields[$relatedField];
  938. }
  939. }
  940. if(isset($langs[$group]) && !empty($langs[$group][$clientLang])) $colLabel = $langs[$group][$clientLang];
  941. $col->label = $colLabel;
  942. $cols[0][] = $col;
  943. }
  944. return $cols;
  945. }
  946. /**
  947. * Get show col position.
  948. *
  949. * @param array $settings
  950. * @access public
  951. * @return string noShow | bottom | row | all
  952. */
  953. public function getShowColPosition($settings)
  954. {
  955. $columnTotal = zget($settings, 'columnTotal', 'noShow');
  956. $columnPosition = zget($settings, 'columnPosition', 'bottom');
  957. if($columnTotal == 'noShow') return 'noShow';
  958. return $columnPosition;
  959. }
  960. /**
  961. * Check whether showColPosition should show last row.
  962. *
  963. * @param string $showColPosition
  964. * @access public
  965. * @return bool
  966. */
  967. public function isShowLastRow($showColPosition)
  968. {
  969. return in_array($showColPosition, array('bottom', 'all'));
  970. }
  971. /**
  972. * 计算列的统计值。
  973. * Calculate column statistics.
  974. *
  975. * @param array $records
  976. * @param string $statistic
  977. * @param string $field
  978. * @access public
  979. * @return mixed
  980. */
  981. public function columnStatistics($records, $statistic, $field)
  982. {
  983. $values = array_column($records, $field);
  984. $numericValues = array_map(function($value)
  985. {
  986. return is_numeric($value) ? floatval($value) : 0;
  987. }, $values);
  988. if($statistic == 'count') return count($numericValues);
  989. if($statistic == 'sum') return round(array_sum($numericValues), 2);
  990. if($statistic == 'avg') return round(array_sum($numericValues) / count($numericValues), 2);
  991. if($statistic == 'min') return min($numericValues);
  992. if($statistic == 'max') return max($numericValues);
  993. if($statistic == 'distinct') return count(array_unique($values));
  994. }
  995. /**
  996. * 行数据转树。
  997. * Convert row data to tree.
  998. *
  999. * @param array $data
  1000. * @access public
  1001. * @return array|string
  1002. */
  1003. public function getGroupTreeWithKey($data)
  1004. {
  1005. $first = reset($data);
  1006. if(!isset($first['groups'])) return $first['groupKey'];
  1007. $tree = array();
  1008. foreach($data as $value)
  1009. {
  1010. $groups = $value['groups'];
  1011. $parentKey = array_shift($groups);
  1012. if(!isset($tree[$parentKey])) $tree[$parentKey] = array();
  1013. $value['groups'] = $groups;
  1014. if(count($groups) == 0) unset($value['groups']);
  1015. $tree[$parentKey][] = $value;
  1016. }
  1017. foreach($tree as $key => $value) $tree[$key] = $this->getGroupTreeWithKey($value);
  1018. return $tree;
  1019. }
  1020. /**
  1021. * 获取单元格数据。
  1022. * Get cell data.
  1023. *
  1024. * @param string $key
  1025. * @param array $data
  1026. * @access public
  1027. * @return array
  1028. */
  1029. public function formatCellData($key, $data)
  1030. {
  1031. if(!isset($data[$key])) return array();
  1032. $cellData = $data[$key];
  1033. foreach($cellData as $colKey => $colValue)
  1034. {
  1035. if(is_scalar($colValue))
  1036. {
  1037. $cellData[$colKey] = array('value' => $colValue);
  1038. }
  1039. else
  1040. {
  1041. $value = $colValue['value'];
  1042. $colValue['value'] = is_scalar($value) ? $value : '/';
  1043. $cellData[$colKey] = $colValue;
  1044. }
  1045. }
  1046. return $cellData;
  1047. }
  1048. /**
  1049. * 计算列的总计值。
  1050. * Calculate column total.
  1051. *
  1052. * @param array $data
  1053. * @access public
  1054. * @return array
  1055. * @param string $totalKey
  1056. */
  1057. public function getColumnSummary($data, $totalKey)
  1058. {
  1059. $summary = array();
  1060. foreach($data as $columns)
  1061. {
  1062. foreach($columns as $colKey => $colValue)
  1063. {
  1064. if(!isset($summary[$colKey]))
  1065. {
  1066. $summary[$colKey] = $colValue;
  1067. }
  1068. else
  1069. {
  1070. $isGroup = zget($colValue, 'isGroup', 1);
  1071. $value = zget($colValue, 'value', '');
  1072. $isNumeric = is_numeric($value);
  1073. $summary[$colKey]['value'] = !$isGroup && $isNumeric ? $summary[$colKey]['value'] + $value : $value;
  1074. }
  1075. }
  1076. }
  1077. $summary[$totalKey] = array('value' => '$total$');
  1078. /* 删除汇总行的下钻配置。*/
  1079. /* Delete drilldown config of summary row. */
  1080. foreach($summary as $key => $value)
  1081. {
  1082. if(isset($value['value']) && is_numeric($value['value'])) $summary[$key]['value'] = round($summary[$key]['value'], 2);
  1083. if(isset($value['drillFields']))
  1084. {
  1085. unset($summary[$key]['drillFields']);
  1086. }
  1087. }
  1088. return $summary;
  1089. }
  1090. /**
  1091. * 添加行总计到树数据中。
  1092. * Add row summary to tree data.
  1093. *
  1094. * @param array $groupTree
  1095. * @param array $data
  1096. * @param array $groups
  1097. * @param int $currentGroup
  1098. * @access public
  1099. * @return array
  1100. */
  1101. public function addRowSummary($groupTree, $data, $groups, $currentGroup = 0)
  1102. {
  1103. $first = reset($groupTree);
  1104. if(is_scalar($first))
  1105. {
  1106. $groupData = array();
  1107. $rows = array();
  1108. foreach($groupTree as $groupKey)
  1109. {
  1110. $groupData[$groupKey] = $this->formatCellData($groupKey, $data);
  1111. $rows[$groupKey] = $data[$groupKey];
  1112. }
  1113. return array('rows' => $rows, 'summary' => $this->getColumnSummary($groupData, $groups[$currentGroup]));
  1114. }
  1115. $rows = array();
  1116. foreach($groupTree as $key => $children) $rows[$key] = $this->addRowSummary($children, $data, $groups, $currentGroup + 1);
  1117. $groupData = array_column($rows, 'summary');
  1118. return array('rows' => $rows, 'summary' => $this->getColumnSummary($groupData, $groups[$currentGroup]));
  1119. }
  1120. /**
  1121. * 去除数据中的额外信息,只保留单元格数据。
  1122. * Remove extra info from data, only keep cell data.
  1123. *
  1124. * @param array $records
  1125. * @access public
  1126. * @return array
  1127. */
  1128. public function pureCrystalData($records)
  1129. {
  1130. $pureData = array();
  1131. foreach($records as $key => $record)
  1132. {
  1133. $columns = $record['columns'];
  1134. $groups = $record['groups'];
  1135. $pureData[$key] = $groups;
  1136. foreach($columns as $colKey => $colValue)
  1137. {
  1138. $cellData = $colValue['cellData'];
  1139. if(isset($colValue['rowTotal'])) $cellData['total'] = $colValue['rowTotal'];
  1140. if(isset($cellData['value']))
  1141. {
  1142. $pureData[$key][$colKey] = $cellData;
  1143. }
  1144. else
  1145. {
  1146. foreach($cellData as $sliceKey => $sliceValue) $pureData[$key][$colKey . '_' . $sliceKey] = $sliceValue;
  1147. }
  1148. }
  1149. }
  1150. return $pureData;
  1151. }
  1152. /**
  1153. * 拍平切片列数据。
  1154. * Flatten slice column data.
  1155. *
  1156. * @param array $row
  1157. * @access public
  1158. * @return array
  1159. */
  1160. public function flattenRow($row)
  1161. {
  1162. $record = array();
  1163. foreach($row as $colKey => $cell)
  1164. {
  1165. if(is_scalar($cell))
  1166. {
  1167. $record[$colKey] = array('value' => $cell);
  1168. }
  1169. elseif(isset($cell['value']))
  1170. {
  1171. $record[$colKey] = $cell;
  1172. }
  1173. }
  1174. return $record;
  1175. }
  1176. /**
  1177. * 拍平透视表树结构数据。
  1178. * Flatten pivot table tree structure data.
  1179. *
  1180. * @param array $crystalData
  1181. * @param bool $withGroupSummary
  1182. * @access public
  1183. * @return array
  1184. */
  1185. public function flattenCrystalData($crystalData, $withGroupSummary = false)
  1186. {
  1187. $first = reset($crystalData);
  1188. if(!isset($first['rows']))
  1189. {
  1190. $records = array();
  1191. foreach($crystalData as $row) $records[] = $this->flattenRow($row);
  1192. return $records;
  1193. }
  1194. $records = array();
  1195. foreach($crystalData as $value)
  1196. {
  1197. $groupRecords = $this->flattenCrystalData($value['rows'], $withGroupSummary);
  1198. if($withGroupSummary && isset($value['summary'])) $groupRecords[] = $this->flattenRow($value['summary']);
  1199. $records = array_merge($records, $groupRecords);
  1200. }
  1201. return $records;
  1202. }
  1203. /**
  1204. * 处理行合并单元格。
  1205. * Process row span cell.
  1206. *
  1207. * @param array $records
  1208. * @param array $groups
  1209. * @access public
  1210. * @return array
  1211. */
  1212. public function processRowSpan($records, $groups)
  1213. {
  1214. $lastGroupValue = array();
  1215. foreach($groups as $group) $lastGroupValue[$group] = '';
  1216. /* 定义内部函数:获取当前行数据的分组值。*/
  1217. /* Define internal function: get current row data's group value. */
  1218. $getGroupValue = function($record, $key, $index) use ($groups)
  1219. {
  1220. $value = array($record[$key]['value']);
  1221. $index -= 1;
  1222. while($index >= 0)
  1223. {
  1224. $value[] = $record[$groups[$index]]['value'];
  1225. $index -= 1;
  1226. }
  1227. return $value;
  1228. };
  1229. $groupsRowSpan = array();
  1230. foreach($records as $index => $record)
  1231. {
  1232. $rowSpan = 1;
  1233. foreach($record as $colKey => $cell)
  1234. {
  1235. if(!isset($cell['value']) || !is_array($cell['value'])) continue;
  1236. $rowSpan = max(count($cell['value']), $rowSpan);
  1237. }
  1238. foreach($record as $colKey => $cell)
  1239. {
  1240. $record[$colKey]['rowSpan'] = is_scalar($cell['value']) ? $rowSpan : 1;
  1241. }
  1242. $records[$index] = $record;
  1243. foreach($groups as $groupIndex => $group)
  1244. {
  1245. $groupValue = $getGroupValue($record, $group, $groupIndex);
  1246. $groupValueStr = implode('_', $groupValue);
  1247. if($groupValue[0] !== '$total$' && $groupValueStr === $lastGroupValue[$group] && isset($groupsRowSpan[$group]))
  1248. {
  1249. $groupRowSpan = array_pop($groupsRowSpan[$group]);
  1250. $groupRowSpan['index'][] = $index;
  1251. $groupRowSpan['rowSpan'] += $rowSpan;
  1252. $groupsRowSpan[$group][] = $groupRowSpan;
  1253. }
  1254. else
  1255. {
  1256. $groupsRowSpan[$group][] = array('index' => array($index), 'rowSpan' => $rowSpan);
  1257. }
  1258. $lastGroupValue[$group] = $groupValueStr;
  1259. }
  1260. }
  1261. foreach($groupsRowSpan as $group => $groupRowSpans)
  1262. {
  1263. foreach($groupRowSpans as $groupRowSpan)
  1264. {
  1265. $indexes = $groupRowSpan['index'];
  1266. foreach($indexes as $index)
  1267. {
  1268. $records[$index][$group]['rowSpan'] = $groupRowSpan['rowSpan'];
  1269. }
  1270. }
  1271. }
  1272. return $records;
  1273. }
  1274. /**
  1275. * 计算行汇总值。
  1276. * Calculate row total.
  1277. *
  1278. * @param array $row
  1279. * @access public
  1280. * @return array
  1281. */
  1282. public function getRowTotal($row)
  1283. {
  1284. $rowTotal = array();
  1285. foreach($row as $cell)
  1286. {
  1287. if(!isset($cell['percentage'])) continue;
  1288. list(,,,, $columnKey) = $cell['percentage'];
  1289. if(!isset($rowTotal[$columnKey])) $rowTotal[$columnKey] = 0;
  1290. $rowTotal[$columnKey] += $cell['value'];
  1291. }
  1292. return $rowTotal;
  1293. }
  1294. /**
  1295. * 计算百分比值。
  1296. * Calculate percentage.
  1297. *
  1298. * @param array $row
  1299. * @param array $rowTotal
  1300. * @param array $columnTotal
  1301. * @access public
  1302. * @return array
  1303. */
  1304. public function setPercentage($row, $rowTotal, $columnTotal)
  1305. {
  1306. foreach($row as $key => $cell)
  1307. {
  1308. if(!isset($cell['percentage'])) continue;
  1309. list(,,$showMode,, $columnKey) = $cell['percentage'];
  1310. if($showMode == 'row') $cell['percentage'][1] = $rowTotal[$columnKey];
  1311. if($showMode == 'column') $cell['percentage'][1] = $columnTotal[$key]['value'];
  1312. if($showMode == 'total')
  1313. {
  1314. $total = 0;
  1315. foreach($columnTotal as $column)
  1316. {
  1317. if(!isset($column['percentage'])) continue;
  1318. $percentage = $column['percentage'];
  1319. if($percentage[4] === $columnKey) $total += $column['value'];
  1320. }
  1321. $cell['percentage'][1] = $total;
  1322. }
  1323. $cell['percentage'][0] = $cell['value'];
  1324. $row[$key] = $cell;
  1325. }
  1326. return $row;
  1327. }
  1328. /**
  1329. * 处理百分比值。
  1330. * Process percentage.
  1331. *
  1332. * @param array $crystalData
  1333. * @param array $allSummary
  1334. * @access public
  1335. * @return array
  1336. */
  1337. public function processPercentage($crystalData, $allSummary)
  1338. {
  1339. $rows = $crystalData['rows'];
  1340. $summary = $crystalData['summary'];
  1341. foreach($rows as $key => $row)
  1342. {
  1343. if(isset($row['rows']))
  1344. {
  1345. $rows[$key] = $this->processPercentage($row, $allSummary);
  1346. }
  1347. else
  1348. {
  1349. $rowTotal = $this->getRowTotal($row);
  1350. $rows[$key] = $this->setPercentage($row, $rowTotal, $allSummary);
  1351. }
  1352. }
  1353. $rowTotal = $this->getRowTotal($summary);
  1354. $summary = $this->setPercentage($summary, $rowTotal, $allSummary);
  1355. return array('rows' => $rows, 'summary' => $summary);
  1356. }
  1357. /**
  1358. * 对数据进行分组。
  1359. * Group records.
  1360. *
  1361. * @param array $records
  1362. * @param array $groups
  1363. * @access public
  1364. * @return array
  1365. */
  1366. public function groupRecords($records, $groups)
  1367. {
  1368. $groupsData = array();
  1369. foreach($records as $record)
  1370. {
  1371. $key = $this->getGroupsKey($groups, $record);
  1372. if(!isset($groupsData[$key])) $groupsData[$key] = array();
  1373. $groupsData[$key][] = $record;
  1374. }
  1375. return $groupsData;
  1376. }
  1377. /**
  1378. * 设置切片列去重后的值。
  1379. * Set unique slices.
  1380. *
  1381. * @param array $records
  1382. * @param array $setting
  1383. * @access public
  1384. * @return array
  1385. */
  1386. public function setUniqueSlices($records, $setting)
  1387. {
  1388. static $slices = array();
  1389. $slice = zget($setting, 'slice', 'noSlice');
  1390. if($slice == 'noSlice') return $setting;
  1391. if(isset($slices[$slice]))
  1392. {
  1393. $setting['uniqueSlices'] = $slices[$slice];
  1394. return $setting;
  1395. }
  1396. $uniqueSlices = array();
  1397. foreach($records as $record)
  1398. {
  1399. if(!isset($uniqueSlices[$record->$slice])) $uniqueSlices[$record->$slice] = $record;
  1400. }
  1401. $slices[$slice] = $uniqueSlices;
  1402. $setting['uniqueSlices'] = $uniqueSlices;
  1403. return $setting;
  1404. }
  1405. /**
  1406. * 根据字段的值过滤记录。
  1407. * Filter records by field value.
  1408. *
  1409. * @param array $records
  1410. * @param string $field
  1411. * @access public
  1412. * @return array
  1413. */
  1414. public function getSliceRecords($records, $field)
  1415. {
  1416. $sliceRecords = array();
  1417. foreach($records as $record)
  1418. {
  1419. if(!isset($sliceRecords[$record->$field])) $sliceRecords[$record->$field] = array();
  1420. $sliceRecords[$record->$field][] = $record;
  1421. }
  1422. return $sliceRecords;
  1423. }
  1424. /**
  1425. * 计算单元格数据。
  1426. * Calculate cell data.
  1427. *
  1428. * @param string $columnKey
  1429. * @param array $records
  1430. * @param array $setting
  1431. * @access public
  1432. * @return array
  1433. */
  1434. public function getCellData($columnKey, $records, $setting)
  1435. {
  1436. $field = zget($setting, 'field', '');
  1437. $showOrigin = zget($setting, 'showOrigin', 0);
  1438. if($showOrigin) return array('value' => array_column($records, $field), 'isGroup' => false);
  1439. $stat = zget($setting, 'stat', 'count');
  1440. $slice = zget($setting, 'slice', 'noSlice');
  1441. $showMode = zget($setting, 'showMode', 'default');
  1442. $showTotal = zget($setting, 'showTotal', 'noShow');
  1443. $monopolize = zget($setting, 'monopolize', 0);
  1444. $isSlice = $slice != 'noSlice';
  1445. if(!$isSlice)
  1446. {
  1447. $value = $this->columnStatistics($records, $stat, $field);
  1448. $cell = array('value' => $value, 'isGroup' => false);
  1449. if($showMode == 'default') return $cell;
  1450. $cell['percentage'] = array($value, 1, $showMode, $monopolize, $columnKey);
  1451. return $cell;
  1452. }
  1453. /* 处理切片列的情况。 */
  1454. /* Handle the slice column situation. */
  1455. $uniqueSlices = zget($setting, 'uniqueSlices', array());
  1456. $cell = array();
  1457. $sliceRecords = $this->getSliceRecords($records, $slice);
  1458. foreach($uniqueSlices as $sliceRecord)
  1459. {
  1460. $sliceValue = $sliceRecord->$slice;
  1461. $sliceKey = "{$slice}_{$sliceValue}";
  1462. $value = $this->columnStatistics(zget($sliceRecords, $sliceValue, array()), $stat, $field);
  1463. $sliceCell = array('value' => $value, 'drillFields' => array($slice => $sliceRecord->{$slice . '_origin'}), 'isGroup' => false);
  1464. if($showMode != 'default') $sliceCell['percentage'] = array($value, 1, $showMode, $monopolize, $columnKey);
  1465. $cell[$sliceKey] = $sliceCell;
  1466. }
  1467. if($showTotal != 'noShow')
  1468. {
  1469. $value = array_sum(array_column($cell, 'value'));
  1470. $totalCell = array('value' => $value, 'isGroup' => false);
  1471. if($showMode != 'default') $totalCell['percentage'] = array($value, 1, $showMode, $monopolize, "rowTotal_{$columnKey}");
  1472. $cell['total'] = $totalCell;
  1473. }
  1474. return $cell;
  1475. }
  1476. /**
  1477. * 添加下钻字段信息。
  1478. * Add drill fields information.
  1479. *
  1480. * @param array $cell
  1481. * @param array $drillFields
  1482. * @access public
  1483. * @return array
  1484. */
  1485. public function addDrillFields($cell, $drillFields)
  1486. {
  1487. if(isset($cell['value']))
  1488. {
  1489. if(!isset($cell['drillFields'])) $cell['drillFields'] = array();
  1490. $cell['drillFields'] = array_merge($cell['drillFields'], $drillFields);
  1491. return $cell;
  1492. }
  1493. foreach($cell as $sliceKey => $sliceCell)
  1494. {
  1495. if($sliceKey == 'total') continue;
  1496. $cell[$sliceKey] = $this->addDrillFields($sliceCell, $drillFields);
  1497. }
  1498. return $cell;
  1499. }
  1500. /**
  1501. * 根据列配置,计算透视表数据。
  1502. * Calculate pivot table data.
  1503. *
  1504. * @param array $groups
  1505. * @param array $records
  1506. * @param array $settings
  1507. * @access public
  1508. * @return array
  1509. */
  1510. public function processCrystalData($groups,$records, $settings)
  1511. {
  1512. $crystalData = array();
  1513. $columnSettings = $settings['columns'];
  1514. $groupRecords = $this->groupRecords($records, $groups);
  1515. foreach($groupRecords as $key => $data)
  1516. {
  1517. $record = reset($data);
  1518. $groupValues = array();
  1519. $groupOriginalValues = array();
  1520. foreach($groups as $group)
  1521. {
  1522. $groupValues[$group] = $record->$group;
  1523. $groupOriginalValues[$group] = $record->{$group . '_origin'};
  1524. }
  1525. $columns = array();
  1526. foreach($columnSettings as $colIndex => $setting)
  1527. {
  1528. $setting = $this->setUniqueSlices($records, $setting);
  1529. $field = zget($setting, 'field', '');
  1530. $columnKey = "{$field}{$colIndex}";
  1531. $cellData = $this->getCellData($columnKey, $data, $setting);
  1532. $cellData = $this->addDrillFields($cellData, $groupOriginalValues);
  1533. $columns[$columnKey] = array('setting' => $setting, 'cellData' => $cellData);
  1534. }
  1535. $crystalData[$key] = array('groups' => $groupValues, 'groupKey' => $key, 'columns' => $columns);
  1536. }
  1537. return $crystalData;
  1538. }
  1539. /**
  1540. * 处理透视表数据为可以显示的格式。
  1541. * Process pivot table data for display.
  1542. *
  1543. * @param array $records
  1544. * @access public
  1545. * @return array
  1546. */
  1547. public function processRecordsForDisplay($records)
  1548. {
  1549. $roundIfMoreThanTwoDecimals = function($number)
  1550. {
  1551. if(!is_numeric($number)) return $number;
  1552. $number = (float)$number;
  1553. if(floor($number) === $number) return $number;
  1554. $decimalPart = explode('.', strval($number));
  1555. if(isset($decimalPart[1]) && strlen($decimalPart[1]) > 2) return $number;
  1556. return $number;
  1557. };
  1558. $values = array();
  1559. foreach($records as $record)
  1560. {
  1561. $row = array();
  1562. $arrayValue = false;
  1563. foreach($record as $colKey => $cell)
  1564. {
  1565. $cellValue = $cell['value'] === '$total$' ? $this->lang->pivot->total : $cell['value'];
  1566. if(is_array($cellValue)) $arrayValue = $cellValue;
  1567. $cellValue = $roundIfMoreThanTwoDecimals($cellValue);
  1568. $row[$colKey] = $cellValue;
  1569. if(isset($cell['percentage']))
  1570. {
  1571. list($number, $total,, $monopolize) = $cell['percentage'];
  1572. if($monopolize) $colKey .= '_percentage';
  1573. if(!$total) $total = 100;
  1574. $row[$colKey] = round($number / $total * 100, 2) . '%';
  1575. }
  1576. }
  1577. if(is_array($arrayValue))
  1578. {
  1579. foreach(array_keys($arrayValue) as $index)
  1580. {
  1581. $flattenValue = array();
  1582. foreach($row as $key => $value)
  1583. {
  1584. $value = is_scalar($value) ? $value : $value[$index];
  1585. $flattenValue[$key] = $value;
  1586. }
  1587. $values[] = $flattenValue;
  1588. }
  1589. }
  1590. else
  1591. {
  1592. $values[] = $row;
  1593. }
  1594. }
  1595. return $values;
  1596. }
  1597. /**
  1598. * 获取合并单元格配置。
  1599. * Get row span config.
  1600. *
  1601. * @param array $records
  1602. * @access public
  1603. * @return array
  1604. */
  1605. public function getRowSpanConfig($records)
  1606. {
  1607. $configs = array();
  1608. foreach($records as $record)
  1609. {
  1610. $arrayValue = false;
  1611. foreach($record as $cell)
  1612. {
  1613. if(is_array($cell['value'])) $arrayValue = $cell['value'];
  1614. }
  1615. if(!is_array($arrayValue)) $arrayValue = array(1);
  1616. $configs = array_merge($configs, array_fill(0, count($arrayValue), array_column($record, 'rowSpan')));
  1617. }
  1618. return $configs;
  1619. }
  1620. /**
  1621. * 获取下钻字段配置。
  1622. * Get drill fields config.
  1623. *
  1624. * @param array $records
  1625. * @param array $groups
  1626. * @access public
  1627. * @return array
  1628. */
  1629. public function getDrillsFromRecords($records, $groups)
  1630. {
  1631. $drills = array();
  1632. foreach($records as $record)
  1633. {
  1634. $groupKey = $this->getGroupsKey($groups, (object)$record);
  1635. if(!isset($drills[$groupKey])) $drills[$groupKey] = array('drillFields' => array());
  1636. foreach($record as $colKey => $cell)
  1637. {
  1638. if(isset($cell['drillFields'])) $drills[$groupKey]['drillFields'][$colKey] = $cell['drillFields'];
  1639. }
  1640. }
  1641. return $drills;
  1642. }
  1643. /**
  1644. * 处理查询过滤器的默认值。
  1645. * Process query filter defaults.
  1646. *
  1647. * @param array|false $filters
  1648. * @access public
  1649. * @return array
  1650. */
  1651. public function processQueryFilterDefaults($filters)
  1652. {
  1653. if(!is_array($filters)) return $filters;
  1654. $options = array();
  1655. foreach($filters as $index => $filter)
  1656. {
  1657. if(empty($filter['default'])) continue;
  1658. if(!isset($filter['from']) || $filter['from'] != 'query') continue;
  1659. if($filter['type'] !== 'multipleselect') continue;
  1660. $type = $filter['type'];
  1661. $typeOption = $filter['typeOption'];
  1662. if(strpos($type, 'select') !== false && !isset($options[$typeOption])) $options[$typeOption] = $this->getSysOptions($typeOption);
  1663. $filters[$index]['default'] = array_intersect($filter['default'], array_keys($options[$typeOption]));
  1664. }
  1665. return $filters;
  1666. }
  1667. /**
  1668. * Gen sheet.
  1669. *
  1670. * @param array $fields
  1671. * @param array $settings
  1672. * @param string $sql
  1673. * @param array|false $filters
  1674. * @param array $langs
  1675. * @access public
  1676. * @return array
  1677. * @param string $driver
  1678. */
  1679. public function genSheet($fields, $settings, $sql, $filters, $langs = array(), $driver = 'mysql')
  1680. {
  1681. $groups = $this->getGroupsFromSettings($settings);
  1682. $cols = $this->generateTableCols($fields, $groups, $langs);
  1683. $data = new stdclass();
  1684. $data->groups = $groups;
  1685. $data->cols = $cols;
  1686. $data->array = array();
  1687. $data->drills = array();
  1688. if(!isset($settings['columns'])) return array($data, array());
  1689. $filters = $this->processQueryFilterDefaults($filters);
  1690. /* Replace the variable with the default value. */
  1691. $sql = $this->bi->processVars($sql, (array)$filters);
  1692. $sql = $this->trimSemicolon($sql);
  1693. $sql = $this->appendWhereFilterToSql($sql, $filters, $driver);
  1694. $records = $this->bi->queryWithDriver($driver, $sql);
  1695. $settingFields = $this->filterFieldsWithSettings($fields, $groups, $settings['columns']);
  1696. $records = $this->mapRecordValueWithFieldOptions($records, $settingFields, $driver);
  1697. if(empty($records)) return array($data, array());
  1698. foreach($settings['columns'] as $columnSetting)
  1699. {
  1700. $cols = $this->getTableHeader($records, $columnSetting, $fields, $cols, $sql, $langs, $driver);
  1701. }
  1702. /* 根据列配置和分组配置,计算透视表数据。*/
  1703. /* Calculate crystal data based on column settings and group settings. */
  1704. $crystalData = $this->processCrystalData($groups, $records, $settings);
  1705. /* 将扁平的透视表数据转换成树形结构。*/
  1706. /* Convert flattened pivot table data to tree structure. */
  1707. $groupTree = $this->getGroupTreeWithKey($crystalData);
  1708. /* 净化处理透视表数据中的额外信息,只留下与单元格数据相关的信息。*/
  1709. /* Clean up the extra information in pivot table data. */
  1710. $crystalData = $this->pureCrystalData($crystalData);
  1711. /* 基于各级分组,计算每个分组的总计数据行。*/
  1712. /* Calculate total data rows based on each group. */
  1713. $crystalData = $this->addRowSummary($groupTree, $crystalData, $groups);
  1714. /* 计算百分比的值。*/
  1715. /* Calculate percentage values. */
  1716. $crystalData = $this->processPercentage($crystalData, $crystalData['summary']);
  1717. /* 将树形结构转换成扁平的透视表数据。*/
  1718. /* Convert tree structure to flattened pivot table data. */
  1719. $columnPosition = $this->getShowColPosition($settings);
  1720. $showGroupTotal = in_array($columnPosition, array('row', 'all'));
  1721. $showAllTotal = in_array($columnPosition, array('bottom', 'all'));
  1722. $records = $this->flattenCrystalData($crystalData['rows'], $showGroupTotal);
  1723. if($showAllTotal) $records[] = $this->flattenRow($crystalData['summary']);
  1724. /* 计算行合并单元格的配置。*/
  1725. /* Calculate row span config. */
  1726. $records = $this->processRowSpan($records, $groups);
  1727. $data->cols = $cols;
  1728. $data->array = $this->processRecordsForDisplay($records);
  1729. $data->drills = $this->getDrillsFromRecords($records, $groups);
  1730. $data->showAllTotal = $showAllTotal;
  1731. $configs = $this->getRowSpanConfig($records);
  1732. /* $data->groups array 代表分组,最多三个
  1733. * $data->cols array thead数据,其中对象有三个属性:name:分组,label:列的名字,isGroup:标识是不是分组
  1734. * $data->arrays array tbody数据, 其中每一个数组内是一行td的数据
  1735. *
  1736. * $configs, eg: array(0 => array(0 => 2, 1 => 1), 2 => array(0 => 2))
  1737. * 代表在整个tbody中,位于[0,0]坐标的td rowspan为2,位于[0,1]坐标的td rowspan为1, 位于[2,0]坐标的td rowspan为2
  1738. */
  1739. return array($data, $configs);
  1740. }
  1741. /**
  1742. * Check is filters all default empty.
  1743. *
  1744. * @param array $data
  1745. * @access public
  1746. * @return void
  1747. */
  1748. public function isFiltersAllEmpty($filters)
  1749. {
  1750. return !empty($filters) && empty(array_filter(array_column($filters, 'default')));
  1751. }
  1752. /**
  1753. * Gen sheet by origin sql.
  1754. *
  1755. * @param array $fields
  1756. * @param array $settings
  1757. * @param string $sql
  1758. * @param array|false $filters
  1759. * @param array $langs
  1760. * @access public
  1761. * @return string
  1762. */
  1763. public function genOriginSheet($fields, $settings, $sql, $filters, $langs = array(), $driver = 'mysql')
  1764. {
  1765. $sql = $this->bi->processVars($sql, (array)$filters);
  1766. $sql = $this->trimSemicolon($sql);
  1767. $sql = $this->appendWhereFilterToSql($sql, $filters, $driver);
  1768. $dbh = $this->app->loadDriver($driver);
  1769. $rows = $dbh->query($sql)->fetchAll();
  1770. $rows = $this->filterSpecialChars($rows);
  1771. $fieldOptions = $this->getFieldsOptions($fields, $rows);
  1772. $rows = json_decode(json_encode($rows), true);
  1773. $cols = array();
  1774. $drills = zget($settings, 'drills', array());
  1775. /* Build cols. */
  1776. foreach($fields as $key => $field)
  1777. {
  1778. $col = new stdclass();
  1779. $col->name = $key;
  1780. $col->isGroup = true;
  1781. $col->label = $this->getColLabel($key, $fields, $langs);
  1782. if(isset($drills[$key]))
  1783. {
  1784. $col->isDrilling = true;
  1785. $col->condition = $drills[$key];
  1786. $col->drillField = $key;
  1787. }
  1788. $cols[0][] = $col;
  1789. }
  1790. $dataDrills = array();
  1791. $rowsAfterFields = array();
  1792. foreach($rows as $key => $row)
  1793. {
  1794. $drillFields = array();
  1795. $rowAfterFields = array();
  1796. foreach($row as $field => $value)
  1797. {
  1798. if(isset($drills[$field]))
  1799. {
  1800. $drillField = array();
  1801. foreach($drills[$field] as $condition)
  1802. {
  1803. $queryField = $condition['queryField'];
  1804. $drillField[$queryField] = $row[$queryField];
  1805. }
  1806. $drillFields[$field] = $drillField;
  1807. }
  1808. $optionList = isset($fieldOptions[$field]) ? $fieldOptions[$field] : array();
  1809. $rowAfterFields[$field] = isset($optionList[(string)$value]) ? $optionList[(string)$value] : $value;
  1810. }
  1811. $dataDrills[$key] = array('drillFields' => $drillFields);
  1812. $rowsAfterFields[$key] = $rowAfterFields;
  1813. }
  1814. $data = new stdclass();
  1815. $data->cols = $cols;
  1816. $data->array = $rowsAfterFields;
  1817. $data->drills = $dataDrills;
  1818. $configs = array_fill(0, count($rows), array_fill(0, count($fields), 1));
  1819. return array($data, $configs);
  1820. }
  1821. /**
  1822. * 初始化sql。
  1823. * Init sql.
  1824. *
  1825. * @param string $sql
  1826. * @param array $filters
  1827. * @param string $groupList
  1828. * @access public
  1829. * @return array
  1830. */
  1831. public function initSql($sql, $filters, $groupList)
  1832. {
  1833. $sql = str_replace(';', '', $this->initVarFilter($filters, $sql));
  1834. if(preg_match_all("/[\$]+[a-zA-Z0-9]+/", $sql, $out))
  1835. {
  1836. foreach($out[0] as $match) $sql = str_replace($match, "''", $sql);
  1837. }
  1838. $connectSQL = $this->getConnectSQL($filters);
  1839. $groupSQL = " group by {$groupList}";
  1840. $orderSQL = " order by {$groupList}";
  1841. return array($sql, $connectSQL, $groupSQL, $orderSQL);
  1842. }
  1843. /**
  1844. * 获取connectSQL。
  1845. * Get connectSQL.
  1846. *
  1847. * @param array $filters
  1848. * @access public
  1849. * @return string
  1850. */
  1851. public function getConnectSQL($filters)
  1852. {
  1853. $connectSQL = '';
  1854. if(!empty($filters) && !isset($filters[0]['from']))
  1855. {
  1856. $wheres = array();
  1857. foreach($filters as $field => $filter) $wheres[] = "tt.`{$field}` {$filter['operator']} {$filter['value']}";
  1858. $whereStr = implode(' and ', $wheres);
  1859. $connectSQL .= " where {$whereStr}";
  1860. }
  1861. return $connectSQL;
  1862. }
  1863. /**
  1864. * 通过过滤配置格式化sql。
  1865. * Init sql by filters.
  1866. *
  1867. * @param array $filters
  1868. * @param string $sql
  1869. * @access public
  1870. * @return string
  1871. */
  1872. private function initVarFilter($filters = array(), $sql = '')
  1873. {
  1874. if(empty($filters)) return $sql;
  1875. foreach($filters as $filter)
  1876. {
  1877. if(empty($filter['from'])) continue;
  1878. $default = isset($filter['default']) ? $filter['default'] : '';
  1879. if(is_array($default))
  1880. {
  1881. $default = array_filter($default, function($val){return !empty($val);});
  1882. $default = implode("', '", $default);
  1883. }
  1884. $sql = str_replace('$' . $filter['field'], "'{$default}'", $sql);
  1885. }
  1886. if(preg_match_all("/[\$]+[a-zA-Z0-9]+/", $sql, $out))
  1887. {
  1888. foreach($out[0] as $match) $sql = str_replace($match, "''", $sql);
  1889. }
  1890. return $sql;
  1891. }
  1892. /**
  1893. * 获取列名称。
  1894. * Get col label.
  1895. *
  1896. * @param string $key
  1897. * @param array $fields
  1898. * @param array $langs
  1899. * @access public
  1900. * @return string
  1901. */
  1902. public function getColLabel($key, $fields, $langs)
  1903. {
  1904. $clientLang = $this->app->getClientLang();
  1905. $fieldLang = zget($fields[$key], $clientLang, '');
  1906. if(!empty($fieldLang)) return $fieldLang;
  1907. if(isset($langs[$key]))
  1908. {
  1909. $lang = zget($langs[$key], $clientLang, '');
  1910. if(!empty($lang)) return $lang;
  1911. }
  1912. $object = zget($fields[$key], 'object', '');
  1913. if($object)
  1914. {
  1915. if($this->config->edition != 'open')
  1916. {
  1917. $workflowFields = $this->loadModel('workflowfield')->getFieldPairs($object);
  1918. if(isset($workflowFields[$key])) return $workflowFields[$key];
  1919. }
  1920. $this->app->loadLang($object);
  1921. if(isset($this->lang->{$object}->{$key})) return $this->lang->{$object}->{$key};
  1922. }
  1923. $name = zget($fields[$key], 'name', '');
  1924. if(!empty($name)) return $name;
  1925. return $key;
  1926. }
  1927. /**
  1928. * Get the header of the table.
  1929. *
  1930. * @param array $columnRows
  1931. * @param array $column
  1932. * @param array $fields
  1933. * @param array $cols
  1934. * @param array $langs
  1935. * @access public
  1936. * @return array
  1937. */
  1938. public function getTableHeader($columnRows, $column, $fields, $cols, $sql, $langs = array(), $driver = 'mysql')
  1939. {
  1940. $stat = zget($column, 'stat', '');
  1941. $showMode = zget($column, 'showMode', 'default');
  1942. $monopolize = $showMode == 'default' ? '' : zget($column, 'monopolize', '');
  1943. $showOrigin = (bool)zget($column, 'showOrigin', 0);
  1944. $isDrilling = isset($column['drill']) && zget($column['drill'], 'condition', '');
  1945. $drillField = $isDrilling ? zget($column['drill'], 'field', '') : '';
  1946. $condition = $isDrilling ? zget($column['drill'], 'condition', '') : '';
  1947. $col = new stdclass();
  1948. $col->name = $column['field'];
  1949. $col->isGroup = false;
  1950. $col->showOrigin = $showOrigin;
  1951. $col->isDrilling = $isDrilling;
  1952. $col->drillField = $drillField;
  1953. $col->condition = $condition;
  1954. $fieldObject = $fields[$column['field']]['object'];
  1955. $relatedField = $fields[$column['field']]['field'];
  1956. $colLabel = $this->getColLabel($column['field'], $fields, $langs);
  1957. if(!$showOrigin)
  1958. {
  1959. $colLabel = str_replace('{$field}', $colLabel, $this->lang->pivot->colLabel);
  1960. $colLabel = str_replace('{$stat}', zget($this->lang->pivot->stepDesign->statList, $stat), $colLabel);
  1961. if($showMode != 'default') $colLabel .= sprintf($this->lang->pivot->colShowMode, zget($this->lang->pivot->stepDesign->showModeList, $showMode));
  1962. }
  1963. $col->label = $colLabel;
  1964. $slice = zget($column, 'slice', 'noSlice');
  1965. $col->isSlice = $slice != 'noSlice';
  1966. if($slice != 'noSlice' && !$showOrigin)
  1967. {
  1968. if(!isset($cols[1]))
  1969. {
  1970. foreach($cols[0] as $colData) $colData->rowspan = '2';
  1971. $cols[1] = array();
  1972. }
  1973. $sliceList = array();
  1974. foreach($columnRows as $rows) $sliceList[$rows->{$slice}] = $rows->{$slice};
  1975. $optionList = $this->getSysOptions($fields[$slice]['type'], $fields[$slice]['object'], $fields[$slice]['field'], $sql, '', $driver);
  1976. foreach($sliceList as $field)
  1977. {
  1978. $childCol = new stdclass();
  1979. $childCol->name = $field;
  1980. $childCol->isGroup = false;
  1981. $childCol->label = isset($optionList[$field]) ? $optionList[$field] : $field;
  1982. $childCol->label = empty($childCol->label) ? $this->lang->pivot->empty : $childCol->label;
  1983. $childCol->colspan = $monopolize ? 2 : 1;
  1984. $childCol->isDrilling = $isDrilling;
  1985. $childCol->drillField = $drillField;
  1986. $childCol->condition = $condition;
  1987. $cols[1][] = $childCol;
  1988. }
  1989. $col->colspan = count($sliceList);
  1990. if($monopolize) $col->colspan *= 2;
  1991. if(zget($column, 'showTotal', 'noShow') !== 'noShow')
  1992. {
  1993. $childCol = new stdclass();
  1994. $childCol->name = 'sum';
  1995. $childCol->isGroup = false;
  1996. $childCol->label = $this->lang->pivot->stepDesign->total;
  1997. $childCol->colspan = $monopolize ? 2 : 1;
  1998. $childCol->isDrilling = $isDrilling;
  1999. $childCol->drillField = $drillField;
  2000. $childCol->condition = $condition;
  2001. $cols[1][] = $childCol;
  2002. $col->colspan += $childCol->colspan;
  2003. }
  2004. $cols[0][] = $col;
  2005. }
  2006. else
  2007. {
  2008. $col->rowspan = !isset($cols[1]) ? '1' : '2';
  2009. $col->colspan = $monopolize ? 2 : 1;
  2010. $cols[0][] = $col;
  2011. }
  2012. return $cols;
  2013. }
  2014. /**
  2015. * Implode group keys of record.
  2016. *
  2017. * @param array $groups
  2018. * @param object $record
  2019. * @access public
  2020. * @return string
  2021. */
  2022. public function getGroupsKey($groups, $record)
  2023. {
  2024. $groupsKey = array();
  2025. foreach($groups as $group) $groupsKey[] = is_scalar($record->$group) ? $record->$group : $record->$group['value'];
  2026. return implode('_', $groupsKey);
  2027. }
  2028. /**
  2029. * Get sys options.
  2030. *
  2031. * @param string $type
  2032. * @param string $object
  2033. * @param string $field
  2034. * @access public
  2035. * @return array
  2036. */
  2037. public function getSysOptions($type, $object = '', $field = '', $source = '', $saveAs = '', $driver = 'mysql')
  2038. {
  2039. if(in_array($type, $this->config->pivot->scopeOptionList)) return $this->bi->getScopeOptions($type);
  2040. if(!$field) return array();
  2041. $options = array();
  2042. switch($type)
  2043. {
  2044. case 'option':
  2045. $options = $this->bi->getDataviewOptions($object, $field);
  2046. break;
  2047. case 'object':
  2048. $options = $this->bi->getObjectOptions($object, $field);
  2049. break;
  2050. case 'string':
  2051. case 'number':
  2052. if($source)
  2053. {
  2054. if($this->config->edition != 'open')
  2055. {
  2056. $this->app->loadConfig('dataview');
  2057. static $workflowFields = array();
  2058. if(!isset($workflowFields[$object])) $workflowFields[$object] = $this->loadModel('workflowfield')->getList($object);
  2059. $originalField = zget($_POST, 'originalField', $field);
  2060. $fieldObject = zget($workflowFields[$object], $originalField, null);
  2061. if($fieldObject)
  2062. {
  2063. if($fieldObject->control == 'multi-select') $this->config->dataview->multipleMappingFields[] = $object . '-' . $field;
  2064. $options = $this->workflowfield->getFieldOptions($fieldObject);
  2065. }
  2066. if(!empty(array_filter($options))) break;
  2067. }
  2068. $options = array();
  2069. if(is_array($source))
  2070. {
  2071. foreach($source as $row) if(isset($row->$field)) $options["{$row->$field}"] = $row->$field;
  2072. }
  2073. else
  2074. {
  2075. $keyField = $field;
  2076. $valueField = $saveAs ? $saveAs : $field;
  2077. $options = $this->bi->getOptionsFromSql($source, $driver, $keyField, $valueField);
  2078. }
  2079. }
  2080. break;
  2081. }
  2082. if(is_string($source) && $source && $saveAs && in_array($type, array('user', 'product', 'project', 'execution', 'dept', 'option', 'object')))
  2083. {
  2084. $options = $this->bi->getOptionsFromSql($source, $driver, $field, $saveAs);
  2085. }
  2086. return array_filter($options);
  2087. }
  2088. /**
  2089. * 替换定义的表名。
  2090. * Replace defined table names.
  2091. *
  2092. * @param string $sql
  2093. * @access public
  2094. * @return string
  2095. */
  2096. public function replaceTableNames($sql)
  2097. {
  2098. if(preg_match_all("/TABLE_[A-Z]+/", $sql, $out))
  2099. {
  2100. rsort($out[0]);
  2101. foreach($out[0] as $table)
  2102. {
  2103. if(!defined($table)) continue;
  2104. $sql = str_replace($table, trim(constant($table), '`'), $sql);
  2105. }
  2106. }
  2107. return preg_replace("/= *'\!/U", "!='", $sql);
  2108. }
  2109. /**
  2110. * 设置默认的过滤器。
  2111. * Set default filter.
  2112. *
  2113. * @param array $filters
  2114. * @access private
  2115. * @return array
  2116. * @param bool $processDateVar
  2117. */
  2118. public function setFilterDefault($filters, $processDateVar = true)
  2119. {
  2120. foreach($filters as &$filter)
  2121. {
  2122. $isSelect = isset($filter['type']) && $filter['type'] === 'select';
  2123. if($isSelect && isset($filter['default']) && $filter['default'] === '0')
  2124. {
  2125. $filter['default'] = '';
  2126. }
  2127. if(!isset($filter['default']) || empty($filter['default'])) continue;
  2128. if($processDateVar && is_string($filter['default'])) $filter['default']= $this->processDateVar($filter['default']);
  2129. }
  2130. return $filters;
  2131. }
  2132. /**
  2133. * 根据透视表不同阶段获取不同状态的筛选器。
  2134. * Process filters.
  2135. *
  2136. * @param array $filters
  2137. * @param string $filterStatus
  2138. * @access public
  2139. * @return void
  2140. */
  2141. public function processFilters($filters, $filterStatus)
  2142. {
  2143. foreach($filters as $index => $filter)
  2144. {
  2145. if($filterStatus == 'published' && isset($filter['status']) && $filter['status'] == 'design')
  2146. {
  2147. unset($filters[$index]);
  2148. }
  2149. if($filterStatus == 'design')
  2150. {
  2151. if(isset($filter['status']) && $filter['status'] == 'design' && isset($filter['account']) && $filter['account'] == $this->app->user->account) continue;
  2152. unset($filters[$index]);
  2153. }
  2154. }
  2155. return array_values($filters);
  2156. }
  2157. /**
  2158. * 将筛选器的值填写到查询条件中。
  2159. * Set condition value with filters.
  2160. *
  2161. * @param array $condition
  2162. * @param array $filters
  2163. * @access public
  2164. * @return string
  2165. */
  2166. public function setConditionValueWithFilters($condition, $filters)
  2167. {
  2168. $field = $condition['queryField'];
  2169. if(!isset($filters[$field])) return '';
  2170. $filter = $filters[$field];
  2171. extract($filter);
  2172. return " $operator $value";
  2173. }
  2174. /**
  2175. * 从透视表对象中获取字段。
  2176. * Get fields from pivot object.
  2177. *
  2178. * @param object $pivot
  2179. * @param string $key
  2180. * @param mixed $default
  2181. * @param bool $jsonDecode
  2182. * @param bool $needArray
  2183. * @access private
  2184. * @return mixed
  2185. */
  2186. private function getFieldsFromPivot($pivot, $key, $default, $jsonDecode = false, $needArray = false)
  2187. {
  2188. return isset($pivot->{$key}) && !empty($pivot->{$key}) ? ($jsonDecode ? json_decode($pivot->{$key}, $needArray) : $pivot->{$key}) : $default;
  2189. }
  2190. /**
  2191. * Get field options.
  2192. *
  2193. * @param array $fieldSettings
  2194. * @param string $sql
  2195. * @access public
  2196. * @return array
  2197. *
  2198. * @param mixed[] $records
  2199. * @param string $driver
  2200. */
  2201. public function getFieldsOptions($fieldSettings, $records, $driver = 'mysql')
  2202. {
  2203. $options = array();
  2204. foreach($fieldSettings as $key => $fieldSetting)
  2205. {
  2206. $type = $fieldSetting['type'];
  2207. $object = $fieldSetting['object'];
  2208. $field = $fieldSetting['field'];
  2209. $options[$key] = $this->getSysOptions($type, $object, $field, $records, '', $driver);
  2210. }
  2211. return $options;
  2212. }
  2213. /**
  2214. * Process DTable cols config, let buildPivotTable use.
  2215. *
  2216. * @param array $cols
  2217. * @access public
  2218. * @return array
  2219. */
  2220. public function processDTableCols($cols)
  2221. {
  2222. $formatCols = array();
  2223. foreach($cols as $colField => $colInfo)
  2224. {
  2225. $formatCols[] = (object)array('name' => $colField, 'label' => $colInfo['title'], 'colspan' => 1);
  2226. }
  2227. return array($formatCols);
  2228. }
  2229. /**
  2230. * Process DTable data, let buildPivotTable use.
  2231. *
  2232. * @param array $cols
  2233. * @param array $datas
  2234. * @access public
  2235. * @return array
  2236. */
  2237. public function processDTableData($cols, $datas)
  2238. {
  2239. return array_map(function($data) use ($cols)
  2240. {
  2241. $result = [];
  2242. $data = (array)$data;
  2243. foreach ($cols as $field) $result[] = isset($data[$field]) ? $data[$field] : '';
  2244. return $result;
  2245. }, $datas);
  2246. }
  2247. /**
  2248. * Build table use data and rowspan.
  2249. *
  2250. * @param object $data
  2251. * @param array $configs
  2252. * @access public
  2253. * @return string
  2254. *
  2255. */
  2256. public function buildPivotTable($data, $configs)
  2257. {
  2258. $width = 128;
  2259. $nowSpan = 1;
  2260. $inFlow = false;
  2261. if(!empty($configs))
  2262. {
  2263. /* 处理不需要展示的单元格,设置为0 */
  2264. $columnCount = count(current($configs));
  2265. $lineCount = count($configs);
  2266. for($i = 0; $i < $columnCount; $i ++)
  2267. {
  2268. for($j = 0; $j < $lineCount; $j ++)
  2269. {
  2270. if($configs[$j][$i] > 1 && !$inFlow)
  2271. {
  2272. $inFlow = true;
  2273. $nowSpan = $configs[$j][$i];
  2274. continue;
  2275. }
  2276. if($configs[$j][$i] > 1 && $inFlow)
  2277. {
  2278. $configs[$j][$i] = 0;
  2279. $nowSpan --;
  2280. if($nowSpan == 1) $inFlow = false;
  2281. }
  2282. }
  2283. }
  2284. }
  2285. /* Init table. */
  2286. $table = "<div class='reportData'><table class='table table-condensed table-striped table-bordered table-fixed datatable' style='width: auto; min-width: 100%' data-fixed-left-width='400'>";
  2287. $showOrigins = array();
  2288. $hasShowOrigin = false;
  2289. foreach($data->cols[0] as $col)
  2290. {
  2291. $colspan = zget($col, 'colspan', 1);
  2292. $showOrigin = isset($col->showOrigin) ? $col->showOrigin : false;
  2293. $colShowOrigin = array_fill(0, $colspan, $showOrigin);
  2294. $showOrigins = array_merge($showOrigins, $colShowOrigin);
  2295. if($showOrigin) $hasShowOrigin = true;
  2296. }
  2297. /* Init table thead. */
  2298. $table .= "<thead>";
  2299. foreach($data->cols as $lineCols)
  2300. {
  2301. $table .= "<tr>";
  2302. foreach($lineCols as $col)
  2303. {
  2304. $thName = $col->label;
  2305. $colspan = zget($col, 'colspan', 1);
  2306. $rowspan = zget($col, 'rowspan', 1);
  2307. $isGroup = zget($col, 'isGroup', false);
  2308. if($isGroup) $thHtml = "<th data-flex='false' rowspan='$rowspan' colspan='$colspan' data-width='auto' class='text-center'>$thName</th>";
  2309. else $thHtml = "<th data-flex='true' rowspan='$rowspan' colspan='$colspan' data-type='number' data-width=$width class='text-center'>$thName</th>";
  2310. $table .= $thHtml;
  2311. }
  2312. $table .= "</tr>";
  2313. }
  2314. $table .= "</thead>";
  2315. /* Init table tbody. */
  2316. $table .= "<tbody>";
  2317. $rowCount = 0;
  2318. $showAllTotal = isset($data->showAllTotal) && $data->showAllTotal;
  2319. $users = $this->loadModel('user')->getPairs('noletter,noempty,noclosed');
  2320. for($i = 0; $i < count($data->array); $i ++)
  2321. {
  2322. $rowCount ++;
  2323. if($showAllTotal && $rowCount == count($data->array)) continue;
  2324. $line = array_values($data->array[$i]);
  2325. $table .= "<tr class='text-center'>";
  2326. for($j = 0; $j < count($line); $j ++)
  2327. {
  2328. $cols = isset($data->cols[0][$j]) ? $data->cols[0][$j] : array();
  2329. $isGroup = !empty($data->cols[0][$j]->isGroup) ? $data->cols[0][$j]->isGroup : false;
  2330. $rowspan = isset($configs[$i][$j]) ? $configs[$i][$j] : 1;
  2331. $hidden = (isset($configs[$i][$j]) && $configs[$i][$j]) ? false : (bool)$isGroup;
  2332. $showOrigin = $showOrigins[$j];
  2333. if($hasShowOrigin && !$isGroup && !$showOrigin)
  2334. {
  2335. $rowspan = isset($configs[$i]) ? end($configs[$i]) : 1;
  2336. $hidden = isset($configs[$i]) ? false : true;
  2337. }
  2338. $lineValue = $line[$j];
  2339. if(is_numeric($lineValue)) $lineValue = round($lineValue, 2);
  2340. if(isset($cols->name) && in_array($cols->name, $this->config->pivot->userFields)) $lineValue = isset($users[$lineValue]) ? $users[$lineValue] : $lineValue;
  2341. if(!$hidden) $table .= "<td rowspan='$rowspan'>$lineValue</td>";
  2342. }
  2343. $table .= "</tr>";
  2344. }
  2345. if($showAllTotal && !empty($data->array))
  2346. {
  2347. $table .= "<tr class='text-center'>";
  2348. $table .= "<td colspan='" . count($data->groups) . "'>{$this->lang->pivot->stepDesign->total}</td>";
  2349. foreach(end($data->array) as $field => $total)
  2350. {
  2351. if(in_array($field, $data->groups)) continue;
  2352. if(is_numeric($total)) $total = round($total, 2);
  2353. $table .= "<td>$total</td>";
  2354. }
  2355. $table .= "</tr>";
  2356. }
  2357. $table .= "</tbody>";
  2358. $table .= "</table></div>";
  2359. return $table;
  2360. }
  2361. /* Data Drill */
  2362. /**
  2363. * Get cols for preview data table.
  2364. *
  2365. * @param string $objectTable
  2366. * @access public
  2367. * @return array
  2368. */
  2369. public function getDrillCols($object)
  2370. {
  2371. if($object == 'case') $object = 'testcase';
  2372. $cols = array();
  2373. if(isset($this->config->pivot->drillObjectFields[$object]))
  2374. {
  2375. $this->loadModel($object);
  2376. if(!isset($this->config->$object->dtable->fieldList)) return $this->config->pivot->objectTableFields->$object;
  2377. $fieldList = $object == 'product' ? $this->config->product->all->dtable->fieldList : $this->config->$object->dtable->fieldList;
  2378. $userTypeCols = $this->config->pivot->userTypeCols;
  2379. $nameTypeCols = $this->config->pivot->nameTypeCols;
  2380. $reuseDtableFields = $this->config->pivot->reuseDtableFields;
  2381. foreach($this->config->pivot->drillObjectFields[$object] as $fieldKey)
  2382. {
  2383. $fieldSetting = isset($fieldList[$fieldKey]) ? $fieldList[$fieldKey] : $this->config->pivot->objectTableFields->$object[$fieldKey];
  2384. $fieldSetting['sortType'] = false;
  2385. if(isset($fieldSetting['checkbox']) && $fieldSetting['checkbox']) $fieldSetting['checkbox'] = false;
  2386. if(isset($fieldSetting['link']))
  2387. {
  2388. if(is_string($fieldSetting['link']))
  2389. {
  2390. $fieldSettingLink = $fieldSetting['link'];
  2391. $fieldSetting['link'] = array();
  2392. $fieldSetting['link']['url'] = $fieldSettingLink;
  2393. }
  2394. $fieldSetting['link']['target'] = '_blank';
  2395. }
  2396. if(isset($fieldSetting['type']) && in_array($fieldSetting['type'], $userTypeCols)) $fieldSetting['type'] = 'user';
  2397. foreach(array_keys($fieldSetting) as $settingKey)
  2398. {
  2399. if(!in_array($settingKey, $reuseDtableFields)) unset($fieldSetting[$settingKey]);
  2400. if((!in_array($fieldKey, $nameTypeCols) && $settingKey == 'link') || $object == 'doc') unset($fieldSetting['link']);
  2401. if(isset($this->config->pivot->objectTableFields->$object[$fieldKey][$settingKey])) $fieldSetting[$settingKey] = $this->config->pivot->objectTableFields->$object[$fieldKey][$settingKey];
  2402. }
  2403. $cols[$fieldKey] = $fieldSetting;
  2404. }
  2405. }
  2406. else
  2407. {
  2408. $this->app->loadLang($object);
  2409. $table = isset($this->config->objectTables[$object]) ? $this->config->objectTables[$object] : $this->config->db->prefix . $object;
  2410. $table = str_replace('`', '', $table);
  2411. $fieldList = $this->loadModel('dev')->getFields($table);
  2412. foreach($fieldList as $fieldName => $field)
  2413. {
  2414. if(empty($field['name'])) continue;
  2415. $fieldLabel = $field['name'];
  2416. if(isset($this->lang->$object->$fieldName)) $fieldLabel = $this->lang->$object->$fieldName;
  2417. $cols[$fieldName] = array('name' => $fieldName, 'title' => $fieldLabel);
  2418. }
  2419. }
  2420. return $cols;
  2421. }
  2422. /**
  2423. * getReferSQL
  2424. *
  2425. * @param string $object
  2426. * @param string $whereSQL
  2427. * @param array $fields
  2428. * @access public
  2429. * @return string
  2430. */
  2431. public function getReferSQL($object, $whereSQL = '', $fields = array())
  2432. {
  2433. $fieldStr = empty($fields) ? '' : (',' . implode(',', $fields));
  2434. $table = $this->config->db->prefix . $object;
  2435. $referSQL = "SELECT t1.* {$fieldStr} FROM $table AS t1";
  2436. return "$referSQL $whereSQL";
  2437. }
  2438. /**
  2439. * Get drill sql.
  2440. *
  2441. * @param string $objectTable
  2442. * @param string $whereSQL
  2443. * @param string $conditionsSQL
  2444. * @access public
  2445. * @return string
  2446. */
  2447. public function getDrillSQL($objectTable, $whereSQL = '', $conditions = array())
  2448. {
  2449. $fieldList = array();
  2450. $conditionSQLs = array('1=1');
  2451. foreach($conditions as $condition)
  2452. {
  2453. extract($condition);
  2454. if($drillAlias != 't1')
  2455. {
  2456. $fieldList[] = "{$drillAlias}.{$drillField} AS {$drillAlias}{$drillField}";
  2457. $drillField = $drillAlias . $drillField;
  2458. }
  2459. if(!empty($condition['value']))
  2460. {
  2461. if(!empty($condition['htmlspecialed'])) $value .= " OR t1.{$drillField} = {$condition['htmlspecialed']}";
  2462. $conditionSQLs[] = "(t1.{$drillField}{$value})";
  2463. }
  2464. }
  2465. $referSQL = $this->getReferSQL($objectTable, $whereSQL, $fieldList);
  2466. $conditionSQL = 'WHERE ' . implode(' AND ', $conditionSQLs);
  2467. return "SELECT t1.* FROM ($referSQL) AS t1 {$conditionSQL}";
  2468. }
  2469. /**
  2470. * Execute drill sql.
  2471. *
  2472. * @param string $object
  2473. * @param string $drillSQL
  2474. * @access public
  2475. * @return array
  2476. */
  2477. public function execDrillSQL($object, $drillSQL, $limit = 10)
  2478. {
  2479. $limitSQL = "SELECT * FROM ($drillSQL) AS t1 LIMIT $limit";
  2480. $queryResult = $this->loadModel('bi')->querySQL($drillSQL, $limitSQL);
  2481. $result = array();
  2482. if($queryResult['result'] == 'success')
  2483. {
  2484. $result['data'] = $queryResult['rows'];
  2485. $result['cols'] = $this->getDrillCols($object);
  2486. }
  2487. if($queryResult['result'] == 'fail') $result['error'] = $queryResult['message'];
  2488. $result['status'] = $queryResult['result'];
  2489. return $result;
  2490. }
  2491. /**
  2492. * Parse query filter, then get drill result.
  2493. *
  2494. * @param string $object
  2495. * @param string $whereSQL
  2496. * @param array $filters
  2497. * @param array $conditions
  2498. * @param bool $emptyFilters
  2499. * @param int $limit
  2500. * @access public
  2501. * @return array
  2502. */
  2503. public function getDrillResult($object, $whereSQL, $filters = array(), $conditions = array(), $emptyFilters = true, $limit = 10)
  2504. {
  2505. $drillSQL = $this->getDrillSQL($object, $whereSQL, $conditions);
  2506. if(!empty($filters)) $drillSQL = $this->loadModel('bi')->processVars($drillSQL, $filters, $emptyFilters);
  2507. return $this->execDrillSQL($object, $drillSQL, $limit);
  2508. }
  2509. /**
  2510. * Get drill datas.
  2511. *
  2512. * @param object $pivotState
  2513. * @param object $drill
  2514. * @param array $conditions
  2515. * @param array $filterValues
  2516. * @access public
  2517. * @return array
  2518. */
  2519. public function getDrillDatas($pivotState, $drill, $conditions, $filterValues = array())
  2520. {
  2521. $filters = $pivotState->setFiltersDefaultValue($filterValues);
  2522. foreach($conditions as $index => $condition)
  2523. {
  2524. if(isset($condition['value']))
  2525. {
  2526. $htmlspecialed = htmlspecialchars($condition['value']);
  2527. $conditions[$index]['value'] = " = " . $this->dbh->quote($condition['value']);
  2528. if($htmlspecialed != $condition['value']) $conditions[$index]['htmlspecialed'] = $this->dbh->quote($htmlspecialed);
  2529. }
  2530. }
  2531. $data = array();
  2532. $status = null;
  2533. if($pivotState->isQueryFilter())
  2534. {
  2535. $queryResult = $this->getDrillResult($drill->object, $drill->whereSql, $filters, $conditions, false, 999999);
  2536. $data = $queryResult['data'];
  2537. $status = $queryResult['status'];
  2538. }
  2539. else
  2540. {
  2541. $filters = $pivotState->convertFiltersToWhere($filters);
  2542. foreach($conditions as $index => $condition)
  2543. {
  2544. if(!isset($condition['value'])) $conditions[$index]['value'] = $this->setConditionValueWithFilters($condition, $filters);
  2545. }
  2546. $drillSQL = $this->getDrillSQL($drill->object, $drill->whereSql, $conditions);
  2547. $queryResult = $this->loadModel('bi')->querySQL($drillSQL, $drillSQL);
  2548. $data = $queryResult['rows'];
  2549. $status = $queryResult['result'];
  2550. }
  2551. if($status != 'success') return array();
  2552. return $data;
  2553. }
  2554. /**
  2555. * Process task datas in Drill modal.
  2556. *
  2557. * @param array $datas
  2558. * @access public
  2559. * @return array
  2560. * @param string $object
  2561. */
  2562. public function processKanbanDatas($object, $datas)
  2563. {
  2564. $kanbans = $this->dao->select('id')->from(TABLE_PROJECT)->where('type')->eq('kanban')->fetchPairs();
  2565. if($object == 'story') $projectStory = $this->dao->select('story, project')->from(TABLE_PROJECTSTORY)->fetchPairs();
  2566. foreach($datas as $data)
  2567. {
  2568. $projectID = 0;
  2569. if($object == 'story')
  2570. {
  2571. $projectID = isset($projectStory[$data->id]) ? $projectStory[$data->id] : 0;
  2572. }
  2573. else
  2574. {
  2575. $projectID = zget($data, 'execution', 0);
  2576. }
  2577. if($projectID && isset($kanbans[$projectID])) $data->isModal = true;
  2578. }
  2579. return $datas;
  2580. }
  2581. /**
  2582. * Get versions of a pivot.
  2583. *
  2584. * @param int $pivotID
  2585. * @access public
  2586. * @return array|bool
  2587. */
  2588. public function getPivotVersions($pivotID)
  2589. {
  2590. $pivot = $this->dao->select('*')->from(TABLE_PIVOT)->where('id')->eq($pivotID)->andWhere('deleted')->eq('0')->fetch();
  2591. if(!$pivot) return false;
  2592. $pivotSpecList = $this->dao->select('*')->from(TABLE_PIVOTSPEC)->where('pivot')->eq($pivotID)->fetchAll('', false);
  2593. if(!$pivotSpecList) return false;
  2594. $pivotVersionList = array();
  2595. foreach($pivotSpecList as $specData)
  2596. {
  2597. $pivotVersion = clone $pivot;
  2598. foreach($specData as $specKey => $specValue) $pivotVersion->$specKey = $specValue;
  2599. $this->processNameDesc($pivotVersion);
  2600. $pivotVersionList[] = $pivotVersion;
  2601. }
  2602. return $pivotVersionList;
  2603. }
  2604. /**
  2605. * Get max version.
  2606. *
  2607. * @param int $pivotID
  2608. * @access public
  2609. * @return string
  2610. */
  2611. public function getMaxVersion($pivotID)
  2612. {
  2613. $versions = $this->dao->select('version')->from(TABLE_PIVOTSPEC)->where('pivot')->eq($pivotID)->fetchPairs();
  2614. $maxVersion = current($versions);
  2615. foreach($versions as $version)
  2616. {
  2617. if(version_compare($version, $maxVersion, '>')) $maxVersion = $version;
  2618. }
  2619. return $maxVersion;
  2620. }
  2621. /**
  2622. * Get max version by idList.
  2623. *
  2624. * @param string|array $pivotIDList
  2625. * @access public
  2626. * @return string
  2627. */
  2628. public function getMaxVersionByIDList($pivotIDList)
  2629. {
  2630. $pivotVersions = $this->dao->select('pivot,version')->from(TABLE_PIVOTSPEC)
  2631. ->where('pivot')->in($pivotIDList)
  2632. ->fetchGroup('pivot', 'version');
  2633. if(empty($pivotVersions)) return array();
  2634. $pivotMaxVersion = array();
  2635. foreach($pivotVersions as $pivotID => $versions)
  2636. {
  2637. $versions = array_keys($versions);
  2638. $maxVersion = current($versions);
  2639. foreach($versions as $version)
  2640. {
  2641. if(version_compare($version, $maxVersion, '>')) $maxVersion = $version;
  2642. }
  2643. $pivotMaxVersion[$pivotID] = $maxVersion;
  2644. }
  2645. return $pivotMaxVersion;
  2646. }
  2647. /**
  2648. * @param mixed[]|object $pivots
  2649. * @param bool $isObject
  2650. */
  2651. public function isVersionChange($pivots, $isObject = true)
  2652. {
  2653. if($isObject) $pivots = array($pivots);
  2654. $pivotMaxVersion = $this->getMaxVersionByIDList(array_column($pivots, 'id'));
  2655. foreach($pivots as $index => $pivot)
  2656. {
  2657. $maxVersion = zget($pivotMaxVersion, $pivot->id, '');
  2658. $pivots[$index]->versionChange = $maxVersion != $pivot->version && $pivot->builtin == 1;
  2659. }
  2660. return $isObject ? current($pivots) : $pivots;
  2661. }
  2662. /**
  2663. * Switch pivot to a new version.
  2664. *
  2665. * @param int $pivotID
  2666. * @param string $version
  2667. * @access public
  2668. * @return bool
  2669. */
  2670. public function switchNewVersion($pivotID, $version)
  2671. {
  2672. $this->dao->update(TABLE_PIVOT)->set('version')->eq($version)->where('id')->eq($pivotID)->exec();
  2673. return !dao::isError();
  2674. }
  2675. /**
  2676. * Filter special chars in query data.
  2677. *
  2678. * @param array $records
  2679. * @access public
  2680. * @return array
  2681. */
  2682. public function filterSpecialChars($records)
  2683. {
  2684. if(empty($records)) return $records;
  2685. foreach($records as $index => $record)
  2686. {
  2687. foreach($record as $field => $value)
  2688. {
  2689. $value = is_string($value) ? str_replace('"', '', htmlspecialchars_decode($value)) : $value;
  2690. if(is_object($record)) $record->$field = $value;
  2691. if(is_array($record)) $record[$field] = $value;
  2692. }
  2693. $records[$index] = $record;
  2694. }
  2695. return $records;
  2696. }
  2697. }
  2698. /**
  2699. * Sort summary
  2700. *
  2701. * @param array $pre
  2702. * @param array $next
  2703. * @access public
  2704. * @return mixed
  2705. */
  2706. function sortSummary($pre, $next)
  2707. {
  2708. if($pre['validRate'] == $next['validRate']) return 0;
  2709. return $pre['validRate'] > $next['validRate'] ? -1 : 1;
  2710. }