@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,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,170 @@
1
+ import { CreateOperationDependencySchema, CreateResponseSchema, ErrorResponseSchema, OperationDependencyListResponseSchema, RevisionStatus, } from "@naisys/erp-shared";
2
+ import { z } from "zod/v4";
3
+ import { hasPermission, requirePermission } from "../auth-middleware.js";
4
+ import { conflict, notFound } from "../error-handler.js";
5
+ import { API_PREFIX, selfLink } from "../hateoas.js";
6
+ import { mutationResult, resolveRevision } from "../route-helpers.js";
7
+ import { createDependency, deleteDependency, listDependencies, } from "../services/operation-dependency-service.js";
8
+ import { findExisting } from "../services/operation-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 DepParamsSchema = z.object({
15
+ orderKey: z.string(),
16
+ revNo: z.coerce.number().int(),
17
+ seqNo: z.coerce.number().int(),
18
+ predecessorSeqNo: z.coerce.number().int(),
19
+ });
20
+ function depBasePath(orderKey, revNo, seqNo) {
21
+ return `/orders/${orderKey}/revs/${revNo}/ops/${seqNo}/deps`;
22
+ }
23
+ function formatDependency(dep) {
24
+ return {
25
+ id: dep.id,
26
+ predecessorSeqNo: dep.predecessor.seqNo,
27
+ predecessorTitle: dep.predecessor.title,
28
+ createdAt: dep.createdAt.toISOString(),
29
+ createdBy: dep.createdBy.username,
30
+ };
31
+ }
32
+ function depActionTemplates(basePath, revStatus, user) {
33
+ if (!hasPermission(user, "order_planner") ||
34
+ revStatus !== RevisionStatus.draft)
35
+ return [];
36
+ return [
37
+ {
38
+ rel: "deleteDependency",
39
+ hrefTemplate: `${API_PREFIX}${basePath}/{predecessorSeqNo}`,
40
+ method: "DELETE",
41
+ title: "Remove Dependency",
42
+ },
43
+ ];
44
+ }
45
+ export default function operationDependencyRoutes(fastify) {
46
+ const app = fastify.withTypeProvider();
47
+ // LIST dependencies (predecessors) for an operation
48
+ app.get("/", {
49
+ schema: {
50
+ description: "List predecessor dependencies for an operation",
51
+ tags: ["Operation Dependencies"],
52
+ params: ParamsSchema,
53
+ response: {
54
+ 200: OperationDependencyListResponseSchema,
55
+ 404: ErrorResponseSchema,
56
+ },
57
+ },
58
+ handler: async (request, reply) => {
59
+ const { orderKey, revNo, seqNo } = request.params;
60
+ const resolved = await resolveRevision(orderKey, revNo);
61
+ if (!resolved) {
62
+ return notFound(reply, "Revision not found");
63
+ }
64
+ const operation = await findExisting(resolved.rev.id, seqNo);
65
+ if (!operation) {
66
+ return notFound(reply, `Operation ${seqNo} not found`);
67
+ }
68
+ const items = await listDependencies(operation.id);
69
+ const user = request.erpUser;
70
+ const base = depBasePath(orderKey, revNo, seqNo);
71
+ return {
72
+ items: items.map((dep) => formatDependency(dep)),
73
+ total: items.length,
74
+ _links: [selfLink(base)],
75
+ _actions: hasPermission(user, "order_planner") &&
76
+ resolved.rev.status === RevisionStatus.draft
77
+ ? [
78
+ {
79
+ rel: "create",
80
+ href: `${API_PREFIX}${base}`,
81
+ method: "POST",
82
+ title: "Add Dependency",
83
+ schema: `${API_PREFIX}/schemas/CreateOperationDependency`,
84
+ },
85
+ ]
86
+ : [],
87
+ _actionTemplates: depActionTemplates(base, resolved.rev.status, user),
88
+ };
89
+ },
90
+ });
91
+ // CREATE a dependency
92
+ app.post("/", {
93
+ schema: {
94
+ description: "Add a predecessor dependency to an operation",
95
+ tags: ["Operation Dependencies"],
96
+ params: ParamsSchema,
97
+ body: CreateOperationDependencySchema,
98
+ response: {
99
+ 201: CreateResponseSchema,
100
+ 404: ErrorResponseSchema,
101
+ 409: ErrorResponseSchema,
102
+ },
103
+ },
104
+ preHandler: requirePermission("order_planner"),
105
+ handler: async (request, reply) => {
106
+ const { orderKey, revNo, seqNo } = request.params;
107
+ const { predecessorSeqNo } = request.body;
108
+ const userId = request.erpUser.id;
109
+ const resolved = await resolveRevision(orderKey, revNo);
110
+ if (!resolved) {
111
+ return notFound(reply, "Revision not found");
112
+ }
113
+ if (resolved.rev.status !== RevisionStatus.draft) {
114
+ return conflict(reply, `Cannot modify dependencies on a ${resolved.rev.status} revision`);
115
+ }
116
+ const successor = await findExisting(resolved.rev.id, seqNo);
117
+ if (!successor) {
118
+ return notFound(reply, `Operation ${seqNo} not found`);
119
+ }
120
+ const predecessor = await findExisting(resolved.rev.id, predecessorSeqNo);
121
+ if (!predecessor) {
122
+ return notFound(reply, `Predecessor operation ${predecessorSeqNo} not found`);
123
+ }
124
+ if (successor.id === predecessor.id) {
125
+ return conflict(reply, "An operation cannot depend on itself");
126
+ }
127
+ const dep = await createDependency(successor.id, predecessor.id, userId);
128
+ const full = formatDependency(dep);
129
+ reply.status(201);
130
+ return mutationResult(request, reply, full, {
131
+ id: full.id,
132
+ });
133
+ },
134
+ });
135
+ // DELETE a dependency
136
+ app.delete("/:predecessorSeqNo", {
137
+ schema: {
138
+ description: "Remove a predecessor dependency from an operation",
139
+ tags: ["Operation Dependencies"],
140
+ params: DepParamsSchema,
141
+ response: {
142
+ 204: z.void(),
143
+ 404: ErrorResponseSchema,
144
+ 409: ErrorResponseSchema,
145
+ },
146
+ },
147
+ preHandler: requirePermission("order_planner"),
148
+ handler: async (request, reply) => {
149
+ const { orderKey, revNo, seqNo, predecessorSeqNo } = request.params;
150
+ const resolved = await resolveRevision(orderKey, revNo);
151
+ if (!resolved) {
152
+ return notFound(reply, "Revision not found");
153
+ }
154
+ if (resolved.rev.status !== RevisionStatus.draft) {
155
+ return conflict(reply, `Cannot modify dependencies on a ${resolved.rev.status} revision`);
156
+ }
157
+ const successor = await findExisting(resolved.rev.id, seqNo);
158
+ if (!successor) {
159
+ return notFound(reply, `Operation ${seqNo} not found`);
160
+ }
161
+ const predecessor = await findExisting(resolved.rev.id, predecessorSeqNo);
162
+ if (!predecessor) {
163
+ return notFound(reply, `Predecessor operation ${predecessorSeqNo} not found`);
164
+ }
165
+ await deleteDependency(successor.id, predecessor.id);
166
+ reply.status(204);
167
+ },
168
+ });
169
+ }
170
+ //# sourceMappingURL=operation-dependencies.js.map