@naisys/erp 3.0.0-beta.3

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.

Potentially problematic release.


This version of @naisys/erp might be problematic. Click here for more details.

Files changed (201) hide show
  1. package/bin/naisys-erp +2 -0
  2. package/client-dist/android-chrome-192x192.png +0 -0
  3. package/client-dist/android-chrome-512x512.png +0 -0
  4. package/client-dist/apple-touch-icon.png +0 -0
  5. package/client-dist/assets/index-45dVo30p.css +1 -0
  6. package/client-dist/assets/index-Dffms7F_.js +168 -0
  7. package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
  8. package/client-dist/favicon.ico +0 -0
  9. package/client-dist/index.html +42 -0
  10. package/client-dist/site.webmanifest +22 -0
  11. package/dist/api-reference.d.ts +10 -0
  12. package/dist/api-reference.js +101 -0
  13. package/dist/audit.d.ts +5 -0
  14. package/dist/audit.js +14 -0
  15. package/dist/auth-middleware.d.ts +18 -0
  16. package/dist/auth-middleware.js +203 -0
  17. package/dist/dbConfig.d.ts +5 -0
  18. package/dist/dbConfig.js +10 -0
  19. package/dist/erpDb.d.ts +10 -0
  20. package/dist/erpDb.js +34 -0
  21. package/dist/erpServer.d.ts +10 -0
  22. package/dist/erpServer.js +321 -0
  23. package/dist/error-handler.d.ts +7 -0
  24. package/dist/error-handler.js +17 -0
  25. package/dist/generated/prisma/client.d.ts +154 -0
  26. package/dist/generated/prisma/client.js +35 -0
  27. package/dist/generated/prisma/commonInputTypes.d.ts +637 -0
  28. package/dist/generated/prisma/commonInputTypes.js +11 -0
  29. package/dist/generated/prisma/enums.d.ts +59 -0
  30. package/dist/generated/prisma/enums.js +60 -0
  31. package/dist/generated/prisma/internal/class.d.ts +406 -0
  32. package/dist/generated/prisma/internal/class.js +50 -0
  33. package/dist/generated/prisma/internal/prismaNamespace.d.ts +2722 -0
  34. package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
  35. package/dist/generated/prisma/models/Attachment.d.ts +1455 -0
  36. package/dist/generated/prisma/models/Attachment.js +2 -0
  37. package/dist/generated/prisma/models/AuditLog.d.ts +1359 -0
  38. package/dist/generated/prisma/models/AuditLog.js +2 -0
  39. package/dist/generated/prisma/models/Field.d.ts +1880 -0
  40. package/dist/generated/prisma/models/Field.js +2 -0
  41. package/dist/generated/prisma/models/FieldAttachment.d.ts +1245 -0
  42. package/dist/generated/prisma/models/FieldAttachment.js +2 -0
  43. package/dist/generated/prisma/models/FieldRecord.d.ts +1625 -0
  44. package/dist/generated/prisma/models/FieldRecord.js +2 -0
  45. package/dist/generated/prisma/models/FieldSet.d.ts +1577 -0
  46. package/dist/generated/prisma/models/FieldSet.js +2 -0
  47. package/dist/generated/prisma/models/FieldValue.d.ts +1908 -0
  48. package/dist/generated/prisma/models/FieldValue.js +2 -0
  49. package/dist/generated/prisma/models/Item.d.ts +1858 -0
  50. package/dist/generated/prisma/models/Item.js +2 -0
  51. package/dist/generated/prisma/models/ItemInstance.d.ts +1987 -0
  52. package/dist/generated/prisma/models/ItemInstance.js +2 -0
  53. package/dist/generated/prisma/models/LaborTicket.d.ts +1867 -0
  54. package/dist/generated/prisma/models/LaborTicket.js +2 -0
  55. package/dist/generated/prisma/models/Operation.d.ts +2578 -0
  56. package/dist/generated/prisma/models/Operation.js +2 -0
  57. package/dist/generated/prisma/models/OperationDependency.d.ts +1434 -0
  58. package/dist/generated/prisma/models/OperationDependency.js +2 -0
  59. package/dist/generated/prisma/models/OperationFieldRef.d.ts +1539 -0
  60. package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
  61. package/dist/generated/prisma/models/OperationRun.d.ts +2563 -0
  62. package/dist/generated/prisma/models/OperationRun.js +2 -0
  63. package/dist/generated/prisma/models/OperationRunComment.d.ts +1366 -0
  64. package/dist/generated/prisma/models/OperationRunComment.js +2 -0
  65. package/dist/generated/prisma/models/Order.d.ts +1931 -0
  66. package/dist/generated/prisma/models/Order.js +2 -0
  67. package/dist/generated/prisma/models/OrderRevision.d.ts +1962 -0
  68. package/dist/generated/prisma/models/OrderRevision.js +2 -0
  69. package/dist/generated/prisma/models/OrderRun.d.ts +2310 -0
  70. package/dist/generated/prisma/models/OrderRun.js +2 -0
  71. package/dist/generated/prisma/models/SchemaVersion.d.ts +985 -0
  72. package/dist/generated/prisma/models/SchemaVersion.js +2 -0
  73. package/dist/generated/prisma/models/Session.d.ts +1213 -0
  74. package/dist/generated/prisma/models/Session.js +2 -0
  75. package/dist/generated/prisma/models/Step.d.ts +2180 -0
  76. package/dist/generated/prisma/models/Step.js +2 -0
  77. package/dist/generated/prisma/models/StepRun.d.ts +1963 -0
  78. package/dist/generated/prisma/models/StepRun.js +2 -0
  79. package/dist/generated/prisma/models/User.d.ts +11819 -0
  80. package/dist/generated/prisma/models/User.js +2 -0
  81. package/dist/generated/prisma/models/UserPermission.d.ts +1348 -0
  82. package/dist/generated/prisma/models/UserPermission.js +2 -0
  83. package/dist/generated/prisma/models/WorkCenter.d.ts +1657 -0
  84. package/dist/generated/prisma/models/WorkCenter.js +2 -0
  85. package/dist/generated/prisma/models/WorkCenterUser.d.ts +1390 -0
  86. package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
  87. package/dist/generated/prisma/models.d.ts +28 -0
  88. package/dist/generated/prisma/models.js +2 -0
  89. package/dist/hateoas.d.ts +7 -0
  90. package/dist/hateoas.js +61 -0
  91. package/dist/route-helpers.d.ts +318 -0
  92. package/dist/route-helpers.js +220 -0
  93. package/dist/routes/admin.d.ts +3 -0
  94. package/dist/routes/admin.js +147 -0
  95. package/dist/routes/audit.d.ts +3 -0
  96. package/dist/routes/audit.js +36 -0
  97. package/dist/routes/auth.d.ts +3 -0
  98. package/dist/routes/auth.js +112 -0
  99. package/dist/routes/dispatch.d.ts +3 -0
  100. package/dist/routes/dispatch.js +174 -0
  101. package/dist/routes/inventory.d.ts +3 -0
  102. package/dist/routes/inventory.js +70 -0
  103. package/dist/routes/item-fields.d.ts +3 -0
  104. package/dist/routes/item-fields.js +220 -0
  105. package/dist/routes/item-instances.d.ts +3 -0
  106. package/dist/routes/item-instances.js +426 -0
  107. package/dist/routes/items.d.ts +3 -0
  108. package/dist/routes/items.js +252 -0
  109. package/dist/routes/labor-tickets.d.ts +3 -0
  110. package/dist/routes/labor-tickets.js +268 -0
  111. package/dist/routes/operation-dependencies.d.ts +3 -0
  112. package/dist/routes/operation-dependencies.js +170 -0
  113. package/dist/routes/operation-field-refs.d.ts +3 -0
  114. package/dist/routes/operation-field-refs.js +263 -0
  115. package/dist/routes/operation-run-comments.d.ts +3 -0
  116. package/dist/routes/operation-run-comments.js +108 -0
  117. package/dist/routes/operation-run-transitions.d.ts +3 -0
  118. package/dist/routes/operation-run-transitions.js +249 -0
  119. package/dist/routes/operation-runs.d.ts +112 -0
  120. package/dist/routes/operation-runs.js +299 -0
  121. package/dist/routes/operations.d.ts +3 -0
  122. package/dist/routes/operations.js +283 -0
  123. package/dist/routes/order-revision-transitions.d.ts +3 -0
  124. package/dist/routes/order-revision-transitions.js +86 -0
  125. package/dist/routes/order-revisions.d.ts +51 -0
  126. package/dist/routes/order-revisions.js +327 -0
  127. package/dist/routes/order-run-transitions.d.ts +3 -0
  128. package/dist/routes/order-run-transitions.js +215 -0
  129. package/dist/routes/order-runs.d.ts +58 -0
  130. package/dist/routes/order-runs.js +335 -0
  131. package/dist/routes/orders.d.ts +3 -0
  132. package/dist/routes/orders.js +262 -0
  133. package/dist/routes/root.d.ts +3 -0
  134. package/dist/routes/root.js +123 -0
  135. package/dist/routes/schemas.d.ts +3 -0
  136. package/dist/routes/schemas.js +31 -0
  137. package/dist/routes/step-field-attachments.d.ts +3 -0
  138. package/dist/routes/step-field-attachments.js +231 -0
  139. package/dist/routes/step-fields.d.ts +100 -0
  140. package/dist/routes/step-fields.js +315 -0
  141. package/dist/routes/step-run-fields.d.ts +3 -0
  142. package/dist/routes/step-run-fields.js +438 -0
  143. package/dist/routes/step-run-transitions.d.ts +3 -0
  144. package/dist/routes/step-run-transitions.js +113 -0
  145. package/dist/routes/step-runs.d.ts +332 -0
  146. package/dist/routes/step-runs.js +324 -0
  147. package/dist/routes/steps.d.ts +3 -0
  148. package/dist/routes/steps.js +283 -0
  149. package/dist/routes/user-permissions.d.ts +3 -0
  150. package/dist/routes/user-permissions.js +100 -0
  151. package/dist/routes/users.d.ts +57 -0
  152. package/dist/routes/users.js +381 -0
  153. package/dist/routes/work-centers.d.ts +3 -0
  154. package/dist/routes/work-centers.js +280 -0
  155. package/dist/schema-registry.d.ts +3 -0
  156. package/dist/schema-registry.js +45 -0
  157. package/dist/services/attachment-service.d.ts +33 -0
  158. package/dist/services/attachment-service.js +118 -0
  159. package/dist/services/field-ref-service.d.ts +96 -0
  160. package/dist/services/field-ref-service.js +74 -0
  161. package/dist/services/field-service.d.ts +49 -0
  162. package/dist/services/field-service.js +114 -0
  163. package/dist/services/field-value-service.d.ts +61 -0
  164. package/dist/services/field-value-service.js +256 -0
  165. package/dist/services/item-instance-service.d.ts +152 -0
  166. package/dist/services/item-instance-service.js +155 -0
  167. package/dist/services/item-service.d.ts +47 -0
  168. package/dist/services/item-service.js +56 -0
  169. package/dist/services/labor-ticket-service.d.ts +40 -0
  170. package/dist/services/labor-ticket-service.js +148 -0
  171. package/dist/services/log-file-service.d.ts +4 -0
  172. package/dist/services/log-file-service.js +11 -0
  173. package/dist/services/operation-dependency-service.d.ts +33 -0
  174. package/dist/services/operation-dependency-service.js +30 -0
  175. package/dist/services/operation-run-comment-service.d.ts +17 -0
  176. package/dist/services/operation-run-comment-service.js +26 -0
  177. package/dist/services/operation-run-service.d.ts +126 -0
  178. package/dist/services/operation-run-service.js +347 -0
  179. package/dist/services/operation-service.d.ts +47 -0
  180. package/dist/services/operation-service.js +132 -0
  181. package/dist/services/order-revision-service.d.ts +53 -0
  182. package/dist/services/order-revision-service.js +264 -0
  183. package/dist/services/order-run-service.d.ts +138 -0
  184. package/dist/services/order-run-service.js +356 -0
  185. package/dist/services/order-service.d.ts +15 -0
  186. package/dist/services/order-service.js +68 -0
  187. package/dist/services/revision-diff-service.d.ts +3 -0
  188. package/dist/services/revision-diff-service.js +194 -0
  189. package/dist/services/step-run-service.d.ts +172 -0
  190. package/dist/services/step-run-service.js +106 -0
  191. package/dist/services/step-service.d.ts +104 -0
  192. package/dist/services/step-service.js +89 -0
  193. package/dist/services/user-service.d.ts +185 -0
  194. package/dist/services/user-service.js +132 -0
  195. package/dist/services/work-center-service.d.ts +29 -0
  196. package/dist/services/work-center-service.js +106 -0
  197. package/dist/supervisorAuth.d.ts +3 -0
  198. package/dist/supervisorAuth.js +16 -0
  199. package/dist/userService.d.ts +20 -0
  200. package/dist/userService.js +118 -0
  201. package/package.json +69 -0
@@ -0,0 +1,252 @@
1
+ import { CreateItemSchema, ErrorResponseSchema, ItemListQuerySchema, ItemListResponseSchema, ItemSchema, KeyCreateResponseSchema, MutateResponseSchema, UpdateItemSchema, } from "@naisys/erp-shared";
2
+ import { z } from "zod/v4";
3
+ import { hasPermission, requirePermission } from "../auth-middleware.js";
4
+ import { notFound } from "../error-handler.js";
5
+ import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
+ import { calcNextSeqNo, childItemLinks, formatAuditFields, mutationResult, } from "../route-helpers.js";
7
+ import { createItem, deleteItem, findExisting, listItems, updateItem, } from "../services/item-service.js";
8
+ const RESOURCE = "items";
9
+ const KeyParamsSchema = z.object({
10
+ key: z.string(),
11
+ });
12
+ function itemLinks(key) {
13
+ return [
14
+ selfLink(`/${RESOURCE}/${key}`),
15
+ collectionLink(RESOURCE),
16
+ schemaLink("Item"),
17
+ ];
18
+ }
19
+ function itemActions(key, user) {
20
+ if (!hasPermission(user, "item_manager"))
21
+ return [];
22
+ const href = `${API_PREFIX}/${RESOURCE}/${key}`;
23
+ return [
24
+ {
25
+ rel: "update",
26
+ href,
27
+ method: "PUT",
28
+ title: "Update",
29
+ schema: `${API_PREFIX}/schemas/UpdateItem`,
30
+ },
31
+ {
32
+ rel: "delete",
33
+ href,
34
+ method: "DELETE",
35
+ title: "Delete",
36
+ },
37
+ ];
38
+ }
39
+ function formatItemFieldListResponse(itemKey, user, fields) {
40
+ const maxSeq = fields.length > 0 ? fields[fields.length - 1].seqNo : 0;
41
+ const base = `/items/${itemKey}/fields`;
42
+ return {
43
+ items: fields.map((field) => formatItemField(itemKey, user, field)),
44
+ total: fields.length,
45
+ nextSeqNo: calcNextSeqNo(maxSeq),
46
+ _links: [selfLink(base)],
47
+ _actions: hasPermission(user, "item_manager")
48
+ ? [
49
+ {
50
+ rel: "create",
51
+ href: `${API_PREFIX}${base}`,
52
+ method: "POST",
53
+ title: "Add Field",
54
+ schema: `${API_PREFIX}/schemas/CreateField`,
55
+ },
56
+ ]
57
+ : [],
58
+ };
59
+ }
60
+ function formatItemField(itemKey, user, field) {
61
+ const base = `/items/${itemKey}/fields`;
62
+ return {
63
+ id: field.id,
64
+ fieldSetId: field.fieldSetId,
65
+ seqNo: field.seqNo,
66
+ label: field.label,
67
+ type: field.type,
68
+ isArray: field.isArray,
69
+ required: field.required,
70
+ ...formatAuditFields(field),
71
+ _links: childItemLinks(base, field.seqNo, "Fields", `/items/${itemKey}`, "Item", "Field"),
72
+ _actions: hasPermission(user, "item_manager")
73
+ ? [
74
+ {
75
+ rel: "update",
76
+ href: `${API_PREFIX}${base}/${field.seqNo}`,
77
+ method: "PUT",
78
+ title: "Update",
79
+ schema: `${API_PREFIX}/schemas/UpdateField`,
80
+ },
81
+ {
82
+ rel: "delete",
83
+ href: `${API_PREFIX}${base}/${field.seqNo}`,
84
+ method: "DELETE",
85
+ title: "Delete",
86
+ },
87
+ ]
88
+ : [],
89
+ };
90
+ }
91
+ function formatItem(item, user) {
92
+ return {
93
+ id: item.id,
94
+ key: item.key,
95
+ description: item.description,
96
+ fields: formatItemFieldListResponse(item.key, user, item.fieldSet?.fields ?? []),
97
+ ...formatAuditFields(item),
98
+ _links: itemLinks(item.key),
99
+ _actions: itemActions(item.key, user),
100
+ };
101
+ }
102
+ function formatListItem(item, user) {
103
+ return {
104
+ id: item.id,
105
+ key: item.key,
106
+ description: item.description,
107
+ fields: formatItemFieldListResponse(item.key, user, item.fieldSet?.fields ?? []),
108
+ ...formatAuditFields(item),
109
+ };
110
+ }
111
+ export default function itemRoutes(fastify) {
112
+ const app = fastify.withTypeProvider();
113
+ // LIST
114
+ app.get("/", {
115
+ schema: {
116
+ description: "List items with pagination and search",
117
+ tags: ["Items"],
118
+ querystring: ItemListQuerySchema,
119
+ response: {
120
+ 200: ItemListResponseSchema,
121
+ },
122
+ },
123
+ handler: async (request) => {
124
+ const { page, pageSize, search } = request.query;
125
+ const where = {};
126
+ if (search) {
127
+ where.OR = [
128
+ { key: { contains: search } },
129
+ { description: { contains: search } },
130
+ ];
131
+ }
132
+ const [items, total] = await listItems(where, page, pageSize);
133
+ return {
134
+ items: items.map((item) => formatListItem(item, request.erpUser)),
135
+ total,
136
+ page,
137
+ pageSize,
138
+ _links: paginationLinks(RESOURCE, page, pageSize, total, { search }),
139
+ _linkTemplates: [
140
+ { rel: "item", hrefTemplate: `${API_PREFIX}/items/{key}` },
141
+ ],
142
+ _actions: hasPermission(request.erpUser, "item_manager")
143
+ ? [
144
+ {
145
+ rel: "create",
146
+ href: `${API_PREFIX}/${RESOURCE}`,
147
+ method: "POST",
148
+ title: "Create Item",
149
+ schema: `${API_PREFIX}/schemas/CreateItem`,
150
+ },
151
+ ]
152
+ : [],
153
+ };
154
+ },
155
+ });
156
+ // CREATE
157
+ app.post("/", {
158
+ schema: {
159
+ description: "Create a new item",
160
+ tags: ["Items"],
161
+ body: CreateItemSchema,
162
+ response: {
163
+ 201: KeyCreateResponseSchema,
164
+ },
165
+ },
166
+ preHandler: requirePermission("item_manager"),
167
+ handler: async (request, reply) => {
168
+ const { key, description } = request.body;
169
+ const userId = request.erpUser.id;
170
+ const item = await createItem(key, description, userId);
171
+ const full = formatItem(item, request.erpUser);
172
+ reply.status(201);
173
+ return mutationResult(request, reply, full, {
174
+ id: full.id,
175
+ key: full.key,
176
+ _links: full._links,
177
+ _actions: full._actions,
178
+ });
179
+ },
180
+ });
181
+ // GET by key
182
+ app.get("/:key", {
183
+ schema: {
184
+ description: "Get a single item by key",
185
+ tags: ["Items"],
186
+ params: KeyParamsSchema,
187
+ response: {
188
+ 200: ItemSchema,
189
+ 404: ErrorResponseSchema,
190
+ },
191
+ },
192
+ handler: async (request, reply) => {
193
+ const { key } = request.params;
194
+ const item = await findExisting(key);
195
+ if (!item) {
196
+ return notFound(reply, `Item '${key}' not found`);
197
+ }
198
+ return formatItem(item, request.erpUser);
199
+ },
200
+ });
201
+ // UPDATE
202
+ app.put("/:key", {
203
+ schema: {
204
+ description: "Update an item",
205
+ tags: ["Items"],
206
+ params: KeyParamsSchema,
207
+ body: UpdateItemSchema,
208
+ response: {
209
+ 200: MutateResponseSchema,
210
+ 404: ErrorResponseSchema,
211
+ },
212
+ },
213
+ preHandler: requirePermission("item_manager"),
214
+ handler: async (request, reply) => {
215
+ const { key } = request.params;
216
+ const data = request.body;
217
+ const userId = request.erpUser.id;
218
+ const existing = await findExisting(key);
219
+ if (!existing) {
220
+ return notFound(reply, `Item '${key}' not found`);
221
+ }
222
+ const item = await updateItem(key, data, userId);
223
+ const full = formatItem(item, request.erpUser);
224
+ return mutationResult(request, reply, full, {
225
+ _actions: full._actions,
226
+ });
227
+ },
228
+ });
229
+ // DELETE
230
+ app.delete("/:key", {
231
+ schema: {
232
+ description: "Delete an item",
233
+ tags: ["Items"],
234
+ params: KeyParamsSchema,
235
+ response: {
236
+ 204: z.void(),
237
+ 404: ErrorResponseSchema,
238
+ },
239
+ },
240
+ preHandler: requirePermission("item_manager"),
241
+ handler: async (request, reply) => {
242
+ const { key } = request.params;
243
+ const existing = await findExisting(key);
244
+ if (!existing) {
245
+ return notFound(reply, `Item '${key}' not found`);
246
+ }
247
+ await deleteItem(key);
248
+ reply.status(204);
249
+ },
250
+ });
251
+ }
252
+ //# sourceMappingURL=items.js.map
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function laborTicketRoutes(fastify: FastifyInstance): void;
3
+ //# sourceMappingURL=labor-tickets.d.ts.map
@@ -0,0 +1,268 @@
1
+ import { ClockOutLaborTicketSchema, CreateResponseSchema, ErrorResponseSchema, LaborTicketListResponseSchema, MutateResponseSchema, OperationRunStatus, } from "@naisys/erp-shared";
2
+ import { z } from "zod/v4";
3
+ import { hasPermission, requirePermission } from "../auth-middleware.js";
4
+ import { notFound } from "../error-handler.js";
5
+ import { API_PREFIX, selfLink } from "../hateoas.js";
6
+ import { checkOpRunInProgress, formatAuditFields, formatDate, mutationResult, permGate, resolveOpRun, } from "../route-helpers.js";
7
+ import { clockIn, clockOut, deleteLaborTicket, listLaborTickets, } from "../services/labor-ticket-service.js";
8
+ function laborResource(orderKey, runNo, seqNo) {
9
+ return `orders/${orderKey}/runs/${runNo}/ops/${seqNo}/labor`;
10
+ }
11
+ function laborTicketListActions(orderKey, runNo, seqNo, opRunStatus, user, tickets) {
12
+ const actions = [];
13
+ const base = `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}`;
14
+ const isInProgress = opRunStatus === OperationRunStatus.in_progress;
15
+ const isExecutor = hasPermission(user, "order_executor");
16
+ const executorGate = permGate(isExecutor, "order_executor");
17
+ const userHasOpenTicket = user != null && tickets.some((t) => t.userId === user.id && !t.clockOut);
18
+ const anyOpenTickets = tickets.some((t) => !t.clockOut);
19
+ // clock-in: show for non-executors (disabled for permission), or executors with status logic
20
+ if (!isExecutor) {
21
+ actions.push({
22
+ rel: "clock-in",
23
+ href: `${base}/clock-in`,
24
+ method: "POST",
25
+ title: "Clock In",
26
+ ...executorGate,
27
+ });
28
+ }
29
+ else if (!isInProgress) {
30
+ actions.push({
31
+ rel: "clock-in",
32
+ href: `${base}/clock-in`,
33
+ method: "POST",
34
+ title: "Clock In",
35
+ disabled: true,
36
+ disabledReason: "Operation must be in progress to clock in",
37
+ });
38
+ }
39
+ else if (!userHasOpenTicket) {
40
+ actions.push({
41
+ rel: "clock-in",
42
+ href: `${base}/clock-in`,
43
+ method: "POST",
44
+ title: "Clock In",
45
+ });
46
+ }
47
+ // clock-out for executor
48
+ if (isExecutor && isInProgress && userHasOpenTicket) {
49
+ actions.push({
50
+ rel: "clock-out",
51
+ href: `${base}/clock-out`,
52
+ method: "POST",
53
+ title: "Clock Out",
54
+ schema: `${API_PREFIX}/schemas/ClockOutLaborTicket`,
55
+ body: {},
56
+ });
57
+ }
58
+ // manager clock-out: for non-executor managers only
59
+ if (!isExecutor &&
60
+ hasPermission(user, "order_manager") &&
61
+ isInProgress &&
62
+ anyOpenTickets) {
63
+ actions.push({
64
+ rel: "clock-out",
65
+ href: `${base}/clock-out`,
66
+ method: "POST",
67
+ title: "Clock Out",
68
+ schema: `${API_PREFIX}/schemas/ClockOutLaborTicket`,
69
+ body: { userId: 0, ticketId: 0 },
70
+ });
71
+ }
72
+ return actions;
73
+ }
74
+ function laborTicketActionTemplates(orderKey, runNo, seqNo, user) {
75
+ if (!hasPermission(user, "order_manager"))
76
+ return [];
77
+ return [
78
+ {
79
+ rel: "deleteTicket",
80
+ hrefTemplate: `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}/{ticketId}`,
81
+ method: "DELETE",
82
+ title: "Delete Ticket",
83
+ },
84
+ ];
85
+ }
86
+ function formatLaborTicket(orderKey, runNo, seqNo, ticket) {
87
+ return {
88
+ id: ticket.id,
89
+ operationRunId: ticket.operationRunId,
90
+ userId: ticket.userId,
91
+ username: ticket.user.username,
92
+ runId: ticket.runId,
93
+ clockIn: ticket.clockIn.toISOString(),
94
+ clockOut: formatDate(ticket.clockOut),
95
+ cost: ticket.cost,
96
+ ...formatAuditFields(ticket),
97
+ _links: [
98
+ selfLink(`/${laborResource(orderKey, runNo, seqNo)}/${ticket.id}`),
99
+ ],
100
+ };
101
+ }
102
+ const LaborParamsSchema = z.object({
103
+ orderKey: z.string(),
104
+ runNo: z.coerce.number().int(),
105
+ seqNo: z.coerce.number().int(),
106
+ });
107
+ const LaborTicketParamsSchema = z.object({
108
+ orderKey: z.string(),
109
+ runNo: z.coerce.number().int(),
110
+ seqNo: z.coerce.number().int(),
111
+ ticketId: z.coerce.number().int(),
112
+ });
113
+ export default function laborTicketRoutes(fastify) {
114
+ const app = fastify.withTypeProvider();
115
+ // LIST
116
+ app.get("/", {
117
+ schema: {
118
+ description: "List labor tickets for an operation run",
119
+ tags: ["Labor Tickets"],
120
+ params: LaborParamsSchema,
121
+ response: {
122
+ 200: LaborTicketListResponseSchema,
123
+ 404: ErrorResponseSchema,
124
+ },
125
+ },
126
+ handler: async (request, reply) => {
127
+ const { orderKey, runNo, seqNo } = request.params;
128
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
129
+ if (!resolved) {
130
+ return notFound(reply, "Operation run not found");
131
+ }
132
+ const items = await listLaborTickets(resolved.opRun.id);
133
+ return {
134
+ items: items.map((ticket) => {
135
+ const { _links, ...rest } = formatLaborTicket(orderKey, runNo, seqNo, ticket);
136
+ return rest;
137
+ }),
138
+ total: items.length,
139
+ _links: [selfLink(`/${laborResource(orderKey, runNo, seqNo)}`)],
140
+ _linkTemplates: [
141
+ {
142
+ rel: "item",
143
+ hrefTemplate: `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}/{id}`,
144
+ },
145
+ ],
146
+ _actions: laborTicketListActions(orderKey, runNo, seqNo, resolved.opRun.status, request.erpUser, items),
147
+ _actionTemplates: laborTicketActionTemplates(orderKey, runNo, seqNo, request.erpUser),
148
+ };
149
+ },
150
+ });
151
+ // CLOCK IN
152
+ app.post("/clock-in", {
153
+ schema: {
154
+ description: "Clock in to an operation run (auto clocks out any open tickets for this user)",
155
+ tags: ["Labor Tickets"],
156
+ params: LaborParamsSchema,
157
+ response: {
158
+ 200: CreateResponseSchema,
159
+ 404: ErrorResponseSchema,
160
+ 409: ErrorResponseSchema,
161
+ },
162
+ },
163
+ preHandler: requirePermission("order_executor"),
164
+ handler: async (request, reply) => {
165
+ const { orderKey, runNo, seqNo } = request.params;
166
+ const userId = request.erpUser.id;
167
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
168
+ if (!resolved)
169
+ return notFound(reply, "Operation run not found");
170
+ const opErr = checkOpRunInProgress(resolved.opRun.status);
171
+ if (opErr) {
172
+ reply.status(409);
173
+ return { statusCode: 409, error: "Conflict", message: opErr };
174
+ }
175
+ const ticket = await clockIn(resolved.opRun.id, userId, userId);
176
+ const full = formatLaborTicket(orderKey, runNo, seqNo, ticket);
177
+ return mutationResult(request, reply, full, {
178
+ id: full.id,
179
+ _links: full._links,
180
+ });
181
+ },
182
+ });
183
+ // CLOCK OUT
184
+ app.post("/clock-out", {
185
+ schema: {
186
+ description: "Clock out of an operation run",
187
+ tags: ["Labor Tickets"],
188
+ params: LaborParamsSchema,
189
+ body: ClockOutLaborTicketSchema,
190
+ response: {
191
+ 200: MutateResponseSchema,
192
+ 404: ErrorResponseSchema,
193
+ 409: ErrorResponseSchema,
194
+ },
195
+ },
196
+ preHandler: requirePermission("order_executor"),
197
+ handler: async (request, reply) => {
198
+ const { orderKey, runNo, seqNo } = request.params;
199
+ const userId = request.erpUser.id;
200
+ const body = request.body;
201
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
202
+ if (!resolved)
203
+ return notFound(reply, "Operation run not found");
204
+ const opErr = checkOpRunInProgress(resolved.opRun.status);
205
+ if (opErr) {
206
+ reply.status(409);
207
+ return { statusCode: 409, error: "Conflict", message: opErr };
208
+ }
209
+ // Non-managers can only clock out their own tickets
210
+ const isManager = hasPermission(request.erpUser, "order_manager");
211
+ const opts = {
212
+ userId: body.userId ?? (isManager ? undefined : userId),
213
+ ticketId: body.ticketId,
214
+ };
215
+ // Non-managers cannot specify another user's ID
216
+ if (!isManager && body.userId && body.userId !== userId) {
217
+ reply.status(409);
218
+ return {
219
+ statusCode: 409,
220
+ error: "Conflict",
221
+ message: "Non-managers can only clock out their own tickets",
222
+ };
223
+ }
224
+ await clockOut(resolved.opRun.id, opts, userId);
225
+ // Return full list after clock-out
226
+ const items = await listLaborTickets(resolved.opRun.id);
227
+ const full = {
228
+ items: items.map((ticket) => {
229
+ const { _links, ...rest } = formatLaborTicket(orderKey, runNo, seqNo, ticket);
230
+ return rest;
231
+ }),
232
+ total: items.length,
233
+ _links: [selfLink(`/${laborResource(orderKey, runNo, seqNo)}`)],
234
+ _linkTemplates: [
235
+ {
236
+ rel: "item",
237
+ hrefTemplate: `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}/{id}`,
238
+ },
239
+ ],
240
+ _actions: laborTicketListActions(orderKey, runNo, seqNo, resolved.opRun.status, request.erpUser, items),
241
+ _actionTemplates: laborTicketActionTemplates(orderKey, runNo, seqNo, request.erpUser),
242
+ };
243
+ return mutationResult(request, reply, full, {
244
+ _actions: full._actions,
245
+ });
246
+ },
247
+ });
248
+ // DELETE
249
+ app.delete("/:ticketId", {
250
+ schema: {
251
+ description: "Delete a labor ticket",
252
+ tags: ["Labor Tickets"],
253
+ params: LaborTicketParamsSchema,
254
+ response: {
255
+ 204: z.void(),
256
+ 404: ErrorResponseSchema,
257
+ },
258
+ },
259
+ preHandler: requirePermission("order_manager"),
260
+ handler: async (request, reply) => {
261
+ const { ticketId } = request.params;
262
+ const userId = request.erpUser.id;
263
+ await deleteLaborTicket(ticketId, userId);
264
+ reply.status(204);
265
+ },
266
+ });
267
+ }
268
+ //# sourceMappingURL=labor-tickets.js.map
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function operationDependencyRoutes(fastify: FastifyInstance): void;
3
+ //# sourceMappingURL=operation-dependencies.d.ts.map