@naisys/erp 3.0.1 → 3.0.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.
- package/README.md +60 -0
- package/client-dist/assets/{index-D5R6NBeW.js → index-CiuQ3CYv.js} +192 -87
- package/client-dist/index.html +1 -1
- package/dist/database/dbConfig.js +1 -1
- package/dist/erpServer.js +2 -3
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.js +1 -0
- package/dist/middleware/auth-middleware.js +39 -7
- package/dist/route-helpers.js +4 -2
- package/dist/routes/operations/operation-dependencies.js +1 -0
- package/dist/routes/operations/operation-field-refs.js +1 -0
- package/dist/routes/operations/operation-run-comments.js +1 -0
- package/dist/routes/operations/operation-runs.js +3 -0
- package/dist/routes/production/dispatch.js +15 -9
- package/dist/routes/production/labor-tickets.js +1 -0
- package/dist/routes/production/work-centers.js +2 -0
- package/dist/routes/users/auth.js +4 -1
- package/dist/routes/users/users.js +28 -16
- package/dist/services/operations/operation-dependency-service.js +1 -1
- package/dist/services/operations/operation-run-comment-service.js +1 -1
- package/dist/services/operations/operation-run-service.js +6 -6
- package/dist/services/operations/step-run-service.js +4 -4
- package/dist/services/orders/order-run-service.js +2 -2
- package/dist/services/production/field-ref-service.js +1 -1
- package/dist/services/production/labor-ticket-service.js +3 -3
- package/dist/services/production/work-center-service.js +2 -2
- package/dist/services/user-service.js +58 -9
- package/npm-shrinkwrap.json +28 -28
- package/package.json +11 -6
- package/prisma/migrations/20260528000000_add_user_title/migration.sql +1 -0
- package/prisma/schema.prisma +1 -0
- package/dist/services/production/labor-ticket-backfill.js +0 -67
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AuthCache, urlMatchesPrefix } from "@naisys/common";
|
|
2
2
|
import { extractBearerToken, hashToken, SESSION_COOKIE_NAME, } from "@naisys/common-node";
|
|
3
|
-
import { findAgentByApiKey } from "@naisys/hub-database";
|
|
3
|
+
import { findAgentByApiKey, findHubUserTitleByUuid } from "@naisys/hub-database";
|
|
4
4
|
import { findSession, findUserByApiKey } from "@naisys/supervisor-database";
|
|
5
5
|
import erpDb from "../database/erpDb.js";
|
|
6
6
|
import { isSupervisorAuth } from "./supervisorAuth.js";
|
|
@@ -17,6 +17,7 @@ async function materializeErpUser(localUser) {
|
|
|
17
17
|
return {
|
|
18
18
|
id: localUser.id,
|
|
19
19
|
username: localUser.username,
|
|
20
|
+
title: localUser.title,
|
|
20
21
|
permissions: await loadPermissions(localUser.id),
|
|
21
22
|
};
|
|
22
23
|
}
|
|
@@ -33,11 +34,26 @@ async function loadCookieUserSso(tokenHash) {
|
|
|
33
34
|
const session = await findSession(tokenHash);
|
|
34
35
|
if (!session)
|
|
35
36
|
return null;
|
|
36
|
-
|
|
37
|
+
const existing = await erpDb.user.findUnique({
|
|
37
38
|
where: { uuid: session.uuid },
|
|
38
|
-
create: { uuid: session.uuid, username: session.username },
|
|
39
|
-
update: {},
|
|
40
39
|
});
|
|
40
|
+
if (!existing) {
|
|
41
|
+
const title = (await findHubUserTitleByUuid(session.uuid)) ?? "";
|
|
42
|
+
return erpDb.user.create({
|
|
43
|
+
data: { uuid: session.uuid, username: session.username, title },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Agents mirror hub title; non-agents own their title locally.
|
|
47
|
+
if (existing.isAgent) {
|
|
48
|
+
const title = (await findHubUserTitleByUuid(session.uuid)) ?? "";
|
|
49
|
+
if (title !== existing.title) {
|
|
50
|
+
return erpDb.user.update({
|
|
51
|
+
where: { id: existing.id },
|
|
52
|
+
data: { title },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return existing;
|
|
41
57
|
}
|
|
42
58
|
async function loadCookieUserStandalone(tokenHash) {
|
|
43
59
|
const session = await erpDb.session.findUnique({
|
|
@@ -64,11 +80,27 @@ async function loadApiKeyUserSso(apiKey) {
|
|
|
64
80
|
if (!match)
|
|
65
81
|
return null;
|
|
66
82
|
const isAgent = supervisorUser?.isAgent ?? !!hubAgent;
|
|
67
|
-
|
|
83
|
+
const existing = await erpDb.user.findUnique({
|
|
68
84
|
where: { uuid: match.uuid },
|
|
69
|
-
create: { uuid: match.uuid, username: match.username, isAgent },
|
|
70
|
-
update: {},
|
|
71
85
|
});
|
|
86
|
+
const fetchTitle = async () => hubAgent?.title ?? (await findHubUserTitleByUuid(match.uuid)) ?? "";
|
|
87
|
+
if (!existing) {
|
|
88
|
+
const title = await fetchTitle();
|
|
89
|
+
return erpDb.user.create({
|
|
90
|
+
data: { uuid: match.uuid, username: match.username, isAgent, title },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Agents mirror hub title; non-agents own their title locally.
|
|
94
|
+
if (existing.isAgent) {
|
|
95
|
+
const title = await fetchTitle();
|
|
96
|
+
if (title !== existing.title) {
|
|
97
|
+
return erpDb.user.update({
|
|
98
|
+
where: { id: existing.id },
|
|
99
|
+
data: { title },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return existing;
|
|
72
104
|
}
|
|
73
105
|
export function hasPermission(user, permission) {
|
|
74
106
|
if (!user)
|
package/dist/route-helpers.js
CHANGED
|
@@ -37,16 +37,18 @@ export function mutationResult(request, reply, full, slim) {
|
|
|
37
37
|
}
|
|
38
38
|
// --- Shared Prisma include for audit user fields ---
|
|
39
39
|
export const includeUsers = {
|
|
40
|
-
createdBy: { select: { username: true } },
|
|
41
|
-
updatedBy: { select: { username: true } },
|
|
40
|
+
createdBy: { select: { username: true, title: true } },
|
|
41
|
+
updatedBy: { select: { username: true, title: true } },
|
|
42
42
|
};
|
|
43
43
|
// --- Formatting helpers ---
|
|
44
44
|
export function formatAuditFields(item) {
|
|
45
45
|
return {
|
|
46
46
|
createdAt: item.createdAt.toISOString(),
|
|
47
47
|
createdBy: item.createdBy.username,
|
|
48
|
+
createdByTitle: item.createdBy.title,
|
|
48
49
|
updatedAt: item.updatedAt.toISOString(),
|
|
49
50
|
updatedBy: item.updatedBy.username,
|
|
51
|
+
updatedByTitle: item.updatedBy.title,
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
54
|
export function formatDate(d) {
|
|
@@ -27,6 +27,7 @@ function formatDependency(dep) {
|
|
|
27
27
|
predecessorTitle: dep.predecessor.title,
|
|
28
28
|
createdAt: dep.createdAt.toISOString(),
|
|
29
29
|
createdBy: dep.createdBy.username,
|
|
30
|
+
createdByTitle: dep.createdBy.title,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
function depActionTemplates(basePath, revStatus, user) {
|
|
@@ -37,6 +37,7 @@ function formatFieldRef(orderKey, revNo, seqNo, revStatus, user, ref) {
|
|
|
37
37
|
})),
|
|
38
38
|
createdAt: ref.createdAt.toISOString(),
|
|
39
39
|
createdBy: ref.createdBy.username,
|
|
40
|
+
createdByTitle: ref.createdBy.title,
|
|
40
41
|
_links: childItemLinks(base, ref.seqNo, "Field Refs", `/orders/${orderKey}/revs/${revNo}/ops/${seqNo}`, "Operation", "FieldRef"),
|
|
41
42
|
_actions: deleteAction(`${API_PREFIX}${base}/${ref.seqNo}`, revStatus, user),
|
|
42
43
|
};
|
|
@@ -27,6 +27,7 @@ function formatComment(orderKey, runNo, seqNo, comment) {
|
|
|
27
27
|
body: comment.body,
|
|
28
28
|
createdAt: comment.createdAt.toISOString(),
|
|
29
29
|
createdBy: comment.createdBy.username,
|
|
30
|
+
createdByTitle: comment.createdBy.title,
|
|
30
31
|
_links: [
|
|
31
32
|
selfLink(`/${commentResource(orderKey, runNo, seqNo)}/${comment.id}`),
|
|
32
33
|
],
|
|
@@ -156,6 +156,7 @@ export async function formatOpRun(orderKey, runNo, user, opRun) {
|
|
|
156
156
|
workCenterKey: opRun.operation.workCenter?.key ?? null,
|
|
157
157
|
status: opRun.status,
|
|
158
158
|
assignedTo: opRun.assignedTo?.username ?? null,
|
|
159
|
+
assignedToTitle: opRun.assignedTo?.title ?? null,
|
|
159
160
|
cost: opRun.cost,
|
|
160
161
|
tokens: opRun.tokens,
|
|
161
162
|
note: opRun.statusNote ?? null,
|
|
@@ -203,6 +204,7 @@ export async function formatOpRunTransition(orderKey, runNo, user, opRun) {
|
|
|
203
204
|
id: opRun.id,
|
|
204
205
|
status: opRun.status,
|
|
205
206
|
assignedTo: opRun.assignedTo?.username ?? null,
|
|
207
|
+
assignedToTitle: opRun.assignedTo?.title ?? null,
|
|
206
208
|
cost: opRun.cost,
|
|
207
209
|
tokens: opRun.tokens,
|
|
208
210
|
note: opRun.statusNote ?? null,
|
|
@@ -225,6 +227,7 @@ function formatListOpRun(opRun) {
|
|
|
225
227
|
workCenterKey: opRun.operation.workCenter?.key ?? null,
|
|
226
228
|
status: opRun.status,
|
|
227
229
|
assignedTo: opRun.assignedTo?.username ?? null,
|
|
230
|
+
assignedToTitle: opRun.assignedTo?.title ?? null,
|
|
228
231
|
cost: opRun.cost,
|
|
229
232
|
tokens: opRun.tokens,
|
|
230
233
|
note: opRun.statusNote ?? null,
|
|
@@ -20,7 +20,7 @@ export default function dispatchRoutes(fastify) {
|
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
handler: async (request) => {
|
|
23
|
-
const { page, pageSize, status,
|
|
23
|
+
const { page, pageSize, status, workCenter, search, viewAs, canWork, clockedIn, } = request.query;
|
|
24
24
|
// Resolve the perspective user for canWork computation
|
|
25
25
|
let perspectiveUserId = request.erpUser?.id;
|
|
26
26
|
let perspectivePerms = new Set(request.erpUser?.permissions ?? []);
|
|
@@ -48,9 +48,9 @@ export default function dispatchRoutes(fastify) {
|
|
|
48
48
|
status: { in: status ? [status] : DEFAULT_OP_STATUSES },
|
|
49
49
|
orderRun: {
|
|
50
50
|
status: { in: OPEN_ORDER_STATUSES },
|
|
51
|
-
...(priority ? { priority } : {}),
|
|
52
51
|
},
|
|
53
52
|
};
|
|
53
|
+
const operationFilters = {};
|
|
54
54
|
// canWork filter: only show ops where the perspective user can work
|
|
55
55
|
if (canWork) {
|
|
56
56
|
// Restrict to statuses the user has permission for
|
|
@@ -67,11 +67,18 @@ export default function dispatchRoutes(fastify) {
|
|
|
67
67
|
where.status = { in: filteredStatuses };
|
|
68
68
|
// Work center access
|
|
69
69
|
if (userWcIds.length > 0) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
operationFilters.OR = [
|
|
71
|
+
{ workCenterId: null },
|
|
72
|
+
{ workCenterId: { in: userWcIds } },
|
|
73
|
+
];
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
if (workCenter) {
|
|
77
|
+
operationFilters.workCenter = { key: workCenter };
|
|
78
|
+
}
|
|
79
|
+
if (Object.keys(operationFilters).length > 0) {
|
|
80
|
+
where.operation = operationFilters;
|
|
81
|
+
}
|
|
75
82
|
if (search) {
|
|
76
83
|
where.OR = [
|
|
77
84
|
{ operation: { title: { contains: search } } },
|
|
@@ -93,11 +100,10 @@ export default function dispatchRoutes(fastify) {
|
|
|
93
100
|
workCenter: { select: { key: true, id: true } },
|
|
94
101
|
},
|
|
95
102
|
},
|
|
96
|
-
assignedTo: { select: { username: true } },
|
|
103
|
+
assignedTo: { select: { username: true, title: true } },
|
|
97
104
|
orderRun: {
|
|
98
105
|
select: {
|
|
99
106
|
runNo: true,
|
|
100
|
-
priority: true,
|
|
101
107
|
dueAt: true,
|
|
102
108
|
order: { select: { key: true } },
|
|
103
109
|
orderRev: { select: { revNo: true } },
|
|
@@ -127,8 +133,8 @@ export default function dispatchRoutes(fastify) {
|
|
|
127
133
|
workCenterKey: opRun.operation.workCenter?.key ?? null,
|
|
128
134
|
canWork: itemCanWork,
|
|
129
135
|
status: opRun.status,
|
|
130
|
-
priority: opRun.orderRun.priority,
|
|
131
136
|
assignedTo: opRun.assignedTo?.username ?? null,
|
|
137
|
+
assignedToTitle: opRun.assignedTo?.title ?? null,
|
|
132
138
|
dueAt: opRun.orderRun.dueAt,
|
|
133
139
|
createdAt: opRun.createdAt.toISOString(),
|
|
134
140
|
};
|
|
@@ -139,7 +145,7 @@ export default function dispatchRoutes(fastify) {
|
|
|
139
145
|
_links: [
|
|
140
146
|
...paginationLinks("dispatch", page, pageSize, total, {
|
|
141
147
|
status,
|
|
142
|
-
|
|
148
|
+
workCenter,
|
|
143
149
|
search,
|
|
144
150
|
viewAs,
|
|
145
151
|
canWork: canWork ? "true" : undefined,
|
|
@@ -88,6 +88,7 @@ function formatLaborTicket(orderKey, runNo, seqNo, ticket) {
|
|
|
88
88
|
operationRunId: ticket.operationRunId,
|
|
89
89
|
userId: ticket.userId,
|
|
90
90
|
username: ticket.user.username,
|
|
91
|
+
userTitle: ticket.user.title,
|
|
91
92
|
runId: ticket.runId,
|
|
92
93
|
clockIn: ticket.clockIn.toISOString(),
|
|
93
94
|
clockOut: formatDate(ticket.clockOut),
|
|
@@ -58,8 +58,10 @@ function formatWorkCenter(wc, user) {
|
|
|
58
58
|
userAssignments: wc.userAssignments.map((a) => ({
|
|
59
59
|
userId: a.user.id,
|
|
60
60
|
username: a.user.username,
|
|
61
|
+
userTitle: a.user.title,
|
|
61
62
|
createdAt: a.createdAt.toISOString(),
|
|
62
63
|
createdBy: a.createdBy?.username ?? null,
|
|
64
|
+
createdByTitle: a.createdBy?.title ?? null,
|
|
63
65
|
_actions: [
|
|
64
66
|
{
|
|
65
67
|
rel: "remove",
|
|
@@ -52,7 +52,9 @@ export default function authRoutes(fastify) {
|
|
|
52
52
|
data: { userId: user.id, tokenHash, expiresAt },
|
|
53
53
|
});
|
|
54
54
|
reply.setCookie(SESSION_COOKIE_NAME, token, sessionCookieOptions(expiresAt));
|
|
55
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
user: { id: user.id, username: user.username, title: user.title },
|
|
57
|
+
};
|
|
56
58
|
},
|
|
57
59
|
});
|
|
58
60
|
// LOGOUT
|
|
@@ -92,6 +94,7 @@ export default function authRoutes(fastify) {
|
|
|
92
94
|
return {
|
|
93
95
|
id: request.erpUser.id,
|
|
94
96
|
username: request.erpUser.username,
|
|
97
|
+
title: request.erpUser.title,
|
|
95
98
|
permissions: request.erpUser.permissions,
|
|
96
99
|
};
|
|
97
100
|
},
|
|
@@ -13,9 +13,15 @@ function userItemLinks(username) {
|
|
|
13
13
|
schemaLink("UpdateUser"),
|
|
14
14
|
];
|
|
15
15
|
}
|
|
16
|
-
function userActions(username, isSelf, isAdmin) {
|
|
16
|
+
function userActions(username, isAgent, isSelf, isAdmin) {
|
|
17
17
|
const href = `${API_PREFIX}/users/${username}`;
|
|
18
18
|
const actions = [];
|
|
19
|
+
// In SSO mode the supervisor owns passwords (passkey-only) and external API
|
|
20
|
+
// keys, so ERP doesn't expose its own change-password / rotate-key actions.
|
|
21
|
+
const sso = isSupervisorAuth();
|
|
22
|
+
// Title is editable when the user isn't an SSO-managed agent (whose title
|
|
23
|
+
// is mirrored from the hub on every auth).
|
|
24
|
+
const titleEditable = !isAgent || !sso;
|
|
19
25
|
if (isAdmin) {
|
|
20
26
|
actions.push({
|
|
21
27
|
rel: "update",
|
|
@@ -23,12 +29,9 @@ function userActions(username, isSelf, isAdmin) {
|
|
|
23
29
|
method: "PUT",
|
|
24
30
|
title: "Update",
|
|
25
31
|
schema: `${API_PREFIX}/schemas/UpdateUser`,
|
|
26
|
-
body: { username: "" },
|
|
32
|
+
body: { username: "", ...(titleEditable ? { title: "" } : {}) },
|
|
27
33
|
});
|
|
28
34
|
}
|
|
29
|
-
// In SSO mode the supervisor owns passwords (passkey-only) and external API
|
|
30
|
-
// keys, so ERP doesn't expose its own change-password / rotate-key actions.
|
|
31
|
-
const sso = isSupervisorAuth();
|
|
32
35
|
if (isSelf && !sso) {
|
|
33
36
|
actions.push({
|
|
34
37
|
rel: "change-password",
|
|
@@ -90,6 +93,7 @@ export function formatUser(user, currentUserId, currentUserPermissions, options)
|
|
|
90
93
|
return {
|
|
91
94
|
id: user.id,
|
|
92
95
|
username: user.username,
|
|
96
|
+
title: user.title,
|
|
93
97
|
isAgent: user.isAgent,
|
|
94
98
|
createdAt: user.createdAt.toISOString(),
|
|
95
99
|
updatedAt: user.updatedAt.toISOString(),
|
|
@@ -101,13 +105,14 @@ export function formatUser(user, currentUserId, currentUserPermissions, options)
|
|
|
101
105
|
_actions: permissionActions(user.username, p.permission, isSelf, isAdmin),
|
|
102
106
|
})),
|
|
103
107
|
_links: userItemLinks(user.username),
|
|
104
|
-
_actions: userActions(user.username, isSelf, isAdmin),
|
|
108
|
+
_actions: userActions(user.username, user.isAgent, isSelf, isAdmin),
|
|
105
109
|
};
|
|
106
110
|
}
|
|
107
111
|
function formatListUser(user) {
|
|
108
112
|
return {
|
|
109
113
|
id: user.id,
|
|
110
114
|
username: user.username,
|
|
115
|
+
title: user.title,
|
|
111
116
|
isAgent: user.isAgent,
|
|
112
117
|
createdAt: user.createdAt.toISOString(),
|
|
113
118
|
permissionCount: user.permissions.length,
|
|
@@ -293,7 +298,7 @@ export default function userRoutes(fastify) {
|
|
|
293
298
|
};
|
|
294
299
|
}
|
|
295
300
|
try {
|
|
296
|
-
const user = await createUserForAgent(hubAgent.username, hubAgent.uuid);
|
|
301
|
+
const user = await createUserForAgent(hubAgent.username, hubAgent.uuid, hubAgent.title);
|
|
297
302
|
const full = formatUser(user, request.erpUser.id, request.erpUser.permissions);
|
|
298
303
|
reply.code(201);
|
|
299
304
|
return mutationResult(request, reply, full, {
|
|
@@ -350,16 +355,23 @@ export default function userRoutes(fastify) {
|
|
|
350
355
|
return { success: false, message: "User not found" };
|
|
351
356
|
}
|
|
352
357
|
const isAdmin = hasPermission(request.erpUser, "erp_admin");
|
|
353
|
-
// In SSO mode the supervisor owns passwords, so we strip any password
|
|
354
|
-
// field before forwarding the update — even from admins.
|
|
355
358
|
const sso = isSupervisorAuth();
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
359
|
+
// Field-level gating:
|
|
360
|
+
// - username: admin-only.
|
|
361
|
+
// - password: non-SSO only (supervisor owns passwords in SSO mode).
|
|
362
|
+
// - title: editable unless the target is an SSO-managed agent (whose
|
|
363
|
+
// title is mirrored from the hub on every auth).
|
|
364
|
+
const titleEditable = !targetUser.isAgent || !sso;
|
|
365
|
+
const body = {};
|
|
366
|
+
if (isAdmin && request.body.username !== undefined) {
|
|
367
|
+
body.username = request.body.username;
|
|
368
|
+
}
|
|
369
|
+
if (!sso && request.body.password !== undefined) {
|
|
370
|
+
body.password = request.body.password;
|
|
371
|
+
}
|
|
372
|
+
if (titleEditable && request.body.title !== undefined) {
|
|
373
|
+
body.title = request.body.title;
|
|
374
|
+
}
|
|
363
375
|
try {
|
|
364
376
|
const user = await updateUser(targetUser.id, body);
|
|
365
377
|
authCache.clear();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import erpDb from "../../database/erpDb.js";
|
|
2
2
|
const depInclude = {
|
|
3
3
|
predecessor: { select: { seqNo: true, title: true } },
|
|
4
|
-
createdBy: { select: { username: true } },
|
|
4
|
+
createdBy: { select: { username: true, title: true } },
|
|
5
5
|
};
|
|
6
6
|
export async function listDependencies(operationId) {
|
|
7
7
|
return erpDb.operationDependency.findMany({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import erpDb from "../../database/erpDb.js";
|
|
2
2
|
// --- Prisma include & result type ---
|
|
3
3
|
export const includeComment = {
|
|
4
|
-
createdBy: { select: { username: true } },
|
|
4
|
+
createdBy: { select: { username: true, title: true } },
|
|
5
5
|
};
|
|
6
6
|
// --- Lookups ---
|
|
7
7
|
export async function listComments(operationRunId) {
|
|
@@ -20,9 +20,9 @@ export const includeOp = {
|
|
|
20
20
|
orderRev: { select: { revNo: true, description: true } },
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
-
assignedTo: { select: { username: true } },
|
|
24
|
-
createdBy: { select: { username: true } },
|
|
25
|
-
updatedBy: { select: { username: true } },
|
|
23
|
+
assignedTo: { select: { username: true, title: true } },
|
|
24
|
+
createdBy: { select: { username: true, title: true } },
|
|
25
|
+
updatedBy: { select: { username: true, title: true } },
|
|
26
26
|
};
|
|
27
27
|
// --- Lookups ---
|
|
28
28
|
export async function listOpRuns(runId) {
|
|
@@ -47,9 +47,9 @@ export async function listOpRuns(runId) {
|
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
49
|
_count: { select: { stepRuns: true } },
|
|
50
|
-
assignedTo: { select: { username: true } },
|
|
51
|
-
createdBy: { select: { username: true } },
|
|
52
|
-
updatedBy: { select: { username: true } },
|
|
50
|
+
assignedTo: { select: { username: true, title: true } },
|
|
51
|
+
createdBy: { select: { username: true, title: true } },
|
|
52
|
+
updatedBy: { select: { username: true, title: true } },
|
|
53
53
|
},
|
|
54
54
|
orderBy: { operation: { seqNo: "asc" } },
|
|
55
55
|
});
|
|
@@ -44,8 +44,8 @@ export const includeStepRunWithFields = {
|
|
|
44
44
|
},
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
|
-
createdBy: { select: { username: true } },
|
|
48
|
-
updatedBy: { select: { username: true } },
|
|
47
|
+
createdBy: { select: { username: true, title: true } },
|
|
48
|
+
updatedBy: { select: { username: true, title: true } },
|
|
49
49
|
};
|
|
50
50
|
// --- Lightweight include (step metadata only, no field values) ---
|
|
51
51
|
export const includeStepRun = {
|
|
@@ -62,8 +62,8 @@ export const includeStepRun = {
|
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
|
-
createdBy: { select: { username: true } },
|
|
66
|
-
updatedBy: { select: { username: true } },
|
|
65
|
+
createdBy: { select: { username: true, title: true } },
|
|
66
|
+
updatedBy: { select: { username: true, title: true } },
|
|
67
67
|
};
|
|
68
68
|
// --- Lookups ---
|
|
69
69
|
export async function listStepRuns(opRunId) {
|
|
@@ -8,8 +8,8 @@ export const includeRev = {
|
|
|
8
8
|
orderRev: { select: { revNo: true, description: true } },
|
|
9
9
|
order: { select: { item: { select: { key: true } } } },
|
|
10
10
|
itemInstances: { select: { id: true, key: true }, take: 1 },
|
|
11
|
-
createdBy: { select: { username: true } },
|
|
12
|
-
updatedBy: { select: { username: true } },
|
|
11
|
+
createdBy: { select: { username: true, title: true } },
|
|
12
|
+
updatedBy: { select: { username: true, title: true } },
|
|
13
13
|
};
|
|
14
14
|
// --- Lookups ---
|
|
15
15
|
export async function listOrderRuns(where, page, pageSize) {
|
|
@@ -3,9 +3,9 @@ import erpDb from "../../database/erpDb.js";
|
|
|
3
3
|
import { writeAuditEntry } from "../audit.js";
|
|
4
4
|
// --- Prisma include & result type ---
|
|
5
5
|
export const includeLaborTicket = {
|
|
6
|
-
user: { select: { username: true } },
|
|
7
|
-
createdBy: { select: { username: true } },
|
|
8
|
-
updatedBy: { select: { username: true } },
|
|
6
|
+
user: { select: { username: true, title: true } },
|
|
7
|
+
createdBy: { select: { username: true, title: true } },
|
|
8
|
+
updatedBy: { select: { username: true, title: true } },
|
|
9
9
|
};
|
|
10
10
|
// --- Helpers ---
|
|
11
11
|
/**
|
|
@@ -5,8 +5,8 @@ const includeDetail = {
|
|
|
5
5
|
...includeUsers,
|
|
6
6
|
userAssignments: {
|
|
7
7
|
include: {
|
|
8
|
-
user: { select: { id: true, username: true } },
|
|
9
|
-
createdBy: { select: { username: true } },
|
|
8
|
+
user: { select: { id: true, username: true, title: true } },
|
|
9
|
+
createdBy: { select: { username: true, title: true } },
|
|
10
10
|
},
|
|
11
11
|
orderBy: { user: { username: "asc" } },
|
|
12
12
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SUPER_ADMIN_USERNAME } from "@naisys/common";
|
|
2
2
|
import { generatePersistentUserApiKey } from "@naisys/common-node";
|
|
3
|
+
import { findHubUserTitleByUuid, findHubUserTitlesByUuids, } from "@naisys/hub-database";
|
|
3
4
|
import { ensureSuperAdmin } from "@naisys/supervisor-database";
|
|
4
5
|
import bcrypt from "bcryptjs";
|
|
5
6
|
import { randomUUID } from "crypto";
|
|
@@ -52,11 +53,12 @@ export async function getUserByUuid(uuid) {
|
|
|
52
53
|
include: includePermissions,
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
|
-
export async function createUserForAgent(username, uuid) {
|
|
56
|
+
export async function createUserForAgent(username, uuid, title) {
|
|
56
57
|
return erpDb.user.create({
|
|
57
58
|
data: {
|
|
58
59
|
username,
|
|
59
60
|
uuid,
|
|
61
|
+
title,
|
|
60
62
|
isAgent: true,
|
|
61
63
|
},
|
|
62
64
|
include: includePermissions,
|
|
@@ -80,6 +82,9 @@ export async function updateUser(id, data) {
|
|
|
80
82
|
if (data.username !== undefined) {
|
|
81
83
|
updateData.username = data.username;
|
|
82
84
|
}
|
|
85
|
+
if (data.title !== undefined) {
|
|
86
|
+
updateData.title = data.title;
|
|
87
|
+
}
|
|
83
88
|
if (data.password !== undefined) {
|
|
84
89
|
updateData.passwordHash = await bcrypt.hash(data.password, SALT_ROUNDS);
|
|
85
90
|
}
|
|
@@ -165,16 +170,29 @@ export async function ensureLocalSuperAdmin(password) {
|
|
|
165
170
|
*/
|
|
166
171
|
export async function ensureSupervisorSuperAdmin() {
|
|
167
172
|
const result = await ensureSuperAdmin();
|
|
168
|
-
|
|
173
|
+
// Super admin is a non-agent — seed title from hub on first bootstrap,
|
|
174
|
+
// but leave it alone afterwards so local edits stick.
|
|
175
|
+
const existing = await erpDb.user.findUnique({
|
|
169
176
|
where: { uuid: result.user.uuid },
|
|
170
|
-
create: {
|
|
171
|
-
uuid: result.user.uuid,
|
|
172
|
-
username: result.user.username,
|
|
173
|
-
},
|
|
174
|
-
update: {
|
|
175
|
-
username: result.user.username,
|
|
176
|
-
},
|
|
177
177
|
});
|
|
178
|
+
if (existing) {
|
|
179
|
+
if (existing.username !== result.user.username) {
|
|
180
|
+
await erpDb.user.update({
|
|
181
|
+
where: { id: existing.id },
|
|
182
|
+
data: { username: result.user.username },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const title = (await findHubUserTitleByUuid(result.user.uuid)) ?? "";
|
|
188
|
+
await erpDb.user.create({
|
|
189
|
+
data: {
|
|
190
|
+
uuid: result.user.uuid,
|
|
191
|
+
username: result.user.username,
|
|
192
|
+
title,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
178
196
|
const localSuperAdmin = await erpDb.user.findUnique({
|
|
179
197
|
where: { uuid: result.user.uuid },
|
|
180
198
|
});
|
|
@@ -182,6 +200,37 @@ export async function ensureSupervisorSuperAdmin() {
|
|
|
182
200
|
await ensureErpAdminPermission(localSuperAdmin.id);
|
|
183
201
|
}
|
|
184
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Pull the latest title from the hub for every agent user in ERP and update
|
|
205
|
+
* any drift. Non-agents are skipped — they own their title locally. Runs at
|
|
206
|
+
* ERP startup (SSO mode) so agent titles stay fresh even if the agent hasn't
|
|
207
|
+
* authenticated recently to trigger the per-request refresh in auth-middleware.
|
|
208
|
+
*/
|
|
209
|
+
export async function syncAgentTitlesFromHub() {
|
|
210
|
+
const agents = await erpDb.user.findMany({
|
|
211
|
+
where: { isAgent: true },
|
|
212
|
+
select: { id: true, uuid: true, title: true },
|
|
213
|
+
});
|
|
214
|
+
if (agents.length === 0)
|
|
215
|
+
return;
|
|
216
|
+
const hubTitles = await findHubUserTitlesByUuids(agents.map((a) => a.uuid));
|
|
217
|
+
let updated = 0;
|
|
218
|
+
for (const agent of agents) {
|
|
219
|
+
const hubTitle = hubTitles.get(agent.uuid);
|
|
220
|
+
if (hubTitle === undefined)
|
|
221
|
+
continue; // not in hub, leave as-is
|
|
222
|
+
if (hubTitle === agent.title)
|
|
223
|
+
continue;
|
|
224
|
+
await erpDb.user.update({
|
|
225
|
+
where: { id: agent.id },
|
|
226
|
+
data: { title: hubTitle },
|
|
227
|
+
});
|
|
228
|
+
updated++;
|
|
229
|
+
}
|
|
230
|
+
if (updated > 0) {
|
|
231
|
+
console.log(`[ERP] Synced ${updated} agent title(s) from hub.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
185
234
|
/**
|
|
186
235
|
* Ensure a user has the erp_admin permission.
|
|
187
236
|
*/
|