@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,327 @@
|
|
|
1
|
+
import { CreateOrderRevisionSchema, ErrorResponseSchema, MutateResponseSchema, OrderRevisionListQuerySchema, OrderRevisionListResponseSchema, OrderRevisionSchema, RevisionCreateResponseSchema, RevisionDiffQuerySchema, RevisionDiffResponseSchema, RevisionStatus, UpdateOrderRevisionSchema, } from "@naisys/erp-shared";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { hasPermission, requirePermission } from "../auth-middleware.js";
|
|
4
|
+
import { conflict, notFound } from "../error-handler.js";
|
|
5
|
+
import { API_PREFIX, paginationLinks } from "../hateoas.js";
|
|
6
|
+
import { childItemLinks, formatAuditFields, mutationResult, permGate, resolveActions, resolveOrder, } from "../route-helpers.js";
|
|
7
|
+
import { checkHasOrderRuns, createRevision, deleteRevision, findExisting, getRevision, getRevisionOpSummary, listRevisions, updateRevision, validateDraftStatus, } from "../services/order-revision-service.js";
|
|
8
|
+
import { diffRevisions } from "../services/revision-diff-service.js";
|
|
9
|
+
export function revisionItemActions(parentResource, orderKey, revNo, status, user) {
|
|
10
|
+
const href = `${API_PREFIX}/${parentResource}/${orderKey}/revs/${revNo}`;
|
|
11
|
+
return resolveActions([
|
|
12
|
+
{
|
|
13
|
+
rel: "update",
|
|
14
|
+
method: "PUT",
|
|
15
|
+
title: "Update",
|
|
16
|
+
schema: `${API_PREFIX}/schemas/UpdateOrderRevision`,
|
|
17
|
+
permission: "order_planner",
|
|
18
|
+
statuses: [RevisionStatus.draft, RevisionStatus.approved],
|
|
19
|
+
disabledWhen: (ctx) => ctx.status === RevisionStatus.approved
|
|
20
|
+
? "Revision is no longer in draft"
|
|
21
|
+
: null,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
rel: "approve",
|
|
25
|
+
path: "/approve",
|
|
26
|
+
method: "POST",
|
|
27
|
+
title: "Approve",
|
|
28
|
+
permission: "order_planner",
|
|
29
|
+
statuses: [RevisionStatus.draft, RevisionStatus.approved],
|
|
30
|
+
disabledWhen: (ctx) => ctx.status === RevisionStatus.approved
|
|
31
|
+
? "Revision has already been approved"
|
|
32
|
+
: null,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
rel: "delete",
|
|
36
|
+
method: "DELETE",
|
|
37
|
+
title: "Delete",
|
|
38
|
+
permission: "order_planner",
|
|
39
|
+
statuses: [RevisionStatus.draft],
|
|
40
|
+
hideWithoutPermission: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
rel: "cut-order",
|
|
44
|
+
href: `${API_PREFIX}/orders/${orderKey}/runs`,
|
|
45
|
+
method: "POST",
|
|
46
|
+
title: "Cut Order",
|
|
47
|
+
schema: `${API_PREFIX}/schemas/CreateOrderRun`,
|
|
48
|
+
permission: "order_manager",
|
|
49
|
+
statuses: [RevisionStatus.draft, RevisionStatus.approved],
|
|
50
|
+
disabledWhen: (ctx) => ctx.status === RevisionStatus.draft
|
|
51
|
+
? "Revision must be approved before cutting an order run"
|
|
52
|
+
: null,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
rel: "obsolete",
|
|
56
|
+
path: "/obsolete",
|
|
57
|
+
method: "POST",
|
|
58
|
+
title: "Mark Obsolete",
|
|
59
|
+
permission: "order_planner",
|
|
60
|
+
statuses: [RevisionStatus.draft, RevisionStatus.approved],
|
|
61
|
+
disabledWhen: (ctx) => ctx.status === RevisionStatus.draft
|
|
62
|
+
? "Revision must be approved before marking obsolete"
|
|
63
|
+
: null,
|
|
64
|
+
},
|
|
65
|
+
], href, { status, user });
|
|
66
|
+
}
|
|
67
|
+
const PARENT_RESOURCE = "orders";
|
|
68
|
+
const OrderKeyParamsSchema = z.object({
|
|
69
|
+
orderKey: z.string(),
|
|
70
|
+
});
|
|
71
|
+
export const RevNoParamsSchema = z.object({
|
|
72
|
+
orderKey: z.string(),
|
|
73
|
+
revNo: z.coerce.number().int(),
|
|
74
|
+
});
|
|
75
|
+
export async function formatRevision(orderKey, user, revision) {
|
|
76
|
+
const opSummaryRows = await getRevisionOpSummary(revision.id);
|
|
77
|
+
return {
|
|
78
|
+
id: revision.id,
|
|
79
|
+
orderId: revision.orderId,
|
|
80
|
+
revNo: revision.revNo,
|
|
81
|
+
status: revision.status,
|
|
82
|
+
description: revision.description,
|
|
83
|
+
changeSummary: revision.changeSummary,
|
|
84
|
+
itemKey: revision.order?.item?.key ?? null,
|
|
85
|
+
operationSummary: opSummaryRows.map((op) => ({
|
|
86
|
+
seqNo: op.seqNo,
|
|
87
|
+
title: op.title,
|
|
88
|
+
})),
|
|
89
|
+
...formatAuditFields(revision),
|
|
90
|
+
_links: [
|
|
91
|
+
...childItemLinks(`/${PARENT_RESOURCE}/${orderKey}/revs`, revision.revNo, "Revisions", `/${PARENT_RESOURCE}/${orderKey}`, "Order", "OrderRevision"),
|
|
92
|
+
{
|
|
93
|
+
rel: "operations",
|
|
94
|
+
href: `${API_PREFIX}/${PARENT_RESOURCE}/${orderKey}/revs/${revision.revNo}/ops`,
|
|
95
|
+
title: "Operations",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
_actions: revisionItemActions(PARENT_RESOURCE, orderKey, revision.revNo, revision.status, user),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function formatListRevision(orderKey, user, revision) {
|
|
102
|
+
return {
|
|
103
|
+
id: revision.id,
|
|
104
|
+
orderId: revision.orderId,
|
|
105
|
+
revNo: revision.revNo,
|
|
106
|
+
status: revision.status,
|
|
107
|
+
description: revision.description,
|
|
108
|
+
changeSummary: revision.changeSummary,
|
|
109
|
+
itemKey: revision.order?.item?.key ?? null,
|
|
110
|
+
...formatAuditFields(revision),
|
|
111
|
+
_actions: revisionItemActions(PARENT_RESOURCE, orderKey, revision.revNo, revision.status, user),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export default function orderRevisionRoutes(fastify) {
|
|
115
|
+
const app = fastify.withTypeProvider();
|
|
116
|
+
// LIST
|
|
117
|
+
app.get("/", {
|
|
118
|
+
schema: {
|
|
119
|
+
description: "List revisions for an order",
|
|
120
|
+
tags: ["Order Revisions"],
|
|
121
|
+
params: OrderKeyParamsSchema,
|
|
122
|
+
querystring: OrderRevisionListQuerySchema,
|
|
123
|
+
response: {
|
|
124
|
+
200: OrderRevisionListResponseSchema,
|
|
125
|
+
404: ErrorResponseSchema,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
handler: async (request, reply) => {
|
|
129
|
+
const { orderKey } = request.params;
|
|
130
|
+
const { page, pageSize, status, includeObsolete } = request.query;
|
|
131
|
+
const order = await resolveOrder(orderKey);
|
|
132
|
+
if (!order) {
|
|
133
|
+
return notFound(reply, `Order '${orderKey}' not found`);
|
|
134
|
+
}
|
|
135
|
+
const where = {};
|
|
136
|
+
if (status) {
|
|
137
|
+
where.status = status;
|
|
138
|
+
}
|
|
139
|
+
else if (!includeObsolete) {
|
|
140
|
+
where.status = { not: RevisionStatus.obsolete };
|
|
141
|
+
}
|
|
142
|
+
const [items, total] = await listRevisions(order.id, where, page, pageSize);
|
|
143
|
+
const revBasePath = `${PARENT_RESOURCE}/${orderKey}/revs`;
|
|
144
|
+
return {
|
|
145
|
+
items: items.map((revision) => formatListRevision(orderKey, request.erpUser, revision)),
|
|
146
|
+
total,
|
|
147
|
+
page,
|
|
148
|
+
pageSize,
|
|
149
|
+
_links: paginationLinks(revBasePath, page, pageSize, total, {
|
|
150
|
+
status,
|
|
151
|
+
includeObsolete: includeObsolete ? "true" : undefined,
|
|
152
|
+
}),
|
|
153
|
+
_linkTemplates: [
|
|
154
|
+
{
|
|
155
|
+
rel: "item",
|
|
156
|
+
hrefTemplate: `${API_PREFIX}/orders/${orderKey}/revs/{revNo}`,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
_actions: [
|
|
160
|
+
{
|
|
161
|
+
rel: "create",
|
|
162
|
+
href: `${API_PREFIX}/${revBasePath}`,
|
|
163
|
+
method: "POST",
|
|
164
|
+
title: "New Revision",
|
|
165
|
+
schema: `${API_PREFIX}/schemas/CreateOrderRevision`,
|
|
166
|
+
...permGate(hasPermission(request.erpUser, "order_planner"), "order_planner"),
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
// DIFF two revisions
|
|
173
|
+
app.get("/diff", {
|
|
174
|
+
schema: {
|
|
175
|
+
description: "Compare two revisions and return a structured diff",
|
|
176
|
+
tags: ["Order Revisions"],
|
|
177
|
+
params: OrderKeyParamsSchema,
|
|
178
|
+
querystring: RevisionDiffQuerySchema,
|
|
179
|
+
response: {
|
|
180
|
+
200: RevisionDiffResponseSchema,
|
|
181
|
+
404: ErrorResponseSchema,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
handler: async (request, reply) => {
|
|
185
|
+
const { orderKey } = request.params;
|
|
186
|
+
const { from, to } = request.query;
|
|
187
|
+
const order = await resolveOrder(orderKey);
|
|
188
|
+
if (!order) {
|
|
189
|
+
return notFound(reply, `Order '${orderKey}' not found`);
|
|
190
|
+
}
|
|
191
|
+
const diff = await diffRevisions(order.id, from, to);
|
|
192
|
+
if (!diff) {
|
|
193
|
+
return notFound(reply, `One or both revisions not found (rev ${from}, rev ${to})`);
|
|
194
|
+
}
|
|
195
|
+
return diff;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
// CREATE
|
|
199
|
+
app.post("/", {
|
|
200
|
+
schema: {
|
|
201
|
+
description: "Create a new revision for an order",
|
|
202
|
+
tags: ["Order Revisions"],
|
|
203
|
+
params: OrderKeyParamsSchema,
|
|
204
|
+
body: CreateOrderRevisionSchema,
|
|
205
|
+
response: {
|
|
206
|
+
201: RevisionCreateResponseSchema,
|
|
207
|
+
404: ErrorResponseSchema,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
preHandler: requirePermission("order_planner"),
|
|
211
|
+
handler: async (request, reply) => {
|
|
212
|
+
const { orderKey } = request.params;
|
|
213
|
+
const { description, changeSummary } = request.body;
|
|
214
|
+
const userId = request.erpUser.id;
|
|
215
|
+
const order = await resolveOrder(orderKey);
|
|
216
|
+
if (!order) {
|
|
217
|
+
return notFound(reply, `Order '${orderKey}' not found`);
|
|
218
|
+
}
|
|
219
|
+
const revision = await createRevision(order.id, { description, changeSummary }, userId);
|
|
220
|
+
const full = await formatRevision(orderKey, request.erpUser, revision);
|
|
221
|
+
reply.status(201);
|
|
222
|
+
return mutationResult(request, reply, full, {
|
|
223
|
+
id: full.id,
|
|
224
|
+
revNo: full.revNo,
|
|
225
|
+
_links: full._links,
|
|
226
|
+
_actions: full._actions,
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
// GET by revNo
|
|
231
|
+
app.get("/:revNo", {
|
|
232
|
+
schema: {
|
|
233
|
+
description: "Get a single revision by revision number",
|
|
234
|
+
tags: ["Order Revisions"],
|
|
235
|
+
params: RevNoParamsSchema,
|
|
236
|
+
response: {
|
|
237
|
+
200: OrderRevisionSchema,
|
|
238
|
+
404: ErrorResponseSchema,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
handler: async (request, reply) => {
|
|
242
|
+
const { orderKey, revNo } = request.params;
|
|
243
|
+
const order = await resolveOrder(orderKey);
|
|
244
|
+
if (!order) {
|
|
245
|
+
return notFound(reply, `Order '${orderKey}' not found`);
|
|
246
|
+
}
|
|
247
|
+
const revision = await getRevision(order.id, revNo);
|
|
248
|
+
if (!revision) {
|
|
249
|
+
return notFound(reply, `Revision ${revNo} not found for order '${orderKey}'`);
|
|
250
|
+
}
|
|
251
|
+
return await formatRevision(orderKey, request.erpUser, revision);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
// UPDATE (draft only)
|
|
255
|
+
app.put("/:revNo", {
|
|
256
|
+
schema: {
|
|
257
|
+
description: "Update a revision (draft status only)",
|
|
258
|
+
tags: ["Order Revisions"],
|
|
259
|
+
params: RevNoParamsSchema,
|
|
260
|
+
body: UpdateOrderRevisionSchema,
|
|
261
|
+
response: {
|
|
262
|
+
200: MutateResponseSchema,
|
|
263
|
+
404: ErrorResponseSchema,
|
|
264
|
+
409: ErrorResponseSchema,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
preHandler: requirePermission("order_planner"),
|
|
268
|
+
handler: async (request, reply) => {
|
|
269
|
+
const { orderKey, revNo } = request.params;
|
|
270
|
+
const { description, changeSummary } = request.body;
|
|
271
|
+
const userId = request.erpUser.id;
|
|
272
|
+
const order = await resolveOrder(orderKey);
|
|
273
|
+
if (!order) {
|
|
274
|
+
return notFound(reply, `Order '${orderKey}' not found`);
|
|
275
|
+
}
|
|
276
|
+
const existing = await findExisting(order.id, revNo);
|
|
277
|
+
if (!existing) {
|
|
278
|
+
return notFound(reply, `Revision ${revNo} not found for order '${orderKey}'`);
|
|
279
|
+
}
|
|
280
|
+
const statusError = validateDraftStatus(existing.status);
|
|
281
|
+
if (statusError) {
|
|
282
|
+
return conflict(reply, statusError);
|
|
283
|
+
}
|
|
284
|
+
const revision = await updateRevision(existing.id, { description, changeSummary }, userId);
|
|
285
|
+
const full = await formatRevision(orderKey, request.erpUser, revision);
|
|
286
|
+
return mutationResult(request, reply, full, {
|
|
287
|
+
_actions: full._actions,
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
// DELETE (draft only)
|
|
292
|
+
app.delete("/:revNo", {
|
|
293
|
+
schema: {
|
|
294
|
+
description: "Delete a revision (draft status only)",
|
|
295
|
+
tags: ["Order Revisions"],
|
|
296
|
+
params: RevNoParamsSchema,
|
|
297
|
+
response: {
|
|
298
|
+
204: z.void(),
|
|
299
|
+
404: ErrorResponseSchema,
|
|
300
|
+
409: ErrorResponseSchema,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
preHandler: requirePermission("order_planner"),
|
|
304
|
+
handler: async (request, reply) => {
|
|
305
|
+
const { orderKey, revNo } = request.params;
|
|
306
|
+
const order = await resolveOrder(orderKey);
|
|
307
|
+
if (!order) {
|
|
308
|
+
return notFound(reply, `Order '${orderKey}' not found`);
|
|
309
|
+
}
|
|
310
|
+
const existing = await findExisting(order.id, revNo);
|
|
311
|
+
if (!existing) {
|
|
312
|
+
return notFound(reply, `Revision ${revNo} not found for order '${orderKey}'`);
|
|
313
|
+
}
|
|
314
|
+
const statusError = validateDraftStatus(existing.status);
|
|
315
|
+
if (statusError) {
|
|
316
|
+
return conflict(reply, statusError);
|
|
317
|
+
}
|
|
318
|
+
const runsError = await checkHasOrderRuns(existing.id);
|
|
319
|
+
if (runsError) {
|
|
320
|
+
return conflict(reply, runsError);
|
|
321
|
+
}
|
|
322
|
+
await deleteRevision(existing.id);
|
|
323
|
+
reply.status(204);
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=order-revisions.js.map
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { CompleteOrderRunSchema, ErrorResponseSchema, OrderRunStatus, OrderRunTransitionSchema, } from "@naisys/erp-shared";
|
|
2
|
+
import { requirePermission } from "../auth-middleware.js";
|
|
3
|
+
import { conflict, notFound, unprocessable } from "../error-handler.js";
|
|
4
|
+
import { resolveOrderRun, useFullSerializer, wantsFullResponse, } from "../route-helpers.js";
|
|
5
|
+
import { checkOpsComplete, completeOrderRun, getReopenTarget, sumOpRunCosts, transitionStatus, validateStatusFor, } from "../services/order-run-service.js";
|
|
6
|
+
import { formatRun, orderRunItemActions, RunNoParamsSchema, } from "./order-runs.js";
|
|
7
|
+
export default function orderRunTransitionRoutes(fastify) {
|
|
8
|
+
const app = fastify.withTypeProvider();
|
|
9
|
+
// START (released -> started)
|
|
10
|
+
app.post("/:runNo/start", {
|
|
11
|
+
schema: {
|
|
12
|
+
description: "Start an order run (released -> started)",
|
|
13
|
+
tags: ["Order Runs"],
|
|
14
|
+
params: RunNoParamsSchema,
|
|
15
|
+
response: {
|
|
16
|
+
200: OrderRunTransitionSchema,
|
|
17
|
+
404: ErrorResponseSchema,
|
|
18
|
+
409: ErrorResponseSchema,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
preHandler: requirePermission("order_executor"),
|
|
22
|
+
handler: async (request, reply) => {
|
|
23
|
+
const { orderKey, runNo } = request.params;
|
|
24
|
+
const resolved = await resolveOrderRun(orderKey, runNo);
|
|
25
|
+
if (!resolved) {
|
|
26
|
+
return notFound(reply, `Order run not found for order '${orderKey}'`);
|
|
27
|
+
}
|
|
28
|
+
const statusErr = validateStatusFor("start", resolved.run.status, [
|
|
29
|
+
OrderRunStatus.released,
|
|
30
|
+
]);
|
|
31
|
+
if (statusErr)
|
|
32
|
+
return conflict(reply, statusErr);
|
|
33
|
+
const userId = request.erpUser.id;
|
|
34
|
+
const run = await transitionStatus(resolved.run.id, "start", OrderRunStatus.released, OrderRunStatus.started, userId);
|
|
35
|
+
if (wantsFullResponse(request)) {
|
|
36
|
+
useFullSerializer(reply);
|
|
37
|
+
return formatRun(orderKey, request.erpUser, run);
|
|
38
|
+
}
|
|
39
|
+
const itemKey = run.order?.item?.key ?? null;
|
|
40
|
+
return {
|
|
41
|
+
status: run.status,
|
|
42
|
+
_actions: await orderRunItemActions(orderKey, runNo, run.id, run.status, itemKey, request.erpUser),
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
// CLOSE (started -> closed)
|
|
47
|
+
app.post("/:runNo/close", {
|
|
48
|
+
schema: {
|
|
49
|
+
description: "Close an order run (started -> closed)",
|
|
50
|
+
tags: ["Order Runs"],
|
|
51
|
+
params: RunNoParamsSchema,
|
|
52
|
+
response: {
|
|
53
|
+
200: OrderRunTransitionSchema,
|
|
54
|
+
404: ErrorResponseSchema,
|
|
55
|
+
409: ErrorResponseSchema,
|
|
56
|
+
422: ErrorResponseSchema,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
preHandler: requirePermission("order_executor"),
|
|
60
|
+
handler: async (request, reply) => {
|
|
61
|
+
const { orderKey, runNo } = request.params;
|
|
62
|
+
const resolved = await resolveOrderRun(orderKey, runNo);
|
|
63
|
+
if (!resolved) {
|
|
64
|
+
return notFound(reply, `Order run not found for order '${orderKey}'`);
|
|
65
|
+
}
|
|
66
|
+
const statusErr = validateStatusFor("close", resolved.run.status, [
|
|
67
|
+
OrderRunStatus.started,
|
|
68
|
+
]);
|
|
69
|
+
if (statusErr)
|
|
70
|
+
return conflict(reply, statusErr);
|
|
71
|
+
// Validate all operation runs are completed or skipped
|
|
72
|
+
const opsErr = await checkOpsComplete(resolved.run.id);
|
|
73
|
+
if (opsErr)
|
|
74
|
+
return unprocessable(reply, opsErr);
|
|
75
|
+
const userId = request.erpUser.id;
|
|
76
|
+
const cost = await sumOpRunCosts(resolved.run.id);
|
|
77
|
+
const run = await transitionStatus(resolved.run.id, "close", OrderRunStatus.started, OrderRunStatus.closed, userId, { cost });
|
|
78
|
+
if (wantsFullResponse(request)) {
|
|
79
|
+
useFullSerializer(reply);
|
|
80
|
+
return formatRun(orderKey, request.erpUser, run);
|
|
81
|
+
}
|
|
82
|
+
const itemKey = run.order?.item?.key ?? null;
|
|
83
|
+
return {
|
|
84
|
+
status: run.status,
|
|
85
|
+
_actions: await orderRunItemActions(orderKey, runNo, run.id, run.status, itemKey, request.erpUser),
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
// COMPLETE (started -> closed, creates item instance)
|
|
90
|
+
app.post("/:runNo/complete", {
|
|
91
|
+
schema: {
|
|
92
|
+
description: "Complete an order run — creates an item instance and closes the run",
|
|
93
|
+
tags: ["Order Runs"],
|
|
94
|
+
params: RunNoParamsSchema,
|
|
95
|
+
body: CompleteOrderRunSchema,
|
|
96
|
+
response: {
|
|
97
|
+
200: OrderRunTransitionSchema,
|
|
98
|
+
404: ErrorResponseSchema,
|
|
99
|
+
409: ErrorResponseSchema,
|
|
100
|
+
422: ErrorResponseSchema,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
preHandler: requirePermission("order_executor"),
|
|
104
|
+
handler: async (request, reply) => {
|
|
105
|
+
const { orderKey, runNo } = request.params;
|
|
106
|
+
const resolved = await resolveOrderRun(orderKey, runNo);
|
|
107
|
+
if (!resolved) {
|
|
108
|
+
return notFound(reply, `Order run not found for order '${orderKey}'`);
|
|
109
|
+
}
|
|
110
|
+
const statusErr = validateStatusFor("complete", resolved.run.status, [
|
|
111
|
+
OrderRunStatus.started,
|
|
112
|
+
]);
|
|
113
|
+
if (statusErr)
|
|
114
|
+
return conflict(reply, statusErr);
|
|
115
|
+
// Validate all operation runs are completed or skipped
|
|
116
|
+
const opsErr = await checkOpsComplete(resolved.run.id);
|
|
117
|
+
if (opsErr)
|
|
118
|
+
return unprocessable(reply, opsErr);
|
|
119
|
+
const userId = request.erpUser.id;
|
|
120
|
+
const result = await completeOrderRun(resolved.run.id, resolved.order.id, request.body, userId);
|
|
121
|
+
if (result.error) {
|
|
122
|
+
return unprocessable(reply, result.error);
|
|
123
|
+
}
|
|
124
|
+
const run = result.run;
|
|
125
|
+
if (wantsFullResponse(request)) {
|
|
126
|
+
useFullSerializer(reply);
|
|
127
|
+
return formatRun(orderKey, request.erpUser, run);
|
|
128
|
+
}
|
|
129
|
+
const itemKey = run.order?.item?.key ?? null;
|
|
130
|
+
return {
|
|
131
|
+
status: run.status,
|
|
132
|
+
_actions: await orderRunItemActions(orderKey, runNo, run.id, run.status, itemKey, request.erpUser),
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
// CANCEL (released/started -> cancelled)
|
|
137
|
+
app.post("/:runNo/cancel", {
|
|
138
|
+
schema: {
|
|
139
|
+
description: "Cancel an order run (released/started -> cancelled)",
|
|
140
|
+
tags: ["Order Runs"],
|
|
141
|
+
params: RunNoParamsSchema,
|
|
142
|
+
response: {
|
|
143
|
+
200: OrderRunTransitionSchema,
|
|
144
|
+
404: ErrorResponseSchema,
|
|
145
|
+
409: ErrorResponseSchema,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
preHandler: requirePermission("order_manager"),
|
|
149
|
+
handler: async (request, reply) => {
|
|
150
|
+
const { orderKey, runNo } = request.params;
|
|
151
|
+
const resolved = await resolveOrderRun(orderKey, runNo);
|
|
152
|
+
if (!resolved) {
|
|
153
|
+
return notFound(reply, `Order run not found for order '${orderKey}'`);
|
|
154
|
+
}
|
|
155
|
+
const statusErr = validateStatusFor("cancel", resolved.run.status, [
|
|
156
|
+
OrderRunStatus.released,
|
|
157
|
+
OrderRunStatus.started,
|
|
158
|
+
]);
|
|
159
|
+
if (statusErr)
|
|
160
|
+
return conflict(reply, statusErr);
|
|
161
|
+
const userId = request.erpUser.id;
|
|
162
|
+
const cost = await sumOpRunCosts(resolved.run.id);
|
|
163
|
+
const run = await transitionStatus(resolved.run.id, "cancel", resolved.run.status, OrderRunStatus.cancelled, userId, cost > 0 ? { cost } : undefined);
|
|
164
|
+
if (wantsFullResponse(request)) {
|
|
165
|
+
useFullSerializer(reply);
|
|
166
|
+
return formatRun(orderKey, request.erpUser, run);
|
|
167
|
+
}
|
|
168
|
+
const itemKey = run.order?.item?.key ?? null;
|
|
169
|
+
return {
|
|
170
|
+
status: run.status,
|
|
171
|
+
_actions: await orderRunItemActions(orderKey, runNo, run.id, run.status, itemKey, request.erpUser),
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
// REOPEN (closed -> started, cancelled -> released)
|
|
176
|
+
app.post("/:runNo/reopen", {
|
|
177
|
+
schema: {
|
|
178
|
+
description: "Reopen an order run (closed -> started, cancelled -> released)",
|
|
179
|
+
tags: ["Order Runs"],
|
|
180
|
+
params: RunNoParamsSchema,
|
|
181
|
+
response: {
|
|
182
|
+
200: OrderRunTransitionSchema,
|
|
183
|
+
404: ErrorResponseSchema,
|
|
184
|
+
409: ErrorResponseSchema,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
preHandler: requirePermission("order_manager"),
|
|
188
|
+
handler: async (request, reply) => {
|
|
189
|
+
const { orderKey, runNo } = request.params;
|
|
190
|
+
const resolved = await resolveOrderRun(orderKey, runNo);
|
|
191
|
+
if (!resolved) {
|
|
192
|
+
return notFound(reply, `Order run not found for order '${orderKey}'`);
|
|
193
|
+
}
|
|
194
|
+
const statusErr = validateStatusFor("reopen", resolved.run.status, [
|
|
195
|
+
OrderRunStatus.closed,
|
|
196
|
+
OrderRunStatus.cancelled,
|
|
197
|
+
]);
|
|
198
|
+
if (statusErr)
|
|
199
|
+
return conflict(reply, statusErr);
|
|
200
|
+
const reopenTo = getReopenTarget(resolved.run.status);
|
|
201
|
+
const userId = request.erpUser.id;
|
|
202
|
+
const run = await transitionStatus(resolved.run.id, "reopen", resolved.run.status, reopenTo, userId);
|
|
203
|
+
if (wantsFullResponse(request)) {
|
|
204
|
+
useFullSerializer(reply);
|
|
205
|
+
return formatRun(orderKey, request.erpUser, run);
|
|
206
|
+
}
|
|
207
|
+
const itemKey = run.order?.item?.key ?? null;
|
|
208
|
+
return {
|
|
209
|
+
status: run.status,
|
|
210
|
+
_actions: await orderRunItemActions(orderKey, runNo, run.id, run.status, itemKey, request.erpUser),
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=order-run-transitions.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { HateoasAction } from "@naisys/common";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
import { z } from "zod/v4";
|
|
4
|
+
import type { ErpUser } from "../auth-middleware.js";
|
|
5
|
+
import { type OrderRunWithRev } from "../services/order-run-service.js";
|
|
6
|
+
export declare function orderRunItemActions(orderKey: string, runNo: number, runId: number, status: string, itemKey: string | null, user: ErpUser | undefined): Promise<HateoasAction[]>;
|
|
7
|
+
export declare const RunNoParamsSchema: z.ZodObject<{
|
|
8
|
+
orderKey: z.ZodString;
|
|
9
|
+
runNo: z.ZodCoercedNumber<unknown>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export declare function formatRun(orderKey: string, user: ErpUser | undefined, run: OrderRunWithRev): Promise<{
|
|
12
|
+
_links: {
|
|
13
|
+
rel: string;
|
|
14
|
+
href: string;
|
|
15
|
+
method?: string | undefined;
|
|
16
|
+
title?: string | undefined;
|
|
17
|
+
schema?: string | undefined;
|
|
18
|
+
}[];
|
|
19
|
+
_actions: {
|
|
20
|
+
rel: string;
|
|
21
|
+
href: string;
|
|
22
|
+
method: string;
|
|
23
|
+
title?: string | undefined;
|
|
24
|
+
schema?: string | undefined;
|
|
25
|
+
body?: Record<string, unknown> | undefined;
|
|
26
|
+
alternateEncoding?: {
|
|
27
|
+
contentType: string;
|
|
28
|
+
fileFields: string[];
|
|
29
|
+
description?: string | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
disabled?: boolean | undefined;
|
|
32
|
+
disabledReason?: string | string[] | undefined;
|
|
33
|
+
}[];
|
|
34
|
+
createdAt: string;
|
|
35
|
+
createdBy: string;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
updatedBy: string;
|
|
38
|
+
id: number;
|
|
39
|
+
runNo: number;
|
|
40
|
+
orderId: number;
|
|
41
|
+
orderKey: string;
|
|
42
|
+
revNo: number;
|
|
43
|
+
itemKey: string | null;
|
|
44
|
+
instanceId: number;
|
|
45
|
+
instanceKey: string;
|
|
46
|
+
status: import("../generated/prisma/enums.js").OrderRunStatus;
|
|
47
|
+
priority: import("../generated/prisma/enums.js").OrderRunPriority;
|
|
48
|
+
cost: number | null;
|
|
49
|
+
dueAt: string | null;
|
|
50
|
+
releaseNote: string | null;
|
|
51
|
+
operationSummary: {
|
|
52
|
+
seqNo: number;
|
|
53
|
+
title: string;
|
|
54
|
+
status: import("../generated/prisma/enums.js").OperationRunStatus;
|
|
55
|
+
}[];
|
|
56
|
+
}>;
|
|
57
|
+
export default function orderRunRoutes(fastify: FastifyInstance): void;
|
|
58
|
+
//# sourceMappingURL=order-runs.d.ts.map
|