@naisys/erp 3.0.0-beta.6

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 (150) 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.js +101 -0
  12. package/dist/audit.js +14 -0
  13. package/dist/auth-middleware.js +203 -0
  14. package/dist/dbConfig.js +10 -0
  15. package/dist/erpDb.js +34 -0
  16. package/dist/erpServer.js +321 -0
  17. package/dist/error-handler.js +17 -0
  18. package/dist/generated/prisma/client.js +35 -0
  19. package/dist/generated/prisma/commonInputTypes.js +11 -0
  20. package/dist/generated/prisma/enums.js +60 -0
  21. package/dist/generated/prisma/internal/class.js +50 -0
  22. package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
  23. package/dist/generated/prisma/models/Attachment.js +2 -0
  24. package/dist/generated/prisma/models/AuditLog.js +2 -0
  25. package/dist/generated/prisma/models/Field.js +2 -0
  26. package/dist/generated/prisma/models/FieldAttachment.js +2 -0
  27. package/dist/generated/prisma/models/FieldRecord.js +2 -0
  28. package/dist/generated/prisma/models/FieldSet.js +2 -0
  29. package/dist/generated/prisma/models/FieldValue.js +2 -0
  30. package/dist/generated/prisma/models/Item.js +2 -0
  31. package/dist/generated/prisma/models/ItemInstance.js +2 -0
  32. package/dist/generated/prisma/models/LaborTicket.js +2 -0
  33. package/dist/generated/prisma/models/Operation.js +2 -0
  34. package/dist/generated/prisma/models/OperationDependency.js +2 -0
  35. package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
  36. package/dist/generated/prisma/models/OperationRun.js +2 -0
  37. package/dist/generated/prisma/models/OperationRunComment.js +2 -0
  38. package/dist/generated/prisma/models/Order.js +2 -0
  39. package/dist/generated/prisma/models/OrderRevision.js +2 -0
  40. package/dist/generated/prisma/models/OrderRun.js +2 -0
  41. package/dist/generated/prisma/models/SchemaVersion.js +2 -0
  42. package/dist/generated/prisma/models/Session.js +2 -0
  43. package/dist/generated/prisma/models/Step.js +2 -0
  44. package/dist/generated/prisma/models/StepRun.js +2 -0
  45. package/dist/generated/prisma/models/User.js +2 -0
  46. package/dist/generated/prisma/models/UserPermission.js +2 -0
  47. package/dist/generated/prisma/models/WorkCenter.js +2 -0
  48. package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
  49. package/dist/generated/prisma/models.js +2 -0
  50. package/dist/hateoas.js +61 -0
  51. package/dist/route-helpers.js +220 -0
  52. package/dist/routes/admin.js +147 -0
  53. package/dist/routes/audit.js +36 -0
  54. package/dist/routes/auth.js +112 -0
  55. package/dist/routes/dispatch.js +174 -0
  56. package/dist/routes/inventory.js +70 -0
  57. package/dist/routes/item-fields.js +220 -0
  58. package/dist/routes/item-instances.js +426 -0
  59. package/dist/routes/items.js +252 -0
  60. package/dist/routes/labor-tickets.js +268 -0
  61. package/dist/routes/operation-dependencies.js +170 -0
  62. package/dist/routes/operation-field-refs.js +263 -0
  63. package/dist/routes/operation-run-comments.js +108 -0
  64. package/dist/routes/operation-run-transitions.js +249 -0
  65. package/dist/routes/operation-runs.js +299 -0
  66. package/dist/routes/operations.js +283 -0
  67. package/dist/routes/order-revision-transitions.js +86 -0
  68. package/dist/routes/order-revisions.js +327 -0
  69. package/dist/routes/order-run-transitions.js +215 -0
  70. package/dist/routes/order-runs.js +335 -0
  71. package/dist/routes/orders.js +262 -0
  72. package/dist/routes/root.js +123 -0
  73. package/dist/routes/schemas.js +31 -0
  74. package/dist/routes/step-field-attachments.js +231 -0
  75. package/dist/routes/step-fields.js +315 -0
  76. package/dist/routes/step-run-fields.js +438 -0
  77. package/dist/routes/step-run-transitions.js +113 -0
  78. package/dist/routes/step-runs.js +324 -0
  79. package/dist/routes/steps.js +283 -0
  80. package/dist/routes/user-permissions.js +100 -0
  81. package/dist/routes/users.js +381 -0
  82. package/dist/routes/work-centers.js +280 -0
  83. package/dist/schema-registry.js +45 -0
  84. package/dist/services/attachment-service.js +118 -0
  85. package/dist/services/field-ref-service.js +74 -0
  86. package/dist/services/field-service.js +114 -0
  87. package/dist/services/field-value-service.js +256 -0
  88. package/dist/services/item-instance-service.js +155 -0
  89. package/dist/services/item-service.js +56 -0
  90. package/dist/services/labor-ticket-service.js +148 -0
  91. package/dist/services/log-file-service.js +11 -0
  92. package/dist/services/operation-dependency-service.js +30 -0
  93. package/dist/services/operation-run-comment-service.js +26 -0
  94. package/dist/services/operation-run-service.js +347 -0
  95. package/dist/services/operation-service.js +132 -0
  96. package/dist/services/order-revision-service.js +264 -0
  97. package/dist/services/order-run-service.js +356 -0
  98. package/dist/services/order-service.js +68 -0
  99. package/dist/services/revision-diff-service.js +194 -0
  100. package/dist/services/step-run-service.js +106 -0
  101. package/dist/services/step-service.js +89 -0
  102. package/dist/services/user-service.js +132 -0
  103. package/dist/services/work-center-service.js +106 -0
  104. package/dist/supervisorAuth.js +16 -0
  105. package/dist/userService.js +118 -0
  106. package/package.json +75 -0
  107. package/prisma/migrations/20260212170352_init/migration.sql +125 -0
  108. package/prisma/migrations/20260308000000_multi_session/migration.sql +23 -0
  109. package/prisma/migrations/20260309000000_add_user_api_key/migration.sql +5 -0
  110. package/prisma/migrations/20260309010000_add_plan_operations/migration.sql +21 -0
  111. package/prisma/migrations/20260309020000_rename_exec_orders_to_order_runs/migration.sql +13 -0
  112. package/prisma/migrations/20260310000000_rename_plan_order_revs_to_order_revisions/migration.sql +22 -0
  113. package/prisma/migrations/20260310100000_rename_plan_orders_to_orders/migration.sql +23 -0
  114. package/prisma/migrations/20260310200000_rename_order_no_to_run_no/migration.sql +3 -0
  115. package/prisma/migrations/20260312000000_add_user_permissions/migration.sql +16 -0
  116. package/prisma/migrations/20260313000000_rename_plan_operations_to_operations/migration.sql +2 -0
  117. package/prisma/migrations/20260313100000_add_steps/migration.sql +20 -0
  118. package/prisma/migrations/20260314000000_add_step_fields/migration.sql +22 -0
  119. package/prisma/migrations/20260315000000_add_operation_runs/migration.sql +24 -0
  120. package/prisma/migrations/20260315100000_add_step_runs/migration.sql +40 -0
  121. package/prisma/migrations/20260316000000_drop_order_name/migration.sql +12 -0
  122. package/prisma/migrations/20260317000000_add_attachments/migration.sql +28 -0
  123. package/prisma/migrations/20260317000000_add_items/migration.sql +21 -0
  124. package/prisma/migrations/20260317100000_add_order_item_id/migration.sql +8 -0
  125. package/prisma/migrations/20260318000000_add_labor_tickets/migration.sql +27 -0
  126. package/prisma/migrations/20260319000000_add_operation_dependencies/migration.sql +17 -0
  127. package/prisma/migrations/20260320000000_step_field_is_array/migration.sql +5 -0
  128. package/prisma/migrations/20260320100000_rename_is_array_to_multi_value/migration.sql +2 -0
  129. package/prisma/migrations/20260320200000_add_field_types/migration.sql +2 -0
  130. package/prisma/migrations/20260321000000_add_multi_set/migration.sql +13 -0
  131. package/prisma/migrations/20260322000000_add_field_sets/migration.sql +90 -0
  132. package/prisma/migrations/20260323000000_add_item_instances/migration.sql +18 -0
  133. package/prisma/migrations/20260324000000_add_field_records/migration.sql +77 -0
  134. package/prisma/migrations/20260325000000_add_run_costs/migration.sql +3 -0
  135. package/prisma/migrations/20260326000000_add_comments/migration.sql +16 -0
  136. package/prisma/migrations/20260327000000_move_assigned_to_op_run/migration.sql +3 -0
  137. package/prisma/migrations/20260328000000_add_step_completion_note/migration.sql +2 -0
  138. package/prisma/migrations/20260328000000_add_step_title/migration.sql +2 -0
  139. package/prisma/migrations/20260329000000_rename_notes_to_release_note/migration.sql +2 -0
  140. package/prisma/migrations/20260329000000_simplify_order_run_dates/migration.sql +5 -0
  141. package/prisma/migrations/20260330000000_add_operation_run_completion_note/migration.sql +2 -0
  142. package/prisma/migrations/20260331000000_add_work_centers/migration.sql +30 -0
  143. package/prisma/migrations/20260401000000_fix_field_values_column_shift/migration.sql +26 -0
  144. package/prisma/migrations/20260402000000_rename_completion_note_to_status_note/migration.sql +5 -0
  145. package/prisma/migrations/20260403000000_add_operation_field_refs/migration.sql +16 -0
  146. package/prisma/migrations/20260404000000_rename_multi_value_to_is_array/migration.sql +2 -0
  147. package/prisma/migrations/20260404100000_add_attachment_public_id/migration.sql +8 -0
  148. package/prisma/migrations/migration_lock.toml +3 -0
  149. package/prisma/schema.prisma +595 -0
  150. package/prisma.config.ts +18 -0
@@ -0,0 +1,324 @@
1
+ import { ErrorResponseSchema, fieldTypeString, getValueFormatHint, OperationRunStatus, StepRunListQuerySchema, StepRunListResponseSchema, StepRunSchema, } from "@naisys/erp-shared";
2
+ import { z } from "zod/v4";
3
+ import { hasPermission } from "../auth-middleware.js";
4
+ import { notFound } from "../error-handler.js";
5
+ import { API_PREFIX, selfLink } from "../hateoas.js";
6
+ import { checkWorkCenterAccess, childItemLinks, formatAuditFields, resolveActions, resolveOpRun, resolveStepRun, } from "../route-helpers.js";
7
+ import { deserializeFieldValue, validateCompletionFields, validateFieldValue, } from "../services/field-value-service.js";
8
+ import { isUserClockedIn } from "../services/labor-ticket-service.js";
9
+ import { getStepRunWithFields, listStepRuns, listStepRunsWithFields, } from "../services/step-run-service.js";
10
+ export function stepRunResource(orderKey, runNo, seqNo) {
11
+ return `orders/${orderKey}/runs/${runNo}/ops/${seqNo}/steps`;
12
+ }
13
+ async function stepRunItemActions(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, completed, stepRunId, user) {
14
+ const href = `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/${stepSeqNo}`;
15
+ const isExecutor = hasPermission(user, "order_executor");
16
+ const isInProgress = opRunStatus === OperationRunStatus.in_progress;
17
+ // Pre-compute disabled reasons for complete action
18
+ const wcErr = user ? await checkWorkCenterAccess(operationId, user) : null;
19
+ const clockedInErr = isExecutor && isInProgress && !completed
20
+ ? (await isUserClockedIn(opRunId, user.id))
21
+ ? null
22
+ : "You must be clocked in to complete steps"
23
+ : null;
24
+ const fieldsErr = isExecutor && isInProgress && !completed
25
+ ? await (async () => {
26
+ const existing = await getStepRunWithFields(stepRunId);
27
+ return existing ? validateCompletionFields(existing) : null;
28
+ })()
29
+ : null;
30
+ return resolveActions([
31
+ {
32
+ rel: "complete",
33
+ path: "/complete",
34
+ method: "POST",
35
+ title: "Complete",
36
+ schema: `${API_PREFIX}/schemas/CompleteStepRun`,
37
+ body: { note: "" },
38
+ permission: "order_executor",
39
+ visibleWhen: () => !completed,
40
+ disabledWhen: () => !isInProgress
41
+ ? "Parent operation must be in progress"
42
+ : (wcErr ?? clockedInErr ?? fieldsErr),
43
+ },
44
+ {
45
+ rel: "reopen",
46
+ path: "/reopen",
47
+ method: "POST",
48
+ title: "Reopen",
49
+ permission: "order_executor",
50
+ visibleWhen: () => completed,
51
+ disabledWhen: () => !isInProgress ? "Parent operation must be in progress" : wcErr,
52
+ },
53
+ ], href, { status: opRunStatus, user });
54
+ }
55
+ export function buildStepRunActionTemplates(stepRunHref, canUpdate, multiSet, hasAttachmentFields, hasArrayFields) {
56
+ if (!canUpdate)
57
+ return [];
58
+ // Show array example in body hints when there are array fields
59
+ const valueHint = hasArrayFields ? [""] : "";
60
+ const batchValueHint = hasArrayFields
61
+ ? [
62
+ { fieldSeqNo: 0, value: "" },
63
+ { fieldSeqNo: 0, value: [""] },
64
+ ]
65
+ : [{ fieldSeqNo: 0, value: "" }];
66
+ return [
67
+ {
68
+ rel: "updateField",
69
+ hrefTemplate: multiSet
70
+ ? `${stepRunHref}/sets/{setIndex}/fields/{fieldSeqNo}`
71
+ : `${stepRunHref}/fields/{fieldSeqNo}`,
72
+ method: "PUT",
73
+ title: "Update Field Value",
74
+ schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
75
+ body: { value: valueHint },
76
+ },
77
+ {
78
+ rel: "batchUpdateFields",
79
+ hrefTemplate: multiSet
80
+ ? `${stepRunHref}/sets/{setIndex}/fields`
81
+ : `${stepRunHref}/fields`,
82
+ method: "PUT",
83
+ title: "Batch Update Field Values",
84
+ schema: `${API_PREFIX}/schemas/BatchUpdateFieldValues`,
85
+ body: { fieldValues: batchValueHint },
86
+ },
87
+ ...(multiSet
88
+ ? [
89
+ {
90
+ rel: "deleteSet",
91
+ hrefTemplate: `${stepRunHref}/sets/{setIndex}`,
92
+ method: "DELETE",
93
+ title: "Delete Set",
94
+ },
95
+ ]
96
+ : []),
97
+ ...(hasAttachmentFields
98
+ ? [
99
+ {
100
+ rel: "uploadAttachment",
101
+ hrefTemplate: multiSet
102
+ ? `${stepRunHref}/sets/{setIndex}/fields/{fieldSeqNo}/attachments`
103
+ : `${stepRunHref}/fields/{fieldSeqNo}/attachments`,
104
+ method: "POST",
105
+ title: "Upload Attachment",
106
+ alternateEncoding: {
107
+ contentType: "multipart/form-data",
108
+ description: "Upload file as multipart/form-data with field 'file'",
109
+ fileFields: ["file"],
110
+ },
111
+ },
112
+ {
113
+ rel: "downloadAttachment",
114
+ hrefTemplate: multiSet
115
+ ? `${stepRunHref}/sets/{setIndex}/fields/{fieldSeqNo}/attachments/{attachmentId}`
116
+ : `${stepRunHref}/fields/{fieldSeqNo}/attachments/{attachmentId}`,
117
+ method: "GET",
118
+ title: "Download Attachment",
119
+ },
120
+ {
121
+ rel: "deleteAttachment",
122
+ hrefTemplate: multiSet
123
+ ? `${stepRunHref}/sets/{setIndex}/fields/{fieldSeqNo}/attachments/{attachmentId}`
124
+ : `${stepRunHref}/fields/{fieldSeqNo}/attachments/{attachmentId}`,
125
+ method: "DELETE",
126
+ title: "Delete Attachment",
127
+ },
128
+ ]
129
+ : []),
130
+ ];
131
+ }
132
+ /** Compute just the HATEOAS actions + action templates for a step run */
133
+ export async function computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, completed, stepRunId, multiSet, hasAttachmentFields, hasArrayFields, user) {
134
+ const canUpdate = hasPermission(user, "order_executor") &&
135
+ opRunStatus === OperationRunStatus.in_progress;
136
+ const stepRunHref = `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/${stepSeqNo}`;
137
+ return {
138
+ _actions: await stepRunItemActions(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, completed, stepRunId, user),
139
+ _actionTemplates: buildStepRunActionTemplates(stepRunHref, canUpdate, multiSet, hasAttachmentFields, hasArrayFields),
140
+ };
141
+ }
142
+ const OpSeqNoParamsSchema = z.object({
143
+ orderKey: z.string(),
144
+ runNo: z.coerce.number().int(),
145
+ seqNo: z.coerce.number().int(),
146
+ });
147
+ const StepSeqNoParamsSchema = z.object({
148
+ orderKey: z.string(),
149
+ runNo: z.coerce.number().int(),
150
+ seqNo: z.coerce.number().int(),
151
+ stepSeqNo: z.coerce.number().int(),
152
+ });
153
+ export async function formatStepRunTransition(orderKey, runNo, seqNo, opRunId, operationId, opRunStatus, user, stepRun) {
154
+ const stepSeqNo = stepRun.step.seqNo;
155
+ const multiSet = stepRun.step.multiSet;
156
+ const fields = stepRun.step.fieldSet?.fields ?? [];
157
+ const hasAttachmentFields = fields.some((f) => f.type === "attachment");
158
+ const hasArrayFields = fields.some((f) => f.isArray);
159
+ const hateoas = await computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, stepRun.completed, stepRun.id, multiSet, hasAttachmentFields, hasArrayFields, user);
160
+ return {
161
+ id: stepRun.id,
162
+ completed: stepRun.completed,
163
+ note: stepRun.statusNote ?? null,
164
+ ...formatAuditFields(stepRun),
165
+ ...hateoas,
166
+ };
167
+ }
168
+ export async function formatStepRunWithFields(orderKey, runNo, seqNo, opRunId, operationId, opRunStatus, user, stepRun) {
169
+ const stepSeqNo = stepRun.step.seqNo;
170
+ const multiSet = stepRun.step.multiSet;
171
+ // Determine how many sets exist
172
+ const storedFieldValues = stepRun.fieldRecord?.fieldValues ?? [];
173
+ const maxSetIndex = storedFieldValues.reduce((max, fv) => Math.max(max, fv.setIndex), -1);
174
+ const setCount = Math.max(1, maxSetIndex + 1);
175
+ // Merge field definitions with stored values + validation + attachments
176
+ const fieldValues = [];
177
+ const stepRunHref = `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/${stepSeqNo}`;
178
+ for (let si = 0; si < setCount; si++) {
179
+ for (const field of stepRun.step.fieldSet?.fields ?? []) {
180
+ const stored = storedFieldValues.find((fv) => fv.fieldId === field.id && fv.setIndex === si);
181
+ const value = deserializeFieldValue(stored?.value ?? "", field.isArray);
182
+ const setPath = multiSet
183
+ ? `/sets/${si}/fields/${field.seqNo}`
184
+ : `/fields/${field.seqNo}`;
185
+ const attachments = field.type === "attachment" && stored
186
+ ? stored.fieldAttachments.map((sfa) => ({
187
+ id: sfa.attachment.publicId,
188
+ filename: sfa.attachment.filename,
189
+ fileSize: sfa.attachment.fileSize,
190
+ downloadHref: `${stepRunHref}${setPath}/attachments/${sfa.attachment.publicId}`,
191
+ }))
192
+ : undefined;
193
+ const fieldType = fieldTypeString(field.type, field.isArray);
194
+ fieldValues.push({
195
+ fieldId: field.id,
196
+ fieldSeqNo: field.seqNo,
197
+ label: field.label,
198
+ type: fieldType,
199
+ valueFormat: getValueFormatHint(fieldType),
200
+ required: field.required,
201
+ setIndex: si,
202
+ value,
203
+ attachments,
204
+ validation: validateFieldValue(field.type, field.isArray, field.required, value),
205
+ });
206
+ }
207
+ }
208
+ const allFields = stepRun.step.fieldSet?.fields ?? [];
209
+ const hasAttachmentFields = allFields.some((f) => f.type === "attachment");
210
+ const hasArrayFields = allFields.some((f) => f.isArray);
211
+ const hateoas = await computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, stepRun.completed, stepRun.id, multiSet, hasAttachmentFields, hasArrayFields, user);
212
+ return {
213
+ id: stepRun.id,
214
+ operationRunId: stepRun.operationRunId,
215
+ stepId: stepRun.stepId,
216
+ seqNo: stepSeqNo,
217
+ title: stepRun.step.title,
218
+ instructions: stepRun.step.instructions,
219
+ multiSet,
220
+ completed: stepRun.completed,
221
+ note: stepRun.statusNote ?? null,
222
+ fieldCount: stepRun.step.fieldSet?.fields.length ?? 0,
223
+ fieldValues,
224
+ ...formatAuditFields(stepRun),
225
+ _links: childItemLinks("/" + stepRunResource(orderKey, runNo, seqNo), stepSeqNo, "Step Runs", "/orders/" + orderKey + "/runs/" + runNo + "/ops/" + seqNo, "Operation Run", "StepRun", "operationRun"),
226
+ ...hateoas,
227
+ };
228
+ }
229
+ function formatListStepRun(stepRun) {
230
+ const stepSeqNo = stepRun.step.seqNo;
231
+ const fieldCount = stepRun.step.fieldSet?._count.fields ?? 0;
232
+ return {
233
+ id: stepRun.id,
234
+ operationRunId: stepRun.operationRunId,
235
+ stepId: stepRun.stepId,
236
+ seqNo: stepSeqNo,
237
+ title: stepRun.step.title,
238
+ instructions: stepRun.step.instructions,
239
+ multiSet: stepRun.step.multiSet,
240
+ completed: stepRun.completed,
241
+ note: stepRun.statusNote ?? null,
242
+ fieldCount,
243
+ ...formatAuditFields(stepRun),
244
+ };
245
+ }
246
+ export default function stepRunRoutes(fastify) {
247
+ const app = fastify.withTypeProvider();
248
+ // LIST
249
+ app.get("/", {
250
+ schema: {
251
+ description: "List step runs for an operation run",
252
+ tags: ["Step Runs"],
253
+ params: OpSeqNoParamsSchema,
254
+ querystring: StepRunListQuerySchema,
255
+ response: {
256
+ 200: StepRunListResponseSchema,
257
+ 404: ErrorResponseSchema,
258
+ },
259
+ },
260
+ handler: async (request, reply) => {
261
+ const { orderKey, runNo, seqNo } = request.params;
262
+ const { includeFields } = request.query;
263
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
264
+ if (!resolved) {
265
+ return notFound(reply, `Operation run not found`);
266
+ }
267
+ if (includeFields) {
268
+ const items = await listStepRunsWithFields(resolved.opRun.id);
269
+ const formatted = await Promise.all(items.map(async (stepRun) => {
270
+ const { _links, ...rest } = await formatStepRunWithFields(orderKey, runNo, seqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, request.erpUser, stepRun);
271
+ return rest;
272
+ }));
273
+ return {
274
+ items: formatted,
275
+ total: items.length,
276
+ _links: [selfLink(`/${stepRunResource(orderKey, runNo, seqNo)}`)],
277
+ _linkTemplates: [
278
+ {
279
+ rel: "item",
280
+ hrefTemplate: `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/{seqNo}`,
281
+ },
282
+ ],
283
+ };
284
+ }
285
+ const items = await listStepRuns(resolved.opRun.id);
286
+ return {
287
+ items: items.map((stepRun) => formatListStepRun(stepRun)),
288
+ total: items.length,
289
+ _links: [selfLink(`/${stepRunResource(orderKey, runNo, seqNo)}`)],
290
+ _linkTemplates: [
291
+ {
292
+ rel: "item",
293
+ hrefTemplate: `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/{seqNo}`,
294
+ },
295
+ ],
296
+ };
297
+ },
298
+ });
299
+ // GET by stepSeqNo
300
+ app.get("/:stepSeqNo", {
301
+ schema: {
302
+ description: "Get a single step run by step sequence number",
303
+ tags: ["Step Runs"],
304
+ params: StepSeqNoParamsSchema,
305
+ response: {
306
+ 200: StepRunSchema,
307
+ 404: ErrorResponseSchema,
308
+ },
309
+ },
310
+ handler: async (request, reply) => {
311
+ const { orderKey, runNo, seqNo, stepSeqNo } = request.params;
312
+ const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
313
+ if (!resolved) {
314
+ return notFound(reply, `Step run not found`);
315
+ }
316
+ const stepRun = await getStepRunWithFields(resolved.stepRun.id);
317
+ if (!stepRun) {
318
+ return notFound(reply, `Step run not found`);
319
+ }
320
+ return formatStepRunWithFields(orderKey, runNo, seqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, request.erpUser, stepRun);
321
+ },
322
+ });
323
+ }
324
+ //# sourceMappingURL=step-runs.js.map
@@ -0,0 +1,283 @@
1
+ import { BatchCreateStepSchema, BatchSeqNoCreateResponseSchema, CreateStepSchema, ErrorResponseSchema, MutateResponseSchema, RevisionStatus, SeqNoCreateResponseSchema, StepListResponseSchema, StepSchema, UpdateStepSchema, } from "@naisys/erp-shared";
2
+ import { z } from "zod/v4";
3
+ import { requirePermission } from "../auth-middleware.js";
4
+ import { conflict, notFound } from "../error-handler.js";
5
+ import { API_PREFIX, selfLink } from "../hateoas.js";
6
+ import { calcNextSeqNo, childItemLinks, draftCrudActions, formatAuditFields, mutationResult, resolveActions, resolveOperation, } from "../route-helpers.js";
7
+ import { createStep, createSteps, deleteStep, findExisting, getStep, listSteps, updateStep, } from "../services/step-service.js";
8
+ import { formatFieldListResponse } from "./step-fields.js";
9
+ const ParamsSchema = z.object({
10
+ orderKey: z.string(),
11
+ revNo: z.coerce.number().int(),
12
+ seqNo: z.coerce.number().int(),
13
+ });
14
+ const StepParamsSchema = z.object({
15
+ orderKey: z.string(),
16
+ revNo: z.coerce.number().int(),
17
+ seqNo: z.coerce.number().int(),
18
+ stepSeqNo: z.coerce.number().int(),
19
+ });
20
+ function stepBasePath(orderKey, revNo, opSeqNo) {
21
+ return `/orders/${orderKey}/revs/${revNo}/ops/${opSeqNo}/steps`;
22
+ }
23
+ function formatStep(orderKey, revNo, opSeqNo, revStatus, user, step) {
24
+ return {
25
+ id: step.id,
26
+ operationId: step.operationId,
27
+ seqNo: step.seqNo,
28
+ title: step.title,
29
+ instructions: step.instructions,
30
+ multiSet: step.multiSet,
31
+ fieldCount: step.fieldSet?.fields.length ?? 0,
32
+ ...formatAuditFields(step),
33
+ fields: formatFieldListResponse(orderKey, revNo, opSeqNo, step.seqNo, revStatus, user, step.fieldSet?.fields ?? []),
34
+ _links: childItemLinks(stepBasePath(orderKey, revNo, opSeqNo), step.seqNo, "Steps", `/orders/${orderKey}/revs/${revNo}/ops/${opSeqNo}`, "Operation", "Step"),
35
+ _actions: draftCrudActions(`${API_PREFIX}${stepBasePath(orderKey, revNo, opSeqNo)}/${step.seqNo}`, "UpdateStep", revStatus, user),
36
+ };
37
+ }
38
+ const draftCreateDef = {
39
+ rel: "create",
40
+ method: "POST",
41
+ title: "Add Step",
42
+ schema: `${API_PREFIX}/schemas/CreateStep`,
43
+ permission: "order_planner",
44
+ disabledWhen: (ctx) => ctx.status !== RevisionStatus.draft
45
+ ? "Can only add steps in draft revisions"
46
+ : null,
47
+ };
48
+ const draftBatchCreateDef = {
49
+ rel: "batch-create",
50
+ path: "/batch",
51
+ method: "POST",
52
+ title: "Add Steps (Batch)",
53
+ schema: `${API_PREFIX}/schemas/BatchCreateStep`,
54
+ permission: "order_planner",
55
+ disabledWhen: (ctx) => ctx.status !== RevisionStatus.draft
56
+ ? "Can only add steps in draft revisions"
57
+ : null,
58
+ };
59
+ function stepListActions(base, revStatus, user) {
60
+ return resolveActions([draftCreateDef, draftBatchCreateDef], `${API_PREFIX}${base}`, { status: revStatus, user });
61
+ }
62
+ export default function stepRoutes(fastify) {
63
+ const app = fastify.withTypeProvider();
64
+ // LIST
65
+ app.get("/", {
66
+ schema: {
67
+ description: "List steps for an operation",
68
+ tags: ["Steps"],
69
+ params: ParamsSchema,
70
+ response: {
71
+ 200: StepListResponseSchema,
72
+ 404: ErrorResponseSchema,
73
+ },
74
+ },
75
+ handler: async (request, reply) => {
76
+ const { orderKey, revNo, seqNo } = request.params;
77
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
78
+ if (!resolved) {
79
+ return notFound(reply, "Operation not found");
80
+ }
81
+ const items = await listSteps(resolved.operation.id);
82
+ const maxSeq = items.length > 0 ? items[items.length - 1].seqNo : 0;
83
+ const user = request.erpUser;
84
+ const base = stepBasePath(orderKey, revNo, seqNo);
85
+ return {
86
+ items: items.map((step) => {
87
+ const { _links, ...rest } = formatStep(orderKey, revNo, seqNo, resolved.rev.status, user, step);
88
+ return rest;
89
+ }),
90
+ total: items.length,
91
+ nextSeqNo: calcNextSeqNo(maxSeq),
92
+ _links: [selfLink(base)],
93
+ _linkTemplates: [
94
+ {
95
+ rel: "item",
96
+ hrefTemplate: `${API_PREFIX}${stepBasePath(orderKey, revNo, seqNo)}/{seqNo}`,
97
+ },
98
+ ],
99
+ _actions: stepListActions(base, resolved.rev.status, user),
100
+ };
101
+ },
102
+ });
103
+ // BATCH CREATE
104
+ app.post("/batch", {
105
+ schema: {
106
+ description: "Create multiple steps for an operation in one request",
107
+ tags: ["Steps"],
108
+ params: ParamsSchema,
109
+ body: BatchCreateStepSchema,
110
+ response: {
111
+ 201: BatchSeqNoCreateResponseSchema,
112
+ 404: ErrorResponseSchema,
113
+ 409: ErrorResponseSchema,
114
+ },
115
+ },
116
+ preHandler: requirePermission("order_planner"),
117
+ handler: async (request, reply) => {
118
+ const { orderKey, revNo, seqNo } = request.params;
119
+ const { items } = request.body;
120
+ const userId = request.erpUser.id;
121
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
122
+ if (!resolved) {
123
+ return notFound(reply, "Operation not found");
124
+ }
125
+ if (resolved.rev.status !== RevisionStatus.draft) {
126
+ return conflict(reply, `Cannot add steps to a ${resolved.rev.status} revision`);
127
+ }
128
+ const created = await createSteps(resolved.operation.id, items, userId);
129
+ const maxSeq = created.length > 0 ? created[created.length - 1].seqNo : 0;
130
+ const user = request.erpUser;
131
+ const base = stepBasePath(orderKey, revNo, seqNo);
132
+ const full = {
133
+ items: created.map((step) => {
134
+ const { _links, ...rest } = formatStep(orderKey, revNo, seqNo, resolved.rev.status, user, step);
135
+ return rest;
136
+ }),
137
+ total: created.length,
138
+ nextSeqNo: calcNextSeqNo(maxSeq),
139
+ _links: [selfLink(base)],
140
+ _linkTemplates: [
141
+ {
142
+ rel: "item",
143
+ hrefTemplate: `${API_PREFIX}${stepBasePath(orderKey, revNo, seqNo)}/{seqNo}`,
144
+ },
145
+ ],
146
+ _actions: [],
147
+ };
148
+ reply.status(201);
149
+ return mutationResult(request, reply, full, {
150
+ items: created.map((s) => ({ id: s.id, seqNo: s.seqNo })),
151
+ total: created.length,
152
+ _actions: full._actions,
153
+ });
154
+ },
155
+ });
156
+ // CREATE
157
+ app.post("/", {
158
+ schema: {
159
+ description: "Create a step for an operation",
160
+ tags: ["Steps"],
161
+ params: ParamsSchema,
162
+ body: CreateStepSchema,
163
+ response: {
164
+ 201: SeqNoCreateResponseSchema,
165
+ 404: ErrorResponseSchema,
166
+ 409: ErrorResponseSchema,
167
+ },
168
+ },
169
+ preHandler: requirePermission("order_planner"),
170
+ handler: async (request, reply) => {
171
+ const { orderKey, revNo, seqNo } = request.params;
172
+ const { seqNo: requestedSeqNo, title, instructions, multiSet, } = request.body;
173
+ const userId = request.erpUser.id;
174
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
175
+ if (!resolved) {
176
+ return notFound(reply, "Operation not found");
177
+ }
178
+ if (resolved.rev.status !== RevisionStatus.draft) {
179
+ return conflict(reply, `Cannot add steps to a ${resolved.rev.status} revision`);
180
+ }
181
+ const step = await createStep(resolved.operation.id, requestedSeqNo, title, instructions, multiSet, userId);
182
+ const full = formatStep(orderKey, revNo, seqNo, resolved.rev.status, request.erpUser, step);
183
+ reply.status(201);
184
+ return mutationResult(request, reply, full, {
185
+ id: full.id,
186
+ seqNo: full.seqNo,
187
+ _links: full._links,
188
+ _actions: full._actions,
189
+ });
190
+ },
191
+ });
192
+ // GET by stepSeqNo
193
+ app.get("/:stepSeqNo", {
194
+ schema: {
195
+ description: "Get a step by sequence number",
196
+ tags: ["Steps"],
197
+ params: StepParamsSchema,
198
+ response: {
199
+ 200: StepSchema,
200
+ 404: ErrorResponseSchema,
201
+ },
202
+ },
203
+ handler: async (request, reply) => {
204
+ const { orderKey, revNo, seqNo, stepSeqNo } = request.params;
205
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
206
+ if (!resolved) {
207
+ return notFound(reply, "Operation not found");
208
+ }
209
+ const step = await getStep(resolved.operation.id, stepSeqNo);
210
+ if (!step) {
211
+ return notFound(reply, `Step ${stepSeqNo} not found`);
212
+ }
213
+ return formatStep(orderKey, revNo, seqNo, resolved.rev.status, request.erpUser, step);
214
+ },
215
+ });
216
+ // UPDATE (draft only)
217
+ app.put("/:stepSeqNo", {
218
+ schema: {
219
+ description: "Update a step (draft revision only)",
220
+ tags: ["Steps"],
221
+ params: StepParamsSchema,
222
+ body: UpdateStepSchema,
223
+ response: {
224
+ 200: MutateResponseSchema,
225
+ 404: ErrorResponseSchema,
226
+ 409: ErrorResponseSchema,
227
+ },
228
+ },
229
+ preHandler: requirePermission("order_planner"),
230
+ handler: async (request, reply) => {
231
+ const { orderKey, revNo, seqNo, stepSeqNo } = request.params;
232
+ const { title, instructions, seqNo: newSeqNo, multiSet } = request.body;
233
+ const userId = request.erpUser.id;
234
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
235
+ if (!resolved) {
236
+ return notFound(reply, "Operation not found");
237
+ }
238
+ if (resolved.rev.status !== RevisionStatus.draft) {
239
+ return conflict(reply, `Cannot update steps on a ${resolved.rev.status} revision`);
240
+ }
241
+ const existing = await findExisting(resolved.operation.id, stepSeqNo);
242
+ if (!existing) {
243
+ return notFound(reply, `Step ${stepSeqNo} not found`);
244
+ }
245
+ const step = await updateStep(existing.id, { title, instructions, seqNo: newSeqNo, multiSet }, userId);
246
+ const full = formatStep(orderKey, revNo, seqNo, resolved.rev.status, request.erpUser, step);
247
+ return mutationResult(request, reply, full, {
248
+ _actions: full._actions,
249
+ });
250
+ },
251
+ });
252
+ // DELETE (draft only)
253
+ app.delete("/:stepSeqNo", {
254
+ schema: {
255
+ description: "Delete a step (draft revision only)",
256
+ tags: ["Steps"],
257
+ params: StepParamsSchema,
258
+ response: {
259
+ 204: z.void(),
260
+ 404: ErrorResponseSchema,
261
+ 409: ErrorResponseSchema,
262
+ },
263
+ },
264
+ preHandler: requirePermission("order_planner"),
265
+ handler: async (request, reply) => {
266
+ const { orderKey, revNo, seqNo, stepSeqNo } = request.params;
267
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
268
+ if (!resolved) {
269
+ return notFound(reply, "Operation not found");
270
+ }
271
+ if (resolved.rev.status !== RevisionStatus.draft) {
272
+ return conflict(reply, `Cannot delete steps on a ${resolved.rev.status} revision`);
273
+ }
274
+ const existing = await findExisting(resolved.operation.id, stepSeqNo);
275
+ if (!existing) {
276
+ return notFound(reply, `Step ${stepSeqNo} not found`);
277
+ }
278
+ await deleteStep(existing.id);
279
+ reply.status(204);
280
+ },
281
+ });
282
+ }
283
+ //# sourceMappingURL=steps.js.map