@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.
- package/.env.example +5 -0
- package/client-dist/assets/index-CSiMTJfw.css +14 -0
- package/client-dist/assets/index-D6lSIioV.js +11167 -0
- package/client-dist/assets/rolldown-runtime-CvHMtSRF.js +33 -0
- package/client-dist/assets/vendor-CJ0ET9hP.js +75181 -0
- package/client-dist/assets/vendor-CLUPjUnv.css +8747 -0
- package/client-dist/favicon-16x16.png +0 -0
- package/client-dist/favicon-32x32.png +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +17 -2
- package/dist/{dbConfig.js → database/dbConfig.js} +1 -1
- package/dist/{erpDb.js → database/erpDb.js} +1 -1
- package/dist/erpRoutes.js +115 -0
- package/dist/erpServer.js +85 -161
- package/dist/error-handler.js +3 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.js +3 -1
- package/dist/middleware/auth-middleware.js +146 -0
- package/dist/route-helpers.js +2 -2
- package/dist/routes/admin.js +15 -7
- package/dist/routes/audit.js +1 -1
- package/dist/routes/{item-fields.js → items/item-fields.js} +24 -22
- package/dist/routes/{item-instances.js → items/item-instances.js} +42 -24
- package/dist/routes/{items.js → items/items.js} +35 -33
- package/dist/routes/{operation-dependencies.js → operations/operation-dependencies.js} +6 -6
- package/dist/routes/{operation-field-refs.js → operations/operation-field-refs.js} +6 -6
- package/dist/routes/{operation-run-comments.js → operations/operation-run-comments.js} +5 -5
- package/dist/routes/{operation-run-transitions.js → operations/operation-run-transitions.js} +29 -13
- package/dist/routes/{operation-runs.js → operations/operation-runs.js} +48 -10
- package/dist/routes/{operations.js → operations/operations.js} +6 -6
- package/dist/routes/{order-revision-transitions.js → orders/order-revision-transitions.js} +4 -4
- package/dist/routes/{order-revisions.js → orders/order-revisions.js} +6 -6
- package/dist/routes/{order-run-transitions.js → orders/order-run-transitions.js} +11 -5
- package/dist/routes/{order-runs.js → orders/order-runs.js} +7 -5
- package/dist/routes/{orders.js → orders/orders.js} +15 -11
- package/dist/routes/{dispatch.js → production/dispatch.js} +88 -7
- package/dist/routes/{inventory.js → production/inventory.js} +33 -10
- package/dist/routes/{labor-tickets.js → production/labor-tickets.js} +7 -7
- package/dist/routes/{work-centers.js → production/work-centers.js} +29 -29
- package/dist/routes/root.js +1 -1
- package/dist/routes/{step-field-attachments.js → steps/step-field-attachments.js} +8 -8
- package/dist/routes/{step-fields.js → steps/step-fields.js} +6 -6
- package/dist/routes/{step-run-fields.js → steps/step-run-fields.js} +9 -9
- package/dist/routes/{step-run-transitions.js → steps/step-run-transitions.js} +6 -6
- package/dist/routes/{step-runs.js → steps/step-runs.js} +7 -7
- package/dist/routes/{steps.js → steps/steps.js} +5 -5
- package/dist/routes/{auth.js → users/auth.js} +11 -23
- package/dist/routes/{user-permissions.js → users/user-permissions.js} +21 -7
- package/dist/routes/{users.js → users/users.js} +42 -20
- package/dist/services/attachment-service.js +2 -2
- package/dist/services/{item-instance-service.js → inventory/item-instance-service.js} +2 -2
- package/dist/services/{item-service.js → inventory/item-service.js} +2 -2
- package/dist/services/{operation-dependency-service.js → operations/operation-dependency-service.js} +1 -1
- package/dist/services/{operation-run-comment-service.js → operations/operation-run-comment-service.js} +1 -1
- package/dist/services/{operation-run-service.js → operations/operation-run-service.js} +15 -4
- package/dist/services/{operation-service.js → operations/operation-service.js} +2 -2
- package/dist/services/{step-run-service.js → operations/step-run-service.js} +1 -1
- package/dist/services/{step-service.js → operations/step-service.js} +2 -2
- package/dist/services/{order-revision-service.js → orders/order-revision-service.js} +4 -5
- package/dist/services/{order-run-service.js → orders/order-run-service.js} +68 -22
- package/dist/services/{order-service.js → orders/order-service.js} +11 -2
- package/dist/services/{revision-diff-service.js → orders/revision-diff-service.js} +11 -10
- package/dist/services/{field-ref-service.js → production/field-ref-service.js} +1 -1
- package/dist/services/{field-service.js → production/field-service.js} +2 -2
- package/dist/services/{field-value-service.js → production/field-value-service.js} +27 -3
- package/dist/services/production/labor-ticket-backfill.js +67 -0
- package/dist/services/{labor-ticket-service.js → production/labor-ticket-service.js} +21 -15
- package/dist/services/{work-center-service.js → production/work-center-service.js} +2 -2
- package/dist/services/user-service.js +94 -28
- package/dist/version.js +12 -0
- package/npm-shrinkwrap.json +3000 -0
- package/package.json +11 -9
- package/prisma/migrations/20260427010000_hash_user_api_keys/migration.sql +10 -0
- package/prisma/migrations/20260427020000_nullable_user_password_hash/migration.sql +39 -0
- package/prisma/migrations/20260517000000_add_op_run_tokens/migration.sql +2 -0
- package/prisma/schema.prisma +4 -2
- package/client-dist/assets/index-45dVo30p.css +0 -1
- package/client-dist/assets/index-C9uuPHLH.js +0 -168
- package/dist/auth-middleware.js +0 -203
- package/dist/userService.js +0 -118
- /package/bin/{naisys-erp → naisys-erp.js} +0 -0
- /package/dist/{supervisorAuth.js → middleware/supervisorAuth.js} +0 -0
- /package/dist/{audit.js → services/audit.js} +0 -0
package/dist/routes/{operation-run-transitions.js → operations/operation-run-transitions.js}
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ErrorResponseSchema, OperationRunStatus, OperationRunTransitionSlimSchema, OrderRunStatus, TransitionNoteSchema, } from "@naisys/erp-shared";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveOpRun, } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
193
|
-
const opRun = await transitionStatus(resolved.opRun.id, "fail", OperationRunStatus.in_progress, OperationRunStatus.failed, userId, {
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { checkOrderRunStarted, checkWorkCenterAccess, childItemLinks, formatAuditFields, formatDate, resolveActions, resolveOpRun, resolveOrderRun, useFullSerializer, wantsFullResponse, } from "
|
|
7
|
-
import { checkStepsComplete, getOpRun, getOpRunFieldRefSummary, getOpRunStepSummary, listOpRuns, updateOpRun, validateStatusFor, } from "
|
|
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
|
|
15
|
-
|
|
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: [
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { calcNextSeqNo, childItemLinks, draftCrudActions, formatAuditFields, mutationResult, permGate, resolveRevision, } from "
|
|
7
|
-
import { createOperation, deleteOperation, findExisting, getOperation, listOperations, updateOperation, } from "
|
|
8
|
-
import { findWorkCenterByKey } from "
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
import { resolveOrder, useFullSerializer, wantsFullResponse, } from "
|
|
5
|
-
import { findExisting, transitionStatus, } from "
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { childItemLinks, formatAuditFields, mutationResult, permGate, resolveActions, resolveOrder, } from "
|
|
7
|
-
import { checkHasOrderRuns, createRevision, deleteRevision, findExisting, getRevision, getRevisionOpSummary, listRevisions, updateRevision, validateDraftStatus, } from "
|
|
8
|
-
import { diffRevisions } from "
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
import { resolveOrderRun, useFullSerializer, wantsFullResponse, } from "
|
|
5
|
-
import { checkOpsComplete, completeOrderRun, getReopenTarget, sumOpRunCosts, transitionStatus, validateStatusFor, } from "
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { childItemLinks, formatAuditFields, mutationResult, resolveActions, resolveOrder, resolveOrderRun, } from "
|
|
7
|
-
import { checkOpsComplete, createOrderRun, deleteOrderRun, findLatestApprovedRevision, findOrderRevision, getOrderRun, getOrderRunOpSummary, listOrderRuns, updateOrderRun, validateStatusFor, } from "
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { formatAuditFields, mutationResult, permGate, resolveActions, } from "
|
|
7
|
-
import { checkHasRevisions, createOrder, deleteOrder, findExisting, listOrders, resolveItemKey, updateOrder, } from "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 "
|
|
3
|
-
import { API_PREFIX, paginationLinks } from "
|
|
4
|
-
import { getUserWorkCenterIds } from "
|
|
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 "
|
|
3
|
-
import { API_PREFIX, paginationLinks } from "
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { checkOpRunInProgress, formatAuditFields, formatDate, mutationResult, permGate, resolveOpRun, } from "
|
|
7
|
-
import { clockIn, clockOut, deleteLaborTicket, listLaborTickets, } from "
|
|
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}`),
|