@things-factory/kpi 9.0.31 → 9.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +1 -2
  2. package/client/charts/kpi-boxplot-chart.ts +182 -42
  3. package/client/charts/kpi-radar-chart.ts +9 -9
  4. package/client/pages/kpi/kpi-list-page.ts +196 -32
  5. package/client/pages/kpi/kpi-overview.ts +9 -11
  6. package/client/pages/kpi/kpi-tree-page.ts +409 -0
  7. package/client/pages/kpi/kpi-view.ts +187 -0
  8. package/client/pages/kpi-dashboard/cards/kpi-level1-card.ts +1 -1
  9. package/client/pages/kpi-dashboard/cards/kpi-level2-comparison.ts +1 -1
  10. package/client/pages/kpi-dashboard/cards/kpi-level3-comparison.ts +1 -1
  11. package/client/pages/kpi-dashboard/components/kpi-left-panel.ts +198 -160
  12. package/client/pages/kpi-dashboard/components/kpi-map-panel.ts +133 -0
  13. package/client/pages/kpi-dashboard/components/kpi-region-popup.ts +3 -2
  14. package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +291 -48
  15. package/client/pages/kpi-dashboard/kpi-dashboard.ts +28 -30
  16. package/client/pages/kpi-history/kpi-history-list-page.ts +11 -11
  17. package/client/pages/kpi-metric/kpi-metric-list-page.ts +10 -2
  18. package/client/pages/kpi-metric-value/kpi-metric-value-editor-page.ts +7 -7
  19. package/client/pages/kpi-metric-value/kpi-metric-value-importer.ts +2 -2
  20. package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +16 -8
  21. package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.ts +5 -5
  22. package/client/pages/kpi-statistic/kpi-statistic-editor-page.ts +1 -2
  23. package/client/pages/kpi-statistic/kpi-statistic-list-page.ts +10 -2
  24. package/client/pages/kpi-value/kpi-value-editor-page.ts +11 -7
  25. package/client/pages/kpi-value/kpi-value-list-page.ts +31 -7
  26. package/client/route.ts +2 -9
  27. package/design-entities.md +8 -12
  28. package/dist-client/charts/kpi-boxplot-chart.d.ts +2 -0
  29. package/dist-client/charts/kpi-boxplot-chart.js +168 -42
  30. package/dist-client/charts/kpi-boxplot-chart.js.map +1 -1
  31. package/dist-client/charts/kpi-radar-chart.js +9 -9
  32. package/dist-client/charts/kpi-radar-chart.js.map +1 -1
  33. package/dist-client/pages/kpi/kpi-list-page.d.ts +19 -3
  34. package/dist-client/pages/kpi/kpi-list-page.js +188 -32
  35. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  36. package/dist-client/pages/kpi/kpi-overview.js +9 -11
  37. package/dist-client/pages/kpi/kpi-overview.js.map +1 -1
  38. package/dist-client/pages/kpi/kpi-tree-page.d.ts +59 -0
  39. package/dist-client/pages/kpi/kpi-tree-page.js +403 -0
  40. package/dist-client/pages/kpi/kpi-tree-page.js.map +1 -0
  41. package/dist-client/pages/kpi/kpi-view.d.ts +12 -0
  42. package/dist-client/pages/kpi/kpi-view.js +191 -0
  43. package/dist-client/pages/kpi/kpi-view.js.map +1 -0
  44. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +1 -1
  45. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -1
  46. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +1 -1
  47. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -1
  48. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +1 -1
  49. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -1
  50. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +3 -1
  51. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +197 -161
  52. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -1
  53. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +5 -0
  54. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +146 -0
  55. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -1
  56. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +3 -2
  57. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -1
  58. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +3 -1
  59. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +268 -46
  60. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -1
  61. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +28 -30
  62. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  63. package/dist-client/pages/kpi-history/kpi-history-list-page.d.ts +6 -1
  64. package/dist-client/pages/kpi-history/kpi-history-list-page.js +11 -11
  65. package/dist-client/pages/kpi-history/kpi-history-list-page.js.map +1 -1
  66. package/dist-client/pages/kpi-metric/kpi-metric-list-page.d.ts +5 -0
  67. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +10 -2
  68. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
  69. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +1 -1
  70. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +8 -8
  71. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -1
  72. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js +2 -2
  73. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js.map +1 -1
  74. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +5 -0
  75. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +16 -8
  76. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  77. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.d.ts +1 -1
  78. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js +6 -6
  79. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -1
  80. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js +1 -2
  81. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js.map +1 -1
  82. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.d.ts +5 -0
  83. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js +10 -2
  84. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js.map +1 -1
  85. package/dist-client/pages/kpi-value/kpi-value-editor-page.d.ts +2 -1
  86. package/dist-client/pages/kpi-value/kpi-value-editor-page.js +16 -8
  87. package/dist-client/pages/kpi-value/kpi-value-editor-page.js.map +1 -1
  88. package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +5 -0
  89. package/dist-client/pages/kpi-value/kpi-value-list-page.js +31 -7
  90. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  91. package/dist-client/route.d.ts +1 -1
  92. package/dist-client/route.js +2 -8
  93. package/dist-client/route.js.map +1 -1
  94. package/dist-client/tsconfig.tsbuildinfo +1 -1
  95. package/dist-server/controllers/kpi-metric-value-provider.d.ts +1 -1
  96. package/dist-server/controllers/kpi-metric-value-provider.js +4 -4
  97. package/dist-server/controllers/kpi-metric-value-provider.js.map +1 -1
  98. package/dist-server/controllers/kpi-value-provider.d.ts +1 -1
  99. package/dist-server/controllers/kpi-value-provider.js +3 -3
  100. package/dist-server/controllers/kpi-value-provider.js.map +1 -1
  101. package/dist-server/migrations/1752190849680-seed-kpi-metrics.d.ts +6 -0
  102. package/dist-server/migrations/1752190849680-seed-kpi-metrics.js +101 -0
  103. package/dist-server/migrations/1752190849680-seed-kpi-metrics.js.map +1 -0
  104. package/dist-server/migrations/1752190849681-seed-kpi.d.ts +5 -0
  105. package/dist-server/migrations/1752190849681-seed-kpi.js +315 -0
  106. package/dist-server/migrations/1752190849681-seed-kpi.js.map +1 -0
  107. package/dist-server/migrations/1752192090123-add-grades-to-kpi.d.ts +7 -0
  108. package/dist-server/migrations/1752192090123-add-grades-to-kpi.js +51 -0
  109. package/dist-server/migrations/1752192090123-add-grades-to-kpi.js.map +1 -0
  110. package/dist-server/migrations/1752192090124-add-kpi-statistics.d.ts +5 -0
  111. package/dist-server/migrations/1752192090124-add-kpi-statistics.js +710 -0
  112. package/dist-server/migrations/1752192090124-add-kpi-statistics.js.map +1 -0
  113. package/dist-server/migrations/1752192090128-seed-kpi-org-scope.d.ts +6 -0
  114. package/dist-server/migrations/1752192090128-seed-kpi-org-scope.js +111 -0
  115. package/dist-server/migrations/1752192090128-seed-kpi-org-scope.js.map +1 -0
  116. package/dist-server/migrations/1752192090129-seed-kpi-values.d.ts +6 -0
  117. package/dist-server/migrations/1752192090129-seed-kpi-values.js +187 -0
  118. package/dist-server/migrations/1752192090129-seed-kpi-values.js.map +1 -0
  119. package/dist-server/migrations/grade-data/x11-performance-table.json +962 -0
  120. package/dist-server/migrations/grade-data/x12-performance-table.json +611 -0
  121. package/dist-server/migrations/grade-data/x14-performance-table.json +42 -0
  122. package/dist-server/migrations/grade-data/x21-performance-table.json +889 -0
  123. package/dist-server/migrations/grade-data/x22-performance-table.json +1064 -0
  124. package/dist-server/migrations/grade-data/x23-performance-table.json +42 -0
  125. package/dist-server/migrations/grade-data/x31-performance-table.json +644 -0
  126. package/dist-server/migrations/grade-data/x32-performance-table.json +993 -0
  127. package/dist-server/migrations/grade-data/x33-performance-table.json +195 -0
  128. package/dist-server/migrations/grade-data/x34-performance-table.json +12 -0
  129. package/dist-server/migrations/grade-data/x35-performance-table.json +42 -0
  130. package/dist-server/migrations/grade-data/x41-performance-table.json +825 -0
  131. package/dist-server/migrations/grade-data/x42-performance-table.json +786 -0
  132. package/dist-server/migrations/grade-data/x43-performance-table.json +12 -0
  133. package/dist-server/migrations/grade-data/x44-performance-table.json +42 -0
  134. package/dist-server/migrations/grade-data/x51-performance-table.json +924 -0
  135. package/dist-server/migrations/grade-data/x52-performance-table.json +42 -0
  136. package/dist-server/migrations/grade-data/x61-performance-table.json +261 -0
  137. package/dist-server/migrations/grade-data/x62-performance-table.json +42 -0
  138. package/dist-server/migrations/seed-data/kpi-metrics-seed.json +454 -0
  139. package/dist-server/migrations/seed-data/kpi-org-scope-seed.json +1676 -0
  140. package/dist-server/migrations/seed-data/kpi-scopes-seed.json +121 -0
  141. package/dist-server/migrations/seed-data/kpi-values-seed.json +402 -0
  142. package/dist-server/migrations/seed-data/kpis-seed.json +488 -0
  143. package/dist-server/migrations/seed-data/scope-definitions-seed.json +90 -0
  144. package/dist-server/service/index.d.ts +4 -7
  145. package/dist-server/service/index.js +10 -13
  146. package/dist-server/service/index.js.map +1 -1
  147. package/dist-server/service/kpi/aggregate-kpi.js +30 -13
  148. package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
  149. package/dist-server/service/kpi/kpi-formula.service.d.ts +15 -0
  150. package/dist-server/service/kpi/kpi-formula.service.js +90 -0
  151. package/dist-server/service/kpi/kpi-formula.service.js.map +1 -1
  152. package/dist-server/service/kpi/kpi-history.d.ts +0 -3
  153. package/dist-server/service/kpi/kpi-history.js +0 -10
  154. package/dist-server/service/kpi/kpi-history.js.map +1 -1
  155. package/dist-server/service/kpi/kpi-mutation.d.ts +1 -1
  156. package/dist-server/service/kpi/kpi-mutation.js +57 -20
  157. package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
  158. package/dist-server/service/kpi/kpi-query.d.ts +7 -3
  159. package/dist-server/service/kpi/kpi-query.js +126 -10
  160. package/dist-server/service/kpi/kpi-query.js.map +1 -1
  161. package/dist-server/service/kpi/kpi-type.d.ts +4 -2
  162. package/dist-server/service/kpi/kpi-type.js +12 -4
  163. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  164. package/dist-server/service/kpi/kpi.d.ts +4 -3
  165. package/dist-server/service/kpi/kpi.js +20 -8
  166. package/dist-server/service/kpi/kpi.js.map +1 -1
  167. package/dist-server/service/kpi-metric/aggregate-kpi-metric.js +46 -11
  168. package/dist-server/service/kpi-metric/aggregate-kpi-metric.js.map +1 -1
  169. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +1 -1
  170. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +6 -6
  171. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  172. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.d.ts +2 -2
  173. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js +4 -4
  174. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js.map +1 -1
  175. package/dist-server/service/kpi-metric-value/kpi-metric-value.d.ts +1 -1
  176. package/dist-server/service/kpi-metric-value/kpi-metric-value.js +3 -3
  177. package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -1
  178. package/dist-server/service/kpi-org-scope/index.d.ts +5 -0
  179. package/dist-server/service/kpi-org-scope/index.js +9 -0
  180. package/dist-server/service/kpi-org-scope/index.js.map +1 -0
  181. package/dist-server/service/kpi-org-scope/kpi-org-scope-mutation.d.ts +8 -0
  182. package/dist-server/service/kpi-org-scope/kpi-org-scope-mutation.js +170 -0
  183. package/dist-server/service/kpi-org-scope/kpi-org-scope-mutation.js.map +1 -0
  184. package/dist-server/service/kpi-org-scope/kpi-org-scope-query.d.ts +14 -0
  185. package/dist-server/service/kpi-org-scope/kpi-org-scope-query.js +152 -0
  186. package/dist-server/service/kpi-org-scope/kpi-org-scope-query.js.map +1 -0
  187. package/dist-server/service/kpi-org-scope/kpi-org-scope-type.d.ts +26 -0
  188. package/dist-server/service/kpi-org-scope/kpi-org-scope-type.js +101 -0
  189. package/dist-server/service/kpi-org-scope/kpi-org-scope-type.js.map +1 -0
  190. package/dist-server/service/kpi-org-scope/kpi-org-scope.d.ts +26 -0
  191. package/dist-server/service/kpi-org-scope/kpi-org-scope.js +135 -0
  192. package/dist-server/service/kpi-org-scope/kpi-org-scope.js.map +1 -0
  193. package/dist-server/service/kpi-scope/index.d.ts +9 -0
  194. package/dist-server/service/kpi-scope/index.js +14 -0
  195. package/dist-server/service/kpi-scope/index.js.map +1 -0
  196. package/dist-server/service/kpi-scope/kpi-scope-mutation.d.ts +9 -0
  197. package/dist-server/service/kpi-scope/kpi-scope-mutation.js +135 -0
  198. package/dist-server/service/kpi-scope/kpi-scope-mutation.js.map +1 -0
  199. package/dist-server/service/kpi-scope/kpi-scope-query.d.ts +11 -0
  200. package/dist-server/service/kpi-scope/kpi-scope-query.js +89 -0
  201. package/dist-server/service/kpi-scope/kpi-scope-query.js.map +1 -0
  202. package/dist-server/service/kpi-scope/kpi-scope-type.d.ts +35 -0
  203. package/dist-server/service/kpi-scope/kpi-scope-type.js +138 -0
  204. package/dist-server/service/kpi-scope/kpi-scope-type.js.map +1 -0
  205. package/dist-server/service/kpi-scope/kpi-scope.d.ts +38 -0
  206. package/dist-server/service/kpi-scope/kpi-scope.js +144 -0
  207. package/dist-server/service/kpi-scope/kpi-scope.js.map +1 -0
  208. package/dist-server/service/kpi-statistic/kpi-statistic-batch.service.d.ts +43 -0
  209. package/dist-server/service/kpi-statistic/kpi-statistic-batch.service.js +181 -0
  210. package/dist-server/service/kpi-statistic/kpi-statistic-batch.service.js.map +1 -0
  211. package/dist-server/service/kpi-statistic/kpi-statistic-calculation.service.d.ts +50 -0
  212. package/dist-server/service/kpi-statistic/kpi-statistic-calculation.service.js +324 -0
  213. package/dist-server/service/kpi-statistic/kpi-statistic-calculation.service.js.map +1 -0
  214. package/dist-server/service/kpi-statistic/kpi-statistic-mutation.d.ts +4 -0
  215. package/dist-server/service/kpi-statistic/kpi-statistic-mutation.js +76 -0
  216. package/dist-server/service/kpi-statistic/kpi-statistic-mutation.js.map +1 -1
  217. package/dist-server/service/kpi-statistic/kpi-statistic-query.d.ts +5 -1
  218. package/dist-server/service/kpi-statistic/kpi-statistic-query.js +92 -1
  219. package/dist-server/service/kpi-statistic/kpi-statistic-query.js.map +1 -1
  220. package/dist-server/service/kpi-statistic/kpi-statistic.d.ts +4 -0
  221. package/dist-server/service/kpi-statistic/kpi-statistic.js +33 -0
  222. package/dist-server/service/kpi-statistic/kpi-statistic.js.map +1 -1
  223. package/dist-server/service/kpi-value/kpi-value-mutation.js +71 -7
  224. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  225. package/dist-server/service/kpi-value/kpi-value-type.d.ts +4 -2
  226. package/dist-server/service/kpi-value/kpi-value-type.js +12 -4
  227. package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
  228. package/dist-server/service/kpi-value/kpi-value.d.ts +3 -1
  229. package/dist-server/service/kpi-value/kpi-value.js +11 -5
  230. package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
  231. package/dist-server/service/utils/value-date-util.d.ts +1 -0
  232. package/dist-server/service/utils/value-date-util.js +41 -0
  233. package/dist-server/service/utils/value-date-util.js.map +1 -1
  234. package/dist-server/tsconfig.json +10 -0
  235. package/dist-server/tsconfig.tsbuildinfo +1 -1
  236. package/package.json +7 -6
  237. package/server/@types/index.d.ts +11 -0
  238. package/server/controllers/kpi-metric-value-provider.ts +5 -5
  239. package/server/controllers/kpi-value-provider.ts +4 -4
  240. package/server/migrations/1752190849680-seed-kpi-metrics.ts +124 -0
  241. package/server/migrations/1752190849681-seed-kpi.ts +356 -0
  242. package/server/migrations/1752192090123-add-grades-to-kpi.ts +67 -0
  243. package/server/migrations/1752192090124-add-kpi-statistics.ts +719 -0
  244. package/server/migrations/1752192090128-seed-kpi-org-scope.ts +132 -0
  245. package/server/migrations/1752192090129-seed-kpi-values.ts +207 -0
  246. package/server/migrations/grade-data/x11-performance-table.json +962 -0
  247. package/server/migrations/grade-data/x12-performance-table.json +611 -0
  248. package/server/migrations/grade-data/x14-performance-table.json +42 -0
  249. package/server/migrations/grade-data/x21-performance-table.json +889 -0
  250. package/server/migrations/grade-data/x22-performance-table.json +1064 -0
  251. package/server/migrations/grade-data/x23-performance-table.json +42 -0
  252. package/server/migrations/grade-data/x31-performance-table.json +644 -0
  253. package/server/migrations/grade-data/x32-performance-table.json +993 -0
  254. package/server/migrations/grade-data/x33-performance-table.json +195 -0
  255. package/server/migrations/grade-data/x34-performance-table.json +12 -0
  256. package/server/migrations/grade-data/x35-performance-table.json +42 -0
  257. package/server/migrations/grade-data/x41-performance-table.json +825 -0
  258. package/server/migrations/grade-data/x42-performance-table.json +786 -0
  259. package/server/migrations/grade-data/x43-performance-table.json +12 -0
  260. package/server/migrations/grade-data/x44-performance-table.json +42 -0
  261. package/server/migrations/grade-data/x51-performance-table.json +924 -0
  262. package/server/migrations/grade-data/x52-performance-table.json +42 -0
  263. package/server/migrations/grade-data/x61-performance-table.json +261 -0
  264. package/server/migrations/grade-data/x62-performance-table.json +42 -0
  265. package/server/migrations/seed-data/kpi-metrics-seed.json +454 -0
  266. package/server/migrations/seed-data/kpi-org-scope-seed.json +1676 -0
  267. package/server/migrations/seed-data/kpi-scopes-seed.json +121 -0
  268. package/server/migrations/seed-data/kpi-values-seed.json +402 -0
  269. package/server/migrations/seed-data/kpis-seed.json +488 -0
  270. package/server/migrations/seed-data/scope-definitions-seed.json +90 -0
  271. package/server/service/index.ts +10 -13
  272. package/server/service/kpi/aggregate-kpi.ts +31 -13
  273. package/server/service/kpi/kpi-formula.service.ts +101 -0
  274. package/server/service/kpi/kpi-history.ts +0 -8
  275. package/server/service/kpi/kpi-mutation.ts +59 -19
  276. package/server/service/kpi/kpi-query.ts +119 -8
  277. package/server/service/kpi/kpi-type.ts +10 -4
  278. package/server/service/kpi/kpi.ts +17 -7
  279. package/server/service/kpi-metric/aggregate-kpi-metric.ts +55 -11
  280. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +6 -6
  281. package/server/service/kpi-metric-value/kpi-metric-value-type.ts +4 -4
  282. package/server/service/kpi-metric-value/kpi-metric-value.ts +3 -3
  283. package/server/service/kpi-org-scope/index.ts +6 -0
  284. package/server/service/kpi-org-scope/kpi-org-scope-mutation.ts +173 -0
  285. package/server/service/kpi-org-scope/kpi-org-scope-query.ts +127 -0
  286. package/server/service/kpi-org-scope/kpi-org-scope-type.ts +68 -0
  287. package/server/service/kpi-org-scope/kpi-org-scope.ts +123 -0
  288. package/server/service/kpi-scope/index.ts +11 -0
  289. package/server/service/kpi-scope/kpi-scope-mutation.ts +129 -0
  290. package/server/service/kpi-scope/kpi-scope-query.ts +63 -0
  291. package/server/service/kpi-scope/kpi-scope-type.ts +96 -0
  292. package/server/service/kpi-scope/kpi-scope.ts +143 -0
  293. package/server/service/kpi-statistic/kpi-statistic-batch.service.ts +231 -0
  294. package/server/service/kpi-statistic/kpi-statistic-calculation.service.ts +410 -0
  295. package/server/service/kpi-statistic/kpi-statistic-mutation.ts +97 -0
  296. package/server/service/kpi-statistic/kpi-statistic-query.ts +89 -2
  297. package/server/service/kpi-statistic/kpi-statistic.ts +32 -0
  298. package/server/service/kpi-value/kpi-value-mutation.ts +73 -7
  299. package/server/service/kpi-value/kpi-value-type.ts +10 -4
  300. package/server/service/kpi-value/kpi-value.ts +10 -5
  301. package/server/service/utils/value-date-util.ts +47 -0
  302. package/server/types/global.d.ts +8 -0
  303. package/things-factory.config.js +1 -0
  304. package/translations/en.json +15 -3
  305. package/translations/ja.json +13 -3
  306. package/translations/ko.json +15 -3
  307. package/translations/ms.json +13 -3
  308. package/translations/zh.json +13 -3
  309. package/client/pages/kpi-category/kpi-category-importer.ts +0 -90
  310. package/client/pages/kpi-category/kpi-category-list-page.ts +0 -537
  311. package/client/pages/kpi-category/kpi-category-value-calculator.ts +0 -233
  312. package/client/pages/kpi-category-value/kpi-category-value-list-page.ts +0 -404
  313. package/dist-client/pages/kpi-category/kpi-category-importer.d.ts +0 -23
  314. package/dist-client/pages/kpi-category/kpi-category-importer.js +0 -92
  315. package/dist-client/pages/kpi-category/kpi-category-importer.js.map +0 -1
  316. package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +0 -74
  317. package/dist-client/pages/kpi-category/kpi-category-list-page.js +0 -517
  318. package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +0 -1
  319. package/dist-client/pages/kpi-category/kpi-category-value-calculator.d.ts +0 -13
  320. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js +0 -256
  321. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js.map +0 -1
  322. package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.d.ts +0 -63
  323. package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.js +0 -393
  324. package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.js.map +0 -1
  325. package/dist-server/service/kpi-category/index.d.ts +0 -6
  326. package/dist-server/service/kpi-category/index.js +0 -10
  327. package/dist-server/service/kpi-category/index.js.map +0 -1
  328. package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +0 -9
  329. package/dist-server/service/kpi-category/kpi-category-mutation.js +0 -221
  330. package/dist-server/service/kpi-category/kpi-category-mutation.js.map +0 -1
  331. package/dist-server/service/kpi-category/kpi-category-query.d.ts +0 -18
  332. package/dist-server/service/kpi-category/kpi-category-query.js +0 -115
  333. package/dist-server/service/kpi-category/kpi-category-query.js.map +0 -1
  334. package/dist-server/service/kpi-category/kpi-category-type.d.ts +0 -24
  335. package/dist-server/service/kpi-category/kpi-category-type.js +0 -100
  336. package/dist-server/service/kpi-category/kpi-category-type.js.map +0 -1
  337. package/dist-server/service/kpi-category/kpi-category.d.ts +0 -22
  338. package/dist-server/service/kpi-category/kpi-category.js +0 -106
  339. package/dist-server/service/kpi-category/kpi-category.js.map +0 -1
  340. package/dist-server/service/kpi-category-value/index.d.ts +0 -6
  341. package/dist-server/service/kpi-category-value/index.js +0 -10
  342. package/dist-server/service/kpi-category-value/index.js.map +0 -1
  343. package/dist-server/service/kpi-category-value/kpi-category-value-mutation.d.ts +0 -8
  344. package/dist-server/service/kpi-category-value/kpi-category-value-mutation.js +0 -102
  345. package/dist-server/service/kpi-category-value/kpi-category-value-mutation.js.map +0 -1
  346. package/dist-server/service/kpi-category-value/kpi-category-value-query.d.ts +0 -13
  347. package/dist-server/service/kpi-category-value/kpi-category-value-query.js +0 -91
  348. package/dist-server/service/kpi-category-value/kpi-category-value-query.js.map +0 -1
  349. package/dist-server/service/kpi-category-value/kpi-category-value-type.d.ts +0 -19
  350. package/dist-server/service/kpi-category-value/kpi-category-value-type.js +0 -73
  351. package/dist-server/service/kpi-category-value/kpi-category-value-type.js.map +0 -1
  352. package/dist-server/service/kpi-category-value/kpi-category-value.d.ts +0 -19
  353. package/dist-server/service/kpi-category-value/kpi-category-value.js +0 -91
  354. package/dist-server/service/kpi-category-value/kpi-category-value.js.map +0 -1
  355. package/helps/kpi/kpi-category.md +0 -160
  356. package/server/service/kpi-category/index.ts +0 -7
  357. package/server/service/kpi-category/kpi-category-mutation.ts +0 -217
  358. package/server/service/kpi-category/kpi-category-query.ts +0 -87
  359. package/server/service/kpi-category/kpi-category-type.ts +0 -73
  360. package/server/service/kpi-category/kpi-category.ts +0 -95
  361. package/server/service/kpi-category-value/index.ts +0 -7
  362. package/server/service/kpi-category-value/kpi-category-value-mutation.ts +0 -88
  363. package/server/service/kpi-category-value/kpi-category-value-query.ts +0 -62
  364. package/server/service/kpi-category-value/kpi-category-value-type.ts +0 -48
  365. package/server/service/kpi-category-value/kpi-category-value.ts +0 -79
@@ -2,6 +2,7 @@ import { getRepository } from '@things-factory/shell'
2
2
  import { Kpi } from './kpi'
3
3
  import { KpiMetric } from '../kpi-metric/kpi-metric'
4
4
  import { KpiValue, KpiValueInputType } from '../kpi-value/kpi-value'
5
+ import { KpiOrgScope } from '../kpi-org-scope/kpi-org-scope'
5
6
  import { KpiFormulaService } from './kpi-formula.service'
6
7
  import { aggregateKpiMetricValue } from '../kpi-metric/aggregate-kpi-metric'
7
8
  import { KpiValueScoreService } from '../kpi-value/kpi-value-score.service'
@@ -33,34 +34,50 @@ export async function aggregateKpiValue(kpiId: string, domainId: string, context
33
34
  if (!metric) throw new Error(`KPI formula metric '${code}' not found`)
34
35
  codeValueMap[code] = await aggregateKpiMetricValue(metric.id, domainId, context)
35
36
  }
36
- // group/date/period별로 값 매핑(가장 최근 기준, group key 조합)
37
- const groupKey = v => [v.date, v.period, v.group].join('|')
38
- const groupMap: Record<string, any> = {}
37
+ // org/date/period별로 값 매핑(가장 최근 기준, org key 조합)
38
+ const orgKey = v => [v.date, v.period, v.org].join('|')
39
+ const orgMap: Record<string, any> = {}
39
40
  for (const code of metricCodes) {
40
41
  for (const v of codeValueMap[code]) {
41
- const key = groupKey(v)
42
- groupMap[key] = groupMap[key] || { ...v, _values: {} }
43
- groupMap[key]._values[code] = v.value
42
+ const key = orgKey(v)
43
+ orgMap[key] = orgMap[key] || { ...v, _values: {} }
44
+ orgMap[key]._values[code] = v.value
44
45
  }
45
46
  }
46
47
  // formula 계산 (js eval)
47
48
  const savedValues = []
48
- for (const key in groupMap) {
49
- const ctx = groupMap[key]._values
49
+ for (const key in orgMap) {
50
+ const ctx = orgMap[key]._values
50
51
  let value = null
51
52
  try {
52
53
  value = Function(...Object.keys(ctx), `return (${kpi.formula})`)(...Object.values(ctx))
53
54
  } catch (e) {
54
55
  value = null
55
56
  }
56
- const valueDate = groupMap[key].date
57
- const group = groupMap[key].group
57
+ const valueDate = orgMap[key].date
58
+ const org = orgMap[key].org
58
59
  const version = kpi.version || 1
59
60
  if (value == null || isNaN(value)) continue
60
- // upsert(동일 KPI, valueDate, group, version) 기준으로 저장
61
+ // KpiOrgScope 처리 (org 문자열로 찾기 또는 생성)
62
+ const kpiOrgScopeRepo = getRepository(KpiOrgScope, context.state?.tx)
63
+ let kpiOrgScope = await kpiOrgScopeRepo.findOne({
64
+ where: { org: org, domain: { id: domainId } }
65
+ })
66
+ if (!kpiOrgScope) {
67
+ // 새 KpiOrgScope 생성
68
+ kpiOrgScope = await kpiOrgScopeRepo.save({
69
+ entityType: 'AutoAggregate',
70
+ entityId: `auto-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
71
+ entityName: org,
72
+ org: org,
73
+ domain: kpi.domain
74
+ })
75
+ }
76
+
77
+ // upsert(동일 KPI, valueDate, kpiOrgScope, version) 기준으로 저장
61
78
  const repo = getRepository(KpiValue, context.state?.tx)
62
79
  const existing = await repo.findOne({
63
- where: { kpi: { id: kpi.id }, valueDate, group, version, domain: { id: domainId } }
80
+ where: { kpi: { id: kpi.id }, valueDate, kpiOrgScope: { id: kpiOrgScope.id }, version, domain: { id: domainId } }
64
81
  })
65
82
  let entity = existing || repo.create()
66
83
  entity.kpi = kpi
@@ -68,7 +85,8 @@ export async function aggregateKpiValue(kpiId: string, domainId: string, context
68
85
  entity.version = version
69
86
  entity.valueDate = valueDate
70
87
  entity.value = value
71
- entity.group = group
88
+ entity.kpiOrgScope = kpiOrgScope
89
+ entity.kpiOrgScopeId = kpiOrgScope.id
72
90
  entity.inputType = KpiValueInputType.AUTO
73
91
  entity.source = 'AUTO'
74
92
  entity.domain = kpi.domain
@@ -1,5 +1,8 @@
1
1
  import { getRepository } from '@things-factory/shell'
2
2
  import { KpiMetric } from '../kpi-metric/kpi-metric'
3
+ import { Kpi } from './kpi'
4
+ import { KpiValue } from '../kpi-value/kpi-value'
5
+ import { KpiOrgScope } from '../kpi-org-scope/kpi-org-scope'
3
6
 
4
7
  /**
5
8
  * KPI formula 파싱/검증/매핑 유효성 검사 서비스
@@ -60,4 +63,102 @@ export class KpiFormulaService {
60
63
  // ...
61
64
  return { valid: errors.length === 0, errors }
62
65
  }
66
+
67
+ /**
68
+ * 계층 구조 KPI의 값을 자식 KPI들로부터 집계 계산
69
+ * @param kpiId 부모 KPI ID
70
+ * @param valueDate 계산 대상 날짜
71
+ * @param org 조직 단위
72
+ * @returns Promise<number | null> 집계된 값
73
+ */
74
+ async calculateHierarchicalValue(kpiId: string, valueDate: string, org?: string): Promise<number | null> {
75
+ const kpiRepo = getRepository(Kpi)
76
+ const kpi = await kpiRepo.findOne({
77
+ where: { id: kpiId },
78
+ relations: ['children']
79
+ })
80
+
81
+ if (!kpi || !kpi.children || kpi.children.length === 0) {
82
+ return null
83
+ }
84
+
85
+ const childValues: number[] = []
86
+ const childWeights: number[] = []
87
+
88
+ for (const child of kpi.children) {
89
+ let childValue: number | null = null
90
+
91
+ if (child.isLeaf) {
92
+ // Leaf KPI인 경우 KpiValue에서 실제 값을 가져옴
93
+ const kpiValueRepo = getRepository(KpiValue)
94
+ let kpiValue: KpiValue | null = null
95
+ if (org) {
96
+ const kpiOrgScope = await getRepository(KpiOrgScope).findOne({
97
+ where: { org: org, domain: { id: child.domain?.id } }
98
+ })
99
+ if (kpiOrgScope) {
100
+ kpiValue = await kpiValueRepo.findOne({
101
+ where: {
102
+ kpi: { id: child.id },
103
+ valueDate,
104
+ kpiOrgScope: { id: kpiOrgScope.id }
105
+ }
106
+ })
107
+ }
108
+ } else {
109
+ // org가 없는 경우 kpiOrgScope 없이 조회
110
+ kpiValue = await kpiValueRepo.findOne({
111
+ where: {
112
+ kpi: { id: child.id },
113
+ valueDate,
114
+ kpiOrgScope: null
115
+ }
116
+ })
117
+ }
118
+ childValue = kpiValue?.value || null
119
+ } else {
120
+ // 중간 노드인 경우 재귀적으로 계산
121
+ childValue = await this.calculateHierarchicalValue(child.id, valueDate, org)
122
+ }
123
+
124
+ if (childValue !== null) {
125
+ childValues.push(childValue)
126
+ childWeights.push(child.weight || 1)
127
+ }
128
+ }
129
+
130
+ if (childValues.length === 0) {
131
+ return null
132
+ }
133
+
134
+ // 가중 평균 계산
135
+ const totalWeight = childWeights.reduce((sum, weight) => sum + weight, 0)
136
+ const weightedSum = childValues.reduce((sum, value, index) => sum + value * childWeights[index], 0)
137
+
138
+ return totalWeight > 0 ? weightedSum / totalWeight : null
139
+ }
140
+
141
+ /**
142
+ * KPI가 순환 참조를 생성하는지 확인
143
+ * @param kpiId KPI ID
144
+ * @param parentId 설정하려는 부모 KPI ID
145
+ * @returns Promise<boolean> 순환 참조 여부
146
+ */
147
+ async hasCircularReference(kpiId: string, parentId: string): Promise<boolean> {
148
+ if (kpiId === parentId) {
149
+ return true
150
+ }
151
+
152
+ const kpiRepo = getRepository(Kpi)
153
+ const parent = await kpiRepo.findOne({
154
+ where: { id: parentId },
155
+ relations: ['parent']
156
+ })
157
+
158
+ if (!parent || !parent.parent) {
159
+ return false
160
+ }
161
+
162
+ return await this.hasCircularReference(kpiId, parent.parent.id)
163
+ }
63
164
  }
@@ -12,7 +12,6 @@ import { config } from '@things-factory/env'
12
12
  import { Domain, ScalarObject } from '@things-factory/shell'
13
13
 
14
14
  import { Kpi, KpiStatus, KpiPeriodType } from './kpi'
15
- import { KpiCategory } from '../kpi-category/kpi-category'
16
15
  import { KpiScores } from './kpi-grade.types'
17
16
 
18
17
  const ORMCONFIG = config.get('ormconfig', {})
@@ -48,13 +47,6 @@ export class KpiHistory implements HistoryEntityInterface<Kpi> {
48
47
  @Field({ nullable: true })
49
48
  description?: string
50
49
 
51
- @ManyToOne(() => KpiCategory, { nullable: true })
52
- @Field(type => KpiCategory, { nullable: true, description: 'Category to which this KPI belongs.' })
53
- category?: KpiCategory
54
-
55
- @RelationId((kpi: KpiHistory) => kpi.category)
56
- categoryId?: string
57
-
58
50
  @Column({ nullable: true })
59
51
  @Field({ nullable: true, description: 'Calculation formula for the KPI.' })
60
52
  formula?: string
@@ -5,11 +5,11 @@ import { getRepository } from '@things-factory/shell'
5
5
  import { createAttachment, deleteAttachmentsByRef } from '@things-factory/attachment-base'
6
6
  import { Kpi } from './kpi'
7
7
  import { NewKpi, KpiPatch } from './kpi-type'
8
- import { KpiCategory } from '../kpi-category/kpi-category'
9
8
  import { KpiHistory } from './kpi-history'
10
9
  import { KpiFormulaService } from './kpi-formula.service'
11
10
  import { Application, CallbackBase, registerSchedule, unregisterSchedule } from '@things-factory/scheduler-client'
12
11
  import { KpiValue } from '../kpi-value/kpi-value'
12
+ import { KpiOrgScope } from '../kpi-org-scope/kpi-org-scope'
13
13
  import { Between } from 'typeorm'
14
14
  import { KpiMetric } from '../kpi-metric/kpi-metric'
15
15
  import { KpiMetricValue } from '../kpi-metric-value/kpi-metric-value'
@@ -29,13 +29,22 @@ export class KpiMutation {
29
29
  ): Promise<Kpi> {
30
30
  const { domain, user, tx } = context.state
31
31
 
32
- let category = kpi.category
33
- ? await getRepository(KpiCategory).findOne({ where: { id: kpi.category.id } })
32
+ let parent = kpi.parent
33
+ ? await getRepository(Kpi).findOne({ where: { id: kpi.parent.id, domain: { id: domain.id } } })
34
34
  : undefined
35
35
 
36
+ // 순환 참조 검사
37
+ if (parent) {
38
+ const formulaService = new KpiFormulaService()
39
+ const hasCircular = await formulaService.hasCircularReference(parent.id, parent.id)
40
+ if (hasCircular) {
41
+ throw new Error('Circular reference detected in KPI hierarchy')
42
+ }
43
+ }
44
+
36
45
  const result = await getRepository(Kpi, tx).save({
37
46
  ...kpi,
38
- category,
47
+ parent,
39
48
  domain,
40
49
  creator: user,
41
50
  updater: user
@@ -114,14 +123,23 @@ export class KpiMutation {
114
123
  where: { domain: { id: domain.id }, id }
115
124
  })
116
125
 
117
- let category = patch.category
118
- ? await getRepository(KpiCategory).findOne({ where: { id: patch.category.id } })
119
- : kpi.category
126
+ let parent = patch.parent
127
+ ? await getRepository(Kpi).findOne({ where: { id: patch.parent.id, domain: { id: domain.id } } })
128
+ : patch.parent === null ? null : kpi.parent
129
+
130
+ // 순환 참조 검사 (parent가 변경되는 경우)
131
+ if (parent && parent.id !== kpi.parentId) {
132
+ const formulaService = new KpiFormulaService()
133
+ const hasCircular = await formulaService.hasCircularReference(kpi.id, parent.id)
134
+ if (hasCircular) {
135
+ throw new Error('Circular reference detected in KPI hierarchy')
136
+ }
137
+ }
120
138
 
121
139
  const result = await repository.save({
122
140
  ...kpi,
123
141
  ...patch,
124
- category,
142
+ parent,
125
143
  updater: user
126
144
  })
127
145
 
@@ -383,8 +401,6 @@ export class KpiMutation {
383
401
  ...kpi,
384
402
  name: kpiHistory.name,
385
403
  description: kpiHistory.description,
386
- category: kpiHistory.category,
387
- categoryId: kpiHistory.categoryId,
388
404
  formula: kpiHistory.formula,
389
405
  active: kpiHistory.active,
390
406
  state: 'DRAFT',
@@ -400,7 +416,7 @@ export class KpiMutation {
400
416
  async calculateKpiValue(
401
417
  @Arg('kpiId') kpiId: string,
402
418
  @Arg('valueDate', { nullable: true }) valueDate: string,
403
- @Arg('group', { nullable: true }) group: string,
419
+ @Arg('org', { nullable: true }) org: string,
404
420
  @Ctx() context: ResolverContext
405
421
  ): Promise<KpiValue> {
406
422
  const { domain, user, tx } = context.state
@@ -443,7 +459,7 @@ export class KpiMutation {
443
459
  metric: { id: metric.id },
444
460
  valueDate: valueDateToUse,
445
461
  periodType,
446
- group: group ?? '',
462
+ org: org ?? '',
447
463
  domain: { id: domain.id }
448
464
  }
449
465
  })
@@ -458,7 +474,7 @@ export class KpiMutation {
458
474
  metric: { id: metric.id },
459
475
  valueDate: Between(startDate, endDate),
460
476
  periodType: metric.periodType,
461
- group: group ?? '',
477
+ org: org ?? '',
462
478
  domain: { id: domain.id }
463
479
  }
464
480
  })
@@ -475,19 +491,39 @@ export class KpiMutation {
475
491
  const ast = parseFormula(kpi.formula)
476
492
  const provider = new KpiMetricValueProvider({
477
493
  valueDate: valueDateToUse,
478
- group,
494
+ org,
479
495
  domainId: domain.id,
480
496
  tx
481
497
  })
482
498
  const evalContext = { functions: builtinFunctions, provider }
483
499
  const kpiValueResult = await evaluateFormula(ast, evalContext)
484
500
 
485
- // 5. KPI Value upsert
501
+ // 5. KpiOrgScope 처리
502
+ let kpiOrgScope: KpiOrgScope | null = null
503
+ if (org) {
504
+ kpiOrgScope = await getRepository(KpiOrgScope, tx).findOne({
505
+ where: { org: org, domain: { id: domain.id } }
506
+ })
507
+ if (!kpiOrgScope) {
508
+ // 새 KpiOrgScope 생성
509
+ kpiOrgScope = await getRepository(KpiOrgScope, tx).save({
510
+ entityType: 'KpiCalculation',
511
+ entityId: `calc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
512
+ entityName: org,
513
+ org: org,
514
+ domain,
515
+ creator: user,
516
+ updater: user
517
+ })
518
+ }
519
+ }
520
+
521
+ // 6. KPI Value upsert
486
522
  let kpiValue = await kpiValueRepo.findOne({
487
523
  where: {
488
524
  kpi: { id: kpi.id },
489
525
  valueDate: valueDateToUse,
490
- group: group ?? '',
526
+ kpiOrgScope: kpiOrgScope ? { id: kpiOrgScope.id } : null,
491
527
  version: kpi.version,
492
528
  domain: { id: domain.id }
493
529
  }
@@ -496,17 +532,21 @@ export class KpiMutation {
496
532
  kpiValue.value = kpiValueResult
497
533
  kpiValue.updater = user
498
534
  } else {
499
- kpiValue = kpiValueRepo.create({
535
+ const createData: any = {
500
536
  kpi,
501
537
  kpiId: kpi.id,
502
538
  version: kpi.version,
503
539
  valueDate: valueDateToUse,
504
540
  value: kpiValueResult,
505
- group: group ?? '',
506
541
  domain,
507
542
  creator: user,
508
543
  updater: user
509
- })
544
+ }
545
+ if (kpiOrgScope) {
546
+ createData.kpiOrgScope = kpiOrgScope
547
+ createData.kpiOrgScopeId = kpiOrgScope.id
548
+ }
549
+ kpiValue = Object.assign(new KpiValue(), createData)
510
550
  }
511
551
  return await kpiValueRepo.save(kpiValue)
512
552
  }
@@ -1,16 +1,75 @@
1
+ import { IsNull } from 'typeorm'
1
2
  import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx, Directive } from 'type-graphql'
2
3
  import { Attachment } from '@things-factory/attachment-base'
3
4
  import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
4
5
  import { User } from '@things-factory/auth-base'
6
+ import type ResolverContext from '@things-factory/auth-base'
5
7
  import { Kpi } from './kpi'
6
8
  import { KpiList } from './kpi-type'
7
9
  import { KpiValue } from '../kpi-value/kpi-value'
8
10
  import { KpiHistory } from './kpi-history'
9
11
  import { Int } from 'type-graphql'
10
- import { KpiCategory } from '../kpi-category/kpi-category'
11
12
 
12
13
  @Resolver(Kpi)
13
14
  export class KpiQuery {
15
+ @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
16
+ @Query(returns => [Kpi], { description: 'Fetch root KPIs (KPIs without parent)' })
17
+ async kpisLevel0(@Ctx() context: ResolverContext): Promise<Kpi[]> {
18
+ const { domain } = context.state
19
+
20
+ return await getRepository(Kpi).find({
21
+ where: { domain: { id: domain.id }, parent: null },
22
+ order: { name: 'ASC' }
23
+ })
24
+ }
25
+
26
+ @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
27
+ @Query(returns => [Kpi], {
28
+ description:
29
+ 'Fetch first level child KPIs. If rootId is provided, get children of that parent. If not, get all level 1 KPIs (children of root KPIs)'
30
+ })
31
+ async kpisLevel1(
32
+ @Arg('rootId', {
33
+ nullable: true,
34
+ description: 'ID of the parent KPI to get level 1 children. If not provided, returns all level 1 KPIs'
35
+ })
36
+ rootId: string,
37
+ @Ctx() context: ResolverContext
38
+ ): Promise<Kpi[]> {
39
+ const { domain } = context.state
40
+
41
+ let whereCondition: any = { domain: { id: domain.id } }
42
+
43
+ if (rootId) {
44
+ // 특정 부모의 자식들
45
+ whereCondition.parent = { id: rootId }
46
+ } else {
47
+ // 모든 level 1 KPI들 (root KPI들의 직계 자식들)
48
+ // 부모가 있으면서 부모의 부모가 null인 KPI들
49
+ const kpis = await getRepository(Kpi)
50
+ .createQueryBuilder('kpi')
51
+ .leftJoinAndSelect('kpi.parent', 'parent')
52
+ .leftJoinAndSelect('kpi.children', 'children')
53
+ .leftJoinAndSelect('children.children', 'grandchildren')
54
+ .where('kpi.domain = :domainId', { domainId: domain.id })
55
+ .andWhere('parent.id IS NOT NULL')
56
+ .andWhere('parent.parent IS NULL')
57
+ .orderBy('kpi.name', 'ASC')
58
+ .getMany()
59
+
60
+ return kpis
61
+ }
62
+
63
+ // rootId 제공된 경우
64
+ const kpis = await getRepository(Kpi).find({
65
+ where: whereCondition,
66
+ relations: ['children', 'children.children'],
67
+ order: { name: 'ASC' }
68
+ })
69
+
70
+ return kpis
71
+ }
72
+
14
73
  @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
15
74
  @Query(returns => Kpi!, { nullable: true, description: 'Fetch a single KPI by its unique identifier.' })
16
75
  async kpi(
@@ -33,7 +92,10 @@ export class KpiQuery {
33
92
  domain,
34
93
  params,
35
94
  repository: await getRepository(Kpi),
36
- searchables: ['name', 'description']
95
+ searchables: ['name', 'description'],
96
+ filtersMap: {
97
+ parent: { columnName: 'id', relationColumn: 'parent' }
98
+ }
37
99
  })
38
100
 
39
101
  const [items, total] = await queryBuilder.getManyAndCount()
@@ -41,6 +103,13 @@ export class KpiQuery {
41
103
  return { items, total }
42
104
  }
43
105
 
106
+ @FieldResolver(type => [Kpi])
107
+ children(@Root() kpi: Kpi): Promise<Kpi[]> {
108
+ return getRepository(Kpi).find({
109
+ where: { parent: { id: kpi.id } }
110
+ })
111
+ }
112
+
44
113
  @FieldResolver(type => String)
45
114
  async thumbnail(@Root() kpi: Kpi): Promise<string | undefined> {
46
115
  const attachment: Attachment = await getRepository(Attachment).findOne({
@@ -70,10 +139,26 @@ export class KpiQuery {
70
139
  }
71
140
 
72
141
  @FieldResolver(type => KpiValue, { nullable: true })
73
- async value(@Root() kpi: Kpi, @Ctx() context): Promise<KpiValue | null> {
142
+ async value(
143
+ @Root() kpi: Kpi,
144
+ @Ctx() context,
145
+ @Arg('orgScope', { nullable: true, description: 'Organization scope filter for value' }) orgScope?: string
146
+ ): Promise<KpiValue | null> {
74
147
  const { domain } = context.state
148
+
149
+ let whereCondition: any = {
150
+ domain: { id: domain.id },
151
+ kpi: { id: kpi.id }
152
+ }
153
+
154
+ // orgScope 필터가 있으면 KpiOrgScope로 필터링
155
+ if (orgScope) {
156
+ whereCondition.kpiOrgScope = { org: orgScope }
157
+ }
158
+
75
159
  return await getRepository(KpiValue).findOne({
76
- where: { domain: { id: domain.id }, kpi: { id: kpi.id } },
160
+ where: whereCondition,
161
+ relations: ['kpiOrgScope'],
77
162
  order: { valueDate: 'DESC' }
78
163
  })
79
164
  }
@@ -88,10 +173,10 @@ export class KpiQuery {
88
173
  return kpi.vizMeta?.unit
89
174
  }
90
175
 
91
- @FieldResolver(type => KpiCategory, { nullable: true })
92
- async category(@Root() kpi: Kpi): Promise<KpiCategory | null> {
93
- if (!kpi.categoryId) return null
94
- return await getRepository(KpiCategory).findOneBy({ id: kpi.categoryId })
176
+ @FieldResolver(type => Kpi, { nullable: true })
177
+ async parent(@Root() kpi: Kpi): Promise<Kpi | null> {
178
+ if (!kpi.parentId) return null
179
+ return await getRepository(Kpi).findOneBy({ id: kpi.parentId })
95
180
  }
96
181
 
97
182
  @FieldResolver(type => [KpiHistory], { nullable: true })
@@ -110,4 +195,30 @@ export class KpiQuery {
110
195
  else qb.limit(1)
111
196
  return await qb.getMany()
112
197
  }
198
+
199
+ @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
200
+ @Query(returns => [Kpi], { description: 'Fetch KPIs in hierarchical tree structure' })
201
+ async kpiTree(@Ctx() context: ResolverContext): Promise<Kpi[]> {
202
+ const { domain } = context.state
203
+
204
+ return await getRepository(Kpi).find({
205
+ where: { domain: { id: domain.id }, parent: IsNull() },
206
+ relations: ['children', 'children.children', 'children.children.children'],
207
+ order: { name: 'ASC' }
208
+ })
209
+ }
210
+
211
+ @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
212
+ @Query(returns => [Kpi], { description: 'Fetch child KPIs of a given parent KPI' })
213
+ async kpiChildren(
214
+ @Arg('parentId', { description: 'ID of the parent KPI' }) parentId: string,
215
+ @Ctx() context: ResolverContext
216
+ ): Promise<Kpi[]> {
217
+ const { domain } = context.state
218
+
219
+ return await getRepository(Kpi).find({
220
+ where: { domain: { id: domain.id }, parent: { id: parentId } },
221
+ order: { name: 'ASC' }
222
+ })
223
+ }
113
224
  }
@@ -15,8 +15,11 @@ export class NewKpi {
15
15
  @Field({ nullable: true, description: 'Detailed description of the KPI.' })
16
16
  description?: string
17
17
 
18
- @Field(type => ObjectRef, { nullable: true, description: 'Reference to the category to which this KPI belongs.' })
19
- category?: ObjectRef
18
+ @Field(type => ObjectRef, { nullable: true, description: 'Reference to the parent KPI in hierarchical structure.' })
19
+ parent?: ObjectRef
20
+
21
+ @Field({ nullable: true, description: 'Indicates whether this KPI is a leaf node (has actual measured values).' })
22
+ isLeaf?: boolean
20
23
 
21
24
  @Field({ nullable: true, description: 'Calculation formula for the KPI, using metric codes and operators.' })
22
25
  formula?: string
@@ -79,8 +82,11 @@ export class KpiPatch {
79
82
  @Field({ nullable: true, description: 'Detailed description of the KPI.' })
80
83
  description?: string
81
84
 
82
- @Field(type => ObjectRef, { nullable: true, description: 'Reference to the category to which this KPI belongs.' })
83
- category?: ObjectRef
85
+ @Field(type => ObjectRef, { nullable: true, description: 'Reference to the parent KPI in hierarchical structure.' })
86
+ parent?: ObjectRef
87
+
88
+ @Field({ nullable: true, description: 'Indicates whether this KPI is a leaf node (has actual measured values).' })
89
+ isLeaf?: boolean
84
90
 
85
91
  @Field({ nullable: true, description: 'Calculation formula for the KPI, using metric codes and operators.' })
86
92
  formula?: string
@@ -15,7 +15,6 @@ import { ObjectType, Field, Int, ID, registerEnumType } from 'type-graphql'
15
15
 
16
16
  import { Domain } from '@things-factory/shell'
17
17
  import { User } from '@things-factory/auth-base'
18
- import { KpiCategory } from '../kpi-category/kpi-category'
19
18
  import { config } from '@things-factory/env'
20
19
  import { ScalarObject } from '@things-factory/shell'
21
20
 
@@ -78,6 +77,9 @@ registerEnumType(KpiPeriodType, {
78
77
  where: '"deleted_at" IS NULL',
79
78
  unique: true
80
79
  })
80
+ @Index('ix_kpi_hierarchy', (kpi: Kpi) => [kpi.domain, kpi.parent], {
81
+ where: '"deleted_at" IS NULL'
82
+ })
81
83
  @ObjectType({
82
84
  description:
83
85
  'KPI entity. Represents a key performance indicator with calculation formula, target, category, and other attributes.'
@@ -111,13 +113,21 @@ export class Kpi {
111
113
  @Field({ nullable: true, description: 'Detailed description of the KPI.' })
112
114
  description?: string
113
115
 
114
- @ManyToOne(() => KpiCategory, { nullable: true })
115
- @Field(type => KpiCategory, { nullable: true, description: 'Category to which this KPI belongs.' })
116
- category?: KpiCategory
116
+ @ManyToOne(() => Kpi, { nullable: true })
117
+ @Field(type => Kpi, { nullable: true, description: 'Parent KPI in hierarchical structure.' })
118
+ parent?: Kpi
119
+
120
+ @RelationId((kpi: Kpi) => kpi.parent)
121
+ @Field({ nullable: true, description: 'ID of the parent KPI.' })
122
+ parentId?: string
123
+
124
+ @OneToMany(() => Kpi, kpi => kpi.parent)
125
+ @Field(type => [Kpi], { nullable: true, description: 'Child KPIs in hierarchical structure.' })
126
+ children?: Kpi[]
117
127
 
118
- @RelationId((kpi: Kpi) => kpi.category)
119
- @Field({ nullable: true, description: 'ID of the category for this KPI.' })
120
- categoryId?: string
128
+ @Column({ nullable: false, default: true })
129
+ @Field({ nullable: true, description: 'Indicates whether this KPI is a leaf node (has actual measured values).' })
130
+ isLeaf?: boolean
121
131
 
122
132
  @Column({
123
133
  nullable: true,