@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.

Files changed (201) hide show
  1. package/bin/naisys-erp +2 -0
  2. package/client-dist/android-chrome-192x192.png +0 -0
  3. package/client-dist/android-chrome-512x512.png +0 -0
  4. package/client-dist/apple-touch-icon.png +0 -0
  5. package/client-dist/assets/index-45dVo30p.css +1 -0
  6. package/client-dist/assets/index-Dffms7F_.js +168 -0
  7. package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
  8. package/client-dist/favicon.ico +0 -0
  9. package/client-dist/index.html +42 -0
  10. package/client-dist/site.webmanifest +22 -0
  11. package/dist/api-reference.d.ts +10 -0
  12. package/dist/api-reference.js +101 -0
  13. package/dist/audit.d.ts +5 -0
  14. package/dist/audit.js +14 -0
  15. package/dist/auth-middleware.d.ts +18 -0
  16. package/dist/auth-middleware.js +203 -0
  17. package/dist/dbConfig.d.ts +5 -0
  18. package/dist/dbConfig.js +10 -0
  19. package/dist/erpDb.d.ts +10 -0
  20. package/dist/erpDb.js +34 -0
  21. package/dist/erpServer.d.ts +10 -0
  22. package/dist/erpServer.js +321 -0
  23. package/dist/error-handler.d.ts +7 -0
  24. package/dist/error-handler.js +17 -0
  25. package/dist/generated/prisma/client.d.ts +154 -0
  26. package/dist/generated/prisma/client.js +35 -0
  27. package/dist/generated/prisma/commonInputTypes.d.ts +637 -0
  28. package/dist/generated/prisma/commonInputTypes.js +11 -0
  29. package/dist/generated/prisma/enums.d.ts +59 -0
  30. package/dist/generated/prisma/enums.js +60 -0
  31. package/dist/generated/prisma/internal/class.d.ts +406 -0
  32. package/dist/generated/prisma/internal/class.js +50 -0
  33. package/dist/generated/prisma/internal/prismaNamespace.d.ts +2722 -0
  34. package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
  35. package/dist/generated/prisma/models/Attachment.d.ts +1455 -0
  36. package/dist/generated/prisma/models/Attachment.js +2 -0
  37. package/dist/generated/prisma/models/AuditLog.d.ts +1359 -0
  38. package/dist/generated/prisma/models/AuditLog.js +2 -0
  39. package/dist/generated/prisma/models/Field.d.ts +1880 -0
  40. package/dist/generated/prisma/models/Field.js +2 -0
  41. package/dist/generated/prisma/models/FieldAttachment.d.ts +1245 -0
  42. package/dist/generated/prisma/models/FieldAttachment.js +2 -0
  43. package/dist/generated/prisma/models/FieldRecord.d.ts +1625 -0
  44. package/dist/generated/prisma/models/FieldRecord.js +2 -0
  45. package/dist/generated/prisma/models/FieldSet.d.ts +1577 -0
  46. package/dist/generated/prisma/models/FieldSet.js +2 -0
  47. package/dist/generated/prisma/models/FieldValue.d.ts +1908 -0
  48. package/dist/generated/prisma/models/FieldValue.js +2 -0
  49. package/dist/generated/prisma/models/Item.d.ts +1858 -0
  50. package/dist/generated/prisma/models/Item.js +2 -0
  51. package/dist/generated/prisma/models/ItemInstance.d.ts +1987 -0
  52. package/dist/generated/prisma/models/ItemInstance.js +2 -0
  53. package/dist/generated/prisma/models/LaborTicket.d.ts +1867 -0
  54. package/dist/generated/prisma/models/LaborTicket.js +2 -0
  55. package/dist/generated/prisma/models/Operation.d.ts +2578 -0
  56. package/dist/generated/prisma/models/Operation.js +2 -0
  57. package/dist/generated/prisma/models/OperationDependency.d.ts +1434 -0
  58. package/dist/generated/prisma/models/OperationDependency.js +2 -0
  59. package/dist/generated/prisma/models/OperationFieldRef.d.ts +1539 -0
  60. package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
  61. package/dist/generated/prisma/models/OperationRun.d.ts +2563 -0
  62. package/dist/generated/prisma/models/OperationRun.js +2 -0
  63. package/dist/generated/prisma/models/OperationRunComment.d.ts +1366 -0
  64. package/dist/generated/prisma/models/OperationRunComment.js +2 -0
  65. package/dist/generated/prisma/models/Order.d.ts +1931 -0
  66. package/dist/generated/prisma/models/Order.js +2 -0
  67. package/dist/generated/prisma/models/OrderRevision.d.ts +1962 -0
  68. package/dist/generated/prisma/models/OrderRevision.js +2 -0
  69. package/dist/generated/prisma/models/OrderRun.d.ts +2310 -0
  70. package/dist/generated/prisma/models/OrderRun.js +2 -0
  71. package/dist/generated/prisma/models/SchemaVersion.d.ts +985 -0
  72. package/dist/generated/prisma/models/SchemaVersion.js +2 -0
  73. package/dist/generated/prisma/models/Session.d.ts +1213 -0
  74. package/dist/generated/prisma/models/Session.js +2 -0
  75. package/dist/generated/prisma/models/Step.d.ts +2180 -0
  76. package/dist/generated/prisma/models/Step.js +2 -0
  77. package/dist/generated/prisma/models/StepRun.d.ts +1963 -0
  78. package/dist/generated/prisma/models/StepRun.js +2 -0
  79. package/dist/generated/prisma/models/User.d.ts +11819 -0
  80. package/dist/generated/prisma/models/User.js +2 -0
  81. package/dist/generated/prisma/models/UserPermission.d.ts +1348 -0
  82. package/dist/generated/prisma/models/UserPermission.js +2 -0
  83. package/dist/generated/prisma/models/WorkCenter.d.ts +1657 -0
  84. package/dist/generated/prisma/models/WorkCenter.js +2 -0
  85. package/dist/generated/prisma/models/WorkCenterUser.d.ts +1390 -0
  86. package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
  87. package/dist/generated/prisma/models.d.ts +28 -0
  88. package/dist/generated/prisma/models.js +2 -0
  89. package/dist/hateoas.d.ts +7 -0
  90. package/dist/hateoas.js +61 -0
  91. package/dist/route-helpers.d.ts +318 -0
  92. package/dist/route-helpers.js +220 -0
  93. package/dist/routes/admin.d.ts +3 -0
  94. package/dist/routes/admin.js +147 -0
  95. package/dist/routes/audit.d.ts +3 -0
  96. package/dist/routes/audit.js +36 -0
  97. package/dist/routes/auth.d.ts +3 -0
  98. package/dist/routes/auth.js +112 -0
  99. package/dist/routes/dispatch.d.ts +3 -0
  100. package/dist/routes/dispatch.js +174 -0
  101. package/dist/routes/inventory.d.ts +3 -0
  102. package/dist/routes/inventory.js +70 -0
  103. package/dist/routes/item-fields.d.ts +3 -0
  104. package/dist/routes/item-fields.js +220 -0
  105. package/dist/routes/item-instances.d.ts +3 -0
  106. package/dist/routes/item-instances.js +426 -0
  107. package/dist/routes/items.d.ts +3 -0
  108. package/dist/routes/items.js +252 -0
  109. package/dist/routes/labor-tickets.d.ts +3 -0
  110. package/dist/routes/labor-tickets.js +268 -0
  111. package/dist/routes/operation-dependencies.d.ts +3 -0
  112. package/dist/routes/operation-dependencies.js +170 -0
  113. package/dist/routes/operation-field-refs.d.ts +3 -0
  114. package/dist/routes/operation-field-refs.js +263 -0
  115. package/dist/routes/operation-run-comments.d.ts +3 -0
  116. package/dist/routes/operation-run-comments.js +108 -0
  117. package/dist/routes/operation-run-transitions.d.ts +3 -0
  118. package/dist/routes/operation-run-transitions.js +249 -0
  119. package/dist/routes/operation-runs.d.ts +112 -0
  120. package/dist/routes/operation-runs.js +299 -0
  121. package/dist/routes/operations.d.ts +3 -0
  122. package/dist/routes/operations.js +283 -0
  123. package/dist/routes/order-revision-transitions.d.ts +3 -0
  124. package/dist/routes/order-revision-transitions.js +86 -0
  125. package/dist/routes/order-revisions.d.ts +51 -0
  126. package/dist/routes/order-revisions.js +327 -0
  127. package/dist/routes/order-run-transitions.d.ts +3 -0
  128. package/dist/routes/order-run-transitions.js +215 -0
  129. package/dist/routes/order-runs.d.ts +58 -0
  130. package/dist/routes/order-runs.js +335 -0
  131. package/dist/routes/orders.d.ts +3 -0
  132. package/dist/routes/orders.js +262 -0
  133. package/dist/routes/root.d.ts +3 -0
  134. package/dist/routes/root.js +123 -0
  135. package/dist/routes/schemas.d.ts +3 -0
  136. package/dist/routes/schemas.js +31 -0
  137. package/dist/routes/step-field-attachments.d.ts +3 -0
  138. package/dist/routes/step-field-attachments.js +231 -0
  139. package/dist/routes/step-fields.d.ts +100 -0
  140. package/dist/routes/step-fields.js +315 -0
  141. package/dist/routes/step-run-fields.d.ts +3 -0
  142. package/dist/routes/step-run-fields.js +438 -0
  143. package/dist/routes/step-run-transitions.d.ts +3 -0
  144. package/dist/routes/step-run-transitions.js +113 -0
  145. package/dist/routes/step-runs.d.ts +332 -0
  146. package/dist/routes/step-runs.js +324 -0
  147. package/dist/routes/steps.d.ts +3 -0
  148. package/dist/routes/steps.js +283 -0
  149. package/dist/routes/user-permissions.d.ts +3 -0
  150. package/dist/routes/user-permissions.js +100 -0
  151. package/dist/routes/users.d.ts +57 -0
  152. package/dist/routes/users.js +381 -0
  153. package/dist/routes/work-centers.d.ts +3 -0
  154. package/dist/routes/work-centers.js +280 -0
  155. package/dist/schema-registry.d.ts +3 -0
  156. package/dist/schema-registry.js +45 -0
  157. package/dist/services/attachment-service.d.ts +33 -0
  158. package/dist/services/attachment-service.js +118 -0
  159. package/dist/services/field-ref-service.d.ts +96 -0
  160. package/dist/services/field-ref-service.js +74 -0
  161. package/dist/services/field-service.d.ts +49 -0
  162. package/dist/services/field-service.js +114 -0
  163. package/dist/services/field-value-service.d.ts +61 -0
  164. package/dist/services/field-value-service.js +256 -0
  165. package/dist/services/item-instance-service.d.ts +152 -0
  166. package/dist/services/item-instance-service.js +155 -0
  167. package/dist/services/item-service.d.ts +47 -0
  168. package/dist/services/item-service.js +56 -0
  169. package/dist/services/labor-ticket-service.d.ts +40 -0
  170. package/dist/services/labor-ticket-service.js +148 -0
  171. package/dist/services/log-file-service.d.ts +4 -0
  172. package/dist/services/log-file-service.js +11 -0
  173. package/dist/services/operation-dependency-service.d.ts +33 -0
  174. package/dist/services/operation-dependency-service.js +30 -0
  175. package/dist/services/operation-run-comment-service.d.ts +17 -0
  176. package/dist/services/operation-run-comment-service.js +26 -0
  177. package/dist/services/operation-run-service.d.ts +126 -0
  178. package/dist/services/operation-run-service.js +347 -0
  179. package/dist/services/operation-service.d.ts +47 -0
  180. package/dist/services/operation-service.js +132 -0
  181. package/dist/services/order-revision-service.d.ts +53 -0
  182. package/dist/services/order-revision-service.js +264 -0
  183. package/dist/services/order-run-service.d.ts +138 -0
  184. package/dist/services/order-run-service.js +356 -0
  185. package/dist/services/order-service.d.ts +15 -0
  186. package/dist/services/order-service.js +68 -0
  187. package/dist/services/revision-diff-service.d.ts +3 -0
  188. package/dist/services/revision-diff-service.js +194 -0
  189. package/dist/services/step-run-service.d.ts +172 -0
  190. package/dist/services/step-run-service.js +106 -0
  191. package/dist/services/step-service.d.ts +104 -0
  192. package/dist/services/step-service.js +89 -0
  193. package/dist/services/user-service.d.ts +185 -0
  194. package/dist/services/user-service.js +132 -0
  195. package/dist/services/work-center-service.d.ts +29 -0
  196. package/dist/services/work-center-service.js +106 -0
  197. package/dist/supervisorAuth.d.ts +3 -0
  198. package/dist/supervisorAuth.js +16 -0
  199. package/dist/userService.d.ts +20 -0
  200. package/dist/userService.js +118 -0
  201. package/package.json +69 -0
@@ -0,0 +1,147 @@
1
+ import { createReadStream, existsSync, statSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { AdminAttachmentListRequestSchema, AdminAttachmentListResponseSchema, AdminInfoResponseSchema, ErrorResponseSchema, ServerLogRequestSchema, ServerLogResponseSchema, } from "@naisys/erp-shared";
4
+ import { z } from "zod/v4";
5
+ import { hasPermission, requirePermission } from "../auth-middleware.js";
6
+ import { erpDbPath } from "../dbConfig.js";
7
+ import erpDb from "../erpDb.js";
8
+ import { notFound } from "../error-handler.js";
9
+ import { paginationLinks } from "../hateoas.js";
10
+ import { getErpLogPath, tailLogFile } from "../services/log-file-service.js";
11
+ const API_PREFIX = "/api/erp";
12
+ function adminActions(hasAdminPermission) {
13
+ const actions = [];
14
+ if (hasAdminPermission) {
15
+ actions.push({
16
+ rel: "view-logs",
17
+ href: `${API_PREFIX}/admin/logs`,
18
+ method: "GET",
19
+ title: "View Logs",
20
+ });
21
+ actions.push({
22
+ rel: "view-attachments",
23
+ href: `${API_PREFIX}/admin/attachments`,
24
+ method: "GET",
25
+ title: "View Attachments",
26
+ });
27
+ }
28
+ return actions;
29
+ }
30
+ export default function adminRoutes(fastify, _options) {
31
+ // GET / — Admin info
32
+ fastify.get("/", {
33
+ preHandler: [requirePermission("erp_admin")],
34
+ schema: {
35
+ description: "Get ERP admin system info",
36
+ tags: ["Admin"],
37
+ response: {
38
+ 200: AdminInfoResponseSchema,
39
+ 500: ErrorResponseSchema,
40
+ },
41
+ security: [{ cookieAuth: [] }],
42
+ },
43
+ }, async (request, _reply) => {
44
+ const hasAdminPerm = hasPermission(request.erpUser, "erp_admin");
45
+ const actions = adminActions(hasAdminPerm);
46
+ const dbPath = erpDbPath();
47
+ const erpDbSize = await fs
48
+ .stat(dbPath)
49
+ .then((s) => s.size)
50
+ .catch(() => undefined);
51
+ return {
52
+ erpDbPath: dbPath,
53
+ erpDbSize,
54
+ _actions: actions.length > 0 ? actions : undefined,
55
+ };
56
+ });
57
+ // GET /logs — Tail ERP log file
58
+ fastify.get("/logs", {
59
+ preHandler: [requirePermission("erp_admin")],
60
+ schema: {
61
+ description: "Get tail of the ERP server log file",
62
+ tags: ["Admin"],
63
+ querystring: ServerLogRequestSchema,
64
+ response: {
65
+ 200: ServerLogResponseSchema,
66
+ 500: ErrorResponseSchema,
67
+ },
68
+ security: [{ cookieAuth: [] }],
69
+ },
70
+ }, async (request, _reply) => {
71
+ const { lines, minLevel } = request.query;
72
+ const cappedLines = Math.min(lines, 1000);
73
+ const filePath = getErpLogPath();
74
+ const { entries, fileSize } = await tailLogFile(filePath, cappedLines, minLevel);
75
+ return {
76
+ entries,
77
+ fileName: "erp.log",
78
+ fileSize,
79
+ };
80
+ });
81
+ // GET /attachments — List all attachments
82
+ fastify.get("/attachments", {
83
+ preHandler: [requirePermission("erp_admin")],
84
+ schema: {
85
+ description: "List all uploaded attachments",
86
+ tags: ["Admin"],
87
+ querystring: AdminAttachmentListRequestSchema,
88
+ response: {
89
+ 200: AdminAttachmentListResponseSchema,
90
+ 500: ErrorResponseSchema,
91
+ },
92
+ security: [{ cookieAuth: [] }],
93
+ },
94
+ }, async (request, _reply) => {
95
+ const { page, pageSize } = request.query;
96
+ const skip = (page - 1) * pageSize;
97
+ const [rows, total] = await Promise.all([
98
+ erpDb.attachment.findMany({
99
+ orderBy: { createdAt: "desc" },
100
+ include: {
101
+ uploadedBy: { select: { username: true } },
102
+ },
103
+ skip,
104
+ take: pageSize,
105
+ }),
106
+ erpDb.attachment.count(),
107
+ ]);
108
+ return {
109
+ attachments: rows.map((r) => ({
110
+ id: r.publicId,
111
+ filename: r.filename,
112
+ fileSize: r.fileSize,
113
+ fileHash: r.fileHash,
114
+ uploadedBy: r.uploadedBy.username,
115
+ createdAt: r.createdAt.toISOString(),
116
+ })),
117
+ total,
118
+ page,
119
+ pageSize,
120
+ _links: paginationLinks("admin/attachments", page, pageSize, total),
121
+ };
122
+ });
123
+ // GET /attachments/:id — Download an attachment by ID
124
+ fastify.get("/attachments/:id", {
125
+ preHandler: [requirePermission("erp_admin")],
126
+ schema: {
127
+ description: "Download an attachment file by ID",
128
+ tags: ["Admin"],
129
+ params: z.object({ id: z.string() }),
130
+ },
131
+ }, async (request, reply) => {
132
+ const att = await erpDb.attachment.findUnique({
133
+ where: { publicId: request.params.id },
134
+ select: { filepath: true, filename: true, fileSize: true },
135
+ });
136
+ if (!att)
137
+ return notFound(reply, "Attachment not found");
138
+ if (!existsSync(att.filepath))
139
+ return notFound(reply, "Attachment file missing from disk");
140
+ const stat = statSync(att.filepath);
141
+ reply.header("content-type", "application/octet-stream");
142
+ reply.header("content-disposition", `attachment; filename="${att.filename.replace(/"/g, '\\"')}"`);
143
+ reply.header("content-length", stat.size);
144
+ return reply.send(createReadStream(att.filepath));
145
+ });
146
+ }
147
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function auditRoutes(fastify: FastifyInstance): void;
3
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { AuditListResponseSchema, AuditQuerySchema } from "@naisys/erp-shared";
2
+ import erpDb from "../erpDb.js";
3
+ export default function auditRoutes(fastify) {
4
+ const app = fastify.withTypeProvider();
5
+ app.get("/", {
6
+ schema: {
7
+ description: "Get audit log entries for a given entity",
8
+ tags: ["Audit"],
9
+ querystring: AuditQuerySchema,
10
+ response: {
11
+ 200: AuditListResponseSchema,
12
+ },
13
+ },
14
+ handler: async (request) => {
15
+ const { entityType, entityId } = request.query;
16
+ const items = await erpDb.auditLog.findMany({
17
+ where: { entityType, entityId },
18
+ orderBy: { createdAt: "desc" },
19
+ });
20
+ return {
21
+ items: items.map((item) => ({
22
+ id: item.id,
23
+ entityType: item.entityType,
24
+ entityId: item.entityId,
25
+ action: item.action,
26
+ field: item.field,
27
+ oldValue: item.oldValue,
28
+ newValue: item.newValue,
29
+ userId: item.userId,
30
+ createdAt: item.createdAt.toISOString(),
31
+ })),
32
+ };
33
+ },
34
+ });
35
+ }
36
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function authRoutes(fastify: FastifyInstance): void;
3
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1,112 @@
1
+ import { hashToken, SESSION_COOKIE_NAME, sessionCookieOptions, } from "@naisys/common-node";
2
+ import { authenticateAndCreateSession, deleteSession, } from "@naisys/supervisor-database";
3
+ import { AuthUserSchema, ErrorResponseSchema, LoginRequestSchema, LoginResponseSchema, } from "@naisys/erp-shared";
4
+ import bcrypt from "bcryptjs";
5
+ import { randomUUID } from "crypto";
6
+ import { authCache } from "../auth-middleware.js";
7
+ import erpDb from "../erpDb.js";
8
+ import { unauthorized } from "../error-handler.js";
9
+ import { isSupervisorAuth } from "../supervisorAuth.js";
10
+ const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
11
+ export default function authRoutes(fastify) {
12
+ const app = fastify.withTypeProvider();
13
+ // LOGIN
14
+ app.post("/login", {
15
+ config: {
16
+ rateLimit: {
17
+ max: 5,
18
+ timeWindow: "1 minute",
19
+ },
20
+ },
21
+ schema: {
22
+ description: "Authenticate with username and password",
23
+ tags: ["Auth"],
24
+ body: LoginRequestSchema,
25
+ response: {
26
+ 200: LoginResponseSchema,
27
+ 401: ErrorResponseSchema,
28
+ 429: ErrorResponseSchema,
29
+ },
30
+ },
31
+ handler: async (request, reply) => {
32
+ const { username, password } = request.body;
33
+ // SSO mode: authenticate against supervisor DB
34
+ if (isSupervisorAuth()) {
35
+ const authResult = await authenticateAndCreateSession(username, password);
36
+ if (!authResult) {
37
+ return unauthorized(reply, "Invalid username or password");
38
+ }
39
+ const ssoData = {
40
+ username,
41
+ passwordHash: authResult.user.passwordHash,
42
+ };
43
+ const user = await erpDb.user.upsert({
44
+ where: { uuid: authResult.user.uuid },
45
+ create: { uuid: authResult.user.uuid, ...ssoData },
46
+ update: ssoData,
47
+ });
48
+ reply.setCookie(SESSION_COOKIE_NAME, authResult.token, sessionCookieOptions(authResult.expiresAt));
49
+ return { user: { id: user.id, username: user.username } };
50
+ }
51
+ // Standalone mode: authenticate against local DB
52
+ const user = await erpDb.user.findUnique({ where: { username } });
53
+ if (!user) {
54
+ return unauthorized(reply, "Invalid username or password");
55
+ }
56
+ const valid = await bcrypt.compare(password, user.passwordHash);
57
+ if (!valid) {
58
+ return unauthorized(reply, "Invalid username or password");
59
+ }
60
+ const token = randomUUID();
61
+ const tokenHash = hashToken(token);
62
+ const expiresAt = new Date(Date.now() + SESSION_DURATION_MS);
63
+ await erpDb.session.create({
64
+ data: { userId: user.id, tokenHash, expiresAt },
65
+ });
66
+ reply.setCookie(SESSION_COOKIE_NAME, token, sessionCookieOptions(expiresAt));
67
+ return { user: { id: user.id, username: user.username } };
68
+ },
69
+ });
70
+ // LOGOUT
71
+ app.post("/logout", {
72
+ schema: {
73
+ description: "Log out and clear session",
74
+ tags: ["Auth"],
75
+ },
76
+ handler: async (request, reply) => {
77
+ const token = request.cookies?.[SESSION_COOKIE_NAME];
78
+ if (token) {
79
+ const tokenHash = hashToken(token);
80
+ authCache.invalidate(`cookie:${tokenHash}`);
81
+ // Delete from local ERP sessions
82
+ await erpDb.session.deleteMany({ where: { tokenHash } });
83
+ // Also delete from supervisor sessions (SSO mode)
84
+ await deleteSession(tokenHash);
85
+ }
86
+ reply.clearCookie(SESSION_COOKIE_NAME, { path: "/" });
87
+ return { ok: true };
88
+ },
89
+ });
90
+ // ME
91
+ app.get("/me", {
92
+ schema: {
93
+ description: "Get current authenticated user",
94
+ tags: ["Auth"],
95
+ response: {
96
+ 200: AuthUserSchema,
97
+ 401: ErrorResponseSchema,
98
+ },
99
+ },
100
+ handler: async (request, reply) => {
101
+ if (!request.erpUser) {
102
+ return unauthorized(reply, "Not authenticated");
103
+ }
104
+ return {
105
+ id: request.erpUser.id,
106
+ username: request.erpUser.username,
107
+ permissions: request.erpUser.permissions,
108
+ };
109
+ },
110
+ });
111
+ }
112
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function dispatchRoutes(fastify: FastifyInstance): void;
3
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1,174 @@
1
+ import { DispatchListQuerySchema, DispatchListResponseSchema, OperationRunStatus, OrderRunStatus, } from "@naisys/erp-shared";
2
+ import erpDb from "../erpDb.js";
3
+ import { API_PREFIX, paginationLinks } from "../hateoas.js";
4
+ import { getUserWorkCenterIds } from "../services/work-center-service.js";
5
+ const OPEN_ORDER_STATUSES = [OrderRunStatus.released, OrderRunStatus.started];
6
+ const DEFAULT_OP_STATUSES = [
7
+ OperationRunStatus.pending,
8
+ OperationRunStatus.in_progress,
9
+ OperationRunStatus.failed,
10
+ ];
11
+ export default function dispatchRoutes(fastify) {
12
+ const app = fastify.withTypeProvider();
13
+ app.get("/", {
14
+ schema: {
15
+ description: "List operation runs across open orders (dispatch view)",
16
+ tags: ["Dispatch"],
17
+ querystring: DispatchListQuerySchema,
18
+ response: {
19
+ 200: DispatchListResponseSchema,
20
+ },
21
+ },
22
+ handler: async (request) => {
23
+ const { page, pageSize, status, priority, search, viewAs, canWork, clockedIn, } = request.query;
24
+ // Resolve the perspective user for canWork computation
25
+ let perspectiveUserId = request.erpUser?.id;
26
+ let perspectivePerms = new Set(request.erpUser?.permissions ?? []);
27
+ if (viewAs) {
28
+ const viewAsUser = await erpDb.user.findUnique({
29
+ where: { username: viewAs },
30
+ select: {
31
+ id: true,
32
+ permissions: { select: { permission: true } },
33
+ },
34
+ });
35
+ if (viewAsUser) {
36
+ perspectiveUserId = viewAsUser.id;
37
+ perspectivePerms = new Set(viewAsUser.permissions.map((p) => p.permission));
38
+ }
39
+ }
40
+ const isExecutor = perspectivePerms.has("order_executor");
41
+ const isManager = perspectivePerms.has("order_manager");
42
+ // Pre-fetch perspective user's work center IDs for canWork computation + filtering
43
+ const userWcIds = perspectiveUserId
44
+ ? await getUserWorkCenterIds(perspectiveUserId)
45
+ : [];
46
+ const userWcIdSet = new Set(userWcIds);
47
+ const where = {
48
+ status: { in: status ? [status] : DEFAULT_OP_STATUSES },
49
+ orderRun: {
50
+ status: { in: OPEN_ORDER_STATUSES },
51
+ ...(priority ? { priority } : {}),
52
+ },
53
+ };
54
+ // canWork filter: only show ops where the perspective user can work
55
+ if (canWork) {
56
+ // Restrict to statuses the user has permission for
57
+ const workableStatuses = [];
58
+ if (isExecutor) {
59
+ workableStatuses.push(OperationRunStatus.pending, OperationRunStatus.in_progress);
60
+ }
61
+ if (isManager) {
62
+ workableStatuses.push(OperationRunStatus.failed);
63
+ }
64
+ // Intersect with the requested status filter
65
+ const currentStatuses = status ? [status] : DEFAULT_OP_STATUSES;
66
+ const filteredStatuses = currentStatuses.filter((s) => workableStatuses.includes(s));
67
+ where.status = {
68
+ in: filteredStatuses.length > 0 ? filteredStatuses : ["__none__"],
69
+ };
70
+ // Work center access
71
+ if (userWcIds.length > 0) {
72
+ where.operation = {
73
+ OR: [{ workCenterId: null }, { workCenterId: { in: userWcIds } }],
74
+ };
75
+ }
76
+ }
77
+ if (search) {
78
+ where.OR = [
79
+ { operation: { title: { contains: search } } },
80
+ { assignedTo: { username: { contains: search } } },
81
+ { orderRun: { order: { key: { contains: search } } } },
82
+ ];
83
+ }
84
+ if (clockedIn) {
85
+ where.laborTickets = { some: { clockOut: null } };
86
+ }
87
+ const [items, total] = await Promise.all([
88
+ erpDb.operationRun.findMany({
89
+ where,
90
+ include: {
91
+ operation: {
92
+ select: {
93
+ seqNo: true,
94
+ title: true,
95
+ workCenter: { select: { key: true, id: true } },
96
+ },
97
+ },
98
+ assignedTo: { select: { username: true } },
99
+ orderRun: {
100
+ select: {
101
+ runNo: true,
102
+ priority: true,
103
+ dueAt: true,
104
+ order: { select: { key: true } },
105
+ orderRev: { select: { revNo: true } },
106
+ },
107
+ },
108
+ },
109
+ skip: (page - 1) * pageSize,
110
+ take: pageSize,
111
+ orderBy: { orderRun: { dueAt: "asc" } },
112
+ }),
113
+ erpDb.operationRun.count({ where }),
114
+ ]);
115
+ return {
116
+ items: items.map((opRun) => {
117
+ const wcId = opRun.operation.workCenter?.id ?? null;
118
+ const hasWcAccess = wcId === null || userWcIdSet.has(wcId);
119
+ // canWork: work center access + permission for the op status
120
+ const hasStatusPerm = opRun.status === OperationRunStatus.failed ? isManager : isExecutor;
121
+ const itemCanWork = hasWcAccess && hasStatusPerm;
122
+ return {
123
+ id: opRun.id,
124
+ orderKey: opRun.orderRun.order.key,
125
+ revNo: opRun.orderRun.orderRev.revNo,
126
+ runNo: opRun.orderRun.runNo,
127
+ seqNo: opRun.operation.seqNo,
128
+ title: opRun.operation.title,
129
+ workCenterKey: opRun.operation.workCenter?.key ?? null,
130
+ canWork: itemCanWork,
131
+ status: opRun.status,
132
+ priority: opRun.orderRun.priority,
133
+ assignedTo: opRun.assignedTo?.username ?? null,
134
+ dueAt: opRun.orderRun.dueAt,
135
+ createdAt: opRun.createdAt.toISOString(),
136
+ };
137
+ }),
138
+ total,
139
+ page,
140
+ pageSize,
141
+ _links: [
142
+ ...paginationLinks("dispatch", page, pageSize, total, {
143
+ status,
144
+ priority,
145
+ search,
146
+ viewAs,
147
+ canWork: canWork ? "true" : undefined,
148
+ clockedIn: clockedIn ? "true" : undefined,
149
+ }),
150
+ {
151
+ rel: "work-centers",
152
+ href: `${API_PREFIX}/work-centers`,
153
+ title: "Work Centers",
154
+ },
155
+ ],
156
+ _linkTemplates: [
157
+ {
158
+ rel: "item",
159
+ hrefTemplate: `${API_PREFIX}/orders/{orderKey}/runs/{runNo}/ops/{seqNo}`,
160
+ },
161
+ ],
162
+ _actionTemplates: [
163
+ {
164
+ rel: "viewOperationRun",
165
+ hrefTemplate: `${API_PREFIX}/orders/{orderKey}/runs/{runNo}/ops/{seqNo}`,
166
+ method: "GET",
167
+ title: "View Operation Run",
168
+ },
169
+ ],
170
+ };
171
+ },
172
+ });
173
+ }
174
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function inventoryRoutes(fastify: FastifyInstance): void;
3
+ //# sourceMappingURL=inventory.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { InventoryListQuerySchema, InventoryListResponseSchema, } from "@naisys/erp-shared";
2
+ import erpDb from "../erpDb.js";
3
+ import { API_PREFIX, paginationLinks } from "../hateoas.js";
4
+ export default function inventoryRoutes(fastify) {
5
+ const app = fastify.withTypeProvider();
6
+ app.get("/", {
7
+ schema: {
8
+ description: "List all item instances across all items (inventory view)",
9
+ tags: ["Inventory"],
10
+ querystring: InventoryListQuerySchema,
11
+ response: {
12
+ 200: InventoryListResponseSchema,
13
+ },
14
+ },
15
+ handler: async (request) => {
16
+ const { page, pageSize, search } = request.query;
17
+ const where = {};
18
+ if (search) {
19
+ where.OR = [
20
+ { key: { contains: search } },
21
+ { item: { key: { contains: search } } },
22
+ ];
23
+ }
24
+ const [instances, total] = await Promise.all([
25
+ erpDb.itemInstance.findMany({
26
+ where,
27
+ include: {
28
+ item: { select: { key: true } },
29
+ orderRun: {
30
+ select: {
31
+ runNo: true,
32
+ order: { select: { key: true } },
33
+ },
34
+ },
35
+ },
36
+ skip: (page - 1) * pageSize,
37
+ take: pageSize,
38
+ orderBy: { createdAt: "desc" },
39
+ }),
40
+ erpDb.itemInstance.count({ where }),
41
+ ]);
42
+ return {
43
+ items: instances.map((inst) => ({
44
+ id: inst.id,
45
+ itemKey: inst.item.key,
46
+ key: inst.key,
47
+ quantity: inst.quantity,
48
+ orderKey: inst.orderRun?.order.key ?? null,
49
+ orderRunNo: inst.orderRun?.runNo ?? null,
50
+ createdAt: inst.createdAt.toISOString(),
51
+ })),
52
+ total,
53
+ page,
54
+ pageSize,
55
+ _links: paginationLinks("inventory", page, pageSize, total, {
56
+ search,
57
+ }),
58
+ _actionTemplates: [
59
+ {
60
+ rel: "viewInstance",
61
+ hrefTemplate: `${API_PREFIX}/items/{itemKey}/instances/{id}`,
62
+ method: "GET",
63
+ title: "View Instance",
64
+ },
65
+ ],
66
+ };
67
+ },
68
+ });
69
+ }
70
+ //# sourceMappingURL=inventory.js.map
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function itemFieldRoutes(fastify: FastifyInstance): void;
3
+ //# sourceMappingURL=item-fields.d.ts.map