@naisys/erp 3.0.0-beta.9 → 3.0.0
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/.env.example +5 -0
- package/client-dist/assets/index-CSiMTJfw.css +14 -0
- package/client-dist/assets/index-D6lSIioV.js +11167 -0
- package/client-dist/assets/rolldown-runtime-CvHMtSRF.js +33 -0
- package/client-dist/assets/vendor-CJ0ET9hP.js +75181 -0
- package/client-dist/assets/vendor-CLUPjUnv.css +8747 -0
- package/client-dist/favicon-16x16.png +0 -0
- package/client-dist/favicon-32x32.png +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +17 -2
- package/dist/{dbConfig.js → database/dbConfig.js} +1 -1
- package/dist/{erpDb.js → database/erpDb.js} +1 -1
- package/dist/erpRoutes.js +115 -0
- package/dist/erpServer.js +85 -161
- package/dist/error-handler.js +3 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.js +3 -1
- package/dist/middleware/auth-middleware.js +146 -0
- package/dist/route-helpers.js +2 -2
- package/dist/routes/admin.js +15 -7
- package/dist/routes/audit.js +1 -1
- package/dist/routes/{item-fields.js → items/item-fields.js} +24 -22
- package/dist/routes/{item-instances.js → items/item-instances.js} +42 -24
- package/dist/routes/{items.js → items/items.js} +35 -33
- package/dist/routes/{operation-dependencies.js → operations/operation-dependencies.js} +6 -6
- package/dist/routes/{operation-field-refs.js → operations/operation-field-refs.js} +6 -6
- package/dist/routes/{operation-run-comments.js → operations/operation-run-comments.js} +5 -5
- package/dist/routes/{operation-run-transitions.js → operations/operation-run-transitions.js} +29 -13
- package/dist/routes/{operation-runs.js → operations/operation-runs.js} +48 -10
- package/dist/routes/{operations.js → operations/operations.js} +6 -6
- package/dist/routes/{order-revision-transitions.js → orders/order-revision-transitions.js} +4 -4
- package/dist/routes/{order-revisions.js → orders/order-revisions.js} +6 -6
- package/dist/routes/{order-run-transitions.js → orders/order-run-transitions.js} +11 -5
- package/dist/routes/{order-runs.js → orders/order-runs.js} +7 -5
- package/dist/routes/{orders.js → orders/orders.js} +15 -11
- package/dist/routes/{dispatch.js → production/dispatch.js} +88 -7
- package/dist/routes/{inventory.js → production/inventory.js} +33 -10
- package/dist/routes/{labor-tickets.js → production/labor-tickets.js} +7 -7
- package/dist/routes/{work-centers.js → production/work-centers.js} +29 -29
- package/dist/routes/root.js +1 -1
- package/dist/routes/{step-field-attachments.js → steps/step-field-attachments.js} +8 -8
- package/dist/routes/{step-fields.js → steps/step-fields.js} +6 -6
- package/dist/routes/{step-run-fields.js → steps/step-run-fields.js} +9 -9
- package/dist/routes/{step-run-transitions.js → steps/step-run-transitions.js} +6 -6
- package/dist/routes/{step-runs.js → steps/step-runs.js} +7 -7
- package/dist/routes/{steps.js → steps/steps.js} +5 -5
- package/dist/routes/{auth.js → users/auth.js} +11 -23
- package/dist/routes/{user-permissions.js → users/user-permissions.js} +21 -7
- package/dist/routes/{users.js → users/users.js} +42 -20
- package/dist/services/attachment-service.js +2 -2
- package/dist/services/{item-instance-service.js → inventory/item-instance-service.js} +2 -2
- package/dist/services/{item-service.js → inventory/item-service.js} +2 -2
- package/dist/services/{operation-dependency-service.js → operations/operation-dependency-service.js} +1 -1
- package/dist/services/{operation-run-comment-service.js → operations/operation-run-comment-service.js} +1 -1
- package/dist/services/{operation-run-service.js → operations/operation-run-service.js} +15 -4
- package/dist/services/{operation-service.js → operations/operation-service.js} +2 -2
- package/dist/services/{step-run-service.js → operations/step-run-service.js} +1 -1
- package/dist/services/{step-service.js → operations/step-service.js} +2 -2
- package/dist/services/{order-revision-service.js → orders/order-revision-service.js} +4 -5
- package/dist/services/{order-run-service.js → orders/order-run-service.js} +68 -22
- package/dist/services/{order-service.js → orders/order-service.js} +11 -2
- package/dist/services/{revision-diff-service.js → orders/revision-diff-service.js} +11 -10
- package/dist/services/{field-ref-service.js → production/field-ref-service.js} +1 -1
- package/dist/services/{field-service.js → production/field-service.js} +2 -2
- package/dist/services/{field-value-service.js → production/field-value-service.js} +27 -3
- package/dist/services/production/labor-ticket-backfill.js +67 -0
- package/dist/services/{labor-ticket-service.js → production/labor-ticket-service.js} +21 -15
- package/dist/services/{work-center-service.js → production/work-center-service.js} +2 -2
- package/dist/services/user-service.js +94 -28
- package/dist/version.js +12 -0
- package/npm-shrinkwrap.json +3000 -0
- package/package.json +11 -9
- package/prisma/migrations/20260427010000_hash_user_api_keys/migration.sql +10 -0
- package/prisma/migrations/20260427020000_nullable_user_password_hash/migration.sql +39 -0
- package/prisma/migrations/20260517000000_add_op_run_tokens/migration.sql +2 -0
- package/prisma/schema.prisma +4 -2
- package/client-dist/assets/index-45dVo30p.css +0 -1
- package/client-dist/assets/index-C9uuPHLH.js +0 -168
- package/dist/auth-middleware.js +0 -203
- package/dist/userService.js +0 -118
- /package/bin/{naisys-erp → naisys-erp.js} +0 -0
- /package/dist/{supervisorAuth.js → middleware/supervisorAuth.js} +0 -0
- /package/dist/{audit.js → services/audit.js} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { mapDefined } from "@naisys/common";
|
|
1
2
|
import { RevisionStatus as RevisionStatusValues, } from "@naisys/erp-shared";
|
|
3
|
+
import erpDb from "../../database/erpDb.js";
|
|
4
|
+
import { includeUsers } from "../../route-helpers.js";
|
|
2
5
|
import { writeAuditEntry } from "../audit.js";
|
|
3
|
-
import erpDb from "../erpDb.js";
|
|
4
|
-
import { includeUsers } from "../route-helpers.js";
|
|
5
6
|
// --- Prisma include & result type ---
|
|
6
7
|
const includeRevisionRelations = {
|
|
7
8
|
...includeUsers,
|
|
@@ -230,9 +231,7 @@ export async function deleteRevision(id) {
|
|
|
230
231
|
where: { operationId: { in: opIds } },
|
|
231
232
|
select: { id: true, fieldSetId: true },
|
|
232
233
|
});
|
|
233
|
-
const fieldSetIds = steps
|
|
234
|
-
.map((s) => s.fieldSetId)
|
|
235
|
-
.filter((id) => id !== null);
|
|
234
|
+
const fieldSetIds = mapDefined(steps, (s) => s.fieldSetId);
|
|
236
235
|
// Steps reference field_sets via FK, so delete steps first
|
|
237
236
|
await erpTx.step.deleteMany({
|
|
238
237
|
where: { operationId: { in: opIds } },
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { keyBy, mapDefined } from "@naisys/common";
|
|
1
2
|
import { OperationRunStatus as OperationRunStatusValues, OrderRunStatus as OrderRunStatusValues, } from "@naisys/erp-shared";
|
|
3
|
+
import erpDb from "../../database/erpDb.js";
|
|
2
4
|
import { writeAuditEntry } from "../audit.js";
|
|
3
|
-
import
|
|
5
|
+
import { deserializeFieldValue, upsertFieldValue, validateFieldSet, } from "../production/field-value-service.js";
|
|
4
6
|
// --- Prisma include & result type ---
|
|
5
7
|
export const includeRev = {
|
|
6
|
-
orderRev: { select: { revNo: true } },
|
|
8
|
+
orderRev: { select: { revNo: true, description: true } },
|
|
7
9
|
order: { select: { item: { select: { key: true } } } },
|
|
8
10
|
itemInstances: { select: { id: true, key: true }, take: 1 },
|
|
9
11
|
createdBy: { select: { username: true } },
|
|
@@ -187,9 +189,7 @@ export async function deleteOrderRun(id) {
|
|
|
187
189
|
where: { operationRunId: { in: opRunIds } },
|
|
188
190
|
select: { fieldRecordId: true },
|
|
189
191
|
});
|
|
190
|
-
const fieldRecordIds = stepRuns
|
|
191
|
-
.map((s) => s.fieldRecordId)
|
|
192
|
-
.filter((id) => id !== null);
|
|
192
|
+
const fieldRecordIds = mapDefined(stepRuns, (s) => s.fieldRecordId);
|
|
193
193
|
if (fieldRecordIds.length > 0) {
|
|
194
194
|
await tx.fieldValue.deleteMany({
|
|
195
195
|
where: { fieldRecordId: { in: fieldRecordIds } },
|
|
@@ -259,7 +259,7 @@ async function autoGenerateInstanceKey(erpTx, itemId) {
|
|
|
259
259
|
}
|
|
260
260
|
export async function completeOrderRun(orderRunId, orderId, data, userId) {
|
|
261
261
|
return erpDb.$transaction(async (erpTx) => {
|
|
262
|
-
// Load the order with its item
|
|
262
|
+
// Load the order with its item and full item field definitions.
|
|
263
263
|
const order = await erpTx.order.findUniqueOrThrow({
|
|
264
264
|
where: { id: orderId },
|
|
265
265
|
select: {
|
|
@@ -270,7 +270,15 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
|
|
|
270
270
|
fieldSet: {
|
|
271
271
|
select: {
|
|
272
272
|
fields: {
|
|
273
|
-
select: {
|
|
273
|
+
select: {
|
|
274
|
+
id: true,
|
|
275
|
+
seqNo: true,
|
|
276
|
+
label: true,
|
|
277
|
+
type: true,
|
|
278
|
+
isArray: true,
|
|
279
|
+
required: true,
|
|
280
|
+
},
|
|
281
|
+
orderBy: { seqNo: "asc" },
|
|
274
282
|
},
|
|
275
283
|
},
|
|
276
284
|
},
|
|
@@ -279,14 +287,60 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
|
|
|
279
287
|
},
|
|
280
288
|
});
|
|
281
289
|
if (!order.item) {
|
|
282
|
-
return {
|
|
290
|
+
return {
|
|
291
|
+
error: "Order has no item assigned — cannot complete",
|
|
292
|
+
status: 422,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const itemFields = order.item.fieldSet?.fields ?? [];
|
|
296
|
+
const fieldsBySeqNo = keyBy(itemFields, (f) => f.seqNo);
|
|
297
|
+
const fieldsById = keyBy(itemFields, (f) => f.id);
|
|
298
|
+
// Validate caller-supplied fieldSeqNos exist on the item.
|
|
299
|
+
const callerValues = data.fieldValues ?? [];
|
|
300
|
+
for (const fv of callerValues) {
|
|
301
|
+
if (!fieldsBySeqNo.has(fv.fieldSeqNo)) {
|
|
302
|
+
return {
|
|
303
|
+
error: `Unknown item field seqNo ${fv.fieldSeqNo} — item has no field with that sequence number`,
|
|
304
|
+
status: 400,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const resolved = new Map();
|
|
309
|
+
const keyOf = (fieldId, setIndex) => `${fieldId}:${setIndex}`;
|
|
310
|
+
for (const fv of callerValues) {
|
|
311
|
+
const def = fieldsBySeqNo.get(fv.fieldSeqNo);
|
|
312
|
+
const setIndex = fv.setIndex ?? 0;
|
|
313
|
+
resolved.set(keyOf(def.id, setIndex), {
|
|
314
|
+
fieldId: def.id,
|
|
315
|
+
value: fv.value,
|
|
316
|
+
setIndex,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// Validate all item fields against caller-supplied values at setIndex 0.
|
|
320
|
+
// `fieldValues[]` on CompleteOrderRun is flat (no multi-set support), so
|
|
321
|
+
// we only validate set 0. Flags both missing-required and type-invalid.
|
|
322
|
+
const failures = validateFieldSet(itemFields, [0], (fieldId, setIndex) => {
|
|
323
|
+
const def = fieldsById.get(fieldId);
|
|
324
|
+
const r = resolved.get(keyOf(fieldId, setIndex));
|
|
325
|
+
if (r)
|
|
326
|
+
return deserializeFieldValue(r.value, def.isArray);
|
|
327
|
+
return def.isArray ? [] : "";
|
|
328
|
+
});
|
|
329
|
+
if (failures.length > 0) {
|
|
330
|
+
return {
|
|
331
|
+
error: `Cannot complete order run: ${failures
|
|
332
|
+
.map((f) => `${f.label} — ${f.error}`)
|
|
333
|
+
.join("; ")}. Provide values via fieldValues[] using fieldSeqNo.`,
|
|
334
|
+
status: 400,
|
|
335
|
+
missingFields: failures.map((f) => f.label),
|
|
336
|
+
};
|
|
283
337
|
}
|
|
284
338
|
// Determine instance key
|
|
285
339
|
let instanceKey = data.instanceKey;
|
|
286
340
|
if (!instanceKey) {
|
|
287
341
|
const result = await autoGenerateInstanceKey(erpTx, order.item.id);
|
|
288
342
|
if ("error" in result)
|
|
289
|
-
return result;
|
|
343
|
+
return { error: result.error, status: 422 };
|
|
290
344
|
instanceKey = result.key;
|
|
291
345
|
}
|
|
292
346
|
// Check for duplicate key
|
|
@@ -296,6 +350,7 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
|
|
|
296
350
|
if (existing) {
|
|
297
351
|
return {
|
|
298
352
|
error: `Instance key "${instanceKey}" already exists for this item`,
|
|
353
|
+
status: 422,
|
|
299
354
|
};
|
|
300
355
|
}
|
|
301
356
|
// Create the item instance
|
|
@@ -309,8 +364,8 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
|
|
|
309
364
|
updatedById: userId,
|
|
310
365
|
},
|
|
311
366
|
});
|
|
312
|
-
// Create field record and field values if
|
|
313
|
-
if (order.item.fieldSetId &&
|
|
367
|
+
// Create field record and field values if we have any to write.
|
|
368
|
+
if (order.item.fieldSetId && resolved.size > 0) {
|
|
314
369
|
const fieldRecord = await erpTx.fieldRecord.create({
|
|
315
370
|
data: {
|
|
316
371
|
fieldSetId: order.item.fieldSetId,
|
|
@@ -321,17 +376,8 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
|
|
|
321
376
|
where: { id: instance.id },
|
|
322
377
|
data: { fieldRecordId: fieldRecord.id },
|
|
323
378
|
});
|
|
324
|
-
for (const fv of
|
|
325
|
-
await
|
|
326
|
-
data: {
|
|
327
|
-
fieldRecordId: fieldRecord.id,
|
|
328
|
-
fieldId: fv.fieldId,
|
|
329
|
-
setIndex: fv.setIndex ?? 0,
|
|
330
|
-
value: fv.value,
|
|
331
|
-
createdById: userId,
|
|
332
|
-
updatedById: userId,
|
|
333
|
-
},
|
|
334
|
-
});
|
|
379
|
+
for (const fv of resolved.values()) {
|
|
380
|
+
await upsertFieldValue(fieldRecord.id, fv.fieldId, fv.setIndex, fv.value, userId, erpTx);
|
|
335
381
|
}
|
|
336
382
|
}
|
|
337
383
|
// Sum operation run costs and transition run to closed
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { RevisionStatus } from "@naisys/erp-shared";
|
|
2
|
+
import erpDb from "../../database/erpDb.js";
|
|
3
|
+
import { includeUsers } from "../../route-helpers.js";
|
|
3
4
|
// --- Prisma include & result type ---
|
|
4
5
|
const includeOrderRelations = {
|
|
5
6
|
...includeUsers,
|
|
@@ -31,6 +32,14 @@ export async function checkHasRevisions(orderId) {
|
|
|
31
32
|
});
|
|
32
33
|
return revisionCount > 0;
|
|
33
34
|
}
|
|
35
|
+
// --- Derived fields ---
|
|
36
|
+
export async function getLatestApprovedRevNo(orderId) {
|
|
37
|
+
const result = await erpDb.orderRevision.aggregate({
|
|
38
|
+
where: { orderId, status: RevisionStatus.approved },
|
|
39
|
+
_max: { revNo: true },
|
|
40
|
+
});
|
|
41
|
+
return result._max.revNo ?? null;
|
|
42
|
+
}
|
|
34
43
|
// --- Mutations ---
|
|
35
44
|
export async function resolveItemKey(itemKey) {
|
|
36
45
|
if (!itemKey)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { keyBy, unique } from "@naisys/common";
|
|
2
|
+
import erpDb from "../../database/erpDb.js";
|
|
2
3
|
// --- Prisma deep include for full revision tree ---
|
|
3
4
|
const includeFullTree = {
|
|
4
5
|
operations: {
|
|
@@ -41,9 +42,9 @@ function compareProps(pairs) {
|
|
|
41
42
|
return changes;
|
|
42
43
|
}
|
|
43
44
|
function diffFields(fromFields, toFields) {
|
|
44
|
-
const fromMap =
|
|
45
|
-
const toMap =
|
|
46
|
-
const allSeqNos =
|
|
45
|
+
const fromMap = keyBy(fromFields, (f) => f.seqNo);
|
|
46
|
+
const toMap = keyBy(toFields, (f) => f.seqNo);
|
|
47
|
+
const allSeqNos = unique([...fromMap.keys(), ...toMap.keys()]);
|
|
47
48
|
const result = [];
|
|
48
49
|
for (const seqNo of [...allSeqNos].sort((a, b) => a - b)) {
|
|
49
50
|
const from = fromMap.get(seqNo);
|
|
@@ -72,9 +73,9 @@ function diffFields(fromFields, toFields) {
|
|
|
72
73
|
return result;
|
|
73
74
|
}
|
|
74
75
|
function diffSteps(fromSteps, toSteps) {
|
|
75
|
-
const fromMap =
|
|
76
|
-
const toMap =
|
|
77
|
-
const allSeqNos =
|
|
76
|
+
const fromMap = keyBy(fromSteps, (s) => s.seqNo);
|
|
77
|
+
const toMap = keyBy(toSteps, (s) => s.seqNo);
|
|
78
|
+
const allSeqNos = unique([...fromMap.keys(), ...toMap.keys()]);
|
|
78
79
|
const result = [];
|
|
79
80
|
for (const seqNo of [...allSeqNos].sort((a, b) => a - b)) {
|
|
80
81
|
const from = fromMap.get(seqNo);
|
|
@@ -138,9 +139,9 @@ function diffDeps(fromDeps, toDeps) {
|
|
|
138
139
|
return result;
|
|
139
140
|
}
|
|
140
141
|
function diffOperations(fromOps, toOps) {
|
|
141
|
-
const fromMap =
|
|
142
|
-
const toMap =
|
|
143
|
-
const allSeqNos =
|
|
142
|
+
const fromMap = keyBy(fromOps, (op) => op.seqNo);
|
|
143
|
+
const toMap = keyBy(toOps, (op) => op.seqNo);
|
|
144
|
+
const allSeqNos = unique([...fromMap.keys(), ...toMap.keys()]);
|
|
144
145
|
const result = [];
|
|
145
146
|
for (const seqNo of [...allSeqNos].sort((a, b) => a - b)) {
|
|
146
147
|
const from = fromMap.get(seqNo);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FieldType } from "@naisys/erp-shared";
|
|
2
|
-
import erpDb from "
|
|
3
|
-
import { calcNextSeqNo, includeUsers, } from "
|
|
2
|
+
import erpDb from "../../database/erpDb.js";
|
|
3
|
+
import { calcNextSeqNo, includeUsers, } from "../../route-helpers.js";
|
|
4
4
|
// --- Lookups ---
|
|
5
5
|
export async function listFields(fieldSetId) {
|
|
6
6
|
return erpDb.field.findMany({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatFileSize } from "@naisys/common";
|
|
2
2
|
import { FieldType, } from "@naisys/erp-shared";
|
|
3
|
-
import erpDb from "
|
|
3
|
+
import erpDb from "../../database/erpDb.js";
|
|
4
4
|
// --- Lookups ---
|
|
5
5
|
export async function findStepRunWithField(id, opRunId, fieldSeqNo) {
|
|
6
6
|
const stepRun = await erpDb.stepRun.findUnique({
|
|
@@ -113,6 +113,30 @@ export function checkFieldValueShape(label, type, isArray, value) {
|
|
|
113
113
|
}
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Validate a list of field definitions across one or more set indexes, given a
|
|
118
|
+
* way to look up the current value for each (fieldId, setIndex) pair. Returns
|
|
119
|
+
* one entry per invalid cell; callers format as appropriate. Iteration order
|
|
120
|
+
* is ascending setIndex, then the order of `fieldDefs` as given.
|
|
121
|
+
*/
|
|
122
|
+
export function validateFieldSet(fieldDefs, setIndexes, getValue) {
|
|
123
|
+
const failures = [];
|
|
124
|
+
for (const si of [...setIndexes].sort((a, b) => a - b)) {
|
|
125
|
+
for (const def of fieldDefs) {
|
|
126
|
+
const value = getValue(def.id, si);
|
|
127
|
+
const result = validateFieldValue(def.type, def.isArray, def.required, value);
|
|
128
|
+
if (!result.valid) {
|
|
129
|
+
failures.push({
|
|
130
|
+
fieldId: def.id,
|
|
131
|
+
label: def.label,
|
|
132
|
+
setIndex: si,
|
|
133
|
+
error: result.error,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return failures;
|
|
139
|
+
}
|
|
116
140
|
export function validateFieldValue(type, isArray, required, value) {
|
|
117
141
|
const shapeErr = checkFieldValueShape("field", type, isArray, value);
|
|
118
142
|
if (shapeErr)
|
|
@@ -223,9 +247,9 @@ export async function clearAttachmentFieldValue(fieldRecordId, fieldId, setIndex
|
|
|
223
247
|
await upsertFieldValue(fieldRecordId, fieldId, setIndex, empty, userId);
|
|
224
248
|
}
|
|
225
249
|
// --- Mutations ---
|
|
226
|
-
export async function upsertFieldValue(fieldRecordId, fieldId, setIndex, value, userId) {
|
|
250
|
+
export async function upsertFieldValue(fieldRecordId, fieldId, setIndex, value, userId, tx = erpDb) {
|
|
227
251
|
const dbValue = serializeFieldValue(value);
|
|
228
|
-
await
|
|
252
|
+
await tx.fieldValue.upsert({
|
|
229
253
|
where: {
|
|
230
254
|
fieldRecordId_fieldId_setIndex: { fieldRecordId, fieldId, setIndex },
|
|
231
255
|
},
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { sumAgentMetricsByUuid } from "@naisys/hub-database";
|
|
2
|
+
import { OperationRunStatus } from "@naisys/erp-shared";
|
|
3
|
+
import erpDb from "../../database/erpDb.js";
|
|
4
|
+
/**
|
|
5
|
+
* Populate `tokens` on labor tickets and operation runs finalized before the
|
|
6
|
+
* column existed. Idempotent — only touches rows where `tokens IS NULL`, and
|
|
7
|
+
* only for agent users (others can't have tokens, so their rows stay NULL
|
|
8
|
+
* and the next startup ignores them too). Requires the hub DB client.
|
|
9
|
+
*/
|
|
10
|
+
export async function backfillOpRunTokens() {
|
|
11
|
+
const tickets = await erpDb.laborTicket.findMany({
|
|
12
|
+
where: {
|
|
13
|
+
tokens: null,
|
|
14
|
+
clockOut: { not: null },
|
|
15
|
+
operationRun: {
|
|
16
|
+
status: {
|
|
17
|
+
in: [
|
|
18
|
+
OperationRunStatus.completed,
|
|
19
|
+
OperationRunStatus.skipped,
|
|
20
|
+
OperationRunStatus.failed,
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
user: { isAgent: true },
|
|
25
|
+
},
|
|
26
|
+
select: {
|
|
27
|
+
id: true,
|
|
28
|
+
operationRunId: true,
|
|
29
|
+
clockIn: true,
|
|
30
|
+
clockOut: true,
|
|
31
|
+
user: { select: { uuid: true } },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (tickets.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const dirtyOpRunIds = new Set();
|
|
37
|
+
for (const ticket of tickets) {
|
|
38
|
+
const { tokens } = await sumAgentMetricsByUuid(ticket.user.uuid, ticket.clockIn, ticket.clockOut);
|
|
39
|
+
await erpDb.laborTicket.update({
|
|
40
|
+
where: { id: ticket.id },
|
|
41
|
+
data: { tokens: Math.round(tokens) },
|
|
42
|
+
});
|
|
43
|
+
dirtyOpRunIds.add(ticket.operationRunId);
|
|
44
|
+
}
|
|
45
|
+
// Re-aggregate touched op_runs. Skip rows that already have a snapshot so
|
|
46
|
+
// we never clobber a value set by the normal transition path.
|
|
47
|
+
for (const opRunId of dirtyOpRunIds) {
|
|
48
|
+
const opRun = await erpDb.operationRun.findUnique({
|
|
49
|
+
where: { id: opRunId },
|
|
50
|
+
select: { tokens: true },
|
|
51
|
+
});
|
|
52
|
+
if (!opRun || opRun.tokens !== null)
|
|
53
|
+
continue;
|
|
54
|
+
const agg = await erpDb.laborTicket.aggregate({
|
|
55
|
+
where: { operationRunId: opRunId },
|
|
56
|
+
_sum: { tokens: true },
|
|
57
|
+
});
|
|
58
|
+
const total = agg._sum.tokens ?? 0;
|
|
59
|
+
if (total > 0) {
|
|
60
|
+
await erpDb.operationRun.update({
|
|
61
|
+
where: { id: opRunId },
|
|
62
|
+
data: { tokens: total },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=labor-ticket-backfill.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getLatestRunInfoByUuid,
|
|
1
|
+
import { getLatestRunInfoByUuid, sumAgentMetricsByUuid, } from "@naisys/hub-database";
|
|
2
|
+
import erpDb from "../../database/erpDb.js";
|
|
2
3
|
import { writeAuditEntry } from "../audit.js";
|
|
3
|
-
import erpDb from "../erpDb.js";
|
|
4
4
|
// --- Prisma include & result type ---
|
|
5
5
|
export const includeLaborTicket = {
|
|
6
6
|
user: { select: { username: true } },
|
|
@@ -9,19 +9,22 @@ export const includeLaborTicket = {
|
|
|
9
9
|
};
|
|
10
10
|
// --- Helpers ---
|
|
11
11
|
/**
|
|
12
|
-
* Compute
|
|
12
|
+
* Compute cost + tokens for a labor ticket at clock-out time.
|
|
13
13
|
* Agents: sum of hub cost entries for the user within the clock-in/out window.
|
|
14
|
-
* Non-agents:
|
|
14
|
+
* Non-agents: zeros.
|
|
15
15
|
*/
|
|
16
|
-
async function
|
|
16
|
+
async function computeAgentMetrics(userId, clockIn, clockOut) {
|
|
17
17
|
const user = await erpDb.user.findUnique({
|
|
18
18
|
where: { id: userId },
|
|
19
19
|
select: { isAgent: true, uuid: true },
|
|
20
20
|
});
|
|
21
21
|
if (!user?.isAgent)
|
|
22
|
-
return 0;
|
|
23
|
-
const cost = await
|
|
24
|
-
return
|
|
22
|
+
return { cost: 0, tokens: 0 };
|
|
23
|
+
const { cost, tokens } = await sumAgentMetricsByUuid(user.uuid, clockIn, clockOut);
|
|
24
|
+
return {
|
|
25
|
+
cost: Math.round(cost * 100) / 100,
|
|
26
|
+
tokens: Math.round(tokens),
|
|
27
|
+
};
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
30
|
* Get the current hub run info (run_id + session start) for an agent user.
|
|
@@ -67,10 +70,10 @@ export async function clockIn(operationRunId, userId, actorId) {
|
|
|
67
70
|
where: { userId, clockOut: null },
|
|
68
71
|
});
|
|
69
72
|
for (const ticket of openTickets) {
|
|
70
|
-
const cost = await
|
|
73
|
+
const { cost, tokens } = await computeAgentMetrics(userId, ticket.clockIn, now);
|
|
71
74
|
await tx.laborTicket.update({
|
|
72
75
|
where: { id: ticket.id },
|
|
73
|
-
data: { clockOut: now, cost, updatedById: actorId },
|
|
76
|
+
data: { clockOut: now, cost, tokens, updatedById: actorId },
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
79
|
// If no tickets were auto-closed and this is the first ticket for the
|
|
@@ -118,10 +121,10 @@ export async function clockOut(operationRunId, opts, actorId) {
|
|
|
118
121
|
const openTickets = await tx.laborTicket.findMany({ where });
|
|
119
122
|
const updated = [];
|
|
120
123
|
for (const ticket of openTickets) {
|
|
121
|
-
const cost = await
|
|
124
|
+
const { cost, tokens } = await computeAgentMetrics(ticket.userId, ticket.clockIn, now);
|
|
122
125
|
const result = await tx.laborTicket.update({
|
|
123
126
|
where: { id: ticket.id },
|
|
124
|
-
data: { clockOut: now, cost, updatedById: actorId },
|
|
127
|
+
data: { clockOut: now, cost, tokens, updatedById: actorId },
|
|
125
128
|
include: includeLaborTicket,
|
|
126
129
|
});
|
|
127
130
|
updated.push(result);
|
|
@@ -132,12 +135,15 @@ export async function clockOut(operationRunId, opts, actorId) {
|
|
|
132
135
|
export async function clockOutAllForOpRun(operationRunId, actorId) {
|
|
133
136
|
await clockOut(operationRunId, {}, actorId);
|
|
134
137
|
}
|
|
135
|
-
export async function
|
|
138
|
+
export async function sumLaborTicketMetrics(operationRunId) {
|
|
136
139
|
const result = await erpDb.laborTicket.aggregate({
|
|
137
140
|
where: { operationRunId },
|
|
138
|
-
_sum: { cost: true },
|
|
141
|
+
_sum: { cost: true, tokens: true },
|
|
139
142
|
});
|
|
140
|
-
return
|
|
143
|
+
return {
|
|
144
|
+
cost: Math.round((result._sum.cost ?? 0) * 100) / 100,
|
|
145
|
+
tokens: result._sum.tokens ?? 0,
|
|
146
|
+
};
|
|
141
147
|
}
|
|
142
148
|
export async function deleteLaborTicket(ticketId, actorId) {
|
|
143
149
|
await erpDb.$transaction(async (tx) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import erpDb from "
|
|
2
|
-
import { includeUsers } from "
|
|
1
|
+
import erpDb from "../../database/erpDb.js";
|
|
2
|
+
import { includeUsers } from "../../route-helpers.js";
|
|
3
3
|
// --- Prisma include & result type ---
|
|
4
4
|
const includeDetail = {
|
|
5
5
|
...includeUsers,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SUPER_ADMIN_USERNAME } from "@naisys/common";
|
|
2
|
+
import { generatePersistentUserApiKey } from "@naisys/common-node";
|
|
3
|
+
import { ensureSuperAdmin } from "@naisys/supervisor-database";
|
|
2
4
|
import bcrypt from "bcryptjs";
|
|
3
|
-
import {
|
|
4
|
-
import erpDb from "../erpDb.js";
|
|
5
|
-
import { isSupervisorAuth } from "../supervisorAuth.js";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import erpDb from "../database/erpDb.js";
|
|
6
7
|
// --- Prisma include & result type ---
|
|
7
8
|
export const includePermissions = {
|
|
8
9
|
permissions: true,
|
|
@@ -37,19 +38,12 @@ export async function getUserById(id) {
|
|
|
37
38
|
include: includePermissions,
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
|
-
export async function
|
|
41
|
+
export async function hasUserApiKey(id) {
|
|
41
42
|
const user = await erpDb.user.findUnique({
|
|
42
43
|
where: { id },
|
|
43
|
-
select: {
|
|
44
|
+
select: { apiKeyHash: true },
|
|
44
45
|
});
|
|
45
|
-
|
|
46
|
-
return null;
|
|
47
|
-
if (user.isAgent && isSupervisorAuth()) {
|
|
48
|
-
return getAgentApiKeyByUuid(user.uuid);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
return user.apiKey ?? null;
|
|
52
|
-
}
|
|
46
|
+
return !!user?.apiKeyHash;
|
|
53
47
|
}
|
|
54
48
|
// --- Mutations ---
|
|
55
49
|
export async function getUserByUuid(uuid) {
|
|
@@ -63,7 +57,6 @@ export async function createUserForAgent(username, uuid) {
|
|
|
63
57
|
data: {
|
|
64
58
|
username,
|
|
65
59
|
uuid,
|
|
66
|
-
passwordHash: "",
|
|
67
60
|
isAgent: true,
|
|
68
61
|
},
|
|
69
62
|
include: includePermissions,
|
|
@@ -78,7 +71,6 @@ export async function createUserWithPassword(data) {
|
|
|
78
71
|
uuid,
|
|
79
72
|
passwordHash,
|
|
80
73
|
isAgent: false,
|
|
81
|
-
apiKey: randomBytes(32).toString("hex"),
|
|
82
74
|
},
|
|
83
75
|
include: includePermissions,
|
|
84
76
|
});
|
|
@@ -111,22 +103,96 @@ export async function revokePermission(userId, permission) {
|
|
|
111
103
|
});
|
|
112
104
|
}
|
|
113
105
|
export async function rotateUserApiKey(id) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
return generatePersistentUserApiKey(id, {
|
|
107
|
+
userExists: async (userId) => (await erpDb.user.findUnique({
|
|
108
|
+
where: { id: userId },
|
|
109
|
+
select: { id: true },
|
|
110
|
+
})) !== null,
|
|
111
|
+
updateApiKeyHash: (userId, apiKeyHash) => erpDb.user.update({
|
|
112
|
+
where: { id: userId },
|
|
113
|
+
data: { apiKeyHash },
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// --- Superadmin bootstrap ---
|
|
118
|
+
/**
|
|
119
|
+
* Ensure a superadmin user exists in the local ERP database.
|
|
120
|
+
* If a password is supplied, it is used on create and updates the existing one if present.
|
|
121
|
+
* For standalone mode (no supervisor auth).
|
|
122
|
+
*/
|
|
123
|
+
export async function ensureLocalSuperAdmin(password) {
|
|
124
|
+
const existing = await erpDb.user.findUnique({
|
|
125
|
+
where: { username: SUPER_ADMIN_USERNAME },
|
|
118
126
|
});
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
if (existing) {
|
|
128
|
+
await ensureErpAdminPermission(existing.id);
|
|
129
|
+
if (password) {
|
|
130
|
+
const hash = await bcrypt.hash(password, SALT_ROUNDS);
|
|
131
|
+
await erpDb.user.update({
|
|
132
|
+
where: { id: existing.id },
|
|
133
|
+
data: { passwordHash: hash },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
123
136
|
}
|
|
124
137
|
else {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
const finalPassword = password || randomUUID().slice(0, 8);
|
|
139
|
+
const hash = await bcrypt.hash(finalPassword, SALT_ROUNDS);
|
|
140
|
+
const user = await erpDb.user.create({
|
|
141
|
+
data: {
|
|
142
|
+
uuid: randomUUID(),
|
|
143
|
+
username: SUPER_ADMIN_USERNAME,
|
|
144
|
+
passwordHash: hash,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
await ensureErpAdminPermission(user.id);
|
|
148
|
+
if (!password) {
|
|
149
|
+
console.log(`\n ${SUPER_ADMIN_USERNAME} user created. Password: ${finalPassword}`);
|
|
150
|
+
console.log(` Change it via the admin UI or with --setup\n`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Warn if agent users exist without supervisor auth
|
|
154
|
+
const agentCount = await erpDb.user.count({ where: { isAgent: true } });
|
|
155
|
+
if (agentCount > 0) {
|
|
156
|
+
console.warn(`[ERP] Warning: ${agentCount} agent user(s) found but supervisor auth is disabled. ` +
|
|
157
|
+
`Agent API key lookups and authentication will not work. ` +
|
|
158
|
+
`Set SUPERVISOR_AUTH=true to enable.`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Sync superadmin from supervisor into ERP DB and ensure permissions.
|
|
163
|
+
* For supervisor auth mode. Supervisor uses passkey-only auth — the
|
|
164
|
+
* mirrored ERP row has no passwordHash.
|
|
165
|
+
*/
|
|
166
|
+
export async function ensureSupervisorSuperAdmin() {
|
|
167
|
+
const result = await ensureSuperAdmin();
|
|
168
|
+
await erpDb.user.upsert({
|
|
169
|
+
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
|
+
});
|
|
178
|
+
const localSuperAdmin = await erpDb.user.findUnique({
|
|
179
|
+
where: { uuid: result.user.uuid },
|
|
180
|
+
});
|
|
181
|
+
if (localSuperAdmin) {
|
|
182
|
+
await ensureErpAdminPermission(localSuperAdmin.id);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Ensure a user has the erp_admin permission.
|
|
187
|
+
*/
|
|
188
|
+
export async function ensureErpAdminPermission(userId) {
|
|
189
|
+
const existing = await erpDb.userPermission.findUnique({
|
|
190
|
+
where: { userId_permission: { userId, permission: "erp_admin" } },
|
|
191
|
+
});
|
|
192
|
+
if (!existing) {
|
|
193
|
+
await erpDb.userPermission.create({
|
|
194
|
+
data: { userId, permission: "erp_admin" },
|
|
128
195
|
});
|
|
129
196
|
}
|
|
130
|
-
return newKey;
|
|
131
197
|
}
|
|
132
198
|
//# sourceMappingURL=user-service.js.map
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { getGitCommitHash } from "@naisys/common-node";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
// Read the server's own package.json (one level up from dist/)
|
|
7
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
8
|
+
const commitHash = getGitCommitHash(__dirname);
|
|
9
|
+
export function getPackageVersion() {
|
|
10
|
+
return commitHash ? `${pkg.version}/${commitHash}` : pkg.version;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=version.js.map
|