@naisys/erp 3.0.0-beta.10

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-C9uuPHLH.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,263 @@
1
+ import { CreateFieldRefSchema, ErrorResponseSchema, FieldRefListResponseSchema, RevisionStatus, SeqNoCreateResponseSchema, } from "@naisys/erp-shared";
2
+ import { z } from "zod/v4";
3
+ import { requirePermission } from "../auth-middleware.js";
4
+ import erpDb from "../erpDb.js";
5
+ import { conflict, notFound } from "../error-handler.js";
6
+ import { API_PREFIX, selfLink } from "../hateoas.js";
7
+ import { calcNextSeqNo, childItemLinks, mutationResult, resolveActions, resolveOperation, } from "../route-helpers.js";
8
+ import { checkDuplicateSource, createFieldRef, deleteFieldRef, findExistingFieldRef, listFieldRefs, } from "../services/field-ref-service.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 RefParamsSchema = z.object({
15
+ orderKey: z.string(),
16
+ revNo: z.coerce.number().int(),
17
+ seqNo: z.coerce.number().int(),
18
+ refSeqNo: z.coerce.number().int(),
19
+ });
20
+ function basePath(orderKey, revNo, seqNo) {
21
+ return `/orders/${orderKey}/revs/${revNo}/ops/${seqNo}/field-refs`;
22
+ }
23
+ function formatFieldRef(orderKey, revNo, seqNo, revStatus, user, ref) {
24
+ const base = basePath(orderKey, revNo, seqNo);
25
+ return {
26
+ id: ref.id,
27
+ seqNo: ref.seqNo,
28
+ title: ref.title,
29
+ sourceOpSeqNo: ref.sourceStep.operation.seqNo,
30
+ sourceOpTitle: ref.sourceStep.operation.title,
31
+ sourceStepSeqNo: ref.sourceStep.seqNo,
32
+ sourceStepTitle: ref.sourceStep.title,
33
+ fields: (ref.sourceStep.fieldSet?.fields ?? []).map((f) => ({
34
+ seqNo: f.seqNo,
35
+ label: f.label,
36
+ type: f.type,
37
+ })),
38
+ createdAt: ref.createdAt.toISOString(),
39
+ createdBy: ref.createdBy.username,
40
+ _links: childItemLinks(base, ref.seqNo, "Field Refs", `/orders/${orderKey}/revs/${revNo}/ops/${seqNo}`, "Operation", "FieldRef"),
41
+ _actions: deleteAction(`${API_PREFIX}${base}/${ref.seqNo}`, revStatus, user),
42
+ };
43
+ }
44
+ function deleteAction(href, revStatus, user) {
45
+ return resolveActions([
46
+ {
47
+ rel: "delete",
48
+ method: "DELETE",
49
+ title: "Remove Reference",
50
+ permission: "order_planner",
51
+ statuses: [RevisionStatus.draft],
52
+ hideWithoutPermission: true,
53
+ },
54
+ ], href, { status: revStatus, user });
55
+ }
56
+ const draftCreateDef = {
57
+ rel: "create",
58
+ method: "POST",
59
+ title: "Add Field Reference",
60
+ schema: `${API_PREFIX}/schemas/CreateFieldRef`,
61
+ permission: "order_planner",
62
+ disabledWhen: (ctx) => ctx.status !== RevisionStatus.draft
63
+ ? "Can only add field references in draft revisions"
64
+ : null,
65
+ };
66
+ // Schema for the /available response
67
+ const AvailableStepSchema = z.object({
68
+ opSeqNo: z.number(),
69
+ opTitle: z.string(),
70
+ stepSeqNo: z.number(),
71
+ stepTitle: z.string(),
72
+ stepId: z.number(),
73
+ fieldCount: z.number(),
74
+ });
75
+ const AvailableStepsResponseSchema = z.object({
76
+ items: z.array(AvailableStepSchema),
77
+ });
78
+ export default function operationFieldRefRoutes(fastify) {
79
+ const app = fastify.withTypeProvider();
80
+ // LIST available steps with fields (for the "add reference" dialog)
81
+ app.get("/available", {
82
+ schema: {
83
+ description: "List steps with fields in the same revision that can be referenced",
84
+ tags: ["Operation Field Refs"],
85
+ params: ParamsSchema,
86
+ response: {
87
+ 200: AvailableStepsResponseSchema,
88
+ 404: ErrorResponseSchema,
89
+ },
90
+ },
91
+ handler: async (request, reply) => {
92
+ const { orderKey, revNo, seqNo } = request.params;
93
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
94
+ if (!resolved) {
95
+ return notFound(reply, "Operation not found");
96
+ }
97
+ // Find all steps in this revision that have fields
98
+ const steps = await erpDb.step.findMany({
99
+ where: {
100
+ operation: { orderRevId: resolved.rev.id },
101
+ fieldSetId: { not: null },
102
+ fieldSet: { fields: { some: {} } },
103
+ },
104
+ select: {
105
+ id: true,
106
+ seqNo: true,
107
+ title: true,
108
+ operation: { select: { seqNo: true, title: true } },
109
+ fieldSet: { select: { _count: { select: { fields: true } } } },
110
+ },
111
+ orderBy: [{ operation: { seqNo: "asc" } }, { seqNo: "asc" }],
112
+ });
113
+ // Exclude steps already referenced by this operation
114
+ const existingRefs = await erpDb.operationFieldRef.findMany({
115
+ where: { operationId: resolved.operation.id },
116
+ select: { sourceStepId: true },
117
+ });
118
+ const refSet = new Set(existingRefs.map((r) => r.sourceStepId));
119
+ return {
120
+ items: steps
121
+ .filter((s) => !refSet.has(s.id))
122
+ .map((s) => ({
123
+ opSeqNo: s.operation.seqNo,
124
+ opTitle: s.operation.title,
125
+ stepSeqNo: s.seqNo,
126
+ stepTitle: s.title,
127
+ stepId: s.id,
128
+ fieldCount: s.fieldSet?._count.fields ?? 0,
129
+ })),
130
+ };
131
+ },
132
+ });
133
+ // LIST
134
+ app.get("/", {
135
+ schema: {
136
+ description: "List field references for an operation",
137
+ tags: ["Operation Field Refs"],
138
+ params: ParamsSchema,
139
+ response: {
140
+ 200: FieldRefListResponseSchema,
141
+ 404: ErrorResponseSchema,
142
+ },
143
+ },
144
+ handler: async (request, reply) => {
145
+ const { orderKey, revNo, seqNo } = request.params;
146
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
147
+ if (!resolved) {
148
+ return notFound(reply, "Operation not found");
149
+ }
150
+ const items = await listFieldRefs(resolved.operation.id);
151
+ const maxSeq = items.length > 0 ? items[items.length - 1].seqNo : 0;
152
+ const base = basePath(orderKey, revNo, seqNo);
153
+ return {
154
+ items: items.map((ref) => {
155
+ const { _links, ...rest } = formatFieldRef(orderKey, revNo, seqNo, resolved.rev.status, request.erpUser, ref);
156
+ return rest;
157
+ }),
158
+ total: items.length,
159
+ nextSeqNo: calcNextSeqNo(maxSeq),
160
+ _links: [selfLink(base)],
161
+ _linkTemplates: [
162
+ {
163
+ rel: "item",
164
+ hrefTemplate: `${API_PREFIX}${base}/{seqNo}`,
165
+ },
166
+ ],
167
+ _actions: resolveActions([draftCreateDef], `${API_PREFIX}${base}`, {
168
+ status: resolved.rev.status,
169
+ user: request.erpUser,
170
+ }),
171
+ };
172
+ },
173
+ });
174
+ // CREATE
175
+ app.post("/", {
176
+ schema: {
177
+ description: "Add a field reference to an operation (draft revision only)",
178
+ tags: ["Operation Field Refs"],
179
+ params: ParamsSchema,
180
+ body: CreateFieldRefSchema,
181
+ response: {
182
+ 201: SeqNoCreateResponseSchema,
183
+ 404: ErrorResponseSchema,
184
+ 409: ErrorResponseSchema,
185
+ },
186
+ },
187
+ preHandler: requirePermission("order_planner"),
188
+ handler: async (request, reply) => {
189
+ const { orderKey, revNo, seqNo } = request.params;
190
+ const { seqNo: requestedSeqNo, title, sourceOpSeqNo, sourceStepSeqNo, } = request.body;
191
+ const userId = request.erpUser.id;
192
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
193
+ if (!resolved) {
194
+ return notFound(reply, "Operation not found");
195
+ }
196
+ if (resolved.rev.status !== RevisionStatus.draft) {
197
+ return conflict(reply, `Cannot add field references to a ${resolved.rev.status} revision`);
198
+ }
199
+ // Resolve source step
200
+ const sourceOp = await erpDb.operation.findFirst({
201
+ where: { orderRevId: resolved.rev.id, seqNo: sourceOpSeqNo },
202
+ });
203
+ if (!sourceOp) {
204
+ return notFound(reply, `Source operation ${sourceOpSeqNo} not found`);
205
+ }
206
+ const sourceStep = await erpDb.step.findFirst({
207
+ where: { operationId: sourceOp.id, seqNo: sourceStepSeqNo },
208
+ select: { id: true, fieldSetId: true },
209
+ });
210
+ if (!sourceStep) {
211
+ return notFound(reply, `Source step ${sourceStepSeqNo} not found in operation ${sourceOpSeqNo}`);
212
+ }
213
+ if (!sourceStep.fieldSetId) {
214
+ return conflict(reply, "Source step has no fields");
215
+ }
216
+ // Check for duplicate
217
+ const dup = await checkDuplicateSource(resolved.operation.id, sourceStep.id);
218
+ if (dup) {
219
+ return conflict(reply, "This step is already referenced");
220
+ }
221
+ const ref = await createFieldRef(resolved.operation.id, requestedSeqNo, title, sourceStep.id, userId);
222
+ const full = formatFieldRef(orderKey, revNo, seqNo, resolved.rev.status, request.erpUser, ref);
223
+ reply.status(201);
224
+ return mutationResult(request, reply, full, {
225
+ id: full.id,
226
+ seqNo: full.seqNo,
227
+ _links: full._links,
228
+ _actions: full._actions,
229
+ });
230
+ },
231
+ });
232
+ // DELETE
233
+ app.delete("/:refSeqNo", {
234
+ schema: {
235
+ description: "Remove a field reference from an operation (draft revision only)",
236
+ tags: ["Operation Field Refs"],
237
+ params: RefParamsSchema,
238
+ response: {
239
+ 204: z.void(),
240
+ 404: ErrorResponseSchema,
241
+ 409: ErrorResponseSchema,
242
+ },
243
+ },
244
+ preHandler: requirePermission("order_planner"),
245
+ handler: async (request, reply) => {
246
+ const { orderKey, revNo, seqNo, refSeqNo } = request.params;
247
+ const resolved = await resolveOperation(orderKey, revNo, seqNo);
248
+ if (!resolved) {
249
+ return notFound(reply, "Operation not found");
250
+ }
251
+ if (resolved.rev.status !== RevisionStatus.draft) {
252
+ return conflict(reply, `Cannot remove field references from a ${resolved.rev.status} revision`);
253
+ }
254
+ const existing = await findExistingFieldRef(resolved.operation.id, refSeqNo);
255
+ if (!existing) {
256
+ return notFound(reply, `Field reference ${refSeqNo} not found`);
257
+ }
258
+ await deleteFieldRef(existing.id);
259
+ reply.status(204);
260
+ },
261
+ });
262
+ }
263
+ //# sourceMappingURL=operation-field-refs.js.map
@@ -0,0 +1,108 @@
1
+ import { CreateOperationRunCommentSchema, CreateResponseSchema, ErrorResponseSchema, OperationRunCommentListResponseSchema, OperationRunCommentType, } from "@naisys/erp-shared";
2
+ import { z } from "zod/v4";
3
+ import { requirePermission } from "../auth-middleware.js";
4
+ import { notFound } from "../error-handler.js";
5
+ import { API_PREFIX, selfLink } from "../hateoas.js";
6
+ import { mutationResult, resolveActions, resolveOpRun, } from "../route-helpers.js";
7
+ import { createComment, listComments, } from "../services/operation-run-comment-service.js";
8
+ function commentResource(orderKey, runNo, seqNo) {
9
+ return `orders/${orderKey}/runs/${runNo}/ops/${seqNo}/comments`;
10
+ }
11
+ function commentListActions(orderKey, runNo, seqNo, user) {
12
+ return resolveActions([
13
+ {
14
+ rel: "create",
15
+ method: "POST",
16
+ title: "Add Comment",
17
+ schema: `${API_PREFIX}/schemas/CreateOperationRunComment`,
18
+ permission: "order_executor",
19
+ },
20
+ ], `${API_PREFIX}/${commentResource(orderKey, runNo, seqNo)}`, { user });
21
+ }
22
+ function formatComment(orderKey, runNo, seqNo, comment) {
23
+ return {
24
+ id: comment.id,
25
+ operationRunId: comment.operationRunId,
26
+ type: comment.type,
27
+ body: comment.body,
28
+ createdAt: comment.createdAt.toISOString(),
29
+ createdBy: comment.createdBy.username,
30
+ _links: [
31
+ selfLink(`/${commentResource(orderKey, runNo, seqNo)}/${comment.id}`),
32
+ ],
33
+ };
34
+ }
35
+ const CommentParamsSchema = z.object({
36
+ orderKey: z.string(),
37
+ runNo: z.coerce.number().int(),
38
+ seqNo: z.coerce.number().int(),
39
+ });
40
+ export default function operationRunCommentRoutes(fastify) {
41
+ const app = fastify.withTypeProvider();
42
+ // LIST
43
+ app.get("/", {
44
+ schema: {
45
+ description: "List comments for an operation run",
46
+ tags: ["Operation Run Comments"],
47
+ params: CommentParamsSchema,
48
+ response: {
49
+ 200: OperationRunCommentListResponseSchema,
50
+ 404: ErrorResponseSchema,
51
+ },
52
+ },
53
+ handler: async (request, reply) => {
54
+ const { orderKey, runNo, seqNo } = request.params;
55
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
56
+ if (!resolved) {
57
+ return notFound(reply, "Operation run not found");
58
+ }
59
+ const items = await listComments(resolved.opRun.id);
60
+ return {
61
+ items: items.map((c) => {
62
+ const { _links, ...rest } = formatComment(orderKey, runNo, seqNo, c);
63
+ return rest;
64
+ }),
65
+ total: items.length,
66
+ _links: [selfLink(`/${commentResource(orderKey, runNo, seqNo)}`)],
67
+ _linkTemplates: [
68
+ {
69
+ rel: "item",
70
+ hrefTemplate: `${API_PREFIX}/${commentResource(orderKey, runNo, seqNo)}/{id}`,
71
+ },
72
+ ],
73
+ _actions: commentListActions(orderKey, runNo, seqNo, request.erpUser),
74
+ };
75
+ },
76
+ });
77
+ // CREATE
78
+ app.post("/", {
79
+ schema: {
80
+ description: "Add a comment to an operation run",
81
+ tags: ["Operation Run Comments"],
82
+ params: CommentParamsSchema,
83
+ body: CreateOperationRunCommentSchema,
84
+ response: {
85
+ 201: CreateResponseSchema,
86
+ 404: ErrorResponseSchema,
87
+ },
88
+ },
89
+ preHandler: requirePermission("order_executor"),
90
+ handler: async (request, reply) => {
91
+ const { orderKey, runNo, seqNo } = request.params;
92
+ const { type, body } = request.body;
93
+ const userId = request.erpUser.id;
94
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
95
+ if (!resolved) {
96
+ return notFound(reply, "Operation run not found");
97
+ }
98
+ const comment = await createComment(resolved.opRun.id, type ?? OperationRunCommentType.note, body, userId);
99
+ const full = formatComment(orderKey, runNo, seqNo, comment);
100
+ reply.status(201);
101
+ return mutationResult(request, reply, full, {
102
+ id: full.id,
103
+ _links: full._links,
104
+ });
105
+ },
106
+ });
107
+ }
108
+ //# sourceMappingURL=operation-run-comments.js.map
@@ -0,0 +1,249 @@
1
+ import { ErrorResponseSchema, OperationRunStatus, OperationRunTransitionSlimSchema, OrderRunStatus, TransitionNoteSchema, } from "@naisys/erp-shared";
2
+ import { requirePermission } from "../auth-middleware.js";
3
+ import { conflict, notFound, unprocessable } from "../error-handler.js";
4
+ import { checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveOpRun, } from "../route-helpers.js";
5
+ import { clockIn, clockOutAllForOpRun, isUserClockedIn, sumLaborTicketCosts, } from "../services/labor-ticket-service.js";
6
+ import { checkPredecessorsComplete, checkStepsComplete, reblockSuccessors, transitionStatus, unblockSuccessors, validateStatusFor, } from "../services/operation-run-service.js";
7
+ import { transitionStatus as transitionOrderRunStatus } from "../services/order-run-service.js";
8
+ import { formatOpRunTransition, SeqNoParamsSchema } from "./operation-runs.js";
9
+ export default function operationRunTransitionRoutes(fastify) {
10
+ const app = fastify.withTypeProvider();
11
+ // START (pending → in_progress)
12
+ app.post("/:seqNo/start", {
13
+ schema: {
14
+ description: "Start an operation run (pending → in_progress)",
15
+ tags: ["Operation Runs"],
16
+ params: SeqNoParamsSchema,
17
+ body: TransitionNoteSchema,
18
+ response: {
19
+ 200: OperationRunTransitionSlimSchema,
20
+ 404: ErrorResponseSchema,
21
+ 409: ErrorResponseSchema,
22
+ 422: ErrorResponseSchema,
23
+ },
24
+ },
25
+ preHandler: requirePermission("order_executor"),
26
+ handler: async (request, reply) => {
27
+ const { orderKey, runNo, seqNo } = request.params;
28
+ const { note } = request.body;
29
+ const userId = request.erpUser.id;
30
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
31
+ if (!resolved)
32
+ return notFound(reply, `Operation run not found`);
33
+ const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
34
+ if (wcErr)
35
+ return conflict(reply, wcErr);
36
+ // Auto-start the order run if it's still in released status
37
+ if (resolved.run.status === OrderRunStatus.released) {
38
+ await transitionOrderRunStatus(resolved.run.id, "start", OrderRunStatus.released, OrderRunStatus.started, userId);
39
+ }
40
+ else {
41
+ const orderErr = checkOrderRunStarted(resolved.run.status);
42
+ if (orderErr)
43
+ return conflict(reply, orderErr);
44
+ }
45
+ const statusErr = validateStatusFor("start", resolved.opRun.status, [
46
+ OperationRunStatus.pending,
47
+ ]);
48
+ if (statusErr)
49
+ return conflict(reply, statusErr);
50
+ const priorErr = await checkPredecessorsComplete(resolved.run.id, resolved.opRun.operationId);
51
+ if (priorErr)
52
+ return unprocessable(reply, priorErr);
53
+ const opRun = await transitionStatus(resolved.opRun.id, "start", OperationRunStatus.pending, OperationRunStatus.in_progress, userId, { assignedToId: userId, statusNote: note ?? null });
54
+ await clockIn(resolved.opRun.id, userId, userId);
55
+ const full = await formatOpRunTransition(orderKey, runNo, request.erpUser, opRun);
56
+ return mutationResult(request, reply, full, {
57
+ status: opRun.status,
58
+ _actions: full._actions,
59
+ });
60
+ },
61
+ });
62
+ // COMPLETE (in_progress → completed)
63
+ app.post("/:seqNo/complete", {
64
+ schema: {
65
+ description: "Complete an operation run (in_progress → completed)",
66
+ tags: ["Operation Runs"],
67
+ params: SeqNoParamsSchema,
68
+ body: TransitionNoteSchema,
69
+ response: {
70
+ 200: OperationRunTransitionSlimSchema,
71
+ 404: ErrorResponseSchema,
72
+ 409: ErrorResponseSchema,
73
+ 422: ErrorResponseSchema,
74
+ },
75
+ },
76
+ preHandler: requirePermission("order_executor"),
77
+ handler: async (request, reply) => {
78
+ const { orderKey, runNo, seqNo } = request.params;
79
+ const { note } = request.body;
80
+ const userId = request.erpUser.id;
81
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
82
+ if (!resolved)
83
+ return notFound(reply, `Operation run not found`);
84
+ const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
85
+ if (wcErr)
86
+ return conflict(reply, wcErr);
87
+ const orderErr = checkOrderRunStarted(resolved.run.status);
88
+ if (orderErr)
89
+ return conflict(reply, orderErr);
90
+ const statusErr = validateStatusFor("complete", resolved.opRun.status, [
91
+ OperationRunStatus.in_progress,
92
+ ]);
93
+ if (statusErr)
94
+ return conflict(reply, statusErr);
95
+ const clockedIn = await isUserClockedIn(resolved.opRun.id, userId);
96
+ if (!clockedIn)
97
+ return conflict(reply, `You must be clocked in to complete an operation`);
98
+ const stepsErr = await checkStepsComplete(resolved.opRun.id);
99
+ if (stepsErr)
100
+ return unprocessable(reply, stepsErr);
101
+ await clockOutAllForOpRun(resolved.opRun.id, userId);
102
+ const cost = await sumLaborTicketCosts(resolved.opRun.id);
103
+ const opRun = await transitionStatus(resolved.opRun.id, "complete", OperationRunStatus.in_progress, OperationRunStatus.completed, userId, {
104
+ completedAt: new Date(),
105
+ cost,
106
+ statusNote: note ?? null,
107
+ });
108
+ await unblockSuccessors(resolved.run.id, resolved.opRun.operationId, userId);
109
+ const full = await formatOpRunTransition(orderKey, runNo, request.erpUser, opRun);
110
+ return mutationResult(request, reply, full, {
111
+ status: opRun.status,
112
+ _actions: full._actions,
113
+ });
114
+ },
115
+ });
116
+ // SKIP (pending → skipped)
117
+ app.post("/:seqNo/skip", {
118
+ schema: {
119
+ description: "Skip an operation run (pending → skipped)",
120
+ tags: ["Operation Runs"],
121
+ params: SeqNoParamsSchema,
122
+ body: TransitionNoteSchema,
123
+ response: {
124
+ 200: OperationRunTransitionSlimSchema,
125
+ 404: ErrorResponseSchema,
126
+ 409: ErrorResponseSchema,
127
+ },
128
+ },
129
+ preHandler: requirePermission("order_manager"),
130
+ handler: async (request, reply) => {
131
+ const { orderKey, runNo, seqNo } = request.params;
132
+ const { note } = request.body;
133
+ const userId = request.erpUser.id;
134
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
135
+ if (!resolved)
136
+ return notFound(reply, `Operation run not found`);
137
+ const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
138
+ if (wcErr)
139
+ return conflict(reply, wcErr);
140
+ const orderErr = checkOrderRunStarted(resolved.run.status);
141
+ if (orderErr)
142
+ return conflict(reply, orderErr);
143
+ const statusErr = validateStatusFor("skip", resolved.opRun.status, [
144
+ OperationRunStatus.blocked,
145
+ OperationRunStatus.pending,
146
+ ]);
147
+ if (statusErr)
148
+ return conflict(reply, statusErr);
149
+ const cost = await sumLaborTicketCosts(resolved.opRun.id);
150
+ const opRun = await transitionStatus(resolved.opRun.id, "skip", resolved.opRun.status, OperationRunStatus.skipped, userId, { ...(cost > 0 ? { cost } : undefined), statusNote: note ?? null });
151
+ await unblockSuccessors(resolved.run.id, resolved.opRun.operationId, userId);
152
+ const full = await formatOpRunTransition(orderKey, runNo, request.erpUser, opRun);
153
+ return mutationResult(request, reply, full, {
154
+ status: opRun.status,
155
+ _actions: full._actions,
156
+ });
157
+ },
158
+ });
159
+ // FAIL (in_progress → failed)
160
+ app.post("/:seqNo/fail", {
161
+ schema: {
162
+ description: "Fail an operation run (in_progress → failed)",
163
+ tags: ["Operation Runs"],
164
+ params: SeqNoParamsSchema,
165
+ body: TransitionNoteSchema,
166
+ response: {
167
+ 200: OperationRunTransitionSlimSchema,
168
+ 404: ErrorResponseSchema,
169
+ 409: ErrorResponseSchema,
170
+ },
171
+ },
172
+ preHandler: requirePermission("order_manager"),
173
+ handler: async (request, reply) => {
174
+ const { orderKey, runNo, seqNo } = request.params;
175
+ const { note } = request.body;
176
+ const userId = request.erpUser.id;
177
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
178
+ if (!resolved)
179
+ return notFound(reply, `Operation run not found`);
180
+ const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
181
+ if (wcErr)
182
+ return conflict(reply, wcErr);
183
+ const orderErr = checkOrderRunStarted(resolved.run.status);
184
+ if (orderErr)
185
+ return conflict(reply, orderErr);
186
+ const statusErr = validateStatusFor("fail", resolved.opRun.status, [
187
+ OperationRunStatus.in_progress,
188
+ ]);
189
+ if (statusErr)
190
+ return conflict(reply, statusErr);
191
+ await clockOutAllForOpRun(resolved.opRun.id, userId);
192
+ const cost = await sumLaborTicketCosts(resolved.opRun.id);
193
+ const opRun = await transitionStatus(resolved.opRun.id, "fail", OperationRunStatus.in_progress, OperationRunStatus.failed, userId, { ...(cost > 0 ? { cost } : undefined), statusNote: note ?? null });
194
+ const full = await formatOpRunTransition(orderKey, runNo, request.erpUser, opRun);
195
+ return mutationResult(request, reply, full, {
196
+ status: opRun.status,
197
+ _actions: full._actions,
198
+ });
199
+ },
200
+ });
201
+ // REOPEN (completed/skipped/failed → in_progress/pending)
202
+ app.post("/:seqNo/reopen", {
203
+ schema: {
204
+ description: "Reopen an operation run (completed → in_progress)",
205
+ tags: ["Operation Runs"],
206
+ params: SeqNoParamsSchema,
207
+ body: TransitionNoteSchema,
208
+ response: {
209
+ 200: OperationRunTransitionSlimSchema,
210
+ 404: ErrorResponseSchema,
211
+ 409: ErrorResponseSchema,
212
+ },
213
+ },
214
+ preHandler: requirePermission("order_manager"),
215
+ handler: async (request, reply) => {
216
+ const { orderKey, runNo, seqNo } = request.params;
217
+ const { note } = request.body;
218
+ const userId = request.erpUser.id;
219
+ const resolved = await resolveOpRun(orderKey, runNo, seqNo);
220
+ if (!resolved)
221
+ return notFound(reply, `Operation run not found`);
222
+ const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
223
+ if (wcErr)
224
+ return conflict(reply, wcErr);
225
+ const orderErr = checkOrderRunStarted(resolved.run.status);
226
+ if (orderErr)
227
+ return conflict(reply, orderErr);
228
+ const statusErr = validateStatusFor("reopen", resolved.opRun.status, [
229
+ OperationRunStatus.completed,
230
+ OperationRunStatus.skipped,
231
+ OperationRunStatus.failed,
232
+ ]);
233
+ if (statusErr)
234
+ return conflict(reply, statusErr);
235
+ const reopenTo = resolved.opRun.status === OperationRunStatus.skipped
236
+ ? OperationRunStatus.pending
237
+ : OperationRunStatus.in_progress;
238
+ const opRun = await transitionStatus(resolved.opRun.id, "reopen", resolved.opRun.status, reopenTo, userId, { completedAt: null, statusNote: note ?? null });
239
+ // Re-block successor ops that are still pending
240
+ await reblockSuccessors(resolved.run.id, resolved.opRun.operationId, userId);
241
+ const full = await formatOpRunTransition(orderKey, runNo, request.erpUser, opRun);
242
+ return mutationResult(request, reply, full, {
243
+ status: opRun.status,
244
+ _actions: full._actions,
245
+ });
246
+ },
247
+ });
248
+ }
249
+ //# sourceMappingURL=operation-run-transitions.js.map