@naisys/erp 3.0.0-beta.6
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/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.js +101 -0
- package/dist/audit.js +14 -0
- package/dist/auth-middleware.js +203 -0
- package/dist/dbConfig.js +10 -0
- package/dist/erpDb.js +34 -0
- package/dist/erpServer.js +321 -0
- package/dist/error-handler.js +17 -0
- package/dist/generated/prisma/client.js +35 -0
- package/dist/generated/prisma/commonInputTypes.js +11 -0
- package/dist/generated/prisma/enums.js +60 -0
- package/dist/generated/prisma/internal/class.js +50 -0
- package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
- package/dist/generated/prisma/models/Attachment.js +2 -0
- package/dist/generated/prisma/models/AuditLog.js +2 -0
- package/dist/generated/prisma/models/Field.js +2 -0
- package/dist/generated/prisma/models/FieldAttachment.js +2 -0
- package/dist/generated/prisma/models/FieldRecord.js +2 -0
- package/dist/generated/prisma/models/FieldSet.js +2 -0
- package/dist/generated/prisma/models/FieldValue.js +2 -0
- package/dist/generated/prisma/models/Item.js +2 -0
- package/dist/generated/prisma/models/ItemInstance.js +2 -0
- package/dist/generated/prisma/models/LaborTicket.js +2 -0
- package/dist/generated/prisma/models/Operation.js +2 -0
- package/dist/generated/prisma/models/OperationDependency.js +2 -0
- package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
- package/dist/generated/prisma/models/OperationRun.js +2 -0
- package/dist/generated/prisma/models/OperationRunComment.js +2 -0
- package/dist/generated/prisma/models/Order.js +2 -0
- package/dist/generated/prisma/models/OrderRevision.js +2 -0
- package/dist/generated/prisma/models/OrderRun.js +2 -0
- package/dist/generated/prisma/models/SchemaVersion.js +2 -0
- package/dist/generated/prisma/models/Session.js +2 -0
- package/dist/generated/prisma/models/Step.js +2 -0
- package/dist/generated/prisma/models/StepRun.js +2 -0
- package/dist/generated/prisma/models/User.js +2 -0
- package/dist/generated/prisma/models/UserPermission.js +2 -0
- package/dist/generated/prisma/models/WorkCenter.js +2 -0
- package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
- package/dist/generated/prisma/models.js +2 -0
- package/dist/hateoas.js +61 -0
- package/dist/route-helpers.js +220 -0
- package/dist/routes/admin.js +147 -0
- package/dist/routes/audit.js +36 -0
- package/dist/routes/auth.js +112 -0
- package/dist/routes/dispatch.js +174 -0
- package/dist/routes/inventory.js +70 -0
- package/dist/routes/item-fields.js +220 -0
- package/dist/routes/item-instances.js +426 -0
- package/dist/routes/items.js +252 -0
- package/dist/routes/labor-tickets.js +268 -0
- package/dist/routes/operation-dependencies.js +170 -0
- package/dist/routes/operation-field-refs.js +263 -0
- package/dist/routes/operation-run-comments.js +108 -0
- package/dist/routes/operation-run-transitions.js +249 -0
- package/dist/routes/operation-runs.js +299 -0
- package/dist/routes/operations.js +283 -0
- package/dist/routes/order-revision-transitions.js +86 -0
- package/dist/routes/order-revisions.js +327 -0
- package/dist/routes/order-run-transitions.js +215 -0
- package/dist/routes/order-runs.js +335 -0
- package/dist/routes/orders.js +262 -0
- package/dist/routes/root.js +123 -0
- package/dist/routes/schemas.js +31 -0
- package/dist/routes/step-field-attachments.js +231 -0
- package/dist/routes/step-fields.js +315 -0
- package/dist/routes/step-run-fields.js +438 -0
- package/dist/routes/step-run-transitions.js +113 -0
- package/dist/routes/step-runs.js +324 -0
- package/dist/routes/steps.js +283 -0
- package/dist/routes/user-permissions.js +100 -0
- package/dist/routes/users.js +381 -0
- package/dist/routes/work-centers.js +280 -0
- package/dist/schema-registry.js +45 -0
- package/dist/services/attachment-service.js +118 -0
- package/dist/services/field-ref-service.js +74 -0
- package/dist/services/field-service.js +114 -0
- package/dist/services/field-value-service.js +256 -0
- package/dist/services/item-instance-service.js +155 -0
- package/dist/services/item-service.js +56 -0
- package/dist/services/labor-ticket-service.js +148 -0
- package/dist/services/log-file-service.js +11 -0
- package/dist/services/operation-dependency-service.js +30 -0
- package/dist/services/operation-run-comment-service.js +26 -0
- package/dist/services/operation-run-service.js +347 -0
- package/dist/services/operation-service.js +132 -0
- package/dist/services/order-revision-service.js +264 -0
- package/dist/services/order-run-service.js +356 -0
- package/dist/services/order-service.js +68 -0
- package/dist/services/revision-diff-service.js +194 -0
- package/dist/services/step-run-service.js +106 -0
- package/dist/services/step-service.js +89 -0
- package/dist/services/user-service.js +132 -0
- package/dist/services/work-center-service.js +106 -0
- package/dist/supervisorAuth.js +16 -0
- package/dist/userService.js +118 -0
- package/package.json +75 -0
- package/prisma/migrations/20260212170352_init/migration.sql +125 -0
- package/prisma/migrations/20260308000000_multi_session/migration.sql +23 -0
- package/prisma/migrations/20260309000000_add_user_api_key/migration.sql +5 -0
- package/prisma/migrations/20260309010000_add_plan_operations/migration.sql +21 -0
- package/prisma/migrations/20260309020000_rename_exec_orders_to_order_runs/migration.sql +13 -0
- package/prisma/migrations/20260310000000_rename_plan_order_revs_to_order_revisions/migration.sql +22 -0
- package/prisma/migrations/20260310100000_rename_plan_orders_to_orders/migration.sql +23 -0
- package/prisma/migrations/20260310200000_rename_order_no_to_run_no/migration.sql +3 -0
- package/prisma/migrations/20260312000000_add_user_permissions/migration.sql +16 -0
- package/prisma/migrations/20260313000000_rename_plan_operations_to_operations/migration.sql +2 -0
- package/prisma/migrations/20260313100000_add_steps/migration.sql +20 -0
- package/prisma/migrations/20260314000000_add_step_fields/migration.sql +22 -0
- package/prisma/migrations/20260315000000_add_operation_runs/migration.sql +24 -0
- package/prisma/migrations/20260315100000_add_step_runs/migration.sql +40 -0
- package/prisma/migrations/20260316000000_drop_order_name/migration.sql +12 -0
- package/prisma/migrations/20260317000000_add_attachments/migration.sql +28 -0
- package/prisma/migrations/20260317000000_add_items/migration.sql +21 -0
- package/prisma/migrations/20260317100000_add_order_item_id/migration.sql +8 -0
- package/prisma/migrations/20260318000000_add_labor_tickets/migration.sql +27 -0
- package/prisma/migrations/20260319000000_add_operation_dependencies/migration.sql +17 -0
- package/prisma/migrations/20260320000000_step_field_is_array/migration.sql +5 -0
- package/prisma/migrations/20260320100000_rename_is_array_to_multi_value/migration.sql +2 -0
- package/prisma/migrations/20260320200000_add_field_types/migration.sql +2 -0
- package/prisma/migrations/20260321000000_add_multi_set/migration.sql +13 -0
- package/prisma/migrations/20260322000000_add_field_sets/migration.sql +90 -0
- package/prisma/migrations/20260323000000_add_item_instances/migration.sql +18 -0
- package/prisma/migrations/20260324000000_add_field_records/migration.sql +77 -0
- package/prisma/migrations/20260325000000_add_run_costs/migration.sql +3 -0
- package/prisma/migrations/20260326000000_add_comments/migration.sql +16 -0
- package/prisma/migrations/20260327000000_move_assigned_to_op_run/migration.sql +3 -0
- package/prisma/migrations/20260328000000_add_step_completion_note/migration.sql +2 -0
- package/prisma/migrations/20260328000000_add_step_title/migration.sql +2 -0
- package/prisma/migrations/20260329000000_rename_notes_to_release_note/migration.sql +2 -0
- package/prisma/migrations/20260329000000_simplify_order_run_dates/migration.sql +5 -0
- package/prisma/migrations/20260330000000_add_operation_run_completion_note/migration.sql +2 -0
- package/prisma/migrations/20260331000000_add_work_centers/migration.sql +30 -0
- package/prisma/migrations/20260401000000_fix_field_values_column_shift/migration.sql +26 -0
- package/prisma/migrations/20260402000000_rename_completion_note_to_status_note/migration.sql +5 -0
- package/prisma/migrations/20260403000000_add_operation_field_refs/migration.sql +16 -0
- package/prisma/migrations/20260404000000_rename_multi_value_to_is_array/migration.sql +2 -0
- package/prisma/migrations/20260404100000_add_attachment_public_id/migration.sql +8 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +595 -0
- package/prisma.config.ts +18 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { ErrorResponseSchema, fieldTypeString, getValueFormatHint, OperationRunStatus, StepRunListQuerySchema, StepRunListResponseSchema, StepRunSchema, } from "@naisys/erp-shared";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { hasPermission } from "../auth-middleware.js";
|
|
4
|
+
import { notFound } from "../error-handler.js";
|
|
5
|
+
import { API_PREFIX, selfLink } from "../hateoas.js";
|
|
6
|
+
import { checkWorkCenterAccess, childItemLinks, formatAuditFields, resolveActions, resolveOpRun, resolveStepRun, } from "../route-helpers.js";
|
|
7
|
+
import { deserializeFieldValue, validateCompletionFields, validateFieldValue, } from "../services/field-value-service.js";
|
|
8
|
+
import { isUserClockedIn } from "../services/labor-ticket-service.js";
|
|
9
|
+
import { getStepRunWithFields, listStepRuns, listStepRunsWithFields, } from "../services/step-run-service.js";
|
|
10
|
+
export function stepRunResource(orderKey, runNo, seqNo) {
|
|
11
|
+
return `orders/${orderKey}/runs/${runNo}/ops/${seqNo}/steps`;
|
|
12
|
+
}
|
|
13
|
+
async function stepRunItemActions(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, completed, stepRunId, user) {
|
|
14
|
+
const href = `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/${stepSeqNo}`;
|
|
15
|
+
const isExecutor = hasPermission(user, "order_executor");
|
|
16
|
+
const isInProgress = opRunStatus === OperationRunStatus.in_progress;
|
|
17
|
+
// Pre-compute disabled reasons for complete action
|
|
18
|
+
const wcErr = user ? await checkWorkCenterAccess(operationId, user) : null;
|
|
19
|
+
const clockedInErr = isExecutor && isInProgress && !completed
|
|
20
|
+
? (await isUserClockedIn(opRunId, user.id))
|
|
21
|
+
? null
|
|
22
|
+
: "You must be clocked in to complete steps"
|
|
23
|
+
: null;
|
|
24
|
+
const fieldsErr = isExecutor && isInProgress && !completed
|
|
25
|
+
? await (async () => {
|
|
26
|
+
const existing = await getStepRunWithFields(stepRunId);
|
|
27
|
+
return existing ? validateCompletionFields(existing) : null;
|
|
28
|
+
})()
|
|
29
|
+
: null;
|
|
30
|
+
return resolveActions([
|
|
31
|
+
{
|
|
32
|
+
rel: "complete",
|
|
33
|
+
path: "/complete",
|
|
34
|
+
method: "POST",
|
|
35
|
+
title: "Complete",
|
|
36
|
+
schema: `${API_PREFIX}/schemas/CompleteStepRun`,
|
|
37
|
+
body: { note: "" },
|
|
38
|
+
permission: "order_executor",
|
|
39
|
+
visibleWhen: () => !completed,
|
|
40
|
+
disabledWhen: () => !isInProgress
|
|
41
|
+
? "Parent operation must be in progress"
|
|
42
|
+
: (wcErr ?? clockedInErr ?? fieldsErr),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
rel: "reopen",
|
|
46
|
+
path: "/reopen",
|
|
47
|
+
method: "POST",
|
|
48
|
+
title: "Reopen",
|
|
49
|
+
permission: "order_executor",
|
|
50
|
+
visibleWhen: () => completed,
|
|
51
|
+
disabledWhen: () => !isInProgress ? "Parent operation must be in progress" : wcErr,
|
|
52
|
+
},
|
|
53
|
+
], href, { status: opRunStatus, user });
|
|
54
|
+
}
|
|
55
|
+
export function buildStepRunActionTemplates(stepRunHref, canUpdate, multiSet, hasAttachmentFields, hasArrayFields) {
|
|
56
|
+
if (!canUpdate)
|
|
57
|
+
return [];
|
|
58
|
+
// Show array example in body hints when there are array fields
|
|
59
|
+
const valueHint = hasArrayFields ? [""] : "";
|
|
60
|
+
const batchValueHint = hasArrayFields
|
|
61
|
+
? [
|
|
62
|
+
{ fieldSeqNo: 0, value: "" },
|
|
63
|
+
{ fieldSeqNo: 0, value: [""] },
|
|
64
|
+
]
|
|
65
|
+
: [{ fieldSeqNo: 0, value: "" }];
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
rel: "updateField",
|
|
69
|
+
hrefTemplate: multiSet
|
|
70
|
+
? `${stepRunHref}/sets/{setIndex}/fields/{fieldSeqNo}`
|
|
71
|
+
: `${stepRunHref}/fields/{fieldSeqNo}`,
|
|
72
|
+
method: "PUT",
|
|
73
|
+
title: "Update Field Value",
|
|
74
|
+
schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
|
|
75
|
+
body: { value: valueHint },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
rel: "batchUpdateFields",
|
|
79
|
+
hrefTemplate: multiSet
|
|
80
|
+
? `${stepRunHref}/sets/{setIndex}/fields`
|
|
81
|
+
: `${stepRunHref}/fields`,
|
|
82
|
+
method: "PUT",
|
|
83
|
+
title: "Batch Update Field Values",
|
|
84
|
+
schema: `${API_PREFIX}/schemas/BatchUpdateFieldValues`,
|
|
85
|
+
body: { fieldValues: batchValueHint },
|
|
86
|
+
},
|
|
87
|
+
...(multiSet
|
|
88
|
+
? [
|
|
89
|
+
{
|
|
90
|
+
rel: "deleteSet",
|
|
91
|
+
hrefTemplate: `${stepRunHref}/sets/{setIndex}`,
|
|
92
|
+
method: "DELETE",
|
|
93
|
+
title: "Delete Set",
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
: []),
|
|
97
|
+
...(hasAttachmentFields
|
|
98
|
+
? [
|
|
99
|
+
{
|
|
100
|
+
rel: "uploadAttachment",
|
|
101
|
+
hrefTemplate: multiSet
|
|
102
|
+
? `${stepRunHref}/sets/{setIndex}/fields/{fieldSeqNo}/attachments`
|
|
103
|
+
: `${stepRunHref}/fields/{fieldSeqNo}/attachments`,
|
|
104
|
+
method: "POST",
|
|
105
|
+
title: "Upload Attachment",
|
|
106
|
+
alternateEncoding: {
|
|
107
|
+
contentType: "multipart/form-data",
|
|
108
|
+
description: "Upload file as multipart/form-data with field 'file'",
|
|
109
|
+
fileFields: ["file"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
rel: "downloadAttachment",
|
|
114
|
+
hrefTemplate: multiSet
|
|
115
|
+
? `${stepRunHref}/sets/{setIndex}/fields/{fieldSeqNo}/attachments/{attachmentId}`
|
|
116
|
+
: `${stepRunHref}/fields/{fieldSeqNo}/attachments/{attachmentId}`,
|
|
117
|
+
method: "GET",
|
|
118
|
+
title: "Download Attachment",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
rel: "deleteAttachment",
|
|
122
|
+
hrefTemplate: multiSet
|
|
123
|
+
? `${stepRunHref}/sets/{setIndex}/fields/{fieldSeqNo}/attachments/{attachmentId}`
|
|
124
|
+
: `${stepRunHref}/fields/{fieldSeqNo}/attachments/{attachmentId}`,
|
|
125
|
+
method: "DELETE",
|
|
126
|
+
title: "Delete Attachment",
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
: []),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
/** Compute just the HATEOAS actions + action templates for a step run */
|
|
133
|
+
export async function computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, completed, stepRunId, multiSet, hasAttachmentFields, hasArrayFields, user) {
|
|
134
|
+
const canUpdate = hasPermission(user, "order_executor") &&
|
|
135
|
+
opRunStatus === OperationRunStatus.in_progress;
|
|
136
|
+
const stepRunHref = `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/${stepSeqNo}`;
|
|
137
|
+
return {
|
|
138
|
+
_actions: await stepRunItemActions(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, completed, stepRunId, user),
|
|
139
|
+
_actionTemplates: buildStepRunActionTemplates(stepRunHref, canUpdate, multiSet, hasAttachmentFields, hasArrayFields),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const OpSeqNoParamsSchema = z.object({
|
|
143
|
+
orderKey: z.string(),
|
|
144
|
+
runNo: z.coerce.number().int(),
|
|
145
|
+
seqNo: z.coerce.number().int(),
|
|
146
|
+
});
|
|
147
|
+
const StepSeqNoParamsSchema = z.object({
|
|
148
|
+
orderKey: z.string(),
|
|
149
|
+
runNo: z.coerce.number().int(),
|
|
150
|
+
seqNo: z.coerce.number().int(),
|
|
151
|
+
stepSeqNo: z.coerce.number().int(),
|
|
152
|
+
});
|
|
153
|
+
export async function formatStepRunTransition(orderKey, runNo, seqNo, opRunId, operationId, opRunStatus, user, stepRun) {
|
|
154
|
+
const stepSeqNo = stepRun.step.seqNo;
|
|
155
|
+
const multiSet = stepRun.step.multiSet;
|
|
156
|
+
const fields = stepRun.step.fieldSet?.fields ?? [];
|
|
157
|
+
const hasAttachmentFields = fields.some((f) => f.type === "attachment");
|
|
158
|
+
const hasArrayFields = fields.some((f) => f.isArray);
|
|
159
|
+
const hateoas = await computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, stepRun.completed, stepRun.id, multiSet, hasAttachmentFields, hasArrayFields, user);
|
|
160
|
+
return {
|
|
161
|
+
id: stepRun.id,
|
|
162
|
+
completed: stepRun.completed,
|
|
163
|
+
note: stepRun.statusNote ?? null,
|
|
164
|
+
...formatAuditFields(stepRun),
|
|
165
|
+
...hateoas,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export async function formatStepRunWithFields(orderKey, runNo, seqNo, opRunId, operationId, opRunStatus, user, stepRun) {
|
|
169
|
+
const stepSeqNo = stepRun.step.seqNo;
|
|
170
|
+
const multiSet = stepRun.step.multiSet;
|
|
171
|
+
// Determine how many sets exist
|
|
172
|
+
const storedFieldValues = stepRun.fieldRecord?.fieldValues ?? [];
|
|
173
|
+
const maxSetIndex = storedFieldValues.reduce((max, fv) => Math.max(max, fv.setIndex), -1);
|
|
174
|
+
const setCount = Math.max(1, maxSetIndex + 1);
|
|
175
|
+
// Merge field definitions with stored values + validation + attachments
|
|
176
|
+
const fieldValues = [];
|
|
177
|
+
const stepRunHref = `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/${stepSeqNo}`;
|
|
178
|
+
for (let si = 0; si < setCount; si++) {
|
|
179
|
+
for (const field of stepRun.step.fieldSet?.fields ?? []) {
|
|
180
|
+
const stored = storedFieldValues.find((fv) => fv.fieldId === field.id && fv.setIndex === si);
|
|
181
|
+
const value = deserializeFieldValue(stored?.value ?? "", field.isArray);
|
|
182
|
+
const setPath = multiSet
|
|
183
|
+
? `/sets/${si}/fields/${field.seqNo}`
|
|
184
|
+
: `/fields/${field.seqNo}`;
|
|
185
|
+
const attachments = field.type === "attachment" && stored
|
|
186
|
+
? stored.fieldAttachments.map((sfa) => ({
|
|
187
|
+
id: sfa.attachment.publicId,
|
|
188
|
+
filename: sfa.attachment.filename,
|
|
189
|
+
fileSize: sfa.attachment.fileSize,
|
|
190
|
+
downloadHref: `${stepRunHref}${setPath}/attachments/${sfa.attachment.publicId}`,
|
|
191
|
+
}))
|
|
192
|
+
: undefined;
|
|
193
|
+
const fieldType = fieldTypeString(field.type, field.isArray);
|
|
194
|
+
fieldValues.push({
|
|
195
|
+
fieldId: field.id,
|
|
196
|
+
fieldSeqNo: field.seqNo,
|
|
197
|
+
label: field.label,
|
|
198
|
+
type: fieldType,
|
|
199
|
+
valueFormat: getValueFormatHint(fieldType),
|
|
200
|
+
required: field.required,
|
|
201
|
+
setIndex: si,
|
|
202
|
+
value,
|
|
203
|
+
attachments,
|
|
204
|
+
validation: validateFieldValue(field.type, field.isArray, field.required, value),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const allFields = stepRun.step.fieldSet?.fields ?? [];
|
|
209
|
+
const hasAttachmentFields = allFields.some((f) => f.type === "attachment");
|
|
210
|
+
const hasArrayFields = allFields.some((f) => f.isArray);
|
|
211
|
+
const hateoas = await computeStepRunHateoas(orderKey, runNo, seqNo, stepSeqNo, opRunId, operationId, opRunStatus, stepRun.completed, stepRun.id, multiSet, hasAttachmentFields, hasArrayFields, user);
|
|
212
|
+
return {
|
|
213
|
+
id: stepRun.id,
|
|
214
|
+
operationRunId: stepRun.operationRunId,
|
|
215
|
+
stepId: stepRun.stepId,
|
|
216
|
+
seqNo: stepSeqNo,
|
|
217
|
+
title: stepRun.step.title,
|
|
218
|
+
instructions: stepRun.step.instructions,
|
|
219
|
+
multiSet,
|
|
220
|
+
completed: stepRun.completed,
|
|
221
|
+
note: stepRun.statusNote ?? null,
|
|
222
|
+
fieldCount: stepRun.step.fieldSet?.fields.length ?? 0,
|
|
223
|
+
fieldValues,
|
|
224
|
+
...formatAuditFields(stepRun),
|
|
225
|
+
_links: childItemLinks("/" + stepRunResource(orderKey, runNo, seqNo), stepSeqNo, "Step Runs", "/orders/" + orderKey + "/runs/" + runNo + "/ops/" + seqNo, "Operation Run", "StepRun", "operationRun"),
|
|
226
|
+
...hateoas,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function formatListStepRun(stepRun) {
|
|
230
|
+
const stepSeqNo = stepRun.step.seqNo;
|
|
231
|
+
const fieldCount = stepRun.step.fieldSet?._count.fields ?? 0;
|
|
232
|
+
return {
|
|
233
|
+
id: stepRun.id,
|
|
234
|
+
operationRunId: stepRun.operationRunId,
|
|
235
|
+
stepId: stepRun.stepId,
|
|
236
|
+
seqNo: stepSeqNo,
|
|
237
|
+
title: stepRun.step.title,
|
|
238
|
+
instructions: stepRun.step.instructions,
|
|
239
|
+
multiSet: stepRun.step.multiSet,
|
|
240
|
+
completed: stepRun.completed,
|
|
241
|
+
note: stepRun.statusNote ?? null,
|
|
242
|
+
fieldCount,
|
|
243
|
+
...formatAuditFields(stepRun),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export default function stepRunRoutes(fastify) {
|
|
247
|
+
const app = fastify.withTypeProvider();
|
|
248
|
+
// LIST
|
|
249
|
+
app.get("/", {
|
|
250
|
+
schema: {
|
|
251
|
+
description: "List step runs for an operation run",
|
|
252
|
+
tags: ["Step Runs"],
|
|
253
|
+
params: OpSeqNoParamsSchema,
|
|
254
|
+
querystring: StepRunListQuerySchema,
|
|
255
|
+
response: {
|
|
256
|
+
200: StepRunListResponseSchema,
|
|
257
|
+
404: ErrorResponseSchema,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
handler: async (request, reply) => {
|
|
261
|
+
const { orderKey, runNo, seqNo } = request.params;
|
|
262
|
+
const { includeFields } = request.query;
|
|
263
|
+
const resolved = await resolveOpRun(orderKey, runNo, seqNo);
|
|
264
|
+
if (!resolved) {
|
|
265
|
+
return notFound(reply, `Operation run not found`);
|
|
266
|
+
}
|
|
267
|
+
if (includeFields) {
|
|
268
|
+
const items = await listStepRunsWithFields(resolved.opRun.id);
|
|
269
|
+
const formatted = await Promise.all(items.map(async (stepRun) => {
|
|
270
|
+
const { _links, ...rest } = await formatStepRunWithFields(orderKey, runNo, seqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, request.erpUser, stepRun);
|
|
271
|
+
return rest;
|
|
272
|
+
}));
|
|
273
|
+
return {
|
|
274
|
+
items: formatted,
|
|
275
|
+
total: items.length,
|
|
276
|
+
_links: [selfLink(`/${stepRunResource(orderKey, runNo, seqNo)}`)],
|
|
277
|
+
_linkTemplates: [
|
|
278
|
+
{
|
|
279
|
+
rel: "item",
|
|
280
|
+
hrefTemplate: `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/{seqNo}`,
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const items = await listStepRuns(resolved.opRun.id);
|
|
286
|
+
return {
|
|
287
|
+
items: items.map((stepRun) => formatListStepRun(stepRun)),
|
|
288
|
+
total: items.length,
|
|
289
|
+
_links: [selfLink(`/${stepRunResource(orderKey, runNo, seqNo)}`)],
|
|
290
|
+
_linkTemplates: [
|
|
291
|
+
{
|
|
292
|
+
rel: "item",
|
|
293
|
+
hrefTemplate: `${API_PREFIX}/${stepRunResource(orderKey, runNo, seqNo)}/{seqNo}`,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
// GET by stepSeqNo
|
|
300
|
+
app.get("/:stepSeqNo", {
|
|
301
|
+
schema: {
|
|
302
|
+
description: "Get a single step run by step sequence number",
|
|
303
|
+
tags: ["Step Runs"],
|
|
304
|
+
params: StepSeqNoParamsSchema,
|
|
305
|
+
response: {
|
|
306
|
+
200: StepRunSchema,
|
|
307
|
+
404: ErrorResponseSchema,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
handler: async (request, reply) => {
|
|
311
|
+
const { orderKey, runNo, seqNo, stepSeqNo } = request.params;
|
|
312
|
+
const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
|
|
313
|
+
if (!resolved) {
|
|
314
|
+
return notFound(reply, `Step run not found`);
|
|
315
|
+
}
|
|
316
|
+
const stepRun = await getStepRunWithFields(resolved.stepRun.id);
|
|
317
|
+
if (!stepRun) {
|
|
318
|
+
return notFound(reply, `Step run not found`);
|
|
319
|
+
}
|
|
320
|
+
return formatStepRunWithFields(orderKey, runNo, seqNo, resolved.opRun.id, resolved.opRun.operationId, resolved.opRun.status, request.erpUser, stepRun);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=step-runs.js.map
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { BatchCreateStepSchema, BatchSeqNoCreateResponseSchema, CreateStepSchema, ErrorResponseSchema, MutateResponseSchema, RevisionStatus, SeqNoCreateResponseSchema, StepListResponseSchema, StepSchema, UpdateStepSchema, } from "@naisys/erp-shared";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { 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, resolveActions, resolveOperation, } from "../route-helpers.js";
|
|
7
|
+
import { createStep, createSteps, deleteStep, findExisting, getStep, listSteps, updateStep, } from "../services/step-service.js";
|
|
8
|
+
import { formatFieldListResponse } from "./step-fields.js";
|
|
9
|
+
const ParamsSchema = z.object({
|
|
10
|
+
orderKey: z.string(),
|
|
11
|
+
revNo: z.coerce.number().int(),
|
|
12
|
+
seqNo: z.coerce.number().int(),
|
|
13
|
+
});
|
|
14
|
+
const StepParamsSchema = z.object({
|
|
15
|
+
orderKey: z.string(),
|
|
16
|
+
revNo: z.coerce.number().int(),
|
|
17
|
+
seqNo: z.coerce.number().int(),
|
|
18
|
+
stepSeqNo: z.coerce.number().int(),
|
|
19
|
+
});
|
|
20
|
+
function stepBasePath(orderKey, revNo, opSeqNo) {
|
|
21
|
+
return `/orders/${orderKey}/revs/${revNo}/ops/${opSeqNo}/steps`;
|
|
22
|
+
}
|
|
23
|
+
function formatStep(orderKey, revNo, opSeqNo, revStatus, user, step) {
|
|
24
|
+
return {
|
|
25
|
+
id: step.id,
|
|
26
|
+
operationId: step.operationId,
|
|
27
|
+
seqNo: step.seqNo,
|
|
28
|
+
title: step.title,
|
|
29
|
+
instructions: step.instructions,
|
|
30
|
+
multiSet: step.multiSet,
|
|
31
|
+
fieldCount: step.fieldSet?.fields.length ?? 0,
|
|
32
|
+
...formatAuditFields(step),
|
|
33
|
+
fields: formatFieldListResponse(orderKey, revNo, opSeqNo, step.seqNo, revStatus, user, step.fieldSet?.fields ?? []),
|
|
34
|
+
_links: childItemLinks(stepBasePath(orderKey, revNo, opSeqNo), step.seqNo, "Steps", `/orders/${orderKey}/revs/${revNo}/ops/${opSeqNo}`, "Operation", "Step"),
|
|
35
|
+
_actions: draftCrudActions(`${API_PREFIX}${stepBasePath(orderKey, revNo, opSeqNo)}/${step.seqNo}`, "UpdateStep", revStatus, user),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const draftCreateDef = {
|
|
39
|
+
rel: "create",
|
|
40
|
+
method: "POST",
|
|
41
|
+
title: "Add Step",
|
|
42
|
+
schema: `${API_PREFIX}/schemas/CreateStep`,
|
|
43
|
+
permission: "order_planner",
|
|
44
|
+
disabledWhen: (ctx) => ctx.status !== RevisionStatus.draft
|
|
45
|
+
? "Can only add steps in draft revisions"
|
|
46
|
+
: null,
|
|
47
|
+
};
|
|
48
|
+
const draftBatchCreateDef = {
|
|
49
|
+
rel: "batch-create",
|
|
50
|
+
path: "/batch",
|
|
51
|
+
method: "POST",
|
|
52
|
+
title: "Add Steps (Batch)",
|
|
53
|
+
schema: `${API_PREFIX}/schemas/BatchCreateStep`,
|
|
54
|
+
permission: "order_planner",
|
|
55
|
+
disabledWhen: (ctx) => ctx.status !== RevisionStatus.draft
|
|
56
|
+
? "Can only add steps in draft revisions"
|
|
57
|
+
: null,
|
|
58
|
+
};
|
|
59
|
+
function stepListActions(base, revStatus, user) {
|
|
60
|
+
return resolveActions([draftCreateDef, draftBatchCreateDef], `${API_PREFIX}${base}`, { status: revStatus, user });
|
|
61
|
+
}
|
|
62
|
+
export default function stepRoutes(fastify) {
|
|
63
|
+
const app = fastify.withTypeProvider();
|
|
64
|
+
// LIST
|
|
65
|
+
app.get("/", {
|
|
66
|
+
schema: {
|
|
67
|
+
description: "List steps for an operation",
|
|
68
|
+
tags: ["Steps"],
|
|
69
|
+
params: ParamsSchema,
|
|
70
|
+
response: {
|
|
71
|
+
200: StepListResponseSchema,
|
|
72
|
+
404: ErrorResponseSchema,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
handler: async (request, reply) => {
|
|
76
|
+
const { orderKey, revNo, seqNo } = request.params;
|
|
77
|
+
const resolved = await resolveOperation(orderKey, revNo, seqNo);
|
|
78
|
+
if (!resolved) {
|
|
79
|
+
return notFound(reply, "Operation not found");
|
|
80
|
+
}
|
|
81
|
+
const items = await listSteps(resolved.operation.id);
|
|
82
|
+
const maxSeq = items.length > 0 ? items[items.length - 1].seqNo : 0;
|
|
83
|
+
const user = request.erpUser;
|
|
84
|
+
const base = stepBasePath(orderKey, revNo, seqNo);
|
|
85
|
+
return {
|
|
86
|
+
items: items.map((step) => {
|
|
87
|
+
const { _links, ...rest } = formatStep(orderKey, revNo, seqNo, resolved.rev.status, user, step);
|
|
88
|
+
return rest;
|
|
89
|
+
}),
|
|
90
|
+
total: items.length,
|
|
91
|
+
nextSeqNo: calcNextSeqNo(maxSeq),
|
|
92
|
+
_links: [selfLink(base)],
|
|
93
|
+
_linkTemplates: [
|
|
94
|
+
{
|
|
95
|
+
rel: "item",
|
|
96
|
+
hrefTemplate: `${API_PREFIX}${stepBasePath(orderKey, revNo, seqNo)}/{seqNo}`,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
_actions: stepListActions(base, resolved.rev.status, user),
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
// BATCH CREATE
|
|
104
|
+
app.post("/batch", {
|
|
105
|
+
schema: {
|
|
106
|
+
description: "Create multiple steps for an operation in one request",
|
|
107
|
+
tags: ["Steps"],
|
|
108
|
+
params: ParamsSchema,
|
|
109
|
+
body: BatchCreateStepSchema,
|
|
110
|
+
response: {
|
|
111
|
+
201: BatchSeqNoCreateResponseSchema,
|
|
112
|
+
404: ErrorResponseSchema,
|
|
113
|
+
409: ErrorResponseSchema,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
preHandler: requirePermission("order_planner"),
|
|
117
|
+
handler: async (request, reply) => {
|
|
118
|
+
const { orderKey, revNo, seqNo } = request.params;
|
|
119
|
+
const { items } = request.body;
|
|
120
|
+
const userId = request.erpUser.id;
|
|
121
|
+
const resolved = await resolveOperation(orderKey, revNo, seqNo);
|
|
122
|
+
if (!resolved) {
|
|
123
|
+
return notFound(reply, "Operation not found");
|
|
124
|
+
}
|
|
125
|
+
if (resolved.rev.status !== RevisionStatus.draft) {
|
|
126
|
+
return conflict(reply, `Cannot add steps to a ${resolved.rev.status} revision`);
|
|
127
|
+
}
|
|
128
|
+
const created = await createSteps(resolved.operation.id, items, userId);
|
|
129
|
+
const maxSeq = created.length > 0 ? created[created.length - 1].seqNo : 0;
|
|
130
|
+
const user = request.erpUser;
|
|
131
|
+
const base = stepBasePath(orderKey, revNo, seqNo);
|
|
132
|
+
const full = {
|
|
133
|
+
items: created.map((step) => {
|
|
134
|
+
const { _links, ...rest } = formatStep(orderKey, revNo, seqNo, resolved.rev.status, user, step);
|
|
135
|
+
return rest;
|
|
136
|
+
}),
|
|
137
|
+
total: created.length,
|
|
138
|
+
nextSeqNo: calcNextSeqNo(maxSeq),
|
|
139
|
+
_links: [selfLink(base)],
|
|
140
|
+
_linkTemplates: [
|
|
141
|
+
{
|
|
142
|
+
rel: "item",
|
|
143
|
+
hrefTemplate: `${API_PREFIX}${stepBasePath(orderKey, revNo, seqNo)}/{seqNo}`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
_actions: [],
|
|
147
|
+
};
|
|
148
|
+
reply.status(201);
|
|
149
|
+
return mutationResult(request, reply, full, {
|
|
150
|
+
items: created.map((s) => ({ id: s.id, seqNo: s.seqNo })),
|
|
151
|
+
total: created.length,
|
|
152
|
+
_actions: full._actions,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
// CREATE
|
|
157
|
+
app.post("/", {
|
|
158
|
+
schema: {
|
|
159
|
+
description: "Create a step for an operation",
|
|
160
|
+
tags: ["Steps"],
|
|
161
|
+
params: ParamsSchema,
|
|
162
|
+
body: CreateStepSchema,
|
|
163
|
+
response: {
|
|
164
|
+
201: SeqNoCreateResponseSchema,
|
|
165
|
+
404: ErrorResponseSchema,
|
|
166
|
+
409: ErrorResponseSchema,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
preHandler: requirePermission("order_planner"),
|
|
170
|
+
handler: async (request, reply) => {
|
|
171
|
+
const { orderKey, revNo, seqNo } = request.params;
|
|
172
|
+
const { seqNo: requestedSeqNo, title, instructions, multiSet, } = request.body;
|
|
173
|
+
const userId = request.erpUser.id;
|
|
174
|
+
const resolved = await resolveOperation(orderKey, revNo, seqNo);
|
|
175
|
+
if (!resolved) {
|
|
176
|
+
return notFound(reply, "Operation not found");
|
|
177
|
+
}
|
|
178
|
+
if (resolved.rev.status !== RevisionStatus.draft) {
|
|
179
|
+
return conflict(reply, `Cannot add steps to a ${resolved.rev.status} revision`);
|
|
180
|
+
}
|
|
181
|
+
const step = await createStep(resolved.operation.id, requestedSeqNo, title, instructions, multiSet, userId);
|
|
182
|
+
const full = formatStep(orderKey, revNo, seqNo, resolved.rev.status, request.erpUser, step);
|
|
183
|
+
reply.status(201);
|
|
184
|
+
return mutationResult(request, reply, full, {
|
|
185
|
+
id: full.id,
|
|
186
|
+
seqNo: full.seqNo,
|
|
187
|
+
_links: full._links,
|
|
188
|
+
_actions: full._actions,
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
// GET by stepSeqNo
|
|
193
|
+
app.get("/:stepSeqNo", {
|
|
194
|
+
schema: {
|
|
195
|
+
description: "Get a step by sequence number",
|
|
196
|
+
tags: ["Steps"],
|
|
197
|
+
params: StepParamsSchema,
|
|
198
|
+
response: {
|
|
199
|
+
200: StepSchema,
|
|
200
|
+
404: ErrorResponseSchema,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
handler: async (request, reply) => {
|
|
204
|
+
const { orderKey, revNo, seqNo, stepSeqNo } = request.params;
|
|
205
|
+
const resolved = await resolveOperation(orderKey, revNo, seqNo);
|
|
206
|
+
if (!resolved) {
|
|
207
|
+
return notFound(reply, "Operation not found");
|
|
208
|
+
}
|
|
209
|
+
const step = await getStep(resolved.operation.id, stepSeqNo);
|
|
210
|
+
if (!step) {
|
|
211
|
+
return notFound(reply, `Step ${stepSeqNo} not found`);
|
|
212
|
+
}
|
|
213
|
+
return formatStep(orderKey, revNo, seqNo, resolved.rev.status, request.erpUser, step);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
// UPDATE (draft only)
|
|
217
|
+
app.put("/:stepSeqNo", {
|
|
218
|
+
schema: {
|
|
219
|
+
description: "Update a step (draft revision only)",
|
|
220
|
+
tags: ["Steps"],
|
|
221
|
+
params: StepParamsSchema,
|
|
222
|
+
body: UpdateStepSchema,
|
|
223
|
+
response: {
|
|
224
|
+
200: MutateResponseSchema,
|
|
225
|
+
404: ErrorResponseSchema,
|
|
226
|
+
409: ErrorResponseSchema,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
preHandler: requirePermission("order_planner"),
|
|
230
|
+
handler: async (request, reply) => {
|
|
231
|
+
const { orderKey, revNo, seqNo, stepSeqNo } = request.params;
|
|
232
|
+
const { title, instructions, seqNo: newSeqNo, multiSet } = request.body;
|
|
233
|
+
const userId = request.erpUser.id;
|
|
234
|
+
const resolved = await resolveOperation(orderKey, revNo, seqNo);
|
|
235
|
+
if (!resolved) {
|
|
236
|
+
return notFound(reply, "Operation not found");
|
|
237
|
+
}
|
|
238
|
+
if (resolved.rev.status !== RevisionStatus.draft) {
|
|
239
|
+
return conflict(reply, `Cannot update steps on a ${resolved.rev.status} revision`);
|
|
240
|
+
}
|
|
241
|
+
const existing = await findExisting(resolved.operation.id, stepSeqNo);
|
|
242
|
+
if (!existing) {
|
|
243
|
+
return notFound(reply, `Step ${stepSeqNo} not found`);
|
|
244
|
+
}
|
|
245
|
+
const step = await updateStep(existing.id, { title, instructions, seqNo: newSeqNo, multiSet }, userId);
|
|
246
|
+
const full = formatStep(orderKey, revNo, seqNo, resolved.rev.status, request.erpUser, step);
|
|
247
|
+
return mutationResult(request, reply, full, {
|
|
248
|
+
_actions: full._actions,
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
// DELETE (draft only)
|
|
253
|
+
app.delete("/:stepSeqNo", {
|
|
254
|
+
schema: {
|
|
255
|
+
description: "Delete a step (draft revision only)",
|
|
256
|
+
tags: ["Steps"],
|
|
257
|
+
params: StepParamsSchema,
|
|
258
|
+
response: {
|
|
259
|
+
204: z.void(),
|
|
260
|
+
404: ErrorResponseSchema,
|
|
261
|
+
409: ErrorResponseSchema,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
preHandler: requirePermission("order_planner"),
|
|
265
|
+
handler: async (request, reply) => {
|
|
266
|
+
const { orderKey, revNo, seqNo, stepSeqNo } = request.params;
|
|
267
|
+
const resolved = await resolveOperation(orderKey, revNo, seqNo);
|
|
268
|
+
if (!resolved) {
|
|
269
|
+
return notFound(reply, "Operation not found");
|
|
270
|
+
}
|
|
271
|
+
if (resolved.rev.status !== RevisionStatus.draft) {
|
|
272
|
+
return conflict(reply, `Cannot delete steps on a ${resolved.rev.status} revision`);
|
|
273
|
+
}
|
|
274
|
+
const existing = await findExisting(resolved.operation.id, stepSeqNo);
|
|
275
|
+
if (!existing) {
|
|
276
|
+
return notFound(reply, `Step ${stepSeqNo} not found`);
|
|
277
|
+
}
|
|
278
|
+
await deleteStep(existing.id);
|
|
279
|
+
reply.status(204);
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=steps.js.map
|