@naisys/erp 3.0.0-beta.3
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.
Potentially problematic release.
This version of @naisys/erp might be problematic. Click here for more details.
- package/bin/naisys-erp +2 -0
- package/client-dist/android-chrome-192x192.png +0 -0
- package/client-dist/android-chrome-512x512.png +0 -0
- package/client-dist/apple-touch-icon.png +0 -0
- package/client-dist/assets/index-45dVo30p.css +1 -0
- package/client-dist/assets/index-Dffms7F_.js +168 -0
- package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +42 -0
- package/client-dist/site.webmanifest +22 -0
- package/dist/api-reference.d.ts +10 -0
- package/dist/api-reference.js +101 -0
- package/dist/audit.d.ts +5 -0
- package/dist/audit.js +14 -0
- package/dist/auth-middleware.d.ts +18 -0
- package/dist/auth-middleware.js +203 -0
- package/dist/dbConfig.d.ts +5 -0
- package/dist/dbConfig.js +10 -0
- package/dist/erpDb.d.ts +10 -0
- package/dist/erpDb.js +34 -0
- package/dist/erpServer.d.ts +10 -0
- package/dist/erpServer.js +321 -0
- package/dist/error-handler.d.ts +7 -0
- package/dist/error-handler.js +17 -0
- package/dist/generated/prisma/client.d.ts +154 -0
- package/dist/generated/prisma/client.js +35 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +637 -0
- package/dist/generated/prisma/commonInputTypes.js +11 -0
- package/dist/generated/prisma/enums.d.ts +59 -0
- package/dist/generated/prisma/enums.js +60 -0
- package/dist/generated/prisma/internal/class.d.ts +406 -0
- package/dist/generated/prisma/internal/class.js +50 -0
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +2722 -0
- package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
- package/dist/generated/prisma/models/Attachment.d.ts +1455 -0
- package/dist/generated/prisma/models/Attachment.js +2 -0
- package/dist/generated/prisma/models/AuditLog.d.ts +1359 -0
- package/dist/generated/prisma/models/AuditLog.js +2 -0
- package/dist/generated/prisma/models/Field.d.ts +1880 -0
- package/dist/generated/prisma/models/Field.js +2 -0
- package/dist/generated/prisma/models/FieldAttachment.d.ts +1245 -0
- package/dist/generated/prisma/models/FieldAttachment.js +2 -0
- package/dist/generated/prisma/models/FieldRecord.d.ts +1625 -0
- package/dist/generated/prisma/models/FieldRecord.js +2 -0
- package/dist/generated/prisma/models/FieldSet.d.ts +1577 -0
- package/dist/generated/prisma/models/FieldSet.js +2 -0
- package/dist/generated/prisma/models/FieldValue.d.ts +1908 -0
- package/dist/generated/prisma/models/FieldValue.js +2 -0
- package/dist/generated/prisma/models/Item.d.ts +1858 -0
- package/dist/generated/prisma/models/Item.js +2 -0
- package/dist/generated/prisma/models/ItemInstance.d.ts +1987 -0
- package/dist/generated/prisma/models/ItemInstance.js +2 -0
- package/dist/generated/prisma/models/LaborTicket.d.ts +1867 -0
- package/dist/generated/prisma/models/LaborTicket.js +2 -0
- package/dist/generated/prisma/models/Operation.d.ts +2578 -0
- package/dist/generated/prisma/models/Operation.js +2 -0
- package/dist/generated/prisma/models/OperationDependency.d.ts +1434 -0
- package/dist/generated/prisma/models/OperationDependency.js +2 -0
- package/dist/generated/prisma/models/OperationFieldRef.d.ts +1539 -0
- package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
- package/dist/generated/prisma/models/OperationRun.d.ts +2563 -0
- package/dist/generated/prisma/models/OperationRun.js +2 -0
- package/dist/generated/prisma/models/OperationRunComment.d.ts +1366 -0
- package/dist/generated/prisma/models/OperationRunComment.js +2 -0
- package/dist/generated/prisma/models/Order.d.ts +1931 -0
- package/dist/generated/prisma/models/Order.js +2 -0
- package/dist/generated/prisma/models/OrderRevision.d.ts +1962 -0
- package/dist/generated/prisma/models/OrderRevision.js +2 -0
- package/dist/generated/prisma/models/OrderRun.d.ts +2310 -0
- package/dist/generated/prisma/models/OrderRun.js +2 -0
- package/dist/generated/prisma/models/SchemaVersion.d.ts +985 -0
- package/dist/generated/prisma/models/SchemaVersion.js +2 -0
- package/dist/generated/prisma/models/Session.d.ts +1213 -0
- package/dist/generated/prisma/models/Session.js +2 -0
- package/dist/generated/prisma/models/Step.d.ts +2180 -0
- package/dist/generated/prisma/models/Step.js +2 -0
- package/dist/generated/prisma/models/StepRun.d.ts +1963 -0
- package/dist/generated/prisma/models/StepRun.js +2 -0
- package/dist/generated/prisma/models/User.d.ts +11819 -0
- package/dist/generated/prisma/models/User.js +2 -0
- package/dist/generated/prisma/models/UserPermission.d.ts +1348 -0
- package/dist/generated/prisma/models/UserPermission.js +2 -0
- package/dist/generated/prisma/models/WorkCenter.d.ts +1657 -0
- package/dist/generated/prisma/models/WorkCenter.js +2 -0
- package/dist/generated/prisma/models/WorkCenterUser.d.ts +1390 -0
- package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
- package/dist/generated/prisma/models.d.ts +28 -0
- package/dist/generated/prisma/models.js +2 -0
- package/dist/hateoas.d.ts +7 -0
- package/dist/hateoas.js +61 -0
- package/dist/route-helpers.d.ts +318 -0
- package/dist/route-helpers.js +220 -0
- package/dist/routes/admin.d.ts +3 -0
- package/dist/routes/admin.js +147 -0
- package/dist/routes/audit.d.ts +3 -0
- package/dist/routes/audit.js +36 -0
- package/dist/routes/auth.d.ts +3 -0
- package/dist/routes/auth.js +112 -0
- package/dist/routes/dispatch.d.ts +3 -0
- package/dist/routes/dispatch.js +174 -0
- package/dist/routes/inventory.d.ts +3 -0
- package/dist/routes/inventory.js +70 -0
- package/dist/routes/item-fields.d.ts +3 -0
- package/dist/routes/item-fields.js +220 -0
- package/dist/routes/item-instances.d.ts +3 -0
- package/dist/routes/item-instances.js +426 -0
- package/dist/routes/items.d.ts +3 -0
- package/dist/routes/items.js +252 -0
- package/dist/routes/labor-tickets.d.ts +3 -0
- package/dist/routes/labor-tickets.js +268 -0
- package/dist/routes/operation-dependencies.d.ts +3 -0
- package/dist/routes/operation-dependencies.js +170 -0
- package/dist/routes/operation-field-refs.d.ts +3 -0
- package/dist/routes/operation-field-refs.js +263 -0
- package/dist/routes/operation-run-comments.d.ts +3 -0
- package/dist/routes/operation-run-comments.js +108 -0
- package/dist/routes/operation-run-transitions.d.ts +3 -0
- package/dist/routes/operation-run-transitions.js +249 -0
- package/dist/routes/operation-runs.d.ts +112 -0
- package/dist/routes/operation-runs.js +299 -0
- package/dist/routes/operations.d.ts +3 -0
- package/dist/routes/operations.js +283 -0
- package/dist/routes/order-revision-transitions.d.ts +3 -0
- package/dist/routes/order-revision-transitions.js +86 -0
- package/dist/routes/order-revisions.d.ts +51 -0
- package/dist/routes/order-revisions.js +327 -0
- package/dist/routes/order-run-transitions.d.ts +3 -0
- package/dist/routes/order-run-transitions.js +215 -0
- package/dist/routes/order-runs.d.ts +58 -0
- package/dist/routes/order-runs.js +335 -0
- package/dist/routes/orders.d.ts +3 -0
- package/dist/routes/orders.js +262 -0
- package/dist/routes/root.d.ts +3 -0
- package/dist/routes/root.js +123 -0
- package/dist/routes/schemas.d.ts +3 -0
- package/dist/routes/schemas.js +31 -0
- package/dist/routes/step-field-attachments.d.ts +3 -0
- package/dist/routes/step-field-attachments.js +231 -0
- package/dist/routes/step-fields.d.ts +100 -0
- package/dist/routes/step-fields.js +315 -0
- package/dist/routes/step-run-fields.d.ts +3 -0
- package/dist/routes/step-run-fields.js +438 -0
- package/dist/routes/step-run-transitions.d.ts +3 -0
- package/dist/routes/step-run-transitions.js +113 -0
- package/dist/routes/step-runs.d.ts +332 -0
- package/dist/routes/step-runs.js +324 -0
- package/dist/routes/steps.d.ts +3 -0
- package/dist/routes/steps.js +283 -0
- package/dist/routes/user-permissions.d.ts +3 -0
- package/dist/routes/user-permissions.js +100 -0
- package/dist/routes/users.d.ts +57 -0
- package/dist/routes/users.js +381 -0
- package/dist/routes/work-centers.d.ts +3 -0
- package/dist/routes/work-centers.js +280 -0
- package/dist/schema-registry.d.ts +3 -0
- package/dist/schema-registry.js +45 -0
- package/dist/services/attachment-service.d.ts +33 -0
- package/dist/services/attachment-service.js +118 -0
- package/dist/services/field-ref-service.d.ts +96 -0
- package/dist/services/field-ref-service.js +74 -0
- package/dist/services/field-service.d.ts +49 -0
- package/dist/services/field-service.js +114 -0
- package/dist/services/field-value-service.d.ts +61 -0
- package/dist/services/field-value-service.js +256 -0
- package/dist/services/item-instance-service.d.ts +152 -0
- package/dist/services/item-instance-service.js +155 -0
- package/dist/services/item-service.d.ts +47 -0
- package/dist/services/item-service.js +56 -0
- package/dist/services/labor-ticket-service.d.ts +40 -0
- package/dist/services/labor-ticket-service.js +148 -0
- package/dist/services/log-file-service.d.ts +4 -0
- package/dist/services/log-file-service.js +11 -0
- package/dist/services/operation-dependency-service.d.ts +33 -0
- package/dist/services/operation-dependency-service.js +30 -0
- package/dist/services/operation-run-comment-service.d.ts +17 -0
- package/dist/services/operation-run-comment-service.js +26 -0
- package/dist/services/operation-run-service.d.ts +126 -0
- package/dist/services/operation-run-service.js +347 -0
- package/dist/services/operation-service.d.ts +47 -0
- package/dist/services/operation-service.js +132 -0
- package/dist/services/order-revision-service.d.ts +53 -0
- package/dist/services/order-revision-service.js +264 -0
- package/dist/services/order-run-service.d.ts +138 -0
- package/dist/services/order-run-service.js +356 -0
- package/dist/services/order-service.d.ts +15 -0
- package/dist/services/order-service.js +68 -0
- package/dist/services/revision-diff-service.d.ts +3 -0
- package/dist/services/revision-diff-service.js +194 -0
- package/dist/services/step-run-service.d.ts +172 -0
- package/dist/services/step-run-service.js +106 -0
- package/dist/services/step-service.d.ts +104 -0
- package/dist/services/step-service.js +89 -0
- package/dist/services/user-service.d.ts +185 -0
- package/dist/services/user-service.js +132 -0
- package/dist/services/work-center-service.d.ts +29 -0
- package/dist/services/work-center-service.js +106 -0
- package/dist/supervisorAuth.d.ts +3 -0
- package/dist/supervisorAuth.js +16 -0
- package/dist/userService.d.ts +20 -0
- package/dist/userService.js +118 -0
- package/package.json +69 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { BatchFieldValueMutateResponseSchema, BatchFieldValueUpdateResponseSchema, BatchUpdateFieldValuesSchema, DeleteSetMutateResponseSchema, ErrorResponseSchema, fieldTypeString, FieldValueMutateResponseSchema, getValueFormatHint, UpdateFieldValueSchema, } from "@naisys/erp-shared";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { requirePermission } from "../auth-middleware.js";
|
|
4
|
+
import erpDb from "../erpDb.js";
|
|
5
|
+
import { conflict, notFound, unprocessable } from "../error-handler.js";
|
|
6
|
+
import { API_PREFIX } from "../hateoas.js";
|
|
7
|
+
import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveStepRun, } from "../route-helpers.js";
|
|
8
|
+
import { ensureStepRunFieldRecord } from "../services/field-service.js";
|
|
9
|
+
import { checkFieldValueShape, clearAttachmentFieldValue, deleteFieldValueSet, deserializeFieldValue, findStepRunWithField, serializeFieldValue, upsertFieldValue, validateFieldValue, } from "../services/field-value-service.js";
|
|
10
|
+
import { isUserClockedIn } from "../services/labor-ticket-service.js";
|
|
11
|
+
import { getStepRunWithFields } from "../services/step-run-service.js";
|
|
12
|
+
import { computeStepRunHateoas, stepRunResource } from "./step-runs.js";
|
|
13
|
+
const FieldSeqNoParamsSchema = z.object({
|
|
14
|
+
orderKey: z.string(),
|
|
15
|
+
runNo: z.coerce.number().int(),
|
|
16
|
+
seqNo: z.coerce.number().int(),
|
|
17
|
+
stepSeqNo: z.coerce.number().int(),
|
|
18
|
+
fieldSeqNo: z.coerce.number().int(),
|
|
19
|
+
});
|
|
20
|
+
const SetFieldSeqNoParamsSchema = z.object({
|
|
21
|
+
orderKey: z.string(),
|
|
22
|
+
runNo: z.coerce.number().int(),
|
|
23
|
+
seqNo: z.coerce.number().int(),
|
|
24
|
+
stepSeqNo: z.coerce.number().int(),
|
|
25
|
+
setIndex: z.coerce.number().int().min(0),
|
|
26
|
+
fieldSeqNo: z.coerce.number().int(),
|
|
27
|
+
});
|
|
28
|
+
const StepSeqNoParamsSchema = z.object({
|
|
29
|
+
orderKey: z.string(),
|
|
30
|
+
runNo: z.coerce.number().int(),
|
|
31
|
+
seqNo: z.coerce.number().int(),
|
|
32
|
+
stepSeqNo: z.coerce.number().int(),
|
|
33
|
+
});
|
|
34
|
+
const SetIndexParamsSchema = z.object({
|
|
35
|
+
orderKey: z.string(),
|
|
36
|
+
runNo: z.coerce.number().int(),
|
|
37
|
+
seqNo: z.coerce.number().int(),
|
|
38
|
+
stepSeqNo: z.coerce.number().int(),
|
|
39
|
+
setIndex: z.coerce.number().int(),
|
|
40
|
+
});
|
|
41
|
+
export default function stepRunFieldRoutes(fastify) {
|
|
42
|
+
const app = fastify.withTypeProvider();
|
|
43
|
+
// Shared handler for updating a single field value
|
|
44
|
+
async function handleFieldUpdate(request, reply, setIndex) {
|
|
45
|
+
const { orderKey, runNo, seqNo, stepSeqNo, fieldSeqNo } = request.params;
|
|
46
|
+
const { value } = request.body;
|
|
47
|
+
const userId = request.erpUser.id;
|
|
48
|
+
const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
|
|
49
|
+
if (!resolved) {
|
|
50
|
+
return notFound(reply, `Step run not found`);
|
|
51
|
+
}
|
|
52
|
+
const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
|
|
53
|
+
if (wcErr)
|
|
54
|
+
return conflict(reply, wcErr);
|
|
55
|
+
const orderErr = checkOrderRunStarted(resolved.run.status);
|
|
56
|
+
if (orderErr)
|
|
57
|
+
return conflict(reply, orderErr);
|
|
58
|
+
const opErr = checkOpRunInProgress(resolved.opRun.status);
|
|
59
|
+
if (opErr)
|
|
60
|
+
return conflict(reply, opErr);
|
|
61
|
+
const clockedIn = await isUserClockedIn(resolved.opRun.id, userId);
|
|
62
|
+
if (!clockedIn)
|
|
63
|
+
return conflict(reply, `You must be clocked in to update field values`);
|
|
64
|
+
const stepRun = await findStepRunWithField(resolved.stepRun.id, resolved.opRun.id, fieldSeqNo);
|
|
65
|
+
if (!stepRun)
|
|
66
|
+
return notFound(reply, `Step run not found`);
|
|
67
|
+
if (stepRun.completed) {
|
|
68
|
+
return conflict(reply, `Cannot update field: step run is completed`);
|
|
69
|
+
}
|
|
70
|
+
const field = stepRun.step.fieldSet?.fields[0];
|
|
71
|
+
if (!field) {
|
|
72
|
+
return notFound(reply, `Step field not found`);
|
|
73
|
+
}
|
|
74
|
+
const shapeErr = checkFieldValueShape(field.label, field.type, field.isArray, value);
|
|
75
|
+
if (shapeErr)
|
|
76
|
+
return unprocessable(reply, shapeErr);
|
|
77
|
+
// Reject setIndex > 0 on non-multiSet steps
|
|
78
|
+
if (setIndex > 0 && !stepRun.step.multiSet) {
|
|
79
|
+
return unprocessable(reply, `setIndex > 0 is only allowed on multi-set steps. For multi-value fields, pass an array of strings instead.`);
|
|
80
|
+
}
|
|
81
|
+
// Block explicit value setting for attachment fields (managed by uploads)
|
|
82
|
+
if (field.type === "attachment") {
|
|
83
|
+
const isEmpty = Array.isArray(value)
|
|
84
|
+
? value.every((v) => !v.trim())
|
|
85
|
+
: !value.trim();
|
|
86
|
+
if (!isEmpty) {
|
|
87
|
+
return reply.code(400).send({
|
|
88
|
+
statusCode: 400,
|
|
89
|
+
error: "Bad Request",
|
|
90
|
+
message: "Attachment field values are managed by file uploads. " +
|
|
91
|
+
"Use the upload endpoint to add files, or set an empty value to clear.",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const fieldRecordId = await ensureStepRunFieldRecord(resolved.stepRun.id, userId);
|
|
96
|
+
if (!fieldRecordId)
|
|
97
|
+
return notFound(reply, "Step has no field set");
|
|
98
|
+
if (field.type === "attachment") {
|
|
99
|
+
await clearAttachmentFieldValue(fieldRecordId, field.id, setIndex, userId);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
await upsertFieldValue(fieldRecordId, field.id, setIndex, value, userId);
|
|
103
|
+
}
|
|
104
|
+
// Return deserialized value + updated step-level actions
|
|
105
|
+
const responseValue = field.type === "attachment"
|
|
106
|
+
? field.isArray
|
|
107
|
+
? []
|
|
108
|
+
: ""
|
|
109
|
+
: deserializeFieldValue(serializeFieldValue(value), field.isArray);
|
|
110
|
+
// Check ALL fields in the fieldSet (not just the filtered single field)
|
|
111
|
+
// so that _actionTemplates include the right hints
|
|
112
|
+
const [attachmentCount, arrayCount] = await Promise.all([
|
|
113
|
+
erpDb.field.count({
|
|
114
|
+
where: { fieldSetId: field.fieldSetId, type: "attachment" },
|
|
115
|
+
}),
|
|
116
|
+
erpDb.field.count({
|
|
117
|
+
where: { fieldSetId: field.fieldSetId, isArray: true },
|
|
118
|
+
}),
|
|
119
|
+
]);
|
|
120
|
+
const hateoas = await computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, stepRun.completed, resolved.stepRun.id, stepRun.step.multiSet, attachmentCount > 0, arrayCount > 0, request.erpUser);
|
|
121
|
+
const validation = validateFieldValue(field.type, field.isArray, field.required, responseValue);
|
|
122
|
+
const fieldType = fieldTypeString(field.type, field.isArray);
|
|
123
|
+
const full = {
|
|
124
|
+
fieldId: field.id,
|
|
125
|
+
fieldSeqNo: field.seqNo,
|
|
126
|
+
label: field.label,
|
|
127
|
+
type: fieldType,
|
|
128
|
+
valueFormat: getValueFormatHint(fieldType),
|
|
129
|
+
required: field.required,
|
|
130
|
+
setIndex,
|
|
131
|
+
value: responseValue,
|
|
132
|
+
validation,
|
|
133
|
+
...hateoas,
|
|
134
|
+
};
|
|
135
|
+
return mutationResult(request, reply, full, {
|
|
136
|
+
value: responseValue,
|
|
137
|
+
validation,
|
|
138
|
+
_actions: hateoas._actions,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// UPDATE single field value (non-multiSet shorthand — implicit set 0)
|
|
142
|
+
app.put("/:stepSeqNo/fields/:fieldSeqNo", {
|
|
143
|
+
schema: {
|
|
144
|
+
description: "Update a single field value on a step run (implicit set 0). " +
|
|
145
|
+
"For multi-set steps, use /sets/{setIndex}/fields/{fieldSeqNo} instead.",
|
|
146
|
+
tags: ["Step Runs"],
|
|
147
|
+
params: FieldSeqNoParamsSchema,
|
|
148
|
+
body: UpdateFieldValueSchema,
|
|
149
|
+
response: {
|
|
150
|
+
200: FieldValueMutateResponseSchema,
|
|
151
|
+
404: ErrorResponseSchema,
|
|
152
|
+
409: ErrorResponseSchema,
|
|
153
|
+
422: ErrorResponseSchema,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
preHandler: requirePermission("order_executor"),
|
|
157
|
+
handler: async (request, reply) => handleFieldUpdate(request, reply, 0),
|
|
158
|
+
});
|
|
159
|
+
// UPDATE single field value (explicit set index for multi-set steps)
|
|
160
|
+
app.put("/:stepSeqNo/sets/:setIndex/fields/:fieldSeqNo", {
|
|
161
|
+
schema: {
|
|
162
|
+
description: "Update a single field value on a specific set of a multi-set step run",
|
|
163
|
+
tags: ["Step Runs"],
|
|
164
|
+
params: SetFieldSeqNoParamsSchema,
|
|
165
|
+
body: UpdateFieldValueSchema,
|
|
166
|
+
response: {
|
|
167
|
+
200: FieldValueMutateResponseSchema,
|
|
168
|
+
404: ErrorResponseSchema,
|
|
169
|
+
409: ErrorResponseSchema,
|
|
170
|
+
422: ErrorResponseSchema,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
preHandler: requirePermission("order_executor"),
|
|
174
|
+
handler: async (request, reply) => handleFieldUpdate(request, reply, request.params.setIndex),
|
|
175
|
+
});
|
|
176
|
+
// Shared handler for batch updating field values
|
|
177
|
+
async function handleBatchFieldUpdate(request, reply, setIndex) {
|
|
178
|
+
const { orderKey, runNo, seqNo, stepSeqNo } = request.params;
|
|
179
|
+
const { fieldValues } = request.body;
|
|
180
|
+
const userId = request.erpUser.id;
|
|
181
|
+
const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
|
|
182
|
+
if (!resolved) {
|
|
183
|
+
return notFound(reply, `Step run not found`);
|
|
184
|
+
}
|
|
185
|
+
const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
|
|
186
|
+
if (wcErr)
|
|
187
|
+
return conflict(reply, wcErr);
|
|
188
|
+
const orderErr = checkOrderRunStarted(resolved.run.status);
|
|
189
|
+
if (orderErr)
|
|
190
|
+
return conflict(reply, orderErr);
|
|
191
|
+
const opErr = checkOpRunInProgress(resolved.opRun.status);
|
|
192
|
+
if (opErr)
|
|
193
|
+
return conflict(reply, opErr);
|
|
194
|
+
const clockedIn = await isUserClockedIn(resolved.opRun.id, userId);
|
|
195
|
+
if (!clockedIn)
|
|
196
|
+
return conflict(reply, `You must be clocked in to update field values`);
|
|
197
|
+
const existing = await getStepRunWithFields(resolved.stepRun.id);
|
|
198
|
+
if (!existing)
|
|
199
|
+
return notFound(reply, `Step run not found`);
|
|
200
|
+
if (existing.completed) {
|
|
201
|
+
return conflict(reply, `Cannot update fields: step run is completed`);
|
|
202
|
+
}
|
|
203
|
+
// Reject setIndex > 0 on non-multiSet steps
|
|
204
|
+
if (setIndex > 0 && !existing.step.multiSet) {
|
|
205
|
+
return unprocessable(reply, `setIndex > 0 is only allowed on multi-set steps`);
|
|
206
|
+
}
|
|
207
|
+
// Build a map of fieldSeqNo -> field definition
|
|
208
|
+
const fieldDefs = new Map((existing.step.fieldSet?.fields ?? []).map((f) => [f.seqNo, f]));
|
|
209
|
+
// Validate all fieldSeqNos exist, block attachments, enforce array shape
|
|
210
|
+
for (const item of fieldValues) {
|
|
211
|
+
if (!fieldDefs.has(item.fieldSeqNo)) {
|
|
212
|
+
return notFound(reply, `Step field ${item.fieldSeqNo} not found`);
|
|
213
|
+
}
|
|
214
|
+
const def = fieldDefs.get(item.fieldSeqNo);
|
|
215
|
+
if (def.type === "attachment") {
|
|
216
|
+
return reply.code(400).send({
|
|
217
|
+
statusCode: 400,
|
|
218
|
+
error: "Bad Request",
|
|
219
|
+
message: `Field "${def.label}" is an attachment field. Attachment values are managed by file uploads, not batch updates.`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const shapeErr = checkFieldValueShape(def.label, def.type, def.isArray, item.value);
|
|
223
|
+
if (shapeErr)
|
|
224
|
+
return unprocessable(reply, shapeErr);
|
|
225
|
+
}
|
|
226
|
+
const fieldRecordId = await ensureStepRunFieldRecord(resolved.stepRun.id, userId);
|
|
227
|
+
if (!fieldRecordId)
|
|
228
|
+
return notFound(reply, "Step has no field set");
|
|
229
|
+
// Upsert all field values
|
|
230
|
+
const results = [];
|
|
231
|
+
for (const item of fieldValues) {
|
|
232
|
+
const field = fieldDefs.get(item.fieldSeqNo);
|
|
233
|
+
await upsertFieldValue(fieldRecordId, field.id, setIndex, item.value, userId);
|
|
234
|
+
const responseValue = deserializeFieldValue(serializeFieldValue(item.value), field.isArray);
|
|
235
|
+
const fieldType = fieldTypeString(field.type, field.isArray);
|
|
236
|
+
results.push({
|
|
237
|
+
fieldId: field.id,
|
|
238
|
+
fieldSeqNo: field.seqNo,
|
|
239
|
+
label: field.label,
|
|
240
|
+
type: fieldType,
|
|
241
|
+
valueFormat: getValueFormatHint(fieldType),
|
|
242
|
+
required: field.required,
|
|
243
|
+
setIndex,
|
|
244
|
+
value: responseValue,
|
|
245
|
+
validation: validateFieldValue(field.type, field.isArray, field.required, responseValue),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const allFields = existing.step.fieldSet?.fields ?? [];
|
|
249
|
+
const hasAttachmentFields = allFields.some((f) => f.type === "attachment");
|
|
250
|
+
const hasArrayFields = allFields.some((f) => f.isArray);
|
|
251
|
+
const hateoas = await computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, existing.completed, resolved.stepRun.id, existing.step.multiSet, hasAttachmentFields, hasArrayFields, request.erpUser);
|
|
252
|
+
const full = { items: results, total: results.length, ...hateoas };
|
|
253
|
+
return mutationResult(request, reply, full, {
|
|
254
|
+
items: results.map((r) => ({
|
|
255
|
+
fieldSeqNo: r.fieldSeqNo,
|
|
256
|
+
value: r.value,
|
|
257
|
+
validation: r.validation,
|
|
258
|
+
})),
|
|
259
|
+
total: results.length,
|
|
260
|
+
_actions: hateoas._actions,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// Shared handler for batch reading field values
|
|
264
|
+
async function handleBatchFieldGet(request, reply, setIndex) {
|
|
265
|
+
const { orderKey, runNo, seqNo, stepSeqNo } = request.params;
|
|
266
|
+
const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
|
|
267
|
+
if (!resolved) {
|
|
268
|
+
return notFound(reply, `Step run not found`);
|
|
269
|
+
}
|
|
270
|
+
const existing = await getStepRunWithFields(resolved.stepRun.id);
|
|
271
|
+
if (!existing)
|
|
272
|
+
return notFound(reply, `Step run not found`);
|
|
273
|
+
const storedFieldValues = existing.fieldRecord?.fieldValues ?? [];
|
|
274
|
+
const maxSetIndex = storedFieldValues.reduce((max, fv) => Math.max(max, fv.setIndex), -1);
|
|
275
|
+
const totalSets = Math.max(1, maxSetIndex + 1);
|
|
276
|
+
const startSet = setIndex ?? 0;
|
|
277
|
+
const endSet = setIndex !== undefined ? setIndex + 1 : totalSets;
|
|
278
|
+
const stepRunHref = `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/${stepSeqNo}`;
|
|
279
|
+
const isMultiSet = existing.step.multiSet;
|
|
280
|
+
const items = [];
|
|
281
|
+
for (let si = startSet; si < endSet; si++) {
|
|
282
|
+
for (const field of existing.step.fieldSet?.fields ?? []) {
|
|
283
|
+
const stored = storedFieldValues.find((fv) => fv.fieldId === field.id && fv.setIndex === si);
|
|
284
|
+
const value = deserializeFieldValue(stored?.value ?? "", field.isArray);
|
|
285
|
+
const setPath = isMultiSet
|
|
286
|
+
? `/sets/${si}/fields/${field.seqNo}`
|
|
287
|
+
: `/fields/${field.seqNo}`;
|
|
288
|
+
const attachments = field.type === "attachment" && stored
|
|
289
|
+
? stored.fieldAttachments.map((sfa) => ({
|
|
290
|
+
id: sfa.attachment.publicId,
|
|
291
|
+
filename: sfa.attachment.filename,
|
|
292
|
+
fileSize: sfa.attachment.fileSize,
|
|
293
|
+
downloadHref: `${stepRunHref}${setPath}/attachments/${sfa.attachment.publicId}`,
|
|
294
|
+
}))
|
|
295
|
+
: undefined;
|
|
296
|
+
const fieldType = fieldTypeString(field.type, field.isArray);
|
|
297
|
+
items.push({
|
|
298
|
+
fieldId: field.id,
|
|
299
|
+
fieldSeqNo: field.seqNo,
|
|
300
|
+
label: field.label,
|
|
301
|
+
type: fieldType,
|
|
302
|
+
valueFormat: getValueFormatHint(fieldType),
|
|
303
|
+
required: field.required,
|
|
304
|
+
setIndex: si,
|
|
305
|
+
value,
|
|
306
|
+
attachments,
|
|
307
|
+
validation: validateFieldValue(field.type, field.isArray, field.required, value),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const allFields2 = existing.step.fieldSet?.fields ?? [];
|
|
312
|
+
const hasAttachmentFields = allFields2.some((f) => f.type === "attachment");
|
|
313
|
+
const hasArrayFields = allFields2.some((f) => f.isArray);
|
|
314
|
+
const hateoas = await computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, existing.completed, resolved.stepRun.id, existing.step.multiSet, hasAttachmentFields, hasArrayFields, request.erpUser);
|
|
315
|
+
return { items, total: items.length, ...hateoas };
|
|
316
|
+
}
|
|
317
|
+
// BATCH GET field values (non-multiSet shorthand — all sets)
|
|
318
|
+
app.get("/:stepSeqNo/fields", {
|
|
319
|
+
schema: {
|
|
320
|
+
description: "Get all field values on a step run. " +
|
|
321
|
+
"For multi-set steps, use /sets/{setIndex}/fields to get a specific set.",
|
|
322
|
+
tags: ["Step Runs"],
|
|
323
|
+
params: StepSeqNoParamsSchema,
|
|
324
|
+
response: {
|
|
325
|
+
200: BatchFieldValueUpdateResponseSchema,
|
|
326
|
+
404: ErrorResponseSchema,
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
handler: async (request, reply) => handleBatchFieldGet(request, reply),
|
|
330
|
+
});
|
|
331
|
+
// BATCH GET field values (explicit set index for multi-set steps)
|
|
332
|
+
app.get("/:stepSeqNo/sets/:setIndex/fields", {
|
|
333
|
+
schema: {
|
|
334
|
+
description: "Get field values for a specific set of a multi-set step run",
|
|
335
|
+
tags: ["Step Runs"],
|
|
336
|
+
params: SetFieldSeqNoParamsSchema.omit({ fieldSeqNo: true }),
|
|
337
|
+
response: {
|
|
338
|
+
200: BatchFieldValueUpdateResponseSchema,
|
|
339
|
+
404: ErrorResponseSchema,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
handler: async (request, reply) => handleBatchFieldGet(request, reply, request.params.setIndex),
|
|
343
|
+
});
|
|
344
|
+
// BATCH UPDATE field values (non-multiSet shorthand — implicit set 0)
|
|
345
|
+
app.put("/:stepSeqNo/fields", {
|
|
346
|
+
schema: {
|
|
347
|
+
description: "Batch update field values on a step run (implicit set 0). " +
|
|
348
|
+
"For multi-set steps, use /sets/{setIndex}/fields instead.",
|
|
349
|
+
tags: ["Step Runs"],
|
|
350
|
+
params: StepSeqNoParamsSchema,
|
|
351
|
+
body: BatchUpdateFieldValuesSchema,
|
|
352
|
+
response: {
|
|
353
|
+
200: BatchFieldValueMutateResponseSchema,
|
|
354
|
+
404: ErrorResponseSchema,
|
|
355
|
+
409: ErrorResponseSchema,
|
|
356
|
+
422: ErrorResponseSchema,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
preHandler: requirePermission("order_executor"),
|
|
360
|
+
handler: async (request, reply) => handleBatchFieldUpdate(request, reply, 0),
|
|
361
|
+
});
|
|
362
|
+
// BATCH UPDATE field values (explicit set index for multi-set steps)
|
|
363
|
+
app.put("/:stepSeqNo/sets/:setIndex/fields", {
|
|
364
|
+
schema: {
|
|
365
|
+
description: "Batch update field values on a specific set of a multi-set step run",
|
|
366
|
+
tags: ["Step Runs"],
|
|
367
|
+
params: SetFieldSeqNoParamsSchema.omit({ fieldSeqNo: true }),
|
|
368
|
+
body: BatchUpdateFieldValuesSchema,
|
|
369
|
+
response: {
|
|
370
|
+
200: BatchFieldValueMutateResponseSchema,
|
|
371
|
+
404: ErrorResponseSchema,
|
|
372
|
+
409: ErrorResponseSchema,
|
|
373
|
+
422: ErrorResponseSchema,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
preHandler: requirePermission("order_executor"),
|
|
377
|
+
handler: async (request, reply) => handleBatchFieldUpdate(request, reply, request.params.setIndex),
|
|
378
|
+
});
|
|
379
|
+
// DELETE a field value set
|
|
380
|
+
app.delete("/:stepSeqNo/sets/:setIndex", {
|
|
381
|
+
schema: {
|
|
382
|
+
description: "Delete all field values for a set and re-index remaining sets",
|
|
383
|
+
tags: ["Step Runs"],
|
|
384
|
+
params: SetIndexParamsSchema,
|
|
385
|
+
response: {
|
|
386
|
+
200: DeleteSetMutateResponseSchema,
|
|
387
|
+
404: ErrorResponseSchema,
|
|
388
|
+
409: ErrorResponseSchema,
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
preHandler: requirePermission("order_executor"),
|
|
392
|
+
handler: async (request, reply) => {
|
|
393
|
+
const { orderKey, runNo, seqNo, stepSeqNo, setIndex } = request.params;
|
|
394
|
+
const userId = request.erpUser.id;
|
|
395
|
+
const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
|
|
396
|
+
if (!resolved) {
|
|
397
|
+
return notFound(reply, `Step run not found`);
|
|
398
|
+
}
|
|
399
|
+
const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
|
|
400
|
+
if (wcErr)
|
|
401
|
+
return conflict(reply, wcErr);
|
|
402
|
+
const orderErr = checkOrderRunStarted(resolved.run.status);
|
|
403
|
+
if (orderErr)
|
|
404
|
+
return conflict(reply, orderErr);
|
|
405
|
+
const opErr = checkOpRunInProgress(resolved.opRun.status);
|
|
406
|
+
if (opErr)
|
|
407
|
+
return conflict(reply, opErr);
|
|
408
|
+
const clockedIn = await isUserClockedIn(resolved.opRun.id, userId);
|
|
409
|
+
if (!clockedIn)
|
|
410
|
+
return conflict(reply, `You must be clocked in to delete sets`);
|
|
411
|
+
const existing = await getStepRunWithFields(resolved.stepRun.id);
|
|
412
|
+
if (!existing)
|
|
413
|
+
return notFound(reply, `Step run not found`);
|
|
414
|
+
if (existing.completed) {
|
|
415
|
+
return conflict(reply, `Cannot delete set: step run is completed`);
|
|
416
|
+
}
|
|
417
|
+
if (!existing.fieldRecord) {
|
|
418
|
+
return notFound(reply, "No field values to delete");
|
|
419
|
+
}
|
|
420
|
+
await deleteFieldValueSet(existing.fieldRecord.id, setIndex);
|
|
421
|
+
// Compute new set count from remaining field values
|
|
422
|
+
const updated = await getStepRunWithFields(resolved.stepRun.id);
|
|
423
|
+
const storedFieldValues = updated?.fieldRecord?.fieldValues ?? [];
|
|
424
|
+
const maxSetIndex = storedFieldValues.reduce((max, fv) => Math.max(max, fv.setIndex), -1);
|
|
425
|
+
const setCount = Math.max(1, maxSetIndex + 1);
|
|
426
|
+
const delFields = existing.step.fieldSet?.fields ?? [];
|
|
427
|
+
const hasAttachmentFields = delFields.some((f) => f.type === "attachment");
|
|
428
|
+
const hasArrayFields = delFields.some((f) => f.isArray);
|
|
429
|
+
const hateoas = await computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, existing.completed, resolved.stepRun.id, existing.step.multiSet, hasAttachmentFields, hasArrayFields, request.erpUser);
|
|
430
|
+
const full = { setCount, ...hateoas };
|
|
431
|
+
return mutationResult(request, reply, full, {
|
|
432
|
+
setCount,
|
|
433
|
+
_actions: hateoas._actions,
|
|
434
|
+
});
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
//# sourceMappingURL=step-run-fields.js.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ErrorResponseSchema, StepRunTransitionSlimSchema, TransitionNoteSchema, } from "@naisys/erp-shared";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { requirePermission } from "../auth-middleware.js";
|
|
4
|
+
import { conflict, notFound, unprocessable } from "../error-handler.js";
|
|
5
|
+
import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveStepRun, } from "../route-helpers.js";
|
|
6
|
+
import { validateCompletionFields } from "../services/field-value-service.js";
|
|
7
|
+
import { isUserClockedIn } from "../services/labor-ticket-service.js";
|
|
8
|
+
import { getStepRunWithFields, updateStepRun, } from "../services/step-run-service.js";
|
|
9
|
+
import { formatStepRunTransition } from "./step-runs.js";
|
|
10
|
+
const StepSeqNoParamsSchema = z.object({
|
|
11
|
+
orderKey: z.string(),
|
|
12
|
+
runNo: z.coerce.number().int(),
|
|
13
|
+
seqNo: z.coerce.number().int(),
|
|
14
|
+
stepSeqNo: z.coerce.number().int(),
|
|
15
|
+
});
|
|
16
|
+
export default function stepRunTransitionRoutes(fastify) {
|
|
17
|
+
const app = fastify.withTypeProvider();
|
|
18
|
+
// COMPLETE (not completed → completed)
|
|
19
|
+
app.post("/:stepSeqNo/complete", {
|
|
20
|
+
schema: {
|
|
21
|
+
description: "Complete a step run (operation run must be in_progress)",
|
|
22
|
+
tags: ["Step Runs"],
|
|
23
|
+
params: StepSeqNoParamsSchema,
|
|
24
|
+
body: TransitionNoteSchema,
|
|
25
|
+
response: {
|
|
26
|
+
200: StepRunTransitionSlimSchema,
|
|
27
|
+
404: ErrorResponseSchema,
|
|
28
|
+
409: ErrorResponseSchema,
|
|
29
|
+
422: ErrorResponseSchema,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
preHandler: requirePermission("order_executor"),
|
|
33
|
+
handler: async (request, reply) => {
|
|
34
|
+
const { orderKey, runNo, seqNo, stepSeqNo } = request.params;
|
|
35
|
+
const { note } = request.body;
|
|
36
|
+
const userId = request.erpUser.id;
|
|
37
|
+
const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
|
|
38
|
+
if (!resolved)
|
|
39
|
+
return notFound(reply, `Step run not found`);
|
|
40
|
+
const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
|
|
41
|
+
if (wcErr)
|
|
42
|
+
return conflict(reply, wcErr);
|
|
43
|
+
const orderErr = checkOrderRunStarted(resolved.run.status);
|
|
44
|
+
if (orderErr)
|
|
45
|
+
return conflict(reply, orderErr);
|
|
46
|
+
const opErr = checkOpRunInProgress(resolved.opRun.status);
|
|
47
|
+
if (opErr)
|
|
48
|
+
return conflict(reply, opErr);
|
|
49
|
+
if (resolved.stepRun.completed) {
|
|
50
|
+
return conflict(reply, "Step is already completed");
|
|
51
|
+
}
|
|
52
|
+
const clockedIn = await isUserClockedIn(resolved.opRun.id, userId);
|
|
53
|
+
if (!clockedIn) {
|
|
54
|
+
return conflict(reply, "You must be clocked in to complete steps");
|
|
55
|
+
}
|
|
56
|
+
// Validate all stored field values
|
|
57
|
+
const existing = await getStepRunWithFields(resolved.stepRun.id);
|
|
58
|
+
if (!existing)
|
|
59
|
+
return notFound(reply, `Step run not found`);
|
|
60
|
+
const completionErr = validateCompletionFields(existing);
|
|
61
|
+
if (completionErr)
|
|
62
|
+
return unprocessable(reply, completionErr);
|
|
63
|
+
const stepRun = await updateStepRun(resolved.stepRun.id, true, note, userId);
|
|
64
|
+
const full = await formatStepRunTransition(orderKey, runNo, seqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, request.erpUser, stepRun);
|
|
65
|
+
return mutationResult(request, reply, full, {
|
|
66
|
+
completed: stepRun.completed,
|
|
67
|
+
_actions: full._actions,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// REOPEN (completed → not completed)
|
|
72
|
+
app.post("/:stepSeqNo/reopen", {
|
|
73
|
+
schema: {
|
|
74
|
+
description: "Reopen a completed step run",
|
|
75
|
+
tags: ["Step Runs"],
|
|
76
|
+
params: StepSeqNoParamsSchema,
|
|
77
|
+
body: TransitionNoteSchema,
|
|
78
|
+
response: {
|
|
79
|
+
200: StepRunTransitionSlimSchema,
|
|
80
|
+
404: ErrorResponseSchema,
|
|
81
|
+
409: ErrorResponseSchema,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
preHandler: requirePermission("order_executor"),
|
|
85
|
+
handler: async (request, reply) => {
|
|
86
|
+
const { orderKey, runNo, seqNo, stepSeqNo } = request.params;
|
|
87
|
+
const { note } = request.body;
|
|
88
|
+
const userId = request.erpUser.id;
|
|
89
|
+
const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
|
|
90
|
+
if (!resolved)
|
|
91
|
+
return notFound(reply, `Step run not found`);
|
|
92
|
+
const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
|
|
93
|
+
if (wcErr)
|
|
94
|
+
return conflict(reply, wcErr);
|
|
95
|
+
const orderErr = checkOrderRunStarted(resolved.run.status);
|
|
96
|
+
if (orderErr)
|
|
97
|
+
return conflict(reply, orderErr);
|
|
98
|
+
const opErr = checkOpRunInProgress(resolved.opRun.status);
|
|
99
|
+
if (opErr)
|
|
100
|
+
return conflict(reply, opErr);
|
|
101
|
+
if (!resolved.stepRun.completed) {
|
|
102
|
+
return conflict(reply, "Step is not completed");
|
|
103
|
+
}
|
|
104
|
+
const stepRun = await updateStepRun(resolved.stepRun.id, false, note, userId);
|
|
105
|
+
const full = await formatStepRunTransition(orderKey, runNo, seqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, request.erpUser, stepRun);
|
|
106
|
+
return mutationResult(request, reply, full, {
|
|
107
|
+
completed: stepRun.completed,
|
|
108
|
+
_actions: full._actions,
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=step-run-transitions.js.map
|