@things-factory/ops-master 10.0.18 → 10.1.0

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 (269) hide show
  1. package/dist-client/bootstrap.d.ts +18 -0
  2. package/dist-client/bootstrap.js +36 -0
  3. package/dist-client/bootstrap.js.map +1 -0
  4. package/dist-client/index.d.ts +15 -1
  5. package/dist-client/index.js +18 -1
  6. package/dist-client/index.js.map +1 -1
  7. package/dist-client/pages/master/equipment-class.d.ts +12 -0
  8. package/dist-client/pages/master/equipment-class.js +42 -0
  9. package/dist-client/pages/master/equipment-class.js.map +1 -0
  10. package/dist-client/pages/master/equipment.d.ts +12 -0
  11. package/dist-client/pages/master/equipment.js +57 -0
  12. package/dist-client/pages/master/equipment.js.map +1 -0
  13. package/dist-client/pages/master/grist-iso-duration.d.ts +20 -0
  14. package/dist-client/pages/master/grist-iso-duration.js +70 -0
  15. package/dist-client/pages/master/grist-iso-duration.js.map +1 -0
  16. package/dist-client/pages/master/grist-reference.d.ts +91 -0
  17. package/dist-client/pages/master/grist-reference.js +419 -0
  18. package/dist-client/pages/master/grist-reference.js.map +1 -0
  19. package/dist-client/pages/master/import-rows.d.ts +35 -0
  20. package/dist-client/pages/master/import-rows.js +66 -0
  21. package/dist-client/pages/master/import-rows.js.map +1 -0
  22. package/dist-client/pages/master/iso-duration.d.ts +34 -0
  23. package/dist-client/pages/master/iso-duration.js +76 -0
  24. package/dist-client/pages/master/iso-duration.js.map +1 -0
  25. package/dist-client/pages/master/master-options.d.ts +33 -0
  26. package/dist-client/pages/master/master-options.js +47 -0
  27. package/dist-client/pages/master/master-options.js.map +1 -0
  28. package/dist-client/pages/master/master-page-base.d.ts +357 -0
  29. package/dist-client/pages/master/master-page-base.js +626 -0
  30. package/dist-client/pages/master/master-page-base.js.map +1 -0
  31. package/dist-client/pages/master/material-class.d.ts +12 -0
  32. package/dist-client/pages/master/material-class.js +42 -0
  33. package/dist-client/pages/master/material-class.js.map +1 -0
  34. package/dist-client/pages/master/material-definition.d.ts +12 -0
  35. package/dist-client/pages/master/material-definition.js +62 -0
  36. package/dist-client/pages/master/material-definition.js.map +1 -0
  37. package/dist-client/pages/master/operation-definition.d.ts +12 -0
  38. package/dist-client/pages/master/operation-definition.js +49 -0
  39. package/dist-client/pages/master/operation-definition.js.map +1 -0
  40. package/dist-client/pages/master/operational-location.d.ts +84 -0
  41. package/dist-client/pages/master/operational-location.js +426 -0
  42. package/dist-client/pages/master/operational-location.js.map +1 -0
  43. package/dist-client/pages/master/person.d.ts +12 -0
  44. package/dist-client/pages/master/person.js +41 -0
  45. package/dist-client/pages/master/person.js.map +1 -0
  46. package/dist-client/pages/master/personnel-class.d.ts +12 -0
  47. package/dist-client/pages/master/personnel-class.js +43 -0
  48. package/dist-client/pages/master/personnel-class.js.map +1 -0
  49. package/dist-client/pages/master/physical-asset-class.d.ts +12 -0
  50. package/dist-client/pages/master/physical-asset-class.js +40 -0
  51. package/dist-client/pages/master/physical-asset-class.js.map +1 -0
  52. package/dist-client/pages/master/physical-asset.d.ts +12 -0
  53. package/dist-client/pages/master/physical-asset.js +41 -0
  54. package/dist-client/pages/master/physical-asset.js.map +1 -0
  55. package/dist-client/pages/master/recipe-definition.d.ts +136 -0
  56. package/dist-client/pages/master/recipe-definition.js +404 -0
  57. package/dist-client/pages/master/recipe-definition.js.map +1 -0
  58. package/dist-client/pages/master/reference-targets.d.ts +42 -0
  59. package/dist-client/pages/master/reference-targets.js +82 -0
  60. package/dist-client/pages/master/reference-targets.js.map +1 -0
  61. package/dist-client/pages/master/route-definition.d.ts +19 -0
  62. package/dist-client/pages/master/route-definition.js +40 -0
  63. package/dist-client/pages/master/route-definition.js.map +1 -0
  64. package/dist-client/pages/master/stop-reason.d.ts +12 -0
  65. package/dist-client/pages/master/stop-reason.js +48 -0
  66. package/dist-client/pages/master/stop-reason.js.map +1 -0
  67. package/dist-client/pages/master/test-specification.d.ts +93 -0
  68. package/dist-client/pages/master/test-specification.js +449 -0
  69. package/dist-client/pages/master/test-specification.js.map +1 -0
  70. package/dist-client/pages/master-axes.d.ts +20 -29
  71. package/dist-client/pages/master-axes.js +32 -169
  72. package/dist-client/pages/master-axes.js.map +1 -1
  73. package/dist-client/route.d.ts +1 -1
  74. package/dist-client/route.js +76 -3
  75. package/dist-client/route.js.map +1 -1
  76. package/dist-client/tsconfig.tsbuildinfo +1 -1
  77. package/dist-server/service/asset/asset-lifecycle.d.ts +30 -7
  78. package/dist-server/service/asset/asset-lifecycle.js +33 -6
  79. package/dist-server/service/asset/asset-lifecycle.js.map +1 -1
  80. package/dist-server/service/asset/asset-master-resolver.d.ts +9 -0
  81. package/dist-server/service/asset/asset-master-resolver.js +41 -0
  82. package/dist-server/service/asset/asset-master-resolver.js.map +1 -1
  83. package/dist-server/service/asset/asset-master.js +1 -1
  84. package/dist-server/service/asset/asset-master.js.map +1 -1
  85. package/dist-server/service/asset/asset-query.d.ts +9 -1
  86. package/dist-server/service/asset/asset-query.js +28 -6
  87. package/dist-server/service/asset/asset-query.js.map +1 -1
  88. package/dist-server/service/asset/physical-asset-class.js +1 -1
  89. package/dist-server/service/asset/physical-asset-class.js.map +1 -1
  90. package/dist-server/service/asset/physical-asset.d.ts +17 -4
  91. package/dist-server/service/asset/physical-asset.js +6 -6
  92. package/dist-server/service/asset/physical-asset.js.map +1 -1
  93. package/dist-server/service/equipment/equipment-class.js +1 -1
  94. package/dist-server/service/equipment/equipment-class.js.map +1 -1
  95. package/dist-server/service/equipment/equipment-master-resolver.d.ts +9 -0
  96. package/dist-server/service/equipment/equipment-master-resolver.js +58 -0
  97. package/dist-server/service/equipment/equipment-master-resolver.js.map +1 -1
  98. package/dist-server/service/equipment/equipment-master.d.ts +3 -2
  99. package/dist-server/service/equipment/equipment-master.js +21 -4
  100. package/dist-server/service/equipment/equipment-master.js.map +1 -1
  101. package/dist-server/service/equipment/equipment-query.d.ts +9 -1
  102. package/dist-server/service/equipment/equipment-query.js +28 -6
  103. package/dist-server/service/equipment/equipment-query.js.map +1 -1
  104. package/dist-server/service/equipment/equipment.d.ts +27 -1
  105. package/dist-server/service/equipment/equipment.js +8 -3
  106. package/dist-server/service/equipment/equipment.js.map +1 -1
  107. package/dist-server/service/equipment/state-period-rules.d.ts +19 -0
  108. package/dist-server/service/equipment/state-period-rules.js +35 -1
  109. package/dist-server/service/equipment/state-period-rules.js.map +1 -1
  110. package/dist-server/service/index.d.ts +12 -2
  111. package/dist-server/service/index.js +21 -1
  112. package/dist-server/service/index.js.map +1 -1
  113. package/dist-server/service/location/location-master-resolver.d.ts +29 -0
  114. package/dist-server/service/location/location-master-resolver.js +228 -1
  115. package/dist-server/service/location/location-master-resolver.js.map +1 -1
  116. package/dist-server/service/location/location-query.d.ts +9 -1
  117. package/dist-server/service/location/location-query.js +24 -9
  118. package/dist-server/service/location/location-query.js.map +1 -1
  119. package/dist-server/service/location/operational-location.d.ts +37 -0
  120. package/dist-server/service/location/operational-location.js +22 -2
  121. package/dist-server/service/location/operational-location.js.map +1 -1
  122. package/dist-server/service/master-axis.d.ts +1 -1
  123. package/dist-server/service/master-axis.js +16 -1
  124. package/dist-server/service/master-axis.js.map +1 -1
  125. package/dist-server/service/master-conformance-resolver.d.ts +34 -0
  126. package/dist-server/service/master-conformance-resolver.js +143 -0
  127. package/dist-server/service/master-conformance-resolver.js.map +1 -0
  128. package/dist-server/service/master-conformance.d.ts +80 -0
  129. package/dist-server/service/master-conformance.js +266 -0
  130. package/dist-server/service/master-conformance.js.map +1 -0
  131. package/dist-server/service/master-page-query.d.ts +33 -0
  132. package/dist-server/service/master-page-query.js +55 -0
  133. package/dist-server/service/master-page-query.js.map +1 -0
  134. package/dist-server/service/master-page.d.ts +85 -0
  135. package/dist-server/service/master-page.js +104 -0
  136. package/dist-server/service/master-page.js.map +1 -0
  137. package/dist-server/service/material/material-class.js +1 -1
  138. package/dist-server/service/material/material-class.js.map +1 -1
  139. package/dist-server/service/material/material-definition.js +2 -2
  140. package/dist-server/service/material/material-definition.js.map +1 -1
  141. package/dist-server/service/material/material-master-resolver.d.ts +62 -0
  142. package/dist-server/service/material/material-master-resolver.js +213 -0
  143. package/dist-server/service/material/material-master-resolver.js.map +1 -1
  144. package/dist-server/service/material/material-query.d.ts +9 -1
  145. package/dist-server/service/material/material-query.js +28 -6
  146. package/dist-server/service/material/material-query.js.map +1 -1
  147. package/dist-server/service/material/recipe-index.d.ts +23 -0
  148. package/dist-server/service/material/recipe-index.js +44 -0
  149. package/dist-server/service/material/recipe-index.js.map +1 -0
  150. package/dist-server/service/operation/index.d.ts +2 -1
  151. package/dist-server/service/operation/index.js +2 -1
  152. package/dist-server/service/operation/index.js.map +1 -1
  153. package/dist-server/service/operation/operation-definition.d.ts +1 -1
  154. package/dist-server/service/operation/operation-definition.js +4 -4
  155. package/dist-server/service/operation/operation-definition.js.map +1 -1
  156. package/dist-server/service/operation/operation-master-resolver.d.ts +24 -0
  157. package/dist-server/service/operation/operation-master-resolver.js +107 -0
  158. package/dist-server/service/operation/operation-master-resolver.js.map +1 -1
  159. package/dist-server/service/operation/operation-query.d.ts +10 -2
  160. package/dist-server/service/operation/operation-query.js +25 -10
  161. package/dist-server/service/operation/operation-query.js.map +1 -1
  162. package/dist-server/service/operation/operation-readiness-resolver.d.ts +39 -0
  163. package/dist-server/service/operation/operation-readiness-resolver.js +111 -0
  164. package/dist-server/service/operation/operation-readiness-resolver.js.map +1 -0
  165. package/dist-server/service/operation/operation-readiness.d.ts +34 -0
  166. package/dist-server/service/operation/operation-readiness.js +133 -0
  167. package/dist-server/service/operation/operation-readiness.js.map +1 -0
  168. package/dist-server/service/personnel/person.js +3 -3
  169. package/dist-server/service/personnel/person.js.map +1 -1
  170. package/dist-server/service/personnel/personnel-class-readiness.d.ts +22 -0
  171. package/dist-server/service/personnel/personnel-class-readiness.js +98 -0
  172. package/dist-server/service/personnel/personnel-class-readiness.js.map +1 -0
  173. package/dist-server/service/personnel/personnel-class.d.ts +1 -1
  174. package/dist-server/service/personnel/personnel-class.js +2 -2
  175. package/dist-server/service/personnel/personnel-class.js.map +1 -1
  176. package/dist-server/service/personnel/personnel-master-resolver.d.ts +9 -0
  177. package/dist-server/service/personnel/personnel-master-resolver.js +43 -0
  178. package/dist-server/service/personnel/personnel-master-resolver.js.map +1 -1
  179. package/dist-server/service/personnel/personnel-master.js +1 -1
  180. package/dist-server/service/personnel/personnel-master.js.map +1 -1
  181. package/dist-server/service/personnel/personnel-query.d.ts +9 -1
  182. package/dist-server/service/personnel/personnel-query.js +28 -6
  183. package/dist-server/service/personnel/personnel-query.js.map +1 -1
  184. package/dist-server/service/personnel/qualification.d.ts +7 -0
  185. package/dist-server/service/personnel/qualification.js +25 -0
  186. package/dist-server/service/personnel/qualification.js.map +1 -1
  187. package/dist-server/service/recipe/index.d.ts +9 -0
  188. package/dist-server/service/recipe/index.js +14 -0
  189. package/dist-server/service/recipe/index.js.map +1 -0
  190. package/dist-server/service/recipe/recipe-definition.d.ts +71 -0
  191. package/dist-server/service/recipe/recipe-definition.js +130 -0
  192. package/dist-server/service/recipe/recipe-definition.js.map +1 -0
  193. package/dist-server/service/recipe/recipe-master-resolver.d.ts +28 -0
  194. package/dist-server/service/recipe/recipe-master-resolver.js +124 -0
  195. package/dist-server/service/recipe/recipe-master-resolver.js.map +1 -0
  196. package/dist-server/service/recipe/recipe-master.d.ts +37 -0
  197. package/dist-server/service/recipe/recipe-master.js +47 -0
  198. package/dist-server/service/recipe/recipe-master.js.map +1 -0
  199. package/dist-server/service/recipe/recipe-query.d.ts +21 -0
  200. package/dist-server/service/recipe/recipe-query.js +75 -0
  201. package/dist-server/service/recipe/recipe-query.js.map +1 -0
  202. package/dist-server/service/recipe/route-definition.d.ts +51 -0
  203. package/dist-server/service/recipe/route-definition.js +113 -0
  204. package/dist-server/service/recipe/route-definition.js.map +1 -0
  205. package/dist-server/service/resource/class-lineage.d.ts +46 -0
  206. package/dist-server/service/resource/class-lineage.js +80 -0
  207. package/dist-server/service/resource/class-lineage.js.map +1 -0
  208. package/dist-server/service/resource/class-members.d.ts +36 -0
  209. package/dist-server/service/resource/class-members.js +50 -0
  210. package/dist-server/service/resource/class-members.js.map +1 -0
  211. package/dist-server/service/resource/referrers.d.ts +54 -0
  212. package/dist-server/service/resource/referrers.js +99 -0
  213. package/dist-server/service/resource/referrers.js.map +1 -0
  214. package/dist-server/service/status/master-status.d.ts +0 -7
  215. package/dist-server/service/status/master-status.js +5 -6
  216. package/dist-server/service/status/master-status.js.map +1 -1
  217. package/dist-server/service/test-spec/criteria-check.d.ts +53 -0
  218. package/dist-server/service/test-spec/criteria-check.js +142 -0
  219. package/dist-server/service/test-spec/criteria-check.js.map +1 -0
  220. package/dist-server/service/test-spec/criteria-rows.d.ts +89 -0
  221. package/dist-server/service/test-spec/criteria-rows.js +139 -0
  222. package/dist-server/service/test-spec/criteria-rows.js.map +1 -0
  223. package/dist-server/service/test-spec/test-spec-master-resolver.d.ts +33 -7
  224. package/dist-server/service/test-spec/test-spec-master-resolver.js +137 -2
  225. package/dist-server/service/test-spec/test-spec-master-resolver.js.map +1 -1
  226. package/dist-server/service/test-spec/test-spec-query.d.ts +9 -1
  227. package/dist-server/service/test-spec/test-spec-query.js +23 -9
  228. package/dist-server/service/test-spec/test-spec-query.js.map +1 -1
  229. package/dist-server/service/test-spec/test-specification.js +1 -1
  230. package/dist-server/service/test-spec/test-specification.js.map +1 -1
  231. package/dist-server/tsconfig.tsbuildinfo +1 -1
  232. package/helps/ops-master/equipment-class.ko.md +11 -0
  233. package/helps/ops-master/equipment-class.md +11 -0
  234. package/helps/ops-master/equipment.ko.md +11 -0
  235. package/helps/ops-master/equipment.md +11 -0
  236. package/helps/ops-master/material-class.ko.md +11 -0
  237. package/helps/ops-master/material-class.md +11 -0
  238. package/helps/ops-master/material-definition.ko.md +11 -0
  239. package/helps/ops-master/material-definition.md +11 -0
  240. package/helps/ops-master/operation-definition.ko.md +11 -0
  241. package/helps/ops-master/operation-definition.md +11 -0
  242. package/helps/ops-master/operational-location.ko.md +11 -0
  243. package/helps/ops-master/operational-location.md +11 -0
  244. package/helps/ops-master/person.ko.md +11 -0
  245. package/helps/ops-master/person.md +11 -0
  246. package/helps/ops-master/personnel-class.ko.md +11 -0
  247. package/helps/ops-master/personnel-class.md +11 -0
  248. package/helps/ops-master/physical-asset-class.ko.md +11 -0
  249. package/helps/ops-master/physical-asset-class.md +11 -0
  250. package/helps/ops-master/physical-asset.ko.md +11 -0
  251. package/helps/ops-master/physical-asset.md +11 -0
  252. package/helps/ops-master/recipe-definition.ko.md +11 -0
  253. package/helps/ops-master/recipe-definition.md +11 -0
  254. package/helps/ops-master/route-definition.ko.md +11 -0
  255. package/helps/ops-master/route-definition.md +11 -0
  256. package/helps/ops-master/stop-reason.ko.md +11 -0
  257. package/helps/ops-master/stop-reason.md +11 -0
  258. package/helps/ops-master/test-specification.ko.md +11 -0
  259. package/helps/ops-master/test-specification.md +11 -0
  260. package/package.json +5 -5
  261. package/things-factory.config.js +22 -2
  262. package/translations/en.json +89 -6
  263. package/translations/ja.json +96 -1
  264. package/translations/ko.json +96 -13
  265. package/translations/ms.json +96 -1
  266. package/translations/zh.json +96 -1
  267. package/dist-client/pages/master-list.d.ts +0 -68
  268. package/dist-client/pages/master-list.js +0 -267
  269. package/dist-client/pages/master-list.js.map +0 -1
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OperationReadinessResolver = exports.OperationReadinessType = exports.AssetReadinessType = exports.PersonnelReadinessType = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const type_graphql_1 = require("type-graphql");
6
+ const shell_1 = require("@things-factory/shell");
7
+ const operation_definition_1 = require("./operation-definition");
8
+ const operation_readiness_1 = require("./operation-readiness");
9
+ let PersonnelReadinessType = class PersonnelReadinessType {
10
+ };
11
+ exports.PersonnelReadinessType = PersonnelReadinessType;
12
+ tslib_1.__decorate([
13
+ (0, type_graphql_1.Field)(),
14
+ tslib_1.__metadata("design:type", String)
15
+ ], PersonnelReadinessType.prototype, "classId", void 0);
16
+ tslib_1.__decorate([
17
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'how many the operation declares it needs' }),
18
+ tslib_1.__metadata("design:type", Number)
19
+ ], PersonnelReadinessType.prototype, "required", void 0);
20
+ tslib_1.__decorate([
21
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'how many people hold that qualification right now' }),
22
+ tslib_1.__metadata("design:type", Number)
23
+ ], PersonnelReadinessType.prototype, "qualified", void 0);
24
+ tslib_1.__decorate([
25
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'of those, how many lose it within 30 days' }),
26
+ tslib_1.__metadata("design:type", Number)
27
+ ], PersonnelReadinessType.prototype, "expiringSoon", void 0);
28
+ tslib_1.__decorate([
29
+ (0, type_graphql_1.Field)(type => [String], { description: 'a few names — not the whole list' }),
30
+ tslib_1.__metadata("design:type", Array)
31
+ ], PersonnelReadinessType.prototype, "sample", void 0);
32
+ exports.PersonnelReadinessType = PersonnelReadinessType = tslib_1.__decorate([
33
+ (0, type_graphql_1.ObjectType)({ description: 'A required personnel class, and how well it is covered right now' })
34
+ ], PersonnelReadinessType);
35
+ let AssetReadinessType = class AssetReadinessType {
36
+ };
37
+ exports.AssetReadinessType = AssetReadinessType;
38
+ tslib_1.__decorate([
39
+ (0, type_graphql_1.Field)(),
40
+ tslib_1.__metadata("design:type", String)
41
+ ], AssetReadinessType.prototype, "classId", void 0);
42
+ tslib_1.__decorate([
43
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int),
44
+ tslib_1.__metadata("design:type", Number)
45
+ ], AssetReadinessType.prototype, "required", void 0);
46
+ tslib_1.__decorate([
47
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'how many are registered and still in service — lifecycle is not counted here' }),
48
+ tslib_1.__metadata("design:type", Number)
49
+ ], AssetReadinessType.prototype, "registered", void 0);
50
+ exports.AssetReadinessType = AssetReadinessType = tslib_1.__decorate([
51
+ (0, type_graphql_1.ObjectType)({ description: 'A required asset class, and how many are registered' })
52
+ ], AssetReadinessType);
53
+ let OperationReadinessType = class OperationReadinessType {
54
+ };
55
+ exports.OperationReadinessType = OperationReadinessType;
56
+ tslib_1.__decorate([
57
+ (0, type_graphql_1.Field)(type => operation_definition_1.OperationDefinition),
58
+ tslib_1.__metadata("design:type", operation_definition_1.OperationDefinition)
59
+ ], OperationReadinessType.prototype, "operation", void 0);
60
+ tslib_1.__decorate([
61
+ (0, type_graphql_1.Field)(type => [PersonnelReadinessType]),
62
+ tslib_1.__metadata("design:type", Array)
63
+ ], OperationReadinessType.prototype, "personnel", void 0);
64
+ tslib_1.__decorate([
65
+ (0, type_graphql_1.Field)(type => [AssetReadinessType]),
66
+ tslib_1.__metadata("design:type", Array)
67
+ ], OperationReadinessType.prototype, "assets", void 0);
68
+ tslib_1.__decorate([
69
+ (0, type_graphql_1.Field)(type => type_graphql_1.Float, { nullable: true, description: 'standard time per item in minutes — absent when not declared' }),
70
+ tslib_1.__metadata("design:type", Number)
71
+ ], OperationReadinessType.prototype, "standardMinutes", void 0);
72
+ tslib_1.__decorate([
73
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'people registered in this domain' }),
74
+ tslib_1.__metadata("design:type", Number)
75
+ ], OperationReadinessType.prototype, "peopleTotal", void 0);
76
+ tslib_1.__decorate([
77
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'how many of them were actually checked' }),
78
+ tslib_1.__metadata("design:type", Number)
79
+ ], OperationReadinessType.prototype, "peopleScanned", void 0);
80
+ exports.OperationReadinessType = OperationReadinessType = tslib_1.__decorate([
81
+ (0, type_graphql_1.ObjectType)({ description: 'What one operation declares it needs, and whether that is met today' })
82
+ ], OperationReadinessType);
83
+ /**
84
+ * 선언 옆에 현실을 놓는다.
85
+ *
86
+ * 「용접 자격자 2명이 필요하다」는 선언이고, 그 선언이 지금 채워지는지는 다른 곳에 있었다. 두 화면을
87
+ * 오가며 사람이 머릿속으로 맞춰야 했고, 맞춰 보지 않으면 아무도 몰랐다.
88
+ */
89
+ let OperationReadinessResolver = class OperationReadinessResolver {
90
+ async operationReadiness(name, context) {
91
+ const { domain } = context.state;
92
+ return (0, operation_readiness_1.operationReadiness)((0, shell_1.getRepository)(operation_definition_1.OperationDefinition).manager, domain, name);
93
+ }
94
+ };
95
+ exports.OperationReadinessResolver = OperationReadinessResolver;
96
+ tslib_1.__decorate([
97
+ (0, type_graphql_1.Directive)('@privilege(category: "ops-master", privilege: "query", domainOwnerGranted: true)'),
98
+ (0, type_graphql_1.Query)(returns => OperationReadinessType, {
99
+ nullable: true,
100
+ description: 'To read what an operation requires and whether that is met today'
101
+ }),
102
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('name')),
103
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
104
+ tslib_1.__metadata("design:type", Function),
105
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
106
+ tslib_1.__metadata("design:returntype", Promise)
107
+ ], OperationReadinessResolver.prototype, "operationReadiness", null);
108
+ exports.OperationReadinessResolver = OperationReadinessResolver = tslib_1.__decorate([
109
+ (0, type_graphql_1.Resolver)()
110
+ ], OperationReadinessResolver);
111
+ //# sourceMappingURL=operation-readiness-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-readiness-resolver.js","sourceRoot":"","sources":["../../../server/service/operation/operation-readiness-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Field, Float, Int, ObjectType, Query, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { OperationDefinition } from './operation-definition'\nimport { operationReadiness } from './operation-readiness'\n\n@ObjectType({ description: 'A required personnel class, and how well it is covered right now' })\nexport class PersonnelReadinessType {\n @Field()\n classId: string\n\n @Field(type => Int, { description: 'how many the operation declares it needs' })\n required: number\n\n @Field(type => Int, { description: 'how many people hold that qualification right now' })\n qualified: number\n\n /** 곧 끊기는 것을 따로 센다 — 합쳐 세면 만료가 닥쳐서야 알게 된다. */\n @Field(type => Int, { description: 'of those, how many lose it within 30 days' })\n expiringSoon: number\n\n @Field(type => [String], { description: 'a few names — not the whole list' })\n sample: string[]\n}\n\n@ObjectType({ description: 'A required asset class, and how many are registered' })\nexport class AssetReadinessType {\n @Field()\n classId: string\n\n @Field(type => Int)\n required: number\n\n /** 수명과 교정은 세지 않는다 — 이 화면이 답하는 물음은 「채울 자산이 있나」다. */\n @Field(type => Int, { description: 'how many are registered and still in service — lifecycle is not counted here' })\n registered: number\n}\n\n@ObjectType({ description: 'What one operation declares it needs, and whether that is met today' })\nexport class OperationReadinessType {\n @Field(type => OperationDefinition)\n operation: OperationDefinition\n\n @Field(type => [PersonnelReadinessType])\n personnel: PersonnelReadinessType[]\n\n @Field(type => [AssetReadinessType])\n assets: AssetReadinessType[]\n\n /** 품목당 표준 시간(분). 선언되지 않았으면 없다 — 0 으로 두면 「즉시 끝난다」가 된다. */\n @Field(type => Float, { nullable: true, description: 'standard time per item in minutes — absent when not declared' })\n standardMinutes?: number\n\n @Field(type => Int, { description: 'people registered in this domain' })\n peopleTotal: number\n\n /**\n * 그중 자격을 실제로 확인한 수.\n *\n * `peopleTotal` 보다 작으면 잘린 것이다. 화면이 그 사실을 말해야 한다 — 자른 것을 말하지 않으면\n * 「자격자 3명」이 실제로는 더 많을 수 있다.\n */\n @Field(type => Int, { description: 'how many of them were actually checked' })\n peopleScanned: number\n}\n\n/**\n * 선언 옆에 현실을 놓는다.\n *\n * 「용접 자격자 2명이 필요하다」는 선언이고, 그 선언이 지금 채워지는지는 다른 곳에 있었다. 두 화면을\n * 오가며 사람이 머릿속으로 맞춰야 했고, 맞춰 보지 않으면 아무도 몰랐다.\n */\n@Resolver()\nexport class OperationReadinessResolver {\n @Directive('@privilege(category: \"ops-master\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => OperationReadinessType, {\n nullable: true,\n description: 'To read what an operation requires and whether that is met today'\n })\n async operationReadiness(\n @Arg('name') name: string,\n @Ctx() context: ResolverContext\n ): Promise<OperationReadinessType | undefined> {\n const { domain } = context.state\n\n return operationReadiness(getRepository(OperationDefinition).manager, domain, name)\n }\n}\n"]}
@@ -0,0 +1,34 @@
1
+ import { EntityManager } from 'typeorm';
2
+ import { Domain } from '@things-factory/shell';
3
+ import { OperationDefinition } from './operation-definition';
4
+ export interface PersonnelReadiness {
5
+ classId: string;
6
+ required: number;
7
+ /** 지금 이 등급의 자격을 갖춘 인원 수. */
8
+ qualified: number;
9
+ /** 그중 곧 자격이 끊기는 인원 수. */
10
+ expiringSoon: number;
11
+ /** 보여 줄 이름 몇 개. 전부 내려보내지 않는다. */
12
+ sample: string[];
13
+ }
14
+ export interface AssetReadiness {
15
+ classId: string;
16
+ required: number;
17
+ /**
18
+ * 이 등급으로 등록된 자산 수.
19
+ *
20
+ * **수명과 교정은 세지 않는다.** 그것을 세려면 자산마다 사용 횟수를 물어야 하고, 이 화면이 답하는
21
+ * 물음은 「선언을 채울 자산이 있나」이지 「지금 쓸 수 있나」가 아니다. 화면이 그 차이를 밝힌다.
22
+ */
23
+ registered: number;
24
+ }
25
+ export interface OperationReadiness {
26
+ operation: OperationDefinition;
27
+ personnel: PersonnelReadiness[];
28
+ assets: AssetReadiness[];
29
+ /** 품목당 표준 시간(분). 선언되지 않았으면 없다. */
30
+ standardMinutes?: number;
31
+ peopleTotal: number;
32
+ peopleScanned: number;
33
+ }
34
+ export declare function operationReadiness(manager: EntityManager, domain: Domain, operationName: string, now?: Date): Promise<OperationReadiness | undefined>;
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.operationReadiness = operationReadiness;
4
+ const ops_contract_1 = require("@operato/ops-contract");
5
+ const physical_asset_1 = require("../asset/physical-asset");
6
+ const physical_asset_class_1 = require("../asset/physical-asset-class");
7
+ const person_1 = require("../personnel/person");
8
+ const personnel_class_1 = require("../personnel/personnel-class");
9
+ const qualification_1 = require("../personnel/qualification");
10
+ const resource_class_1 = require("../resource/resource-class");
11
+ const operation_definition_1 = require("./operation-definition");
12
+ /**
13
+ * 공정 하나의 **선언과 현재 채움 상태**.
14
+ *
15
+ * ── 왜 둘을 한 화면에서 보나 ───────────────────────────────────────────────
16
+ * 「용접 자격자 2명이 필요하다」는 선언이다. 그 선언이 지금 채워지는지는 다른 화면에 있다. 표로 나눠
17
+ * 두면 사람이 두 화면을 오가며 머릿속으로 맞춰야 하고, 맞춰 보지 않으면 아무도 모른다.
18
+ *
19
+ * 선언을 적는 자리에서 **그 선언이 지금 성립하는지** 함께 보인다.
20
+ *
21
+ * ── 곧 끊기는 것을 따로 센다 ───────────────────────────────────────────────
22
+ * 「자격자 3명」과 「자격자 3명인데 그중 1명이 다음 주 만료」는 다른 상황이다. 합쳐 세면 만료가
23
+ * 닥쳐서야 알게 된다.
24
+ *
25
+ * ── 조용히 자르지 않는다 ───────────────────────────────────────────────────
26
+ * 자격은 사람마다 계산해야 한다(등급 상속과 검사 만료를 봐야 하므로). 인원이 아주 많으면 상한까지만
27
+ * 보고, **몇 명 중 몇 명을 봤는지 함께 낸다.**
28
+ */
29
+ /** 한 번에 자격을 확인하는 인원 수의 상한. */
30
+ const MAX_PEOPLE_SCANNED = 2000;
31
+ /** 이 기간 안에 끊기는 자격을 「곧 만료」로 센다. */
32
+ const EXPIRING_SOON_DAYS = 30;
33
+ async function operationReadiness(manager, domain, operationName, now = new Date()) {
34
+ const operation = await manager.findOne(operation_definition_1.OperationDefinition, {
35
+ where: { domain: { id: domain.id }, name: operationName }
36
+ });
37
+ if (!operation) {
38
+ return undefined;
39
+ }
40
+ const at = now.toISOString();
41
+ const spec = (0, qualification_1.personnelSpecificationOf)(operation.personnelSpecification);
42
+ const [personnelClasses, people] = await Promise.all([
43
+ manager.find(personnel_class_1.PersonnelClass, { where: { domain: { id: domain.id } } }),
44
+ spec.length
45
+ ? manager.find(person_1.Person, { where: { domain: { id: domain.id } }, take: MAX_PEOPLE_SCANNED })
46
+ : Promise.resolve([])
47
+ ]);
48
+ const peopleTotal = spec.length ? await manager.count(person_1.Person, { where: { domain: { id: domain.id } } }) : 0;
49
+ const classDefs = personnelClasses.map(resource_class_1.toResourceClassDef);
50
+ const soonMs = now.getTime() + EXPIRING_SOON_DAYS * 24 * 3_600_000;
51
+ const personnel = spec.map(({ personnelClassId, quantity }) => {
52
+ const qualified = [];
53
+ let expiringSoon = 0;
54
+ for (const person of people) {
55
+ const judgeable = {
56
+ name: person.name,
57
+ personnelClassIds: person.personnelClassIds,
58
+ testResults: person.testResults,
59
+ effectiveStart: person.effectiveStart,
60
+ effectiveEnd: person.effectiveEnd
61
+ };
62
+ if ((0, qualification_1.qualificationGapsFor)(judgeable, personnelClassId, classDefs, at).length) {
63
+ continue;
64
+ }
65
+ qualified.push(person.name);
66
+ const expiry = (0, qualification_1.nearestQualificationExpiry)(judgeable, personnelClassId, classDefs, at);
67
+ if (expiry && Date.parse(expiry) <= soonMs) {
68
+ expiringSoon += 1;
69
+ }
70
+ }
71
+ return {
72
+ classId: personnelClassId,
73
+ required: quantity ?? 1,
74
+ qualified: qualified.length,
75
+ expiringSoon,
76
+ sample: qualified.slice(0, 5)
77
+ };
78
+ });
79
+ return {
80
+ operation,
81
+ personnel,
82
+ assets: await assetReadiness(manager, domain, operation, at),
83
+ standardMinutes: minutesOf(operation.duration),
84
+ peopleTotal,
85
+ peopleScanned: people.length
86
+ };
87
+ }
88
+ /** 선언된 요구 자산을 읽는다. 등급 이름이 없는 줄은 걸러 낸다 — 아무도 못 채우는 요구가 된다. */
89
+ function assetSpecificationOf(raw) {
90
+ const entries = [];
91
+ for (const row of raw ?? []) {
92
+ const classId = row?.physicalAssetClassId ?? row?.assetClassId ?? row?.classId;
93
+ if (typeof classId !== 'string' || !classId) {
94
+ continue;
95
+ }
96
+ const quantity = Number(row?.quantity);
97
+ entries.push({ classId, quantity: Number.isFinite(quantity) && quantity > 0 ? quantity : 1 });
98
+ }
99
+ return entries;
100
+ }
101
+ async function assetReadiness(manager, domain, operation, at) {
102
+ const spec = assetSpecificationOf(operation.physicalAssetSpecification);
103
+ if (!spec.length) {
104
+ return [];
105
+ }
106
+ const [classes, assets] = await Promise.all([
107
+ manager.find(physical_asset_class_1.PhysicalAssetClass, { where: { domain: { id: domain.id } } }),
108
+ manager.find(physical_asset_1.PhysicalAsset, { where: { domain: { id: domain.id } } })
109
+ ]);
110
+ const classDefs = classes.map(resource_class_1.toResourceClassDef);
111
+ return spec.map(({ classId, quantity }) => {
112
+ let registered = 0;
113
+ for (const asset of assets) {
114
+ const period = { effectiveStart: iso(asset.effectiveStart), effectiveEnd: iso(asset.effectiveEnd) };
115
+ if ((0, ops_contract_1.effectivityAt)(period, at) !== undefined) {
116
+ continue;
117
+ }
118
+ if ((0, ops_contract_1.classClosure)(asset.assetClassIds, classDefs, at).has(classId)) {
119
+ registered += 1;
120
+ }
121
+ }
122
+ return { classId, required: quantity, registered };
123
+ });
124
+ }
125
+ /** ISO 8601 기간을 분으로. 해석할 수 없으면 없다 — 0 분으로 두면 「즉시 끝난다」가 된다. */
126
+ function minutesOf(duration) {
127
+ const ms = (0, ops_contract_1.parseIsoDuration)(duration);
128
+ return ms == null ? undefined : Math.round((ms / 60_000) * 10) / 10;
129
+ }
130
+ function iso(at) {
131
+ return at ? new Date(at).toISOString() : undefined;
132
+ }
133
+ //# sourceMappingURL=operation-readiness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-readiness.js","sourceRoot":"","sources":["../../../server/service/operation/operation-readiness.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain } from '@things-factory/shell'\nimport { classClosure, effectivityAt, parseIsoDuration } from '@operato/ops-contract'\nimport type { ResourceClassDef, TestResult } from '@operato/ops-contract'\n\nimport { PhysicalAsset } from '../asset/physical-asset'\nimport { PhysicalAssetClass } from '../asset/physical-asset-class'\nimport { Person } from '../personnel/person'\nimport { PersonnelClass } from '../personnel/personnel-class'\nimport { nearestQualificationExpiry, personnelSpecificationOf, qualificationGapsFor } from '../personnel/qualification'\nimport { toResourceClassDef } from '../resource/resource-class'\nimport { OperationDefinition } from './operation-definition'\n\n/**\n * 공정 하나의 **선언과 현재 채움 상태**.\n *\n * ── 왜 둘을 한 화면에서 보나 ───────────────────────────────────────────────\n * 「용접 자격자 2명이 필요하다」는 선언이다. 그 선언이 지금 채워지는지는 다른 화면에 있다. 표로 나눠\n * 두면 사람이 두 화면을 오가며 머릿속으로 맞춰야 하고, 맞춰 보지 않으면 아무도 모른다.\n *\n * 선언을 적는 자리에서 **그 선언이 지금 성립하는지** 함께 보인다.\n *\n * ── 곧 끊기는 것을 따로 센다 ───────────────────────────────────────────────\n * 「자격자 3명」과 「자격자 3명인데 그중 1명이 다음 주 만료」는 다른 상황이다. 합쳐 세면 만료가\n * 닥쳐서야 알게 된다.\n *\n * ── 조용히 자르지 않는다 ───────────────────────────────────────────────────\n * 자격은 사람마다 계산해야 한다(등급 상속과 검사 만료를 봐야 하므로). 인원이 아주 많으면 상한까지만\n * 보고, **몇 명 중 몇 명을 봤는지 함께 낸다.**\n */\n\n/** 한 번에 자격을 확인하는 인원 수의 상한. */\nconst MAX_PEOPLE_SCANNED = 2000\n\n/** 이 기간 안에 끊기는 자격을 「곧 만료」로 센다. */\nconst EXPIRING_SOON_DAYS = 30\n\nexport interface PersonnelReadiness {\n classId: string\n required: number\n /** 지금 이 등급의 자격을 갖춘 인원 수. */\n qualified: number\n /** 그중 곧 자격이 끊기는 인원 수. */\n expiringSoon: number\n /** 보여 줄 이름 몇 개. 전부 내려보내지 않는다. */\n sample: string[]\n}\n\nexport interface AssetReadiness {\n classId: string\n required: number\n /**\n * 이 등급으로 등록된 자산 수.\n *\n * **수명과 교정은 세지 않는다.** 그것을 세려면 자산마다 사용 횟수를 물어야 하고, 이 화면이 답하는\n * 물음은 「선언을 채울 자산이 있나」이지 「지금 쓸 수 있나」가 아니다. 화면이 그 차이를 밝힌다.\n */\n registered: number\n}\n\nexport interface OperationReadiness {\n operation: OperationDefinition\n personnel: PersonnelReadiness[]\n assets: AssetReadiness[]\n /** 품목당 표준 시간(분). 선언되지 않았으면 없다. */\n standardMinutes?: number\n peopleTotal: number\n peopleScanned: number\n}\n\nexport async function operationReadiness(\n manager: EntityManager,\n domain: Domain,\n operationName: string,\n now = new Date()\n): Promise<OperationReadiness | undefined> {\n const operation = await manager.findOne(OperationDefinition, {\n where: { domain: { id: domain.id }, name: operationName }\n })\n\n if (!operation) {\n return undefined\n }\n\n const at = now.toISOString()\n const spec = personnelSpecificationOf(operation.personnelSpecification)\n\n const [personnelClasses, people] = await Promise.all([\n manager.find(PersonnelClass, { where: { domain: { id: domain.id } } }),\n spec.length\n ? manager.find(Person, { where: { domain: { id: domain.id } }, take: MAX_PEOPLE_SCANNED })\n : Promise.resolve([] as Person[])\n ])\n\n const peopleTotal = spec.length ? await manager.count(Person, { where: { domain: { id: domain.id } } }) : 0\n const classDefs = personnelClasses.map(toResourceClassDef)\n const soonMs = now.getTime() + EXPIRING_SOON_DAYS * 24 * 3_600_000\n\n const personnel = spec.map(({ personnelClassId, quantity }) => {\n const qualified: string[] = []\n let expiringSoon = 0\n\n for (const person of people) {\n const judgeable = {\n name: person.name,\n personnelClassIds: person.personnelClassIds,\n testResults: person.testResults as unknown as TestResult[] | undefined,\n effectiveStart: person.effectiveStart,\n effectiveEnd: person.effectiveEnd\n }\n\n if (qualificationGapsFor(judgeable, personnelClassId, classDefs, at).length) {\n continue\n }\n\n qualified.push(person.name)\n\n const expiry = nearestQualificationExpiry(judgeable, personnelClassId, classDefs, at)\n if (expiry && Date.parse(expiry) <= soonMs) {\n expiringSoon += 1\n }\n }\n\n return {\n classId: personnelClassId,\n required: quantity ?? 1,\n qualified: qualified.length,\n expiringSoon,\n sample: qualified.slice(0, 5)\n }\n })\n\n return {\n operation,\n personnel,\n assets: await assetReadiness(manager, domain, operation, at),\n standardMinutes: minutesOf(operation.duration),\n peopleTotal,\n peopleScanned: people.length\n }\n}\n\n/** 선언된 요구 자산을 읽는다. 등급 이름이 없는 줄은 걸러 낸다 — 아무도 못 채우는 요구가 된다. */\nfunction assetSpecificationOf(raw: readonly Record<string, unknown>[] | undefined) {\n const entries: { classId: string; quantity: number }[] = []\n\n for (const row of raw ?? []) {\n const classId = row?.physicalAssetClassId ?? row?.assetClassId ?? row?.classId\n if (typeof classId !== 'string' || !classId) {\n continue\n }\n const quantity = Number(row?.quantity)\n entries.push({ classId, quantity: Number.isFinite(quantity) && quantity > 0 ? quantity : 1 })\n }\n\n return entries\n}\n\nasync function assetReadiness(\n manager: EntityManager,\n domain: Domain,\n operation: OperationDefinition,\n at: string\n): Promise<AssetReadiness[]> {\n const spec = assetSpecificationOf(operation.physicalAssetSpecification)\n if (!spec.length) {\n return []\n }\n\n const [classes, assets] = await Promise.all([\n manager.find(PhysicalAssetClass, { where: { domain: { id: domain.id } } }),\n manager.find(PhysicalAsset, { where: { domain: { id: domain.id } } })\n ])\n\n const classDefs: ResourceClassDef[] = classes.map(toResourceClassDef)\n\n return spec.map(({ classId, quantity }) => {\n let registered = 0\n\n for (const asset of assets) {\n const period = { effectiveStart: iso(asset.effectiveStart), effectiveEnd: iso(asset.effectiveEnd) }\n if (effectivityAt(period, at) !== undefined) {\n continue\n }\n if (classClosure(asset.assetClassIds, classDefs, at).has(classId)) {\n registered += 1\n }\n }\n\n return { classId, required: quantity, registered }\n })\n}\n\n/** ISO 8601 기간을 분으로. 해석할 수 없으면 없다 — 0 분으로 두면 「즉시 끝난다」가 된다. */\nfunction minutesOf(duration: string | undefined): number | undefined {\n const ms = parseIsoDuration(duration)\n\n return ms == null ? undefined : Math.round((ms / 60_000) * 10) / 10\n}\n\nfunction iso(at: Date | undefined): string | undefined {\n return at ? new Date(at).toISOString() : undefined\n}\n"]}
@@ -53,12 +53,12 @@ tslib_1.__decorate([
53
53
  ], Person.prototype, "homeLocation", void 0);
54
54
  tslib_1.__decorate([
55
55
  (0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
56
- (0, type_graphql_1.Field)(type => String, { nullable: true, description: 'WorkCalendarEntry[] as JSON text' }),
56
+ (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'WorkCalendarEntry[]' }),
57
57
  tslib_1.__metadata("design:type", Array)
58
58
  ], Person.prototype, "workCalendar", void 0);
59
59
  tslib_1.__decorate([
60
60
  (0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
61
- (0, type_graphql_1.Field)(type => String, { nullable: true, description: 'ResourceProperty[] as JSON text' }),
61
+ (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'ResourceProperty[]' }),
62
62
  tslib_1.__metadata("design:type", Array)
63
63
  ], Person.prototype, "properties", void 0);
64
64
  tslib_1.__decorate([
@@ -68,7 +68,7 @@ tslib_1.__decorate([
68
68
  ], Person.prototype, "testSpecificationIds", void 0);
69
69
  tslib_1.__decorate([
70
70
  (0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
71
- (0, type_graphql_1.Field)(type => String, { nullable: true, description: 'TestResult[] as JSON text' }),
71
+ (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'TestResult[]' }),
72
72
  tslib_1.__metadata("design:type", Array)
73
73
  ], Person.prototype, "testResults", void 0);
74
74
  tslib_1.__decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"person.js","sourceRoot":"","sources":["../../../server/service/personnel/person.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 작업자 — **ISA-95 `Person`**.\n *\n * 사람은 설비와 다르게 산다. 고장 나지 않고 교대로 일하며, 자격이 만료된다. 그래서 자원 넷을 한 그릇에\n * 담지 않는다 — 한쪽의 어휘가 다른 쪽에 붙으면 둘 다 거짓이 된다.\n *\n * 필드 이름은 커널 계약의 `persons` 항목과 같다(`@operato/ops-contract`). 옮길 때 번역이 없다.\n */\n@Entity({ name: 'ops_person' })\n@Index('ix_ops_person_0', (person: Person) => [person.domain, person.name], { unique: true })\n@ObjectType({ description: 'ISA-95 Person — a worker who can be assigned to operations' })\nexport class Person {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((person: Person) => person.domain)\n domainId?: string\n\n /** 표준 `PersonID` — 트윈으로 나갈 때 `persons[].id` 가 된다. */\n @Column()\n @Field()\n name: string\n\n /** 사람이 읽는 이름. 표준 `PersonName`. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n /**\n * 속한 등급들 — 표준 `PersonnelClassID`(복수).\n *\n * 복수인 것이 중요하다. 한 사람이 용접 자격과 지게차 자격을 함께 갖는 일이 흔하고, 하나만 담으면\n * 그 사람은 한쪽 공정에서 보이지 않는다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true })\n personnelClassIds?: string[]\n\n /** 이 사람이 주로 있는 자리 — 표준 `OperationalLocation`. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n homeLocation?: string\n\n /** 근무 캘린더 — 표준 `WorkCalendar`. 교대 이름과 구간이 여기 있다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => String, { nullable: true, description: 'WorkCalendarEntry[] as JSON text' })\n workCalendar?: Record<string, unknown>[]\n\n /** 개체 속성 — 표준 `PersonProperty`. `classPropertyId` 로 등급 속성을 구체화한다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => String, { nullable: true, description: 'ResourceProperty[] as JSON text' })\n properties?: Record<string, unknown>[]\n\n /** 이 사람에게 요구되는 `TestSpecification` 참조들 — 표준 `TestSpecificationID`. 참조만 갖는다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true })\n testSpecificationIds?: string[]\n\n /**\n * `TestResult` — 표준 `TestResult`.\n *\n * 등급이 `TestSpecification` 을 요구하는데 그 사람에게 결과가 아예 없으면, 자격이 성립하는지 판단할 수 없다.\n * 만료되었거나 떨어졌으면 그 사람에게 작업이 가지 않는다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => String, { nullable: true, description: 'TestResult[] as JSON text' })\n testResults?: Record<string, unknown>[]\n\n /** 언제부터 이 사람이 현장에 있나 — 표준 `EffectiveStartDate`. 입사 예정자를 미리 등록할 수 있다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveStart?: Date\n\n /** 언제까지 — 표준 `EffectiveEndDate`. 퇴사한 사람이 영원히 배정 대상으로 남지 않게 한다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveEnd?: Date\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((person: Person) => person.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((person: Person) => person.updater)\n updaterId?: string\n}\n"]}
1
+ {"version":3,"file":"person.js","sourceRoot":"","sources":["../../../server/service/personnel/person.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 작업자 — **ISA-95 `Person`**.\n *\n * 사람은 설비와 다르게 산다. 고장 나지 않고 교대로 일하며, 자격이 만료된다. 그래서 자원 넷을 한 그릇에\n * 담지 않는다 — 한쪽의 어휘가 다른 쪽에 붙으면 둘 다 거짓이 된다.\n *\n * 필드 이름은 커널 계약의 `persons` 항목과 같다(`@operato/ops-contract`). 옮길 때 번역이 없다.\n */\n@Entity({ name: 'ops_person' })\n@Index('ix_ops_person_0', (person: Person) => [person.domain, person.name], { unique: true })\n@ObjectType({ description: 'ISA-95 Person — a worker who can be assigned to operations' })\nexport class Person {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((person: Person) => person.domain)\n domainId?: string\n\n /** 표준 `PersonID` — 트윈으로 나갈 때 `persons[].id` 가 된다. */\n @Column()\n @Field()\n name: string\n\n /** 사람이 읽는 이름. 표준 `PersonName`. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n /**\n * 속한 등급들 — 표준 `PersonnelClassID`(복수).\n *\n * 복수인 것이 중요하다. 한 사람이 용접 자격과 지게차 자격을 함께 갖는 일이 흔하고, 하나만 담으면\n * 그 사람은 한쪽 공정에서 보이지 않는다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true })\n personnelClassIds?: string[]\n\n /** 이 사람이 주로 있는 자리 — 표준 `OperationalLocation`. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n homeLocation?: string\n\n /** 근무 캘린더 — 표준 `WorkCalendar`. 교대 이름과 구간이 여기 있다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'WorkCalendarEntry[]' })\n workCalendar?: Record<string, unknown>[]\n\n /** 개체 속성 — 표준 `PersonProperty`. `classPropertyId` 로 등급 속성을 구체화한다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'ResourceProperty[]' })\n properties?: Record<string, unknown>[]\n\n /** 이 사람에게 요구되는 `TestSpecification` 참조들 — 표준 `TestSpecificationID`. 참조만 갖는다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true })\n testSpecificationIds?: string[]\n\n /**\n * `TestResult` — 표준 `TestResult`.\n *\n * 등급이 `TestSpecification` 을 요구하는데 그 사람에게 결과가 아예 없으면, 자격이 성립하는지 판단할 수 없다.\n * 만료되었거나 떨어졌으면 그 사람에게 작업이 가지 않는다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'TestResult[]' })\n testResults?: Record<string, unknown>[]\n\n /** 언제부터 이 사람이 현장에 있나 — 표준 `EffectiveStartDate`. 입사 예정자를 미리 등록할 수 있다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveStart?: Date\n\n /** 언제까지 — 표준 `EffectiveEndDate`. 퇴사한 사람이 영원히 배정 대상으로 남지 않게 한다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveEnd?: Date\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((person: Person) => person.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((person: Person) => person.updater)\n updaterId?: string\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import { EntityManager } from 'typeorm';
2
+ import { Domain } from '@things-factory/shell';
3
+ import { PersonnelClass } from './personnel-class';
4
+ export interface RequiringOperation {
5
+ operationName: string;
6
+ /** 그 공정이 이 등급으로 몇 명을 요구하나. */
7
+ required: number;
8
+ }
9
+ export interface PersonnelClassReadiness {
10
+ personnelClass: PersonnelClass;
11
+ /** 이 등급을 상속하는 하위 등급들 — 이들의 자격자도 이 등급을 만족시킨다. */
12
+ descendants: string[];
13
+ /** 지금 이 자격을 갖춘 인원. */
14
+ holders: string[];
15
+ /** 그중 곧 끊기는 인원 수. */
16
+ expiringSoon: number;
17
+ /** 이 등급을 요구하는 공정들 — 역방향이다. */
18
+ requiredBy: RequiringOperation[];
19
+ peopleTotal: number;
20
+ peopleScanned: number;
21
+ }
22
+ export declare function personnelClassReadiness(manager: EntityManager, domain: Domain, name: string, now?: Date): Promise<PersonnelClassReadiness | undefined>;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.personnelClassReadiness = personnelClassReadiness;
4
+ const ops_contract_1 = require("@operato/ops-contract");
5
+ const operation_definition_1 = require("../operation/operation-definition");
6
+ const resource_class_1 = require("../resource/resource-class");
7
+ const person_1 = require("./person");
8
+ const personnel_class_1 = require("./personnel-class");
9
+ const qualification_1 = require("./qualification");
10
+ /**
11
+ * 인원 등급 하나의 **선언과 현실**.
12
+ *
13
+ * ── 이 화면에서 내리는 판단 ────────────────────────────────────────────────
14
+ * 「이 자격은 무엇을 요구하나」와 **「이 자격이 어디에 쓰이나」**다. 두 번째가 표로는 나오지 않는다 —
15
+ * 공정이 등급을 가리키므로 정방향은 가지만, 등급에서 공정으로 되돌아오는 길이 없다.
16
+ *
17
+ * ── 왜 「누가 갖고 있나」를 함께 내나 ──────────────────────────────────────
18
+ * 자격을 등록하는 이유는 그것으로 사람을 고르기 위해서다. 등급만 보이고 보유자를 안 보이면 「이 자격을
19
+ * 만들어 둔 값이 있나」를 알 수 없다.
20
+ */
21
+ /** 한 번에 자격을 확인하는 인원 수의 상한. */
22
+ const MAX_PEOPLE_SCANNED = 2000;
23
+ /** 이 기간 안에 끊기는 자격을 「곧 만료」로 센다. */
24
+ const EXPIRING_SOON_DAYS = 30;
25
+ async function personnelClassReadiness(manager, domain, name, now = new Date()) {
26
+ const personnelClass = await manager.findOne(personnel_class_1.PersonnelClass, { where: { domain: { id: domain.id }, name } });
27
+ if (!personnelClass) {
28
+ return undefined;
29
+ }
30
+ const at = now.toISOString();
31
+ const soonMs = now.getTime() + EXPIRING_SOON_DAYS * 24 * 3_600_000;
32
+ const [classes, people, peopleTotal, operations] = await Promise.all([
33
+ manager.find(personnel_class_1.PersonnelClass, { where: { domain: { id: domain.id } } }),
34
+ manager.find(person_1.Person, { where: { domain: { id: domain.id } }, take: MAX_PEOPLE_SCANNED }),
35
+ manager.count(person_1.Person, { where: { domain: { id: domain.id } } }),
36
+ manager.find(operation_definition_1.OperationDefinition, { where: { domain: { id: domain.id } } })
37
+ ]);
38
+ const classDefs = classes.map(resource_class_1.toResourceClassDef);
39
+ const holders = [];
40
+ let expiringSoon = 0;
41
+ for (const person of people) {
42
+ const judgeable = {
43
+ name: person.name,
44
+ personnelClassIds: person.personnelClassIds,
45
+ testResults: person.testResults,
46
+ effectiveStart: person.effectiveStart,
47
+ effectiveEnd: person.effectiveEnd
48
+ };
49
+ if ((0, qualification_1.qualificationGapsFor)(judgeable, name, classDefs, at).length) {
50
+ continue;
51
+ }
52
+ holders.push(person.name);
53
+ const expiry = (0, qualification_1.nearestQualificationExpiry)(judgeable, name, classDefs, at);
54
+ if (expiry && Date.parse(expiry) <= soonMs) {
55
+ expiringSoon += 1;
56
+ }
57
+ }
58
+ return {
59
+ personnelClass,
60
+ descendants: descendantsOf(name, classes, classDefs, at),
61
+ holders,
62
+ expiringSoon,
63
+ requiredBy: requiringOperations(name, operations),
64
+ peopleTotal,
65
+ peopleScanned: people.length
66
+ };
67
+ }
68
+ /**
69
+ * 이 등급을 상속하는 등급들.
70
+ *
71
+ * 「6축 용접 자격」이 「용접 자격」을 상속하면, 용접을 요구하는 공정에 6축 용접 자격자도 선다. 그것이
72
+ * 이 화면에 보여야 「이 등급의 자격자가 왜 이렇게 많은가」에 답할 수 있다.
73
+ *
74
+ * 상속은 계약의 `classClosure` 가 닫으므로 여기서 다시 만들지 않는다.
75
+ */
76
+ function descendantsOf(name, classes, classDefs, at) {
77
+ return classes
78
+ .filter(candidate => candidate.name !== name && (0, ops_contract_1.classClosure)([candidate.name], classDefs, at).has(name))
79
+ .map(candidate => candidate.name);
80
+ }
81
+ /**
82
+ * 이 등급을 요구하는 공정들.
83
+ *
84
+ * 상속을 보지 않는다 — 공정이 「용접」을 요구했으면 그것이 선언이고, 「6축 용접」 화면에서 「용접을
85
+ * 요구하는 공정」을 내면 선언하지 않은 것을 선언한 것처럼 보이게 된다.
86
+ */
87
+ function requiringOperations(name, operations) {
88
+ const found = [];
89
+ for (const operation of operations) {
90
+ for (const entry of (0, qualification_1.personnelSpecificationOf)(operation.personnelSpecification)) {
91
+ if (entry.personnelClassId === name) {
92
+ found.push({ operationName: operation.name, required: entry.quantity ?? 1 });
93
+ }
94
+ }
95
+ }
96
+ return found;
97
+ }
98
+ //# sourceMappingURL=personnel-class-readiness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"personnel-class-readiness.js","sourceRoot":"","sources":["../../../server/service/personnel/personnel-class-readiness.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain } from '@things-factory/shell'\nimport { classClosure } from '@operato/ops-contract'\nimport type { TestResult } from '@operato/ops-contract'\n\nimport { OperationDefinition } from '../operation/operation-definition'\nimport { toResourceClassDef } from '../resource/resource-class'\nimport { Person } from './person'\nimport { PersonnelClass } from './personnel-class'\nimport { nearestQualificationExpiry, personnelSpecificationOf, qualificationGapsFor } from './qualification'\n\n/**\n * 인원 등급 하나의 **선언과 현실**.\n *\n * ── 이 화면에서 내리는 판단 ────────────────────────────────────────────────\n * 「이 자격은 무엇을 요구하나」와 **「이 자격이 어디에 쓰이나」**다. 두 번째가 표로는 나오지 않는다 —\n * 공정이 등급을 가리키므로 정방향은 가지만, 등급에서 공정으로 되돌아오는 길이 없다.\n *\n * ── 왜 「누가 갖고 있나」를 함께 내나 ──────────────────────────────────────\n * 자격을 등록하는 이유는 그것으로 사람을 고르기 위해서다. 등급만 보이고 보유자를 안 보이면 「이 자격을\n * 만들어 둔 값이 있나」를 알 수 없다.\n */\n\n/** 한 번에 자격을 확인하는 인원 수의 상한. */\nconst MAX_PEOPLE_SCANNED = 2000\n\n/** 이 기간 안에 끊기는 자격을 「곧 만료」로 센다. */\nconst EXPIRING_SOON_DAYS = 30\n\nexport interface RequiringOperation {\n operationName: string\n /** 그 공정이 이 등급으로 몇 명을 요구하나. */\n required: number\n}\n\nexport interface PersonnelClassReadiness {\n personnelClass: PersonnelClass\n /** 이 등급을 상속하는 하위 등급들 — 이들의 자격자도 이 등급을 만족시킨다. */\n descendants: string[]\n /** 지금 이 자격을 갖춘 인원. */\n holders: string[]\n /** 그중 곧 끊기는 인원 수. */\n expiringSoon: number\n /** 이 등급을 요구하는 공정들 — 역방향이다. */\n requiredBy: RequiringOperation[]\n peopleTotal: number\n peopleScanned: number\n}\n\nexport async function personnelClassReadiness(\n manager: EntityManager,\n domain: Domain,\n name: string,\n now = new Date()\n): Promise<PersonnelClassReadiness | undefined> {\n const personnelClass = await manager.findOne(PersonnelClass, { where: { domain: { id: domain.id }, name } })\n\n if (!personnelClass) {\n return undefined\n }\n\n const at = now.toISOString()\n const soonMs = now.getTime() + EXPIRING_SOON_DAYS * 24 * 3_600_000\n\n const [classes, people, peopleTotal, operations] = await Promise.all([\n manager.find(PersonnelClass, { where: { domain: { id: domain.id } } }),\n manager.find(Person, { where: { domain: { id: domain.id } }, take: MAX_PEOPLE_SCANNED }),\n manager.count(Person, { where: { domain: { id: domain.id } } }),\n manager.find(OperationDefinition, { where: { domain: { id: domain.id } } })\n ])\n\n const classDefs = classes.map(toResourceClassDef)\n\n const holders: string[] = []\n let expiringSoon = 0\n\n for (const person of people) {\n const judgeable = {\n name: person.name,\n personnelClassIds: person.personnelClassIds,\n testResults: person.testResults as unknown as TestResult[] | undefined,\n effectiveStart: person.effectiveStart,\n effectiveEnd: person.effectiveEnd\n }\n\n if (qualificationGapsFor(judgeable, name, classDefs, at).length) {\n continue\n }\n\n holders.push(person.name)\n\n const expiry = nearestQualificationExpiry(judgeable, name, classDefs, at)\n if (expiry && Date.parse(expiry) <= soonMs) {\n expiringSoon += 1\n }\n }\n\n return {\n personnelClass,\n descendants: descendantsOf(name, classes, classDefs, at),\n holders,\n expiringSoon,\n requiredBy: requiringOperations(name, operations),\n peopleTotal,\n peopleScanned: people.length\n }\n}\n\n/**\n * 이 등급을 상속하는 등급들.\n *\n * 「6축 용접 자격」이 「용접 자격」을 상속하면, 용접을 요구하는 공정에 6축 용접 자격자도 선다. 그것이\n * 이 화면에 보여야 「이 등급의 자격자가 왜 이렇게 많은가」에 답할 수 있다.\n *\n * 상속은 계약의 `classClosure` 가 닫으므로 여기서 다시 만들지 않는다.\n */\nfunction descendantsOf(\n name: string,\n classes: readonly PersonnelClass[],\n classDefs: readonly ReturnType<typeof toResourceClassDef>[],\n at: string\n): string[] {\n return classes\n .filter(candidate => candidate.name !== name && classClosure([candidate.name], classDefs, at).has(name))\n .map(candidate => candidate.name)\n}\n\n/**\n * 이 등급을 요구하는 공정들.\n *\n * 상속을 보지 않는다 — 공정이 「용접」을 요구했으면 그것이 선언이고, 「6축 용접」 화면에서 「용접을\n * 요구하는 공정」을 내면 선언하지 않은 것을 선언한 것처럼 보이게 된다.\n */\nfunction requiringOperations(name: string, operations: readonly OperationDefinition[]): RequiringOperation[] {\n const found: RequiringOperation[] = []\n\n for (const operation of operations) {\n for (const entry of personnelSpecificationOf(operation.personnelSpecification)) {\n if (entry.personnelClassId === name) {\n found.push({ operationName: operation.name, required: entry.quantity ?? 1 })\n }\n }\n }\n\n return found\n}\n"]}
@@ -12,7 +12,7 @@ import { User } from '@things-factory/auth-base';
12
12
  *
13
13
  * 그리고 트윈 커널에 `personnelClasses` 자리가 있는데, 지금까지 붙인 원본에는 그 사실이 없었다.
14
14
  * 그래서 「자격자가 둘뿐이라 설비가 논다」를 말할 수 없었다
15
- * (`operato-twin/design/integration/operato-mes.md` §5).
15
+ * (`operato-twin/design/integration/operato-plant.md` §5).
16
16
  *
17
17
  * ── 필드 이름을 새로 짓지 않는다 ────────────────────────────────────────────
18
18
  * 커널 계약의 `ResourceClassDef` 와 같은 이름을 쓴다(`@operato/ops-contract`). 같은 사실에 이름이
@@ -18,7 +18,7 @@ const auth_base_1 = require("@things-factory/auth-base");
18
18
  *
19
19
  * 그리고 트윈 커널에 `personnelClasses` 자리가 있는데, 지금까지 붙인 원본에는 그 사실이 없었다.
20
20
  * 그래서 「자격자가 둘뿐이라 설비가 논다」를 말할 수 없었다
21
- * (`operato-twin/design/integration/operato-mes.md` §5).
21
+ * (`operato-twin/design/integration/operato-plant.md` §5).
22
22
  *
23
23
  * ── 필드 이름을 새로 짓지 않는다 ────────────────────────────────────────────
24
24
  * 커널 계약의 `ResourceClassDef` 와 같은 이름을 쓴다(`@operato/ops-contract`). 같은 사실에 이름이
@@ -63,7 +63,7 @@ tslib_1.__decorate([
63
63
  ], PersonnelClass.prototype, "testSpecificationIds", void 0);
64
64
  tslib_1.__decorate([
65
65
  (0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
66
- (0, type_graphql_1.Field)(type => String, { nullable: true, description: 'ResourceProperty[] as JSON text' }),
66
+ (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'ResourceProperty[]' }),
67
67
  tslib_1.__metadata("design:type", Array)
68
68
  ], PersonnelClass.prototype, "properties", void 0);
69
69
  tslib_1.__decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"personnel-class.js","sourceRoot":"","sources":["../../../server/service/personnel/personnel-class.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 인원 등급 — **ISA-95 `PersonnelClass`**.\n *\n * 등급은 자격이다. 「용접 자격」이 등급이고, 그 자격을 가진 사람이 개체다. 공정은 특정인을 지목하지\n * 않고 등급으로 요구한다 — 「용접 자격자 2명」.\n *\n * ── 왜 이 패키지가 이것을 갖나 ──────────────────────────────────────────────\n * 인원은 제품의 사실이 아니라 현장의 사실이다. 같은 사람이 MES 에서도 WMS 에서도 일한다. 제품마다\n * 표를 두면 한 사실에 표가 둘이 된다.\n *\n * 그리고 트윈 커널에 `personnelClasses` 자리가 있는데, 지금까지 붙인 원본에는 그 사실이 없었다.\n * 그래서 「자격자가 둘뿐이라 설비가 논다」를 말할 수 없었다\n * (`operato-twin/design/integration/operato-mes.md` §5).\n *\n * ── 필드 이름을 새로 짓지 않는다 ────────────────────────────────────────────\n * 커널 계약의 `ResourceClassDef` 와 같은 이름을 쓴다(`@operato/ops-contract`). 같은 사실에 이름이\n * 둘이면 옮길 때마다 번역이 생기고, 번역이 있으면 틀릴 자리가 생긴다.\n */\n@Entity({ name: 'ops_personnel_class' })\n@Index('ix_ops_personnel_class_0', (klass: PersonnelClass) => [klass.domain, klass.name], { unique: true })\n@ObjectType({ description: 'ISA-95 PersonnelClass — a qualification that operations require' })\nexport class PersonnelClass {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((klass: PersonnelClass) => klass.domain)\n domainId?: string\n\n /** 표준 `PersonnelClassID` — 트윈으로 나갈 때 `ResourceClassDef.id` 가 된다. */\n @Column()\n @Field()\n name: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n /**\n * 상위 등급들 — 표준 `PersonnelClassBaseID`(복수).\n *\n * 「6축 용접 자격」이 「용접 자격」을 상속한다. 배정 판단이 이 상속을 타므로, 상위 등급으로 요구해도\n * 하위 자격자가 잡힌다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true })\n baseIds?: string[]\n\n /** 이 등급의 적격을 정하는 `TestSpecification` 참조들 — 표준 `TestSpecificationID`. 참조만 갖는다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true })\n testSpecificationIds?: string[]\n\n /** 등급 속성 — 표준 `PersonnelClassProperty`. 개체 속성이 `classPropertyId` 로 이것을 구체화한다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => String, { nullable: true, description: 'ResourceProperty[] as JSON text' })\n properties?: Record<string, unknown>[]\n\n /**\n * 언제부터 유효한가 — 표준 `EffectiveStartDate`.\n *\n * 자격 규정은 미래 시점으로 선언된다(「9월부터 이 자격이 필요하다」). 이 자리가 없으면 그 선언을\n * 받을 수 없거나, 받고 나서 오늘 배정에 바로 적용된다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveStart?: Date\n\n /** 언제까지 유효한가 — 표준 `EffectiveEndDate`. 없으면 끝이 정해지지 않았다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveEnd?: Date\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((klass: PersonnelClass) => klass.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((klass: PersonnelClass) => klass.updater)\n updaterId?: string\n}\n"]}
1
+ {"version":3,"file":"personnel-class.js","sourceRoot":"","sources":["../../../server/service/personnel/personnel-class.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 인원 등급 — **ISA-95 `PersonnelClass`**.\n *\n * 등급은 자격이다. 「용접 자격」이 등급이고, 그 자격을 가진 사람이 개체다. 공정은 특정인을 지목하지\n * 않고 등급으로 요구한다 — 「용접 자격자 2명」.\n *\n * ── 왜 이 패키지가 이것을 갖나 ──────────────────────────────────────────────\n * 인원은 제품의 사실이 아니라 현장의 사실이다. 같은 사람이 MES 에서도 WMS 에서도 일한다. 제품마다\n * 표를 두면 한 사실에 표가 둘이 된다.\n *\n * 그리고 트윈 커널에 `personnelClasses` 자리가 있는데, 지금까지 붙인 원본에는 그 사실이 없었다.\n * 그래서 「자격자가 둘뿐이라 설비가 논다」를 말할 수 없었다\n * (`operato-twin/design/integration/operato-plant.md` §5).\n *\n * ── 필드 이름을 새로 짓지 않는다 ────────────────────────────────────────────\n * 커널 계약의 `ResourceClassDef` 와 같은 이름을 쓴다(`@operato/ops-contract`). 같은 사실에 이름이\n * 둘이면 옮길 때마다 번역이 생기고, 번역이 있으면 틀릴 자리가 생긴다.\n */\n@Entity({ name: 'ops_personnel_class' })\n@Index('ix_ops_personnel_class_0', (klass: PersonnelClass) => [klass.domain, klass.name], { unique: true })\n@ObjectType({ description: 'ISA-95 PersonnelClass — a qualification that operations require' })\nexport class PersonnelClass {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((klass: PersonnelClass) => klass.domain)\n domainId?: string\n\n /** 표준 `PersonnelClassID` — 트윈으로 나갈 때 `ResourceClassDef.id` 가 된다. */\n @Column()\n @Field()\n name: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n /**\n * 상위 등급들 — 표준 `PersonnelClassBaseID`(복수).\n *\n * 「6축 용접 자격」이 「용접 자격」을 상속한다. 배정 판단이 이 상속을 타므로, 상위 등급으로 요구해도\n * 하위 자격자가 잡힌다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true })\n baseIds?: string[]\n\n /** 이 등급의 적격을 정하는 `TestSpecification` 참조들 — 표준 `TestSpecificationID`. 참조만 갖는다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true })\n testSpecificationIds?: string[]\n\n /** 등급 속성 — 표준 `PersonnelClassProperty`. 개체 속성이 `classPropertyId` 로 이것을 구체화한다. */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'ResourceProperty[]' })\n properties?: Record<string, unknown>[]\n\n /**\n * 언제부터 유효한가 — 표준 `EffectiveStartDate`.\n *\n * 자격 규정은 미래 시점으로 선언된다(「9월부터 이 자격이 필요하다」). 이 자리가 없으면 그 선언을\n * 받을 수 없거나, 받고 나서 오늘 배정에 바로 적용된다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveStart?: Date\n\n /** 언제까지 유효한가 — 표준 `EffectiveEndDate`. 없으면 끝이 정해지지 않았다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveEnd?: Date\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((klass: PersonnelClass) => klass.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((klass: PersonnelClass) => klass.updater)\n updaterId?: string\n}\n"]}
@@ -18,6 +18,15 @@ export declare class PersonnelClassList {
18
18
  }
19
19
  export declare class PersonnelClassResolver {
20
20
  personnelClasses(params: ListParam, context: ResolverContext): Promise<PersonnelClassList>;
21
+ /**
22
+ * 이 등급을 가진 사람들
23
+ *
24
+ * 아무도 안 들면 빈 목록이다. 다만 그것이 「없다」인지 「다 못 봤다」인지는 `memberScanCapped`
25
+ * 가 말한다 — 조용히 자르면 고쳐야 할 선언과 멀쩡한 등급이 같아 보인다.
26
+ */
27
+ members(klass: PersonnelClass, context: ResolverContext): Promise<string[]>;
28
+ /** 소속을 다 보지 못했나. */
29
+ memberScanCapped(klass: PersonnelClass, context: ResolverContext): Promise<boolean>;
21
30
  /**
22
31
  * 그리드가 보낸 변경을 한 번에 적용한다.
23
32
  *