@naisys/erp 3.0.0-beta.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.env.example +5 -0
  2. package/client-dist/assets/index-CSiMTJfw.css +14 -0
  3. package/client-dist/assets/index-D6lSIioV.js +11167 -0
  4. package/client-dist/assets/rolldown-runtime-CvHMtSRF.js +33 -0
  5. package/client-dist/assets/vendor-CJ0ET9hP.js +75181 -0
  6. package/client-dist/assets/vendor-CLUPjUnv.css +8747 -0
  7. package/client-dist/favicon-16x16.png +0 -0
  8. package/client-dist/favicon-32x32.png +0 -0
  9. package/client-dist/favicon.ico +0 -0
  10. package/client-dist/index.html +17 -2
  11. package/dist/{dbConfig.js → database/dbConfig.js} +1 -1
  12. package/dist/{erpDb.js → database/erpDb.js} +1 -1
  13. package/dist/erpRoutes.js +115 -0
  14. package/dist/erpServer.js +85 -161
  15. package/dist/error-handler.js +3 -0
  16. package/dist/generated/prisma/internal/class.js +4 -4
  17. package/dist/generated/prisma/internal/prismaNamespace.js +3 -1
  18. package/dist/middleware/auth-middleware.js +146 -0
  19. package/dist/route-helpers.js +2 -2
  20. package/dist/routes/admin.js +15 -7
  21. package/dist/routes/audit.js +1 -1
  22. package/dist/routes/{item-fields.js → items/item-fields.js} +24 -22
  23. package/dist/routes/{item-instances.js → items/item-instances.js} +42 -24
  24. package/dist/routes/{items.js → items/items.js} +35 -33
  25. package/dist/routes/{operation-dependencies.js → operations/operation-dependencies.js} +6 -6
  26. package/dist/routes/{operation-field-refs.js → operations/operation-field-refs.js} +6 -6
  27. package/dist/routes/{operation-run-comments.js → operations/operation-run-comments.js} +5 -5
  28. package/dist/routes/{operation-run-transitions.js → operations/operation-run-transitions.js} +29 -13
  29. package/dist/routes/{operation-runs.js → operations/operation-runs.js} +48 -10
  30. package/dist/routes/{operations.js → operations/operations.js} +6 -6
  31. package/dist/routes/{order-revision-transitions.js → orders/order-revision-transitions.js} +4 -4
  32. package/dist/routes/{order-revisions.js → orders/order-revisions.js} +6 -6
  33. package/dist/routes/{order-run-transitions.js → orders/order-run-transitions.js} +11 -5
  34. package/dist/routes/{order-runs.js → orders/order-runs.js} +7 -5
  35. package/dist/routes/{orders.js → orders/orders.js} +15 -11
  36. package/dist/routes/{dispatch.js → production/dispatch.js} +88 -7
  37. package/dist/routes/{inventory.js → production/inventory.js} +33 -10
  38. package/dist/routes/{labor-tickets.js → production/labor-tickets.js} +7 -7
  39. package/dist/routes/{work-centers.js → production/work-centers.js} +29 -29
  40. package/dist/routes/root.js +1 -1
  41. package/dist/routes/{step-field-attachments.js → steps/step-field-attachments.js} +8 -8
  42. package/dist/routes/{step-fields.js → steps/step-fields.js} +6 -6
  43. package/dist/routes/{step-run-fields.js → steps/step-run-fields.js} +9 -9
  44. package/dist/routes/{step-run-transitions.js → steps/step-run-transitions.js} +6 -6
  45. package/dist/routes/{step-runs.js → steps/step-runs.js} +7 -7
  46. package/dist/routes/{steps.js → steps/steps.js} +5 -5
  47. package/dist/routes/{auth.js → users/auth.js} +11 -23
  48. package/dist/routes/{user-permissions.js → users/user-permissions.js} +21 -7
  49. package/dist/routes/{users.js → users/users.js} +42 -20
  50. package/dist/services/attachment-service.js +2 -2
  51. package/dist/services/{item-instance-service.js → inventory/item-instance-service.js} +2 -2
  52. package/dist/services/{item-service.js → inventory/item-service.js} +2 -2
  53. package/dist/services/{operation-dependency-service.js → operations/operation-dependency-service.js} +1 -1
  54. package/dist/services/{operation-run-comment-service.js → operations/operation-run-comment-service.js} +1 -1
  55. package/dist/services/{operation-run-service.js → operations/operation-run-service.js} +15 -4
  56. package/dist/services/{operation-service.js → operations/operation-service.js} +2 -2
  57. package/dist/services/{step-run-service.js → operations/step-run-service.js} +1 -1
  58. package/dist/services/{step-service.js → operations/step-service.js} +2 -2
  59. package/dist/services/{order-revision-service.js → orders/order-revision-service.js} +4 -5
  60. package/dist/services/{order-run-service.js → orders/order-run-service.js} +68 -22
  61. package/dist/services/{order-service.js → orders/order-service.js} +11 -2
  62. package/dist/services/{revision-diff-service.js → orders/revision-diff-service.js} +11 -10
  63. package/dist/services/{field-ref-service.js → production/field-ref-service.js} +1 -1
  64. package/dist/services/{field-service.js → production/field-service.js} +2 -2
  65. package/dist/services/{field-value-service.js → production/field-value-service.js} +27 -3
  66. package/dist/services/production/labor-ticket-backfill.js +67 -0
  67. package/dist/services/{labor-ticket-service.js → production/labor-ticket-service.js} +21 -15
  68. package/dist/services/{work-center-service.js → production/work-center-service.js} +2 -2
  69. package/dist/services/user-service.js +94 -28
  70. package/dist/version.js +12 -0
  71. package/npm-shrinkwrap.json +3000 -0
  72. package/package.json +11 -9
  73. package/prisma/migrations/20260427010000_hash_user_api_keys/migration.sql +10 -0
  74. package/prisma/migrations/20260427020000_nullable_user_password_hash/migration.sql +39 -0
  75. package/prisma/migrations/20260517000000_add_op_run_tokens/migration.sql +2 -0
  76. package/prisma/schema.prisma +4 -2
  77. package/client-dist/assets/index-45dVo30p.css +0 -1
  78. package/client-dist/assets/index-C9uuPHLH.js +0 -168
  79. package/dist/auth-middleware.js +0 -203
  80. package/dist/userService.js +0 -118
  81. /package/bin/{naisys-erp → naisys-erp.js} +0 -0
  82. /package/dist/{supervisorAuth.js → middleware/supervisorAuth.js} +0 -0
  83. /package/dist/{audit.js → services/audit.js} +0 -0
@@ -1,10 +1,10 @@
1
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";
2
+ import { conflict, notFound, unprocessable } from "../../error-handler.js";
3
+ import { requirePermission } from "../../middleware/auth-middleware.js";
4
+ import { checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveOpRun, } from "../../route-helpers.js";
5
+ import { checkPredecessorsComplete, checkStepsComplete, reblockSuccessors, transitionStatus, unblockSuccessors, validateStatusFor, } from "../../services/operations/operation-run-service.js";
6
+ import { transitionStatus as transitionOrderRunStatus } from "../../services/orders/order-run-service.js";
7
+ import { clockIn, clockOutAllForOpRun, isUserClockedIn, sumLaborTicketMetrics, } from "../../services/production/labor-ticket-service.js";
8
8
  import { formatOpRunTransition, SeqNoParamsSchema } from "./operation-runs.js";
9
9
  export default function operationRunTransitionRoutes(fastify) {
10
10
  const app = fastify.withTypeProvider();
@@ -99,10 +99,11 @@ export default function operationRunTransitionRoutes(fastify) {
99
99
  if (stepsErr)
100
100
  return unprocessable(reply, stepsErr);
101
101
  await clockOutAllForOpRun(resolved.opRun.id, userId);
102
- const cost = await sumLaborTicketCosts(resolved.opRun.id);
102
+ const { cost, tokens } = await sumLaborTicketMetrics(resolved.opRun.id);
103
103
  const opRun = await transitionStatus(resolved.opRun.id, "complete", OperationRunStatus.in_progress, OperationRunStatus.completed, userId, {
104
104
  completedAt: new Date(),
105
105
  cost,
106
+ tokens,
106
107
  statusNote: note ?? null,
107
108
  });
108
109
  await unblockSuccessors(resolved.run.id, resolved.opRun.operationId, userId);
@@ -113,10 +114,11 @@ export default function operationRunTransitionRoutes(fastify) {
113
114
  });
114
115
  },
115
116
  });
116
- // SKIP (pending → skipped)
117
+ // SKIP (blocked/pending/in_progress → skipped)
117
118
  app.post("/:seqNo/skip", {
118
119
  schema: {
119
- description: "Skip an operation run (pending → skipped)",
120
+ description: "Skip an operation run (blocked/pending/in_progress → skipped). " +
121
+ "When skipping an in_progress op, any open labor tickets are clocked out.",
120
122
  tags: ["Operation Runs"],
121
123
  params: SeqNoParamsSchema,
122
124
  body: TransitionNoteSchema,
@@ -143,11 +145,21 @@ export default function operationRunTransitionRoutes(fastify) {
143
145
  const statusErr = validateStatusFor("skip", resolved.opRun.status, [
144
146
  OperationRunStatus.blocked,
145
147
  OperationRunStatus.pending,
148
+ OperationRunStatus.in_progress,
146
149
  ]);
147
150
  if (statusErr)
148
151
  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 });
152
+ // If the op was in progress, close out any active labor tickets so the
153
+ // recorded cost is accurate before we mark the op skipped.
154
+ if (resolved.opRun.status === OperationRunStatus.in_progress) {
155
+ await clockOutAllForOpRun(resolved.opRun.id, userId);
156
+ }
157
+ const { cost, tokens } = await sumLaborTicketMetrics(resolved.opRun.id);
158
+ const opRun = await transitionStatus(resolved.opRun.id, "skip", resolved.opRun.status, OperationRunStatus.skipped, userId, {
159
+ ...(cost > 0 ? { cost } : undefined),
160
+ ...(tokens > 0 ? { tokens } : undefined),
161
+ statusNote: note ?? null,
162
+ });
151
163
  await unblockSuccessors(resolved.run.id, resolved.opRun.operationId, userId);
152
164
  const full = await formatOpRunTransition(orderKey, runNo, request.erpUser, opRun);
153
165
  return mutationResult(request, reply, full, {
@@ -189,8 +201,12 @@ export default function operationRunTransitionRoutes(fastify) {
189
201
  if (statusErr)
190
202
  return conflict(reply, statusErr);
191
203
  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 });
204
+ const { cost, tokens } = await sumLaborTicketMetrics(resolved.opRun.id);
205
+ const opRun = await transitionStatus(resolved.opRun.id, "fail", OperationRunStatus.in_progress, OperationRunStatus.failed, userId, {
206
+ ...(cost > 0 ? { cost } : undefined),
207
+ ...(tokens > 0 ? { tokens } : undefined),
208
+ statusNote: note ?? null,
209
+ });
194
210
  const full = await formatOpRunTransition(orderKey, runNo, request.erpUser, opRun);
195
211
  return mutationResult(request, reply, full, {
196
212
  status: opRun.status,
@@ -1,20 +1,29 @@
1
1
  import { ErrorResponseSchema, MutateResponseSchema, OperationRunListResponseSchema, OperationRunSchema, OperationRunStatus, UpdateOperationRunSchema, } from "@naisys/erp-shared";
2
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 { checkOrderRunStarted, checkWorkCenterAccess, childItemLinks, formatAuditFields, formatDate, resolveActions, resolveOpRun, resolveOrderRun, useFullSerializer, wantsFullResponse, } from "../route-helpers.js";
7
- import { checkStepsComplete, getOpRun, getOpRunFieldRefSummary, getOpRunStepSummary, listOpRuns, updateOpRun, validateStatusFor, } from "../services/operation-run-service.js";
3
+ import { conflict, notFound } from "../../error-handler.js";
4
+ import { API_PREFIX, selfLink } from "../../hateoas.js";
5
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
6
+ import { checkOrderRunStarted, checkWorkCenterAccess, childItemLinks, formatAuditFields, formatDate, resolveActions, resolveOpRun, resolveOrderRun, useFullSerializer, wantsFullResponse, } from "../../route-helpers.js";
7
+ import { checkStepsComplete, getOpRun, getOpRunFieldRefSummary, getOpRunStepSummary, listOpRuns, updateOpRun, validateStatusFor, } from "../../services/operations/operation-run-service.js";
8
+ import { isUserClockedIn } from "../../services/production/labor-ticket-service.js";
8
9
  function opRunResource(orderKey, runNo) {
9
10
  return `orders/${orderKey}/runs/${runNo}/ops`;
10
11
  }
11
12
  async function opRunItemActions(orderKey, runNo, seqNo, opRunId, operationId, status, user) {
12
13
  const href = `${API_PREFIX}/${opRunResource(orderKey, runNo)}/${seqNo}`;
13
14
  const isExecutor = hasPermission(user, "order_executor");
14
- const stepsErr = isExecutor && status === OperationRunStatus.in_progress
15
- ? await checkStepsComplete(opRunId)
16
- : null;
15
+ const isInProgress = status === OperationRunStatus.in_progress;
16
+ const stepsErr = isExecutor && isInProgress ? await checkStepsComplete(opRunId) : null;
17
17
  const wcErr = user ? await checkWorkCenterAccess(operationId, user) : null;
18
+ // Completing an op requires the caller to be clocked in (enforced by the
19
+ // /complete transition). Surface clock-in and the requirement here so an
20
+ // agent inspecting the op run finds it without following the `labor` link.
21
+ const userClockedIn = isExecutor && isInProgress && user
22
+ ? await isUserClockedIn(opRunId, user.id)
23
+ : false;
24
+ const notClockedInErr = isExecutor && isInProgress && !userClockedIn
25
+ ? "You must be clocked in to complete an operation"
26
+ : null;
18
27
  return resolveActions([
19
28
  {
20
29
  rel: "assign",
@@ -50,6 +59,18 @@ async function opRunItemActions(orderKey, runNo, seqNo, opRunId, operationId, st
50
59
  ? "Operation is blocked by incomplete predecessors"
51
60
  : null),
52
61
  },
62
+ {
63
+ rel: "clock-in",
64
+ path: "/labor/clock-in",
65
+ method: "POST",
66
+ title: "Clock In",
67
+ permission: "order_executor",
68
+ statuses: [OperationRunStatus.in_progress],
69
+ disabledWhen: () => wcErr ??
70
+ (userClockedIn
71
+ ? "You are already clocked in to this operation"
72
+ : null),
73
+ },
53
74
  {
54
75
  rel: "update",
55
76
  method: "PUT",
@@ -68,7 +89,7 @@ async function opRunItemActions(orderKey, runNo, seqNo, opRunId, operationId, st
68
89
  body: { note: "" },
69
90
  permission: "order_executor",
70
91
  statuses: [OperationRunStatus.in_progress],
71
- disabledWhen: () => wcErr ?? stepsErr,
92
+ disabledWhen: () => wcErr ?? notClockedInErr ?? stepsErr,
72
93
  },
73
94
  {
74
95
  rel: "skip",
@@ -76,7 +97,11 @@ async function opRunItemActions(orderKey, runNo, seqNo, opRunId, operationId, st
76
97
  method: "POST",
77
98
  title: "Skip",
78
99
  permission: "order_manager",
79
- statuses: [OperationRunStatus.blocked, OperationRunStatus.pending],
100
+ statuses: [
101
+ OperationRunStatus.blocked,
102
+ OperationRunStatus.pending,
103
+ OperationRunStatus.in_progress,
104
+ ],
80
105
  disabledWhen: () => wcErr,
81
106
  },
82
107
  {
@@ -118,10 +143,13 @@ export async function formatOpRun(orderKey, runNo, user, opRun) {
118
143
  getOpRunStepSummary(opRun.id),
119
144
  getOpRunFieldRefSummary(opRun.operationId, opRun.orderRunId, orderKey, runNo),
120
145
  ]);
146
+ const revNo = opRun.orderRun.orderRev.revNo;
121
147
  return {
122
148
  id: opRun.id,
123
149
  orderRunId: opRun.orderRunId,
124
150
  operationId: opRun.operationId,
151
+ revNo,
152
+ orderDescription: opRun.orderRun.orderRev.description,
125
153
  seqNo,
126
154
  title: opRun.operation.title,
127
155
  description: opRun.operation.description,
@@ -129,6 +157,7 @@ export async function formatOpRun(orderKey, runNo, user, opRun) {
129
157
  status: opRun.status,
130
158
  assignedTo: opRun.assignedTo?.username ?? null,
131
159
  cost: opRun.cost,
160
+ tokens: opRun.tokens,
132
161
  note: opRun.statusNote ?? null,
133
162
  completedAt: formatDate(opRun.completedAt),
134
163
  stepSummary: stepSummaryRows.map((sr) => ({
@@ -144,6 +173,11 @@ export async function formatOpRun(orderKey, runNo, user, opRun) {
144
173
  ...formatAuditFields(opRun),
145
174
  _links: [
146
175
  ...childItemLinks("/" + opRunResource(orderKey, runNo), seqNo, "Operation Runs", "/orders/" + orderKey + "/runs/" + runNo, "Order Run", "OperationRun", "run"),
176
+ {
177
+ rel: "order-revision",
178
+ href: `${API_PREFIX}/orders/${orderKey}/revs/${revNo}`,
179
+ title: "Order Revision",
180
+ },
147
181
  {
148
182
  rel: "steps",
149
183
  href: `${API_PREFIX}/${opRunResource(orderKey, runNo)}/${seqNo}/steps`,
@@ -170,6 +204,7 @@ export async function formatOpRunTransition(orderKey, runNo, user, opRun) {
170
204
  status: opRun.status,
171
205
  assignedTo: opRun.assignedTo?.username ?? null,
172
206
  cost: opRun.cost,
207
+ tokens: opRun.tokens,
173
208
  note: opRun.statusNote ?? null,
174
209
  completedAt: formatDate(opRun.completedAt),
175
210
  ...formatAuditFields(opRun),
@@ -182,6 +217,8 @@ function formatListOpRun(opRun) {
182
217
  id: opRun.id,
183
218
  orderRunId: opRun.orderRunId,
184
219
  operationId: opRun.operationId,
220
+ revNo: opRun.orderRun.orderRev.revNo,
221
+ orderDescription: opRun.orderRun.orderRev.description,
185
222
  seqNo,
186
223
  title: opRun.operation.title,
187
224
  description: opRun.operation.description,
@@ -189,6 +226,7 @@ function formatListOpRun(opRun) {
189
226
  status: opRun.status,
190
227
  assignedTo: opRun.assignedTo?.username ?? null,
191
228
  cost: opRun.cost,
229
+ tokens: opRun.tokens,
192
230
  note: opRun.statusNote ?? null,
193
231
  completedAt: formatDate(opRun.completedAt),
194
232
  ...formatAuditFields(opRun),
@@ -1,11 +1,11 @@
1
1
  import { CreateOperationSchema, ErrorResponseSchema, MutateResponseSchema, OperationListResponseSchema, OperationSchema, RevisionStatus, SeqNoCreateResponseSchema, UpdateOperationSchema, } from "@naisys/erp-shared";
2
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 { calcNextSeqNo, childItemLinks, draftCrudActions, formatAuditFields, mutationResult, permGate, resolveRevision, } from "../route-helpers.js";
7
- import { createOperation, deleteOperation, findExisting, getOperation, listOperations, updateOperation, } from "../services/operation-service.js";
8
- import { findWorkCenterByKey } from "../services/work-center-service.js";
3
+ import { conflict, notFound } from "../../error-handler.js";
4
+ import { API_PREFIX, selfLink } from "../../hateoas.js";
5
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
6
+ import { calcNextSeqNo, childItemLinks, draftCrudActions, formatAuditFields, mutationResult, permGate, resolveRevision, } from "../../route-helpers.js";
7
+ import { createOperation, deleteOperation, findExisting, getOperation, listOperations, updateOperation, } from "../../services/operations/operation-service.js";
8
+ import { findWorkCenterByKey } from "../../services/production/work-center-service.js";
9
9
  /** Resolve an optional workCenterKey to a workCenterId. Returns undefined (skip), null (clear), or the ID. */
10
10
  async function resolveWorkCenterId(workCenterKey) {
11
11
  if (workCenterKey === undefined)
@@ -1,8 +1,8 @@
1
1
  import { ErrorResponseSchema, OrderRevisionTransitionSchema, RevisionStatus, } from "@naisys/erp-shared";
2
- import { requirePermission } from "../auth-middleware.js";
3
- import { conflict, notFound } from "../error-handler.js";
4
- import { resolveOrder, useFullSerializer, wantsFullResponse, } from "../route-helpers.js";
5
- import { findExisting, transitionStatus, } from "../services/order-revision-service.js";
2
+ import { conflict, notFound } from "../../error-handler.js";
3
+ import { requirePermission } from "../../middleware/auth-middleware.js";
4
+ import { resolveOrder, useFullSerializer, wantsFullResponse, } from "../../route-helpers.js";
5
+ import { findExisting, transitionStatus, } from "../../services/orders/order-revision-service.js";
6
6
  import { formatRevision, revisionItemActions, RevNoParamsSchema, } from "./order-revisions.js";
7
7
  export default function orderRevisionTransitionRoutes(fastify) {
8
8
  const app = fastify.withTypeProvider();
@@ -1,11 +1,11 @@
1
1
  import { CreateOrderRevisionSchema, ErrorResponseSchema, MutateResponseSchema, OrderRevisionListQuerySchema, OrderRevisionListResponseSchema, OrderRevisionSchema, RevisionCreateResponseSchema, RevisionDiffQuerySchema, RevisionDiffResponseSchema, RevisionStatus, UpdateOrderRevisionSchema, } from "@naisys/erp-shared";
2
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, paginationLinks } from "../hateoas.js";
6
- import { childItemLinks, formatAuditFields, mutationResult, permGate, resolveActions, resolveOrder, } from "../route-helpers.js";
7
- import { checkHasOrderRuns, createRevision, deleteRevision, findExisting, getRevision, getRevisionOpSummary, listRevisions, updateRevision, validateDraftStatus, } from "../services/order-revision-service.js";
8
- import { diffRevisions } from "../services/revision-diff-service.js";
3
+ import { conflict, notFound } from "../../error-handler.js";
4
+ import { API_PREFIX, paginationLinks } from "../../hateoas.js";
5
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
6
+ import { childItemLinks, formatAuditFields, mutationResult, permGate, resolveActions, resolveOrder, } from "../../route-helpers.js";
7
+ import { checkHasOrderRuns, createRevision, deleteRevision, findExisting, getRevision, getRevisionOpSummary, listRevisions, updateRevision, validateDraftStatus, } from "../../services/orders/order-revision-service.js";
8
+ import { diffRevisions } from "../../services/orders/revision-diff-service.js";
9
9
  export function revisionItemActions(parentResource, orderKey, revNo, status, user) {
10
10
  const href = `${API_PREFIX}/${parentResource}/${orderKey}/revs/${revNo}`;
11
11
  return resolveActions([
@@ -1,8 +1,8 @@
1
1
  import { CompleteOrderRunSchema, ErrorResponseSchema, OrderRunStatus, OrderRunTransitionSchema, } from "@naisys/erp-shared";
2
- import { requirePermission } from "../auth-middleware.js";
3
- import { conflict, notFound, unprocessable } from "../error-handler.js";
4
- import { resolveOrderRun, useFullSerializer, wantsFullResponse, } from "../route-helpers.js";
5
- import { checkOpsComplete, completeOrderRun, getReopenTarget, sumOpRunCosts, transitionStatus, validateStatusFor, } from "../services/order-run-service.js";
2
+ import { badRequest, conflict, notFound, unprocessable, } from "../../error-handler.js";
3
+ import { requirePermission } from "../../middleware/auth-middleware.js";
4
+ import { resolveOrderRun, useFullSerializer, wantsFullResponse, } from "../../route-helpers.js";
5
+ import { checkOpsComplete, completeOrderRun, getReopenTarget, sumOpRunCosts, transitionStatus, validateStatusFor, } from "../../services/orders/order-run-service.js";
6
6
  import { formatRun, orderRunItemActions, RunNoParamsSchema, } from "./order-runs.js";
7
7
  export default function orderRunTransitionRoutes(fastify) {
8
8
  const app = fastify.withTypeProvider();
@@ -89,12 +89,15 @@ export default function orderRunTransitionRoutes(fastify) {
89
89
  // COMPLETE (started -> closed, creates item instance)
90
90
  app.post("/:runNo/complete", {
91
91
  schema: {
92
- description: "Complete an order run — creates an item instance and closes the run",
92
+ description: "Complete an order run — creates an item instance and closes the run. " +
93
+ "Returns 400 if any required item field is missing, or if any supplied " +
94
+ "fieldSeqNo doesn't exist on the item.",
93
95
  tags: ["Order Runs"],
94
96
  params: RunNoParamsSchema,
95
97
  body: CompleteOrderRunSchema,
96
98
  response: {
97
99
  200: OrderRunTransitionSchema,
100
+ 400: ErrorResponseSchema,
98
101
  404: ErrorResponseSchema,
99
102
  409: ErrorResponseSchema,
100
103
  422: ErrorResponseSchema,
@@ -119,6 +122,9 @@ export default function orderRunTransitionRoutes(fastify) {
119
122
  const userId = request.erpUser.id;
120
123
  const result = await completeOrderRun(resolved.run.id, resolved.order.id, request.body, userId);
121
124
  if (result.error) {
125
+ if (result.status === 400) {
126
+ return badRequest(reply, result.error);
127
+ }
122
128
  return unprocessable(reply, result.error);
123
129
  }
124
130
  const run = result.run;
@@ -1,10 +1,10 @@
1
1
  import { CreateOrderRunSchema, ErrorResponseSchema, MutateResponseSchema, OrderRunListQuerySchema, OrderRunListResponseSchema, OrderRunSchema, OrderRunStatus, RunCreateResponseSchema, UpdateOrderRunSchema, } from "@naisys/erp-shared";
2
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, paginationLinks } from "../hateoas.js";
6
- import { childItemLinks, formatAuditFields, mutationResult, resolveActions, resolveOrder, resolveOrderRun, } from "../route-helpers.js";
7
- import { checkOpsComplete, createOrderRun, deleteOrderRun, findLatestApprovedRevision, findOrderRevision, getOrderRun, getOrderRunOpSummary, listOrderRuns, updateOrderRun, validateStatusFor, } from "../services/order-run-service.js";
3
+ import { conflict, notFound } from "../../error-handler.js";
4
+ import { API_PREFIX, paginationLinks } from "../../hateoas.js";
5
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
6
+ import { childItemLinks, formatAuditFields, mutationResult, resolveActions, resolveOrder, resolveOrderRun, } from "../../route-helpers.js";
7
+ import { checkOpsComplete, createOrderRun, deleteOrderRun, findLatestApprovedRevision, findOrderRevision, getOrderRun, getOrderRunOpSummary, listOrderRuns, updateOrderRun, validateStatusFor, } from "../../services/orders/order-run-service.js";
8
8
  function runResource(orderKey) {
9
9
  return `orders/${orderKey}/runs`;
10
10
  }
@@ -116,6 +116,7 @@ export async function formatRun(orderKey, user, run) {
116
116
  orderId: run.orderId,
117
117
  orderKey,
118
118
  revNo: run.orderRev.revNo,
119
+ description: run.orderRev.description,
119
120
  itemKey,
120
121
  instanceId: instance?.id ?? null,
121
122
  instanceKey: instance?.key ?? null,
@@ -143,6 +144,7 @@ function formatListRun(orderKey, run) {
143
144
  orderId: run.orderId,
144
145
  orderKey,
145
146
  revNo: run.orderRev.revNo,
147
+ description: run.orderRev.description,
146
148
  itemKey,
147
149
  instanceId: instance?.id ?? null,
148
150
  instanceKey: instance?.key ?? null,
@@ -1,10 +1,10 @@
1
1
  import { CreateOrderSchema, ErrorResponseSchema, KeyCreateResponseSchema, MutateResponseSchema, OrderListQuerySchema, OrderListResponseSchema, OrderSchema, OrderStatus, UpdateOrderSchema, } from "@naisys/erp-shared";
2
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, collectionLink, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
- import { formatAuditFields, mutationResult, permGate, resolveActions, } from "../route-helpers.js";
7
- import { checkHasRevisions, createOrder, deleteOrder, findExisting, listOrders, resolveItemKey, updateOrder, } from "../services/order-service.js";
3
+ import { conflict, notFound } from "../../error-handler.js";
4
+ import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../../hateoas.js";
5
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
6
+ import { formatAuditFields, mutationResult, permGate, resolveActions, } from "../../route-helpers.js";
7
+ import { checkHasRevisions, createOrder, deleteOrder, findExisting, getLatestApprovedRevNo, listOrders, resolveItemKey, updateOrder, } from "../../services/orders/order-service.js";
8
8
  function orderLinks(resource, key, schemaName) {
9
9
  return [
10
10
  selfLink(`/${resource}/${key}`),
@@ -64,13 +64,14 @@ const RESOURCE = "orders";
64
64
  const KeyParamsSchema = z.object({
65
65
  key: z.string(),
66
66
  });
67
- function formatOrder(order, user) {
67
+ function formatOrder(order, user, latestApprovedRevNo) {
68
68
  return {
69
69
  id: order.id,
70
70
  key: order.key,
71
71
  description: order.description,
72
72
  status: order.status,
73
73
  itemKey: order.item?.key ?? null,
74
+ latestApprovedRevNo,
74
75
  ...formatAuditFields(order),
75
76
  _links: [
76
77
  ...orderLinks(RESOURCE, order.key, "Order"),
@@ -81,8 +82,8 @@ function formatOrder(order, user) {
81
82
  };
82
83
  }
83
84
  function formatListOrder(order, user) {
84
- const { _actions, ...rest } = formatOrder(order, user);
85
- const { _links: _, ...withoutLinks } = rest;
85
+ const { _actions, ...rest } = formatOrder(order, user, null);
86
+ const { _links: _, latestApprovedRevNo: __, ...withoutLinks } = rest;
86
87
  return withoutLinks;
87
88
  }
88
89
  export default function orderRoutes(fastify) {
@@ -159,7 +160,8 @@ export default function orderRoutes(fastify) {
159
160
  }
160
161
  }
161
162
  const order = await createOrder(key, description, itemId, userId);
162
- const full = formatOrder(order, request.erpUser);
163
+ // Newly-created order has no revisions yet, so latestApprovedRevNo is null
164
+ const full = formatOrder(order, request.erpUser, null);
163
165
  reply.status(201);
164
166
  return mutationResult(request, reply, full, {
165
167
  id: full.id,
@@ -186,7 +188,8 @@ export default function orderRoutes(fastify) {
186
188
  if (!order) {
187
189
  return notFound(reply, `Order '${key}' not found`);
188
190
  }
189
- return formatOrder(order, request.erpUser);
191
+ const latestApprovedRevNo = await getLatestApprovedRevNo(order.id);
192
+ return formatOrder(order, request.erpUser, latestApprovedRevNo);
190
193
  },
191
194
  });
192
195
  // UPDATE
@@ -225,7 +228,8 @@ export default function orderRoutes(fastify) {
225
228
  }
226
229
  }
227
230
  const order = await updateOrder(key, dbData, userId);
228
- const full = formatOrder(order, request.erpUser);
231
+ const latestApprovedRevNo = await getLatestApprovedRevNo(order.id);
232
+ const full = formatOrder(order, request.erpUser, latestApprovedRevNo);
229
233
  return mutationResult(request, reply, full, {
230
234
  _actions: full._actions,
231
235
  });
@@ -1,7 +1,7 @@
1
- import { DispatchListQuerySchema, DispatchListResponseSchema, OperationRunStatus, OrderRunStatus, } from "@naisys/erp-shared";
2
- import erpDb from "../erpDb.js";
3
- import { API_PREFIX, paginationLinks } from "../hateoas.js";
4
- import { getUserWorkCenterIds } from "../services/work-center-service.js";
1
+ import { DispatchListQuerySchema, DispatchListResponseSchema, OperationRunStatus, OrderRunStatus, ReadyToCloseListQuerySchema, ReadyToCloseListResponseSchema, } from "@naisys/erp-shared";
2
+ import erpDb from "../../database/erpDb.js";
3
+ import { API_PREFIX, paginationLinks } from "../../hateoas.js";
4
+ import { getUserWorkCenterIds } from "../../services/production/work-center-service.js";
5
5
  const OPEN_ORDER_STATUSES = [OrderRunStatus.released, OrderRunStatus.started];
6
6
  const DEFAULT_OP_STATUSES = [
7
7
  OperationRunStatus.pending,
@@ -64,9 +64,7 @@ export default function dispatchRoutes(fastify) {
64
64
  // Intersect with the requested status filter
65
65
  const currentStatuses = status ? [status] : DEFAULT_OP_STATUSES;
66
66
  const filteredStatuses = currentStatuses.filter((s) => workableStatuses.includes(s));
67
- where.status = {
68
- in: filteredStatuses.length > 0 ? filteredStatuses : ["__none__"],
69
- };
67
+ where.status = { in: filteredStatuses };
70
68
  // Work center access
71
69
  if (userWcIds.length > 0) {
72
70
  where.operation = {
@@ -147,6 +145,11 @@ export default function dispatchRoutes(fastify) {
147
145
  canWork: canWork ? "true" : undefined,
148
146
  clockedIn: clockedIn ? "true" : undefined,
149
147
  }),
148
+ {
149
+ rel: "ready-to-close",
150
+ href: `${API_PREFIX}/dispatch/ready-to-close`,
151
+ title: "Ready to Close",
152
+ },
150
153
  {
151
154
  rel: "work-centers",
152
155
  href: `${API_PREFIX}/work-centers`,
@@ -170,5 +173,83 @@ export default function dispatchRoutes(fastify) {
170
173
  };
171
174
  },
172
175
  });
176
+ app.get("/ready-to-close", {
177
+ schema: {
178
+ description: "List open order runs whose operations are all completed or skipped (ready to close)",
179
+ tags: ["Dispatch"],
180
+ querystring: ReadyToCloseListQuerySchema,
181
+ response: {
182
+ 200: ReadyToCloseListResponseSchema,
183
+ },
184
+ },
185
+ handler: async (request) => {
186
+ const { page, pageSize, priority, search } = request.query;
187
+ const where = {
188
+ status: { in: OPEN_ORDER_STATUSES },
189
+ operationRuns: {
190
+ none: {
191
+ status: {
192
+ notIn: [OperationRunStatus.completed, OperationRunStatus.skipped],
193
+ },
194
+ },
195
+ },
196
+ ...(priority ? { priority } : {}),
197
+ };
198
+ if (search) {
199
+ where.OR = [
200
+ { order: { key: { contains: search } } },
201
+ { orderRev: { description: { contains: search } } },
202
+ ];
203
+ }
204
+ const [items, total] = await Promise.all([
205
+ erpDb.orderRun.findMany({
206
+ where,
207
+ include: {
208
+ order: { select: { key: true } },
209
+ orderRev: { select: { revNo: true, description: true } },
210
+ _count: { select: { operationRuns: true } },
211
+ },
212
+ skip: (page - 1) * pageSize,
213
+ take: pageSize,
214
+ orderBy: { dueAt: "asc" },
215
+ }),
216
+ erpDb.orderRun.count({ where }),
217
+ ]);
218
+ return {
219
+ items: items.map((run) => ({
220
+ id: run.id,
221
+ orderKey: run.order.key,
222
+ revNo: run.orderRev.revNo,
223
+ runNo: run.runNo,
224
+ description: run.orderRev.description,
225
+ status: run.status,
226
+ priority: run.priority,
227
+ dueAt: run.dueAt,
228
+ opCount: run._count.operationRuns,
229
+ createdAt: run.createdAt.toISOString(),
230
+ })),
231
+ total,
232
+ page,
233
+ pageSize,
234
+ _links: [
235
+ ...paginationLinks("dispatch/ready-to-close", page, pageSize, total, {
236
+ priority,
237
+ search,
238
+ }),
239
+ {
240
+ rel: "dispatch",
241
+ href: `${API_PREFIX}/dispatch`,
242
+ title: "Open Operations",
243
+ },
244
+ ],
245
+ _linkTemplates: [
246
+ {
247
+ rel: "item",
248
+ hrefTemplate: `${API_PREFIX}/orders/{orderKey}/runs/{runNo}`,
249
+ },
250
+ ],
251
+ };
252
+ },
253
+ });
173
254
  }
174
255
  //# sourceMappingURL=dispatch.js.map
@@ -1,6 +1,36 @@
1
1
  import { InventoryListQuerySchema, InventoryListResponseSchema, } from "@naisys/erp-shared";
2
- import erpDb from "../erpDb.js";
3
- import { API_PREFIX, paginationLinks } from "../hateoas.js";
2
+ import erpDb from "../../database/erpDb.js";
3
+ import { API_PREFIX, paginationLinks } from "../../hateoas.js";
4
+ import { hasPermission } from "../../middleware/auth-middleware.js";
5
+ function buildInventoryActionTemplates(user) {
6
+ const templates = [
7
+ {
8
+ rel: "viewInstance",
9
+ hrefTemplate: `${API_PREFIX}/items/{itemKey}/instances/{id}`,
10
+ method: "GET",
11
+ title: "View Instance",
12
+ },
13
+ ];
14
+ if (hasPermission(user, "item_manager")) {
15
+ templates.push({
16
+ rel: "update-field-value",
17
+ hrefTemplate: `${API_PREFIX}/items/{itemKey}/instances/{id}/fields/{fieldSeqNo}`,
18
+ method: "PUT",
19
+ title: "Update Field Value (implicit set 0)",
20
+ schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
21
+ body: { value: "" },
22
+ });
23
+ templates.push({
24
+ rel: "update-set-field-value",
25
+ hrefTemplate: `${API_PREFIX}/items/{itemKey}/instances/{id}/sets/{setIndex}/fields/{fieldSeqNo}`,
26
+ method: "PUT",
27
+ title: "Update Field Value (explicit set index)",
28
+ schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
29
+ body: { value: "" },
30
+ });
31
+ }
32
+ return templates;
33
+ }
4
34
  export default function inventoryRoutes(fastify) {
5
35
  const app = fastify.withTypeProvider();
6
36
  app.get("/", {
@@ -55,14 +85,7 @@ export default function inventoryRoutes(fastify) {
55
85
  _links: paginationLinks("inventory", page, pageSize, total, {
56
86
  search,
57
87
  }),
58
- _actionTemplates: [
59
- {
60
- rel: "viewInstance",
61
- hrefTemplate: `${API_PREFIX}/items/{itemKey}/instances/{id}`,
62
- method: "GET",
63
- title: "View Instance",
64
- },
65
- ],
88
+ _actionTemplates: buildInventoryActionTemplates(request.erpUser),
66
89
  };
67
90
  },
68
91
  });
@@ -1,10 +1,10 @@
1
1
  import { ClockOutLaborTicketSchema, CreateResponseSchema, ErrorResponseSchema, LaborTicketListResponseSchema, MutateResponseSchema, OperationRunStatus, } from "@naisys/erp-shared";
2
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";
3
+ import { notFound } from "../../error-handler.js";
4
+ import { API_PREFIX, selfLink } from "../../hateoas.js";
5
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
6
+ import { checkOpRunInProgress, formatAuditFields, formatDate, mutationResult, permGate, resolveOpRun, } from "../../route-helpers.js";
7
+ import { clockIn, clockOut, deleteLaborTicket, listLaborTickets, } from "../../services/production/labor-ticket-service.js";
8
8
  function laborResource(orderKey, runNo, seqNo) {
9
9
  return `orders/${orderKey}/runs/${runNo}/ops/${seqNo}/labor`;
10
10
  }
@@ -72,14 +72,13 @@ function laborTicketListActions(orderKey, runNo, seqNo, opRunStatus, user, ticke
72
72
  return actions;
73
73
  }
74
74
  function laborTicketActionTemplates(orderKey, runNo, seqNo, user) {
75
- if (!hasPermission(user, "order_manager"))
76
- return [];
77
75
  return [
78
76
  {
79
77
  rel: "deleteTicket",
80
78
  hrefTemplate: `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}/{ticketId}`,
81
79
  method: "DELETE",
82
80
  title: "Delete Ticket",
81
+ ...permGate(hasPermission(user, "order_manager"), "order_manager"),
83
82
  },
84
83
  ];
85
84
  }
@@ -93,6 +92,7 @@ function formatLaborTicket(orderKey, runNo, seqNo, ticket) {
93
92
  clockIn: ticket.clockIn.toISOString(),
94
93
  clockOut: formatDate(ticket.clockOut),
95
94
  cost: ticket.cost,
95
+ tokens: ticket.tokens,
96
96
  ...formatAuditFields(ticket),
97
97
  _links: [
98
98
  selfLink(`/${laborResource(orderKey, runNo, seqNo)}/${ticket.id}`),