@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,123 @@
|
|
|
1
|
+
import { hasPermission } from "../auth-middleware.js";
|
|
2
|
+
export default function rootRoute(fastify) {
|
|
3
|
+
fastify.get("/", {
|
|
4
|
+
schema: {
|
|
5
|
+
description: "API discovery root - lists all available resources and actions",
|
|
6
|
+
tags: ["Discovery"],
|
|
7
|
+
},
|
|
8
|
+
handler: (request) => {
|
|
9
|
+
const publicRead = process.env.PUBLIC_READ === "true";
|
|
10
|
+
const base = {
|
|
11
|
+
name: "NAISYS ERP API",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
description: "AI-first ERP system",
|
|
14
|
+
};
|
|
15
|
+
const readLinks = [
|
|
16
|
+
{
|
|
17
|
+
rel: "orders",
|
|
18
|
+
href: "/api/erp/orders",
|
|
19
|
+
title: "Orders",
|
|
20
|
+
method: "GET",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
rel: "items",
|
|
24
|
+
href: "/api/erp/items",
|
|
25
|
+
title: "Items",
|
|
26
|
+
method: "GET",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
rel: "dispatch",
|
|
30
|
+
href: "/api/erp/dispatch",
|
|
31
|
+
title: "Dispatch (open order runs)",
|
|
32
|
+
method: "GET",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
rel: "schemas",
|
|
36
|
+
href: "/api/erp/schemas/",
|
|
37
|
+
title: "Schema Catalog",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
rel: "api-reference",
|
|
41
|
+
href: "/erp/api-reference",
|
|
42
|
+
title: "Interactive API Reference (Scalar)",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
if (request.erpUser) {
|
|
46
|
+
const authLinks = [
|
|
47
|
+
{
|
|
48
|
+
rel: "self",
|
|
49
|
+
href: "/api/erp/",
|
|
50
|
+
title: "API Root",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
rel: "me",
|
|
54
|
+
href: "/api/erp/auth/me",
|
|
55
|
+
title: "Current User",
|
|
56
|
+
},
|
|
57
|
+
...readLinks,
|
|
58
|
+
];
|
|
59
|
+
authLinks.push({
|
|
60
|
+
rel: "work-centers",
|
|
61
|
+
href: "/api/erp/work-centers",
|
|
62
|
+
title: "Work Centers",
|
|
63
|
+
method: "GET",
|
|
64
|
+
});
|
|
65
|
+
if (hasPermission(request.erpUser, "erp_admin")) {
|
|
66
|
+
authLinks.push({
|
|
67
|
+
rel: "users",
|
|
68
|
+
href: "/api/erp/users",
|
|
69
|
+
title: "Users",
|
|
70
|
+
method: "GET",
|
|
71
|
+
}, {
|
|
72
|
+
rel: "admin",
|
|
73
|
+
href: "/api/erp/admin",
|
|
74
|
+
title: "Admin",
|
|
75
|
+
method: "GET",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...base,
|
|
80
|
+
_links: authLinks,
|
|
81
|
+
_actions: [
|
|
82
|
+
{
|
|
83
|
+
rel: "logout",
|
|
84
|
+
href: "/api/erp/auth/logout",
|
|
85
|
+
method: "POST",
|
|
86
|
+
title: "Logout",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...base,
|
|
93
|
+
_links: [
|
|
94
|
+
{
|
|
95
|
+
rel: "self",
|
|
96
|
+
href: "/api/erp/",
|
|
97
|
+
title: "API Root",
|
|
98
|
+
},
|
|
99
|
+
...(publicRead
|
|
100
|
+
? readLinks
|
|
101
|
+
: [
|
|
102
|
+
{
|
|
103
|
+
rel: "schemas",
|
|
104
|
+
href: "/api/erp/schemas/",
|
|
105
|
+
title: "Schema Catalog",
|
|
106
|
+
},
|
|
107
|
+
]),
|
|
108
|
+
],
|
|
109
|
+
_actions: [
|
|
110
|
+
{
|
|
111
|
+
rel: "login",
|
|
112
|
+
href: "/api/erp/auth/login",
|
|
113
|
+
method: "POST",
|
|
114
|
+
title: "Login",
|
|
115
|
+
schema: "/api/erp/schemas/LoginRequest",
|
|
116
|
+
body: { username: "", password: "" },
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=root.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
import { schemaRegistry } from "../schema-registry.js";
|
|
3
|
+
export default function schemaRoutes(fastify) {
|
|
4
|
+
// List all available schema names
|
|
5
|
+
fastify.get("/", {
|
|
6
|
+
schema: {
|
|
7
|
+
description: "List all available schema names",
|
|
8
|
+
tags: ["Discovery"],
|
|
9
|
+
},
|
|
10
|
+
handler: () => {
|
|
11
|
+
return { schemas: Object.keys(schemaRegistry) };
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
// Get a single schema by name
|
|
15
|
+
fastify.get("/:schemaName", {
|
|
16
|
+
schema: {
|
|
17
|
+
description: "Get a JSON Schema by name",
|
|
18
|
+
tags: ["Discovery"],
|
|
19
|
+
},
|
|
20
|
+
handler: (request, reply) => {
|
|
21
|
+
const { schemaName } = request.params;
|
|
22
|
+
const zodSchema = schemaRegistry[schemaName];
|
|
23
|
+
if (!zodSchema) {
|
|
24
|
+
reply.code(404);
|
|
25
|
+
return { error: "Schema not found", schemaName };
|
|
26
|
+
}
|
|
27
|
+
return z.toJSONSchema(zodSchema);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { mimeFromFilename } from "@naisys/common";
|
|
2
|
+
import { ErrorResponseSchema, UploadAttachmentResponseSchema, } from "@naisys/erp-shared";
|
|
3
|
+
import { createReadStream, existsSync, statSync } from "fs";
|
|
4
|
+
import { z } from "zod/v4";
|
|
5
|
+
import { requirePermission } from "../auth-middleware.js";
|
|
6
|
+
import erpDb from "../erpDb.js";
|
|
7
|
+
import { conflict, notFound } from "../error-handler.js";
|
|
8
|
+
import { checkOpRunInProgress, checkOrderRunStarted, checkWorkCenterAccess, resolveStepRun, } from "../route-helpers.js";
|
|
9
|
+
import { deleteFieldAttachment, getAttachmentFilePath, uploadAttachment, } from "../services/attachment-service.js";
|
|
10
|
+
import { ensureStepRunFieldRecord } from "../services/field-service.js";
|
|
11
|
+
import { findStepRunWithField, rebuildAttachmentFieldValue, upsertFieldValue, } from "../services/field-value-service.js";
|
|
12
|
+
import { isUserClockedIn } from "../services/labor-ticket-service.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
|
+
setIndex: z.coerce.number().int().optional(),
|
|
19
|
+
fieldSeqNo: z.coerce.number().int(),
|
|
20
|
+
});
|
|
21
|
+
const AttachmentIdParamsSchema = z.object({
|
|
22
|
+
orderKey: z.string(),
|
|
23
|
+
runNo: z.coerce.number().int(),
|
|
24
|
+
seqNo: z.coerce.number().int(),
|
|
25
|
+
stepSeqNo: z.coerce.number().int(),
|
|
26
|
+
setIndex: z.coerce.number().int().optional(),
|
|
27
|
+
fieldSeqNo: z.coerce.number().int(),
|
|
28
|
+
attachmentId: z.string(),
|
|
29
|
+
});
|
|
30
|
+
export default function stepFieldAttachmentRoutes(fastify) {
|
|
31
|
+
const app = fastify.withTypeProvider();
|
|
32
|
+
// UPLOAD attachment for a field value
|
|
33
|
+
app.post("/", {
|
|
34
|
+
schema: {
|
|
35
|
+
description: "Upload a file attachment for an attachment-type field (multipart/form-data)",
|
|
36
|
+
tags: ["Attachments"],
|
|
37
|
+
params: FieldSeqNoParamsSchema,
|
|
38
|
+
// No body schema — multipart parsed manually
|
|
39
|
+
response: {
|
|
40
|
+
200: UploadAttachmentResponseSchema,
|
|
41
|
+
400: ErrorResponseSchema,
|
|
42
|
+
404: ErrorResponseSchema,
|
|
43
|
+
409: ErrorResponseSchema,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
preHandler: requirePermission("order_executor"),
|
|
47
|
+
handler: async (request, reply) => {
|
|
48
|
+
const { orderKey, runNo, seqNo, stepSeqNo, fieldSeqNo } = request.params;
|
|
49
|
+
const userId = request.erpUser.id;
|
|
50
|
+
const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
|
|
51
|
+
if (!resolved) {
|
|
52
|
+
return notFound(reply, `Step run not found`);
|
|
53
|
+
}
|
|
54
|
+
const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
|
|
55
|
+
if (wcErr)
|
|
56
|
+
return conflict(reply, wcErr);
|
|
57
|
+
const orderErr = checkOrderRunStarted(resolved.run.status);
|
|
58
|
+
if (orderErr)
|
|
59
|
+
return conflict(reply, orderErr);
|
|
60
|
+
const opErr = checkOpRunInProgress(resolved.opRun.status);
|
|
61
|
+
if (opErr)
|
|
62
|
+
return conflict(reply, opErr);
|
|
63
|
+
const clockedIn = await isUserClockedIn(resolved.opRun.id, userId);
|
|
64
|
+
if (!clockedIn)
|
|
65
|
+
return conflict(reply, `You must be clocked in to upload attachments`);
|
|
66
|
+
const stepRun = await findStepRunWithField(resolved.stepRun.id, resolved.opRun.id, fieldSeqNo);
|
|
67
|
+
if (!stepRun)
|
|
68
|
+
return notFound(reply, `Step run not found`);
|
|
69
|
+
if (stepRun.completed) {
|
|
70
|
+
return conflict(reply, `Cannot upload: step run is completed`);
|
|
71
|
+
}
|
|
72
|
+
const field = stepRun.step.fieldSet?.fields[0];
|
|
73
|
+
if (!field) {
|
|
74
|
+
return notFound(reply, `Step field not found`);
|
|
75
|
+
}
|
|
76
|
+
if (field.type !== "attachment") {
|
|
77
|
+
return reply.code(400).send({
|
|
78
|
+
statusCode: 400,
|
|
79
|
+
error: "Bad Request",
|
|
80
|
+
message: "Field is not an attachment type",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Parse multipart — expect a single file field named "file"
|
|
84
|
+
const data = await request.file();
|
|
85
|
+
if (!data) {
|
|
86
|
+
return reply.code(400).send({
|
|
87
|
+
statusCode: 400,
|
|
88
|
+
error: "Bad Request",
|
|
89
|
+
message: "No file uploaded",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const fileBuffer = await data.toBuffer();
|
|
93
|
+
const filename = data.filename || "unnamed_file";
|
|
94
|
+
// setIndex comes from URL params (via /sets/:setIndex/ path), default 0
|
|
95
|
+
const setIndex = request.params.setIndex ?? 0;
|
|
96
|
+
const fieldRecordId = await ensureStepRunFieldRecord(resolved.stepRun.id, userId);
|
|
97
|
+
if (!fieldRecordId) {
|
|
98
|
+
return notFound(reply, "Step has no field set");
|
|
99
|
+
}
|
|
100
|
+
// For non-isArray fields, reject if an attachment already exists
|
|
101
|
+
if (!field.isArray) {
|
|
102
|
+
const existing = await erpDb.fieldValue.findUnique({
|
|
103
|
+
where: {
|
|
104
|
+
fieldRecordId_fieldId_setIndex: {
|
|
105
|
+
fieldRecordId,
|
|
106
|
+
fieldId: field.id,
|
|
107
|
+
setIndex,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
select: { _count: { select: { fieldAttachments: true } } },
|
|
111
|
+
});
|
|
112
|
+
if (existing && existing._count.fieldAttachments > 0) {
|
|
113
|
+
return reply.code(400).send({
|
|
114
|
+
statusCode: 400,
|
|
115
|
+
error: "Bad Request",
|
|
116
|
+
message: "Field already has an attachment. Delete the existing attachment before uploading a new one, or enable multi-value on the field.",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Ensure a FieldValue row exists for this field+set
|
|
121
|
+
await upsertFieldValue(fieldRecordId, field.id, setIndex, "", userId);
|
|
122
|
+
// Find the field value ID
|
|
123
|
+
const fieldValueRow = await erpDb.fieldValue.findUnique({
|
|
124
|
+
where: {
|
|
125
|
+
fieldRecordId_fieldId_setIndex: {
|
|
126
|
+
fieldRecordId,
|
|
127
|
+
fieldId: field.id,
|
|
128
|
+
setIndex,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
select: { id: true },
|
|
132
|
+
});
|
|
133
|
+
if (!fieldValueRow) {
|
|
134
|
+
return notFound(reply, `Step field value not found`);
|
|
135
|
+
}
|
|
136
|
+
const result = await uploadAttachment(fileBuffer, filename, userId, fieldValueRow.id);
|
|
137
|
+
// Auto-set the field value to reflect all current attachments
|
|
138
|
+
await rebuildAttachmentFieldValue(fieldRecordId, field.id, setIndex, field.isArray, userId);
|
|
139
|
+
return {
|
|
140
|
+
attachmentId: result.attachmentId,
|
|
141
|
+
filename: result.filename,
|
|
142
|
+
fileSize: result.fileSize,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
// DOWNLOAD attachment
|
|
147
|
+
fastify.get("/:attachmentId", {
|
|
148
|
+
schema: {
|
|
149
|
+
description: "Download an attachment file",
|
|
150
|
+
tags: ["Attachments"],
|
|
151
|
+
params: AttachmentIdParamsSchema,
|
|
152
|
+
},
|
|
153
|
+
}, async (request, reply) => {
|
|
154
|
+
const att = await getAttachmentFilePath(request.params.attachmentId);
|
|
155
|
+
if (!att) {
|
|
156
|
+
return notFound(reply, `Attachment not found`);
|
|
157
|
+
}
|
|
158
|
+
if (!existsSync(att.filepath)) {
|
|
159
|
+
return notFound(reply, `Attachment file missing from disk`);
|
|
160
|
+
}
|
|
161
|
+
const stat = statSync(att.filepath);
|
|
162
|
+
const mimeType = mimeFromFilename(att.filename);
|
|
163
|
+
const isImage = mimeType.startsWith("image/");
|
|
164
|
+
reply.header("content-type", mimeType);
|
|
165
|
+
reply.header("content-disposition", `${isImage ? "inline" : "attachment"}; filename="${att.filename.replace(/"/g, '\\"')}"`);
|
|
166
|
+
reply.header("content-length", stat.size);
|
|
167
|
+
return reply.send(createReadStream(att.filepath));
|
|
168
|
+
});
|
|
169
|
+
// DELETE attachment from a field value
|
|
170
|
+
app.delete("/:attachmentId", {
|
|
171
|
+
schema: {
|
|
172
|
+
description: "Delete an attachment from a field (also updates the field value)",
|
|
173
|
+
tags: ["Attachments"],
|
|
174
|
+
params: AttachmentIdParamsSchema,
|
|
175
|
+
response: {
|
|
176
|
+
200: z.object({ deleted: z.literal(true) }),
|
|
177
|
+
404: ErrorResponseSchema,
|
|
178
|
+
409: ErrorResponseSchema,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
preHandler: requirePermission("order_executor"),
|
|
182
|
+
handler: async (request, reply) => {
|
|
183
|
+
const { orderKey, runNo, seqNo, stepSeqNo, fieldSeqNo, attachmentId } = request.params;
|
|
184
|
+
const userId = request.erpUser.id;
|
|
185
|
+
const resolved = await resolveStepRun(orderKey, runNo, seqNo, stepSeqNo);
|
|
186
|
+
if (!resolved)
|
|
187
|
+
return notFound(reply, "Step run not found");
|
|
188
|
+
const wcErr = await checkWorkCenterAccess(resolved.opRun.operationId, request.erpUser);
|
|
189
|
+
if (wcErr)
|
|
190
|
+
return conflict(reply, wcErr);
|
|
191
|
+
const orderErr = checkOrderRunStarted(resolved.run.status);
|
|
192
|
+
if (orderErr)
|
|
193
|
+
return conflict(reply, orderErr);
|
|
194
|
+
const opErr = checkOpRunInProgress(resolved.opRun.status);
|
|
195
|
+
if (opErr)
|
|
196
|
+
return conflict(reply, opErr);
|
|
197
|
+
const clockedIn = await isUserClockedIn(resolved.opRun.id, userId);
|
|
198
|
+
if (!clockedIn)
|
|
199
|
+
return conflict(reply, "You must be clocked in to delete attachments");
|
|
200
|
+
const stepRun = await findStepRunWithField(resolved.stepRun.id, resolved.opRun.id, fieldSeqNo);
|
|
201
|
+
if (!stepRun)
|
|
202
|
+
return notFound(reply, "Step run not found");
|
|
203
|
+
if (stepRun.completed)
|
|
204
|
+
return conflict(reply, "Cannot delete: step run is completed");
|
|
205
|
+
const field = stepRun.step.fieldSet?.fields[0];
|
|
206
|
+
if (!field)
|
|
207
|
+
return notFound(reply, "Step field not found");
|
|
208
|
+
const setIndex = request.params.setIndex ?? 0;
|
|
209
|
+
const fieldRecordId = await ensureStepRunFieldRecord(resolved.stepRun.id, userId);
|
|
210
|
+
if (!fieldRecordId)
|
|
211
|
+
return notFound(reply, "Step has no field set");
|
|
212
|
+
const fieldValueRow = await erpDb.fieldValue.findUnique({
|
|
213
|
+
where: {
|
|
214
|
+
fieldRecordId_fieldId_setIndex: {
|
|
215
|
+
fieldRecordId,
|
|
216
|
+
fieldId: field.id,
|
|
217
|
+
setIndex,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
select: { id: true },
|
|
221
|
+
});
|
|
222
|
+
if (!fieldValueRow)
|
|
223
|
+
return notFound(reply, "Field value not found");
|
|
224
|
+
await deleteFieldAttachment(fieldValueRow.id, attachmentId);
|
|
225
|
+
// Rebuild field value to reflect remaining attachments
|
|
226
|
+
await rebuildAttachmentFieldValue(fieldRecordId, field.id, setIndex, field.isArray, userId);
|
|
227
|
+
return { deleted: true };
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=step-field-attachments.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { ErpUser } from "../auth-middleware.js";
|
|
3
|
+
import { type FieldWithUsers } from "../services/field-service.js";
|
|
4
|
+
export { type FieldWithUsers } from "../services/field-service.js";
|
|
5
|
+
export declare function formatFieldListResponse(orderKey: string, revNo: number, opSeqNo: number, stepSeqNo: number, revStatus: string, user: ErpUser | undefined, items: FieldWithUsers[]): {
|
|
6
|
+
items: {
|
|
7
|
+
_actions: {
|
|
8
|
+
rel: string;
|
|
9
|
+
href: string;
|
|
10
|
+
method: string;
|
|
11
|
+
title?: string | undefined;
|
|
12
|
+
schema?: string | undefined;
|
|
13
|
+
body?: Record<string, unknown> | undefined;
|
|
14
|
+
alternateEncoding?: {
|
|
15
|
+
contentType: string;
|
|
16
|
+
fileFields: string[];
|
|
17
|
+
description?: string | undefined;
|
|
18
|
+
} | undefined;
|
|
19
|
+
disabled?: boolean | undefined;
|
|
20
|
+
disabledReason?: string | string[] | undefined;
|
|
21
|
+
}[];
|
|
22
|
+
createdAt: string;
|
|
23
|
+
createdBy: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
updatedBy: string;
|
|
26
|
+
id: number;
|
|
27
|
+
fieldSetId: number;
|
|
28
|
+
seqNo: number;
|
|
29
|
+
label: string;
|
|
30
|
+
type: import("../generated/prisma/enums.js").FieldType;
|
|
31
|
+
isArray: boolean;
|
|
32
|
+
required: boolean;
|
|
33
|
+
}[];
|
|
34
|
+
total: number;
|
|
35
|
+
nextSeqNo: number;
|
|
36
|
+
_links: {
|
|
37
|
+
rel: string;
|
|
38
|
+
href: string;
|
|
39
|
+
method?: string | undefined;
|
|
40
|
+
title?: string | undefined;
|
|
41
|
+
schema?: string | undefined;
|
|
42
|
+
}[];
|
|
43
|
+
_linkTemplates: {
|
|
44
|
+
rel: string;
|
|
45
|
+
hrefTemplate: string;
|
|
46
|
+
}[];
|
|
47
|
+
_actions: {
|
|
48
|
+
rel: string;
|
|
49
|
+
href: string;
|
|
50
|
+
method: string;
|
|
51
|
+
title?: string | undefined;
|
|
52
|
+
schema?: string | undefined;
|
|
53
|
+
body?: Record<string, unknown> | undefined;
|
|
54
|
+
alternateEncoding?: {
|
|
55
|
+
contentType: string;
|
|
56
|
+
fileFields: string[];
|
|
57
|
+
description?: string | undefined;
|
|
58
|
+
} | undefined;
|
|
59
|
+
disabled?: boolean | undefined;
|
|
60
|
+
disabledReason?: string | string[] | undefined;
|
|
61
|
+
}[];
|
|
62
|
+
};
|
|
63
|
+
export declare function fieldBasePath(orderKey: string, revNo: number, opSeqNo: number, stepSeqNo: number): string;
|
|
64
|
+
export declare function formatField(orderKey: string, revNo: number, opSeqNo: number, stepSeqNo: number, revStatus: string, user: ErpUser | undefined, field: FieldWithUsers): {
|
|
65
|
+
_links: {
|
|
66
|
+
rel: string;
|
|
67
|
+
href: string;
|
|
68
|
+
method?: string | undefined;
|
|
69
|
+
title?: string | undefined;
|
|
70
|
+
schema?: string | undefined;
|
|
71
|
+
}[];
|
|
72
|
+
_actions: {
|
|
73
|
+
rel: string;
|
|
74
|
+
href: string;
|
|
75
|
+
method: string;
|
|
76
|
+
title?: string | undefined;
|
|
77
|
+
schema?: string | undefined;
|
|
78
|
+
body?: Record<string, unknown> | undefined;
|
|
79
|
+
alternateEncoding?: {
|
|
80
|
+
contentType: string;
|
|
81
|
+
fileFields: string[];
|
|
82
|
+
description?: string | undefined;
|
|
83
|
+
} | undefined;
|
|
84
|
+
disabled?: boolean | undefined;
|
|
85
|
+
disabledReason?: string | string[] | undefined;
|
|
86
|
+
}[];
|
|
87
|
+
createdAt: string;
|
|
88
|
+
createdBy: string;
|
|
89
|
+
updatedAt: string;
|
|
90
|
+
updatedBy: string;
|
|
91
|
+
id: number;
|
|
92
|
+
fieldSetId: number;
|
|
93
|
+
seqNo: number;
|
|
94
|
+
label: string;
|
|
95
|
+
type: import("../generated/prisma/enums.js").FieldType;
|
|
96
|
+
isArray: boolean;
|
|
97
|
+
required: boolean;
|
|
98
|
+
};
|
|
99
|
+
export default function stepFieldRoutes(fastify: FastifyInstance): void;
|
|
100
|
+
//# sourceMappingURL=step-fields.d.ts.map
|