@things-factory/kpi 9.0.31 → 9.0.32

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 (324) 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 +4 -3
  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.js +4 -3
  59. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -1
  60. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +28 -30
  61. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  62. package/dist-client/pages/kpi-history/kpi-history-list-page.d.ts +6 -1
  63. package/dist-client/pages/kpi-history/kpi-history-list-page.js +11 -11
  64. package/dist-client/pages/kpi-history/kpi-history-list-page.js.map +1 -1
  65. package/dist-client/pages/kpi-metric/kpi-metric-list-page.d.ts +5 -0
  66. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +10 -2
  67. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
  68. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +1 -1
  69. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +8 -8
  70. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -1
  71. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js +2 -2
  72. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js.map +1 -1
  73. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +5 -0
  74. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +16 -8
  75. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  76. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.d.ts +1 -1
  77. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js +6 -6
  78. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -1
  79. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js +1 -2
  80. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js.map +1 -1
  81. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.d.ts +5 -0
  82. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js +10 -2
  83. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js.map +1 -1
  84. package/dist-client/pages/kpi-value/kpi-value-editor-page.d.ts +2 -1
  85. package/dist-client/pages/kpi-value/kpi-value-editor-page.js +16 -8
  86. package/dist-client/pages/kpi-value/kpi-value-editor-page.js.map +1 -1
  87. package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +5 -0
  88. package/dist-client/pages/kpi-value/kpi-value-list-page.js +31 -7
  89. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  90. package/dist-client/route.d.ts +1 -1
  91. package/dist-client/route.js +2 -8
  92. package/dist-client/route.js.map +1 -1
  93. package/dist-client/tsconfig.tsbuildinfo +1 -1
  94. package/dist-server/controllers/kpi-metric-value-provider.d.ts +1 -1
  95. package/dist-server/controllers/kpi-metric-value-provider.js +4 -4
  96. package/dist-server/controllers/kpi-metric-value-provider.js.map +1 -1
  97. package/dist-server/controllers/kpi-value-provider.d.ts +1 -1
  98. package/dist-server/controllers/kpi-value-provider.js +3 -3
  99. package/dist-server/controllers/kpi-value-provider.js.map +1 -1
  100. package/dist-server/migrations/1752190849680-seed-kpi-metrics.d.ts +6 -0
  101. package/dist-server/migrations/1752190849680-seed-kpi-metrics.js +101 -0
  102. package/dist-server/migrations/1752190849680-seed-kpi-metrics.js.map +1 -0
  103. package/dist-server/migrations/1752190849681-seed-kpi.d.ts +5 -0
  104. package/dist-server/migrations/1752190849681-seed-kpi.js +315 -0
  105. package/dist-server/migrations/1752190849681-seed-kpi.js.map +1 -0
  106. package/dist-server/migrations/1752192090123-add-grades-to-kpi.d.ts +7 -0
  107. package/dist-server/migrations/1752192090123-add-grades-to-kpi.js +51 -0
  108. package/dist-server/migrations/1752192090123-add-grades-to-kpi.js.map +1 -0
  109. package/dist-server/migrations/1752192090124-add-kpi-statistics.d.ts +5 -0
  110. package/dist-server/migrations/1752192090124-add-kpi-statistics.js +710 -0
  111. package/dist-server/migrations/1752192090124-add-kpi-statistics.js.map +1 -0
  112. package/dist-server/migrations/1752192090128-seed-kpi-org-scope.d.ts +6 -0
  113. package/dist-server/migrations/1752192090128-seed-kpi-org-scope.js +111 -0
  114. package/dist-server/migrations/1752192090128-seed-kpi-org-scope.js.map +1 -0
  115. package/dist-server/migrations/1752192090129-seed-kpi-values.d.ts +6 -0
  116. package/dist-server/migrations/1752192090129-seed-kpi-values.js +187 -0
  117. package/dist-server/migrations/1752192090129-seed-kpi-values.js.map +1 -0
  118. package/dist-server/migrations/grade-data/x11-performance-table.json +962 -0
  119. package/dist-server/migrations/grade-data/x12-performance-table.json +611 -0
  120. package/dist-server/migrations/grade-data/x14-performance-table.json +42 -0
  121. package/dist-server/migrations/grade-data/x21-performance-table.json +889 -0
  122. package/dist-server/migrations/grade-data/x22-performance-table.json +1064 -0
  123. package/dist-server/migrations/grade-data/x23-performance-table.json +42 -0
  124. package/dist-server/migrations/grade-data/x31-performance-table.json +644 -0
  125. package/dist-server/migrations/grade-data/x32-performance-table.json +993 -0
  126. package/dist-server/migrations/grade-data/x33-performance-table.json +195 -0
  127. package/dist-server/migrations/grade-data/x34-performance-table.json +12 -0
  128. package/dist-server/migrations/grade-data/x35-performance-table.json +42 -0
  129. package/dist-server/migrations/grade-data/x41-performance-table.json +825 -0
  130. package/dist-server/migrations/grade-data/x42-performance-table.json +786 -0
  131. package/dist-server/migrations/grade-data/x43-performance-table.json +12 -0
  132. package/dist-server/migrations/grade-data/x44-performance-table.json +42 -0
  133. package/dist-server/migrations/grade-data/x51-performance-table.json +924 -0
  134. package/dist-server/migrations/grade-data/x52-performance-table.json +42 -0
  135. package/dist-server/migrations/grade-data/x61-performance-table.json +261 -0
  136. package/dist-server/migrations/grade-data/x62-performance-table.json +42 -0
  137. package/dist-server/migrations/seed-data/kpi-metrics-seed.json +454 -0
  138. package/dist-server/migrations/seed-data/kpi-org-scope-seed.json +1676 -0
  139. package/dist-server/migrations/seed-data/kpi-values-seed.json +402 -0
  140. package/dist-server/migrations/seed-data/kpis-seed.json +488 -0
  141. package/dist-server/service/index.d.ts +3 -7
  142. package/dist-server/service/index.js +5 -12
  143. package/dist-server/service/index.js.map +1 -1
  144. package/dist-server/service/kpi/aggregate-kpi.js +30 -13
  145. package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
  146. package/dist-server/service/kpi/kpi-formula.service.d.ts +15 -0
  147. package/dist-server/service/kpi/kpi-formula.service.js +90 -0
  148. package/dist-server/service/kpi/kpi-formula.service.js.map +1 -1
  149. package/dist-server/service/kpi/kpi-history.d.ts +0 -3
  150. package/dist-server/service/kpi/kpi-history.js +0 -10
  151. package/dist-server/service/kpi/kpi-history.js.map +1 -1
  152. package/dist-server/service/kpi/kpi-mutation.d.ts +1 -1
  153. package/dist-server/service/kpi/kpi-mutation.js +57 -20
  154. package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
  155. package/dist-server/service/kpi/kpi-query.d.ts +7 -3
  156. package/dist-server/service/kpi/kpi-query.js +126 -10
  157. package/dist-server/service/kpi/kpi-query.js.map +1 -1
  158. package/dist-server/service/kpi/kpi-type.d.ts +4 -2
  159. package/dist-server/service/kpi/kpi-type.js +12 -4
  160. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  161. package/dist-server/service/kpi/kpi.d.ts +4 -3
  162. package/dist-server/service/kpi/kpi.js +20 -8
  163. package/dist-server/service/kpi/kpi.js.map +1 -1
  164. package/dist-server/service/kpi-metric/aggregate-kpi-metric.js +46 -11
  165. package/dist-server/service/kpi-metric/aggregate-kpi-metric.js.map +1 -1
  166. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +1 -1
  167. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +6 -6
  168. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  169. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.d.ts +2 -2
  170. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js +4 -4
  171. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js.map +1 -1
  172. package/dist-server/service/kpi-metric-value/kpi-metric-value.d.ts +1 -1
  173. package/dist-server/service/kpi-metric-value/kpi-metric-value.js +3 -3
  174. package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -1
  175. package/dist-server/service/kpi-org-scope/index.d.ts +9 -0
  176. package/dist-server/service/kpi-org-scope/index.js +14 -0
  177. package/dist-server/service/kpi-org-scope/index.js.map +1 -0
  178. package/dist-server/service/kpi-org-scope/kpi-org-scope-mutation.d.ts +8 -0
  179. package/dist-server/service/kpi-org-scope/kpi-org-scope-mutation.js +170 -0
  180. package/dist-server/service/kpi-org-scope/kpi-org-scope-mutation.js.map +1 -0
  181. package/dist-server/service/kpi-org-scope/kpi-org-scope-query.d.ts +14 -0
  182. package/dist-server/service/kpi-org-scope/kpi-org-scope-query.js +152 -0
  183. package/dist-server/service/kpi-org-scope/kpi-org-scope-query.js.map +1 -0
  184. package/dist-server/service/kpi-org-scope/kpi-org-scope-type.d.ts +26 -0
  185. package/dist-server/service/kpi-org-scope/kpi-org-scope-type.js +101 -0
  186. package/dist-server/service/kpi-org-scope/kpi-org-scope-type.js.map +1 -0
  187. package/dist-server/service/kpi-org-scope/kpi-org-scope.d.ts +26 -0
  188. package/dist-server/service/kpi-org-scope/kpi-org-scope.js +135 -0
  189. package/dist-server/service/kpi-org-scope/kpi-org-scope.js.map +1 -0
  190. package/dist-server/service/kpi-statistic/kpi-statistic.d.ts +1 -0
  191. package/dist-server/service/kpi-statistic/kpi-statistic.js +11 -0
  192. package/dist-server/service/kpi-statistic/kpi-statistic.js.map +1 -1
  193. package/dist-server/service/kpi-value/kpi-value-mutation.js +71 -7
  194. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  195. package/dist-server/service/kpi-value/kpi-value-type.d.ts +4 -2
  196. package/dist-server/service/kpi-value/kpi-value-type.js +12 -4
  197. package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
  198. package/dist-server/service/kpi-value/kpi-value.d.ts +3 -1
  199. package/dist-server/service/kpi-value/kpi-value.js +11 -5
  200. package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
  201. package/dist-server/service/utils/value-date-util.d.ts +1 -0
  202. package/dist-server/service/utils/value-date-util.js +41 -0
  203. package/dist-server/service/utils/value-date-util.js.map +1 -1
  204. package/dist-server/tsconfig.json +10 -0
  205. package/dist-server/tsconfig.tsbuildinfo +1 -1
  206. package/package.json +7 -6
  207. package/server/@types/index.d.ts +11 -0
  208. package/server/controllers/kpi-metric-value-provider.ts +5 -5
  209. package/server/controllers/kpi-value-provider.ts +4 -4
  210. package/server/migrations/1752190849680-seed-kpi-metrics.ts +124 -0
  211. package/server/migrations/1752190849681-seed-kpi.ts +356 -0
  212. package/server/migrations/1752192090123-add-grades-to-kpi.ts +67 -0
  213. package/server/migrations/1752192090124-add-kpi-statistics.ts +719 -0
  214. package/server/migrations/1752192090128-seed-kpi-org-scope.ts +132 -0
  215. package/server/migrations/1752192090129-seed-kpi-values.ts +207 -0
  216. package/server/migrations/grade-data/x11-performance-table.json +962 -0
  217. package/server/migrations/grade-data/x12-performance-table.json +611 -0
  218. package/server/migrations/grade-data/x14-performance-table.json +42 -0
  219. package/server/migrations/grade-data/x21-performance-table.json +889 -0
  220. package/server/migrations/grade-data/x22-performance-table.json +1064 -0
  221. package/server/migrations/grade-data/x23-performance-table.json +42 -0
  222. package/server/migrations/grade-data/x31-performance-table.json +644 -0
  223. package/server/migrations/grade-data/x32-performance-table.json +993 -0
  224. package/server/migrations/grade-data/x33-performance-table.json +195 -0
  225. package/server/migrations/grade-data/x34-performance-table.json +12 -0
  226. package/server/migrations/grade-data/x35-performance-table.json +42 -0
  227. package/server/migrations/grade-data/x41-performance-table.json +825 -0
  228. package/server/migrations/grade-data/x42-performance-table.json +786 -0
  229. package/server/migrations/grade-data/x43-performance-table.json +12 -0
  230. package/server/migrations/grade-data/x44-performance-table.json +42 -0
  231. package/server/migrations/grade-data/x51-performance-table.json +924 -0
  232. package/server/migrations/grade-data/x52-performance-table.json +42 -0
  233. package/server/migrations/grade-data/x61-performance-table.json +261 -0
  234. package/server/migrations/grade-data/x62-performance-table.json +42 -0
  235. package/server/migrations/seed-data/kpi-metrics-seed.json +454 -0
  236. package/server/migrations/seed-data/kpi-org-scope-seed.json +1676 -0
  237. package/server/migrations/seed-data/kpi-values-seed.json +402 -0
  238. package/server/migrations/seed-data/kpis-seed.json +488 -0
  239. package/server/service/index.ts +5 -12
  240. package/server/service/kpi/aggregate-kpi.ts +31 -13
  241. package/server/service/kpi/kpi-formula.service.ts +101 -0
  242. package/server/service/kpi/kpi-history.ts +0 -8
  243. package/server/service/kpi/kpi-mutation.ts +59 -19
  244. package/server/service/kpi/kpi-query.ts +118 -8
  245. package/server/service/kpi/kpi-type.ts +10 -4
  246. package/server/service/kpi/kpi.ts +17 -7
  247. package/server/service/kpi-metric/aggregate-kpi-metric.ts +55 -11
  248. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +6 -6
  249. package/server/service/kpi-metric-value/kpi-metric-value-type.ts +4 -4
  250. package/server/service/kpi-metric-value/kpi-metric-value.ts +3 -3
  251. package/server/service/kpi-org-scope/index.ts +11 -0
  252. package/server/service/kpi-org-scope/kpi-org-scope-mutation.ts +173 -0
  253. package/server/service/kpi-org-scope/kpi-org-scope-query.ts +127 -0
  254. package/server/service/kpi-org-scope/kpi-org-scope-type.ts +68 -0
  255. package/server/service/kpi-org-scope/kpi-org-scope.ts +123 -0
  256. package/server/service/kpi-statistic/kpi-statistic.ts +10 -0
  257. package/server/service/kpi-value/kpi-value-mutation.ts +73 -7
  258. package/server/service/kpi-value/kpi-value-type.ts +10 -4
  259. package/server/service/kpi-value/kpi-value.ts +10 -5
  260. package/server/service/utils/value-date-util.ts +47 -0
  261. package/server/types/global.d.ts +8 -0
  262. package/things-factory.config.js +1 -0
  263. package/translations/en.json +15 -3
  264. package/translations/ja.json +13 -3
  265. package/translations/ko.json +15 -3
  266. package/translations/ms.json +13 -3
  267. package/translations/zh.json +13 -3
  268. package/client/pages/kpi-category/kpi-category-importer.ts +0 -90
  269. package/client/pages/kpi-category/kpi-category-list-page.ts +0 -537
  270. package/client/pages/kpi-category/kpi-category-value-calculator.ts +0 -233
  271. package/client/pages/kpi-category-value/kpi-category-value-list-page.ts +0 -404
  272. package/dist-client/pages/kpi-category/kpi-category-importer.d.ts +0 -23
  273. package/dist-client/pages/kpi-category/kpi-category-importer.js +0 -92
  274. package/dist-client/pages/kpi-category/kpi-category-importer.js.map +0 -1
  275. package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +0 -74
  276. package/dist-client/pages/kpi-category/kpi-category-list-page.js +0 -517
  277. package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +0 -1
  278. package/dist-client/pages/kpi-category/kpi-category-value-calculator.d.ts +0 -13
  279. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js +0 -256
  280. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js.map +0 -1
  281. package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.d.ts +0 -63
  282. package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.js +0 -393
  283. package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.js.map +0 -1
  284. package/dist-server/service/kpi-category/index.d.ts +0 -6
  285. package/dist-server/service/kpi-category/index.js +0 -10
  286. package/dist-server/service/kpi-category/index.js.map +0 -1
  287. package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +0 -9
  288. package/dist-server/service/kpi-category/kpi-category-mutation.js +0 -221
  289. package/dist-server/service/kpi-category/kpi-category-mutation.js.map +0 -1
  290. package/dist-server/service/kpi-category/kpi-category-query.d.ts +0 -18
  291. package/dist-server/service/kpi-category/kpi-category-query.js +0 -115
  292. package/dist-server/service/kpi-category/kpi-category-query.js.map +0 -1
  293. package/dist-server/service/kpi-category/kpi-category-type.d.ts +0 -24
  294. package/dist-server/service/kpi-category/kpi-category-type.js +0 -100
  295. package/dist-server/service/kpi-category/kpi-category-type.js.map +0 -1
  296. package/dist-server/service/kpi-category/kpi-category.d.ts +0 -22
  297. package/dist-server/service/kpi-category/kpi-category.js +0 -106
  298. package/dist-server/service/kpi-category/kpi-category.js.map +0 -1
  299. package/dist-server/service/kpi-category-value/index.d.ts +0 -6
  300. package/dist-server/service/kpi-category-value/index.js +0 -10
  301. package/dist-server/service/kpi-category-value/index.js.map +0 -1
  302. package/dist-server/service/kpi-category-value/kpi-category-value-mutation.d.ts +0 -8
  303. package/dist-server/service/kpi-category-value/kpi-category-value-mutation.js +0 -102
  304. package/dist-server/service/kpi-category-value/kpi-category-value-mutation.js.map +0 -1
  305. package/dist-server/service/kpi-category-value/kpi-category-value-query.d.ts +0 -13
  306. package/dist-server/service/kpi-category-value/kpi-category-value-query.js +0 -91
  307. package/dist-server/service/kpi-category-value/kpi-category-value-query.js.map +0 -1
  308. package/dist-server/service/kpi-category-value/kpi-category-value-type.d.ts +0 -19
  309. package/dist-server/service/kpi-category-value/kpi-category-value-type.js +0 -73
  310. package/dist-server/service/kpi-category-value/kpi-category-value-type.js.map +0 -1
  311. package/dist-server/service/kpi-category-value/kpi-category-value.d.ts +0 -19
  312. package/dist-server/service/kpi-category-value/kpi-category-value.js +0 -91
  313. package/dist-server/service/kpi-category-value/kpi-category-value.js.map +0 -1
  314. package/helps/kpi/kpi-category.md +0 -160
  315. package/server/service/kpi-category/index.ts +0 -7
  316. package/server/service/kpi-category/kpi-category-mutation.ts +0 -217
  317. package/server/service/kpi-category/kpi-category-query.ts +0 -87
  318. package/server/service/kpi-category/kpi-category-type.ts +0 -73
  319. package/server/service/kpi-category/kpi-category.ts +0 -95
  320. package/server/service/kpi-category-value/index.ts +0 -7
  321. package/server/service/kpi-category-value/kpi-category-value-mutation.ts +0 -88
  322. package/server/service/kpi-category-value/kpi-category-value-query.ts +0 -62
  323. package/server/service/kpi-category-value/kpi-category-value-type.ts +0 -48
  324. package/server/service/kpi-category-value/kpi-category-value.ts +0 -79
@@ -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,3 +1,4 @@
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'
@@ -7,10 +8,67 @@ import { KpiList } from './kpi-type'
7
8
  import { KpiValue } from '../kpi-value/kpi-value'
8
9
  import { KpiHistory } from './kpi-history'
9
10
  import { Int } from 'type-graphql'
10
- import { KpiCategory } from '../kpi-category/kpi-category'
11
11
 
12
12
  @Resolver(Kpi)
13
13
  export class KpiQuery {
14
+ @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
15
+ @Query(returns => [Kpi], { description: 'Fetch root KPIs (KPIs without parent)' })
16
+ async kpisLevel0(@Ctx() context: ResolverContext): Promise<Kpi[]> {
17
+ const { domain } = context.state
18
+
19
+ return await getRepository(Kpi).find({
20
+ where: { domain: { id: domain.id }, parent: null },
21
+ order: { name: 'ASC' }
22
+ })
23
+ }
24
+
25
+ @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
26
+ @Query(returns => [Kpi], {
27
+ description:
28
+ '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)'
29
+ })
30
+ async kpisLevel1(
31
+ @Arg('rootId', {
32
+ nullable: true,
33
+ description: 'ID of the parent KPI to get level 1 children. If not provided, returns all level 1 KPIs'
34
+ })
35
+ rootId: string,
36
+ @Ctx() context: ResolverContext
37
+ ): Promise<Kpi[]> {
38
+ const { domain } = context.state
39
+
40
+ let whereCondition: any = { domain: { id: domain.id } }
41
+
42
+ if (rootId) {
43
+ // 특정 부모의 자식들
44
+ whereCondition.parent = { id: rootId }
45
+ } else {
46
+ // 모든 level 1 KPI들 (root KPI들의 직계 자식들)
47
+ // 부모가 있으면서 부모의 부모가 null인 KPI들
48
+ const kpis = await getRepository(Kpi)
49
+ .createQueryBuilder('kpi')
50
+ .leftJoinAndSelect('kpi.parent', 'parent')
51
+ .leftJoinAndSelect('kpi.children', 'children')
52
+ .leftJoinAndSelect('children.children', 'grandchildren')
53
+ .where('kpi.domain = :domainId', { domainId: domain.id })
54
+ .andWhere('parent.id IS NOT NULL')
55
+ .andWhere('parent.parent IS NULL')
56
+ .orderBy('kpi.name', 'ASC')
57
+ .getMany()
58
+
59
+ return kpis
60
+ }
61
+
62
+ // rootId 제공된 경우
63
+ const kpis = await getRepository(Kpi).find({
64
+ where: whereCondition,
65
+ relations: ['children', 'children.children'],
66
+ order: { name: 'ASC' }
67
+ })
68
+
69
+ return kpis
70
+ }
71
+
14
72
  @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
15
73
  @Query(returns => Kpi!, { nullable: true, description: 'Fetch a single KPI by its unique identifier.' })
16
74
  async kpi(
@@ -33,7 +91,10 @@ export class KpiQuery {
33
91
  domain,
34
92
  params,
35
93
  repository: await getRepository(Kpi),
36
- searchables: ['name', 'description']
94
+ searchables: ['name', 'description'],
95
+ filtersMap: {
96
+ parent: { columnName: 'id', relationColumn: 'parent' }
97
+ }
37
98
  })
38
99
 
39
100
  const [items, total] = await queryBuilder.getManyAndCount()
@@ -41,6 +102,13 @@ export class KpiQuery {
41
102
  return { items, total }
42
103
  }
43
104
 
105
+ @FieldResolver(type => [Kpi])
106
+ children(@Root() kpi: Kpi): Promise<Kpi[]> {
107
+ return getRepository(Kpi).find({
108
+ where: { parent: { id: kpi.id } }
109
+ })
110
+ }
111
+
44
112
  @FieldResolver(type => String)
45
113
  async thumbnail(@Root() kpi: Kpi): Promise<string | undefined> {
46
114
  const attachment: Attachment = await getRepository(Attachment).findOne({
@@ -70,10 +138,26 @@ export class KpiQuery {
70
138
  }
71
139
 
72
140
  @FieldResolver(type => KpiValue, { nullable: true })
73
- async value(@Root() kpi: Kpi, @Ctx() context): Promise<KpiValue | null> {
141
+ async value(
142
+ @Root() kpi: Kpi,
143
+ @Ctx() context,
144
+ @Arg('orgScope', { nullable: true, description: 'Organization scope filter for value' }) orgScope?: string
145
+ ): Promise<KpiValue | null> {
74
146
  const { domain } = context.state
147
+
148
+ let whereCondition: any = {
149
+ domain: { id: domain.id },
150
+ kpi: { id: kpi.id }
151
+ }
152
+
153
+ // orgScope 필터가 있으면 KpiOrgScope로 필터링
154
+ if (orgScope) {
155
+ whereCondition.kpiOrgScope = { org: orgScope }
156
+ }
157
+
75
158
  return await getRepository(KpiValue).findOne({
76
- where: { domain: { id: domain.id }, kpi: { id: kpi.id } },
159
+ where: whereCondition,
160
+ relations: ['kpiOrgScope'],
77
161
  order: { valueDate: 'DESC' }
78
162
  })
79
163
  }
@@ -88,10 +172,10 @@ export class KpiQuery {
88
172
  return kpi.vizMeta?.unit
89
173
  }
90
174
 
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 })
175
+ @FieldResolver(type => Kpi, { nullable: true })
176
+ async parent(@Root() kpi: Kpi): Promise<Kpi | null> {
177
+ if (!kpi.parentId) return null
178
+ return await getRepository(Kpi).findOneBy({ id: kpi.parentId })
95
179
  }
96
180
 
97
181
  @FieldResolver(type => [KpiHistory], { nullable: true })
@@ -110,4 +194,30 @@ export class KpiQuery {
110
194
  else qb.limit(1)
111
195
  return await qb.getMany()
112
196
  }
197
+
198
+ @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
199
+ @Query(returns => [Kpi], { description: 'Fetch KPIs in hierarchical tree structure' })
200
+ async kpiTree(@Ctx() context: ResolverContext): Promise<Kpi[]> {
201
+ const { domain } = context.state
202
+
203
+ return await getRepository(Kpi).find({
204
+ where: { domain: { id: domain.id }, parent: IsNull() },
205
+ relations: ['children', 'children.children', 'children.children.children'],
206
+ order: { name: 'ASC' }
207
+ })
208
+ }
209
+
210
+ @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
211
+ @Query(returns => [Kpi], { description: 'Fetch child KPIs of a given parent KPI' })
212
+ async kpiChildren(
213
+ @Arg('parentId', { description: 'ID of the parent KPI' }) parentId: string,
214
+ @Ctx() context: ResolverContext
215
+ ): Promise<Kpi[]> {
216
+ const { domain } = context.state
217
+
218
+ return await getRepository(Kpi).find({
219
+ where: { domain: { id: domain.id }, parent: { id: parentId } },
220
+ order: { name: 'ASC' }
221
+ })
222
+ }
113
223
  }
@@ -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,
@@ -1,9 +1,11 @@
1
1
  import { getRepository } from '@things-factory/shell'
2
+ import type ResolverContext from '@things-factory/auth-base'
2
3
  import { DataSummary } from '@things-factory/dataset'
3
4
  import { finalizeLatestDataCollection } from '@things-factory/dataset/dist-server/controllers/finalize-data-collection'
4
5
  import { KpiMetric } from './kpi-metric'
5
6
  import { KpiValue, KpiValueInputType } from '../kpi-value/kpi-value'
6
7
  import { Kpi } from '../kpi/kpi'
8
+ import { KpiOrgScope } from '../kpi-org-scope/kpi-org-scope'
7
9
  import { KpiFormulaService } from '../kpi/kpi-formula.service'
8
10
 
9
11
  /**
@@ -38,7 +40,7 @@ export async function aggregateKpiMetricValue(metricId: string, domainId: string
38
40
  date: summary.date,
39
41
  period: summary.period,
40
42
  value: summary.summary[metric.fieldName],
41
- group: {
43
+ org: {
42
44
  key01: summary.key01,
43
45
  key02: summary.key02,
44
46
  key03: summary.key03,
@@ -56,32 +58,74 @@ export async function aggregateKpiMetricValue(metricId: string, domainId: string
56
58
  const kpi = await getRepository(Kpi).findOne({ where: { name: metric.name, domain: { id: domainId } } })
57
59
  if (!kpi) throw new Error('KPI 정보 없음 (metric.name과 동일한 KPI name 기준, 실제 연동 구조에 맞게 보완 필요)')
58
60
 
59
- // KPI Value version, valueDate, group 등 매핑
61
+ // KPI Value version, valueDate, org 등 매핑
60
62
  const savedValues = []
63
+ const kpiOrgScopeRepo = getRepository(KpiOrgScope, context.state?.tx)
64
+ const kpiValueRepo = getRepository(KpiValue, context.state?.tx)
65
+
61
66
  for (const v of values) {
62
67
  const valueDate = v.date
63
- const group = v.group
68
+ const orgData = v.org
64
69
  const version = kpi.version || 1
65
70
  const value = v.value
66
71
  if (value == null || isNaN(value)) continue
67
- // upsert(동일 KPI, valueDate, group, version) 기준으로 저장
68
- const repo = getRepository(KpiValue, context.state?.tx)
69
- const existing = await repo.findOne({
70
- where: { kpi: { id: kpi.id }, valueDate, group, version, domain: { id: domainId } }
72
+
73
+ // KpiOrgScope 찾기 또는 생성
74
+ let kpiOrgScope: KpiOrgScope | null = null
75
+
76
+ // key들로 조합된 org 식별자 생성
77
+ const orgIdentifier =
78
+ [orgData.key01, orgData.key02, orgData.key03, orgData.key04, orgData.key05].filter(Boolean).join('-') ||
79
+ 'unknown-org'
80
+
81
+ // 기존 KpiOrgScope 조회 (entityName 기준)
82
+ kpiOrgScope = await kpiOrgScopeRepo.findOne({
83
+ where: { entityName: orgIdentifier, domain: { id: domainId } }
71
84
  })
72
- let entity = existing || repo.create()
85
+
86
+ // 없으면 새로 생성
87
+ if (!kpiOrgScope) {
88
+ kpiOrgScope = await kpiOrgScopeRepo.save({
89
+ entityType: 'DataSummary', // DataSummary에서 온 데이터임을 표시
90
+ entityId: `datasummary-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
91
+ entityName: orgIdentifier,
92
+ org: orgIdentifier, // legacy field
93
+ scope01: orgData.key01,
94
+ scope02: orgData.key02,
95
+ scope03: orgData.key03,
96
+ scope04: orgData.key04,
97
+ scope05: orgData.key05,
98
+ domain: kpi.domain,
99
+ creator: context.state?.user,
100
+ updater: context.state?.user
101
+ })
102
+ }
103
+
104
+ // upsert KpiValue (동일 KPI, valueDate, kpiOrgScope, version 기준)
105
+ const existing = await kpiValueRepo.findOne({
106
+ where: {
107
+ kpi: { id: kpi.id },
108
+ valueDate,
109
+ kpiOrgScope: { id: kpiOrgScope.id },
110
+ version,
111
+ domain: { id: domainId }
112
+ }
113
+ })
114
+
115
+ let entity = existing || kpiValueRepo.create()
73
116
  entity.kpi = kpi
74
117
  entity.kpiId = kpi.id
75
118
  entity.version = version
76
119
  entity.valueDate = valueDate
77
120
  entity.value = value
78
- entity.group = group
121
+ entity.kpiOrgScope = kpiOrgScope
122
+ entity.kpiOrgScopeId = kpiOrgScope.id
79
123
  entity.inputType = KpiValueInputType.AUTO
80
- entity.source = 'AUTO'
124
+ entity.source = 'AUTO-AGGREGATE'
81
125
  entity.domain = kpi.domain
82
126
  entity.creator = context.state?.user
83
127
  entity.updater = context.state?.user
84
- entity = await repo.save(entity)
128
+ entity = await kpiValueRepo.save(entity)
85
129
  savedValues.push(entity)
86
130
  }
87
131
  return savedValues