@naisys/erp 3.0.0-beta.50 → 3.0.0-beta.52
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/client-dist/assets/{index-CNe-LDbP.js → index-D6lSIioV.js} +80 -8
- package/client-dist/assets/{vendor-Co7ZCNxO.js → vendor-CJ0ET9hP.js} +8488 -1
- package/client-dist/index.html +2 -2
- package/dist/database/dbConfig.js +1 -1
- package/dist/erpServer.js +2 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.js +2 -0
- package/dist/routes/operations/operation-run-transitions.js +15 -6
- package/dist/routes/operations/operation-runs.js +28 -4
- package/dist/routes/production/labor-tickets.js +1 -0
- package/dist/services/operations/operation-run-service.js +2 -1
- package/dist/services/orders/order-revision-service.js +2 -3
- package/dist/services/orders/order-run-service.js +4 -5
- package/dist/services/orders/revision-diff-service.js +10 -9
- package/dist/services/production/labor-ticket-backfill.js +67 -0
- package/dist/services/production/labor-ticket-service.js +20 -14
- package/npm-shrinkwrap.json +75 -53
- package/package.json +6 -6
- package/prisma/migrations/20260517000000_add_op_run_tokens/migration.sql +2 -0
- package/prisma/schema.prisma +2 -0
|
@@ -225,6 +225,7 @@ export const OperationRunScalarFieldEnum = {
|
|
|
225
225
|
status: 'status',
|
|
226
226
|
assignedToId: 'assignedToId',
|
|
227
227
|
cost: 'cost',
|
|
228
|
+
tokens: 'tokens',
|
|
228
229
|
statusNote: 'statusNote',
|
|
229
230
|
completedAt: 'completedAt',
|
|
230
231
|
createdAt: 'createdAt',
|
|
@@ -315,6 +316,7 @@ export const LaborTicketScalarFieldEnum = {
|
|
|
315
316
|
clockIn: 'clockIn',
|
|
316
317
|
clockOut: 'clockOut',
|
|
317
318
|
cost: 'cost',
|
|
319
|
+
tokens: 'tokens',
|
|
318
320
|
createdAt: 'createdAt',
|
|
319
321
|
createdById: 'createdById',
|
|
320
322
|
updatedAt: 'updatedAt',
|
|
@@ -4,7 +4,7 @@ import { requirePermission } from "../../middleware/auth-middleware.js";
|
|
|
4
4
|
import { checkOrderRunStarted, checkWorkCenterAccess, mutationResult, resolveOpRun, } from "../../route-helpers.js";
|
|
5
5
|
import { checkPredecessorsComplete, checkStepsComplete, reblockSuccessors, transitionStatus, unblockSuccessors, validateStatusFor, } from "../../services/operations/operation-run-service.js";
|
|
6
6
|
import { transitionStatus as transitionOrderRunStatus } from "../../services/orders/order-run-service.js";
|
|
7
|
-
import { clockIn, clockOutAllForOpRun, isUserClockedIn,
|
|
7
|
+
import { clockIn, clockOutAllForOpRun, isUserClockedIn, sumLaborTicketMetrics, } from "../../services/production/labor-ticket-service.js";
|
|
8
8
|
import { formatOpRunTransition, SeqNoParamsSchema } from "./operation-runs.js";
|
|
9
9
|
export default function operationRunTransitionRoutes(fastify) {
|
|
10
10
|
const app = fastify.withTypeProvider();
|
|
@@ -99,10 +99,11 @@ export default function operationRunTransitionRoutes(fastify) {
|
|
|
99
99
|
if (stepsErr)
|
|
100
100
|
return unprocessable(reply, stepsErr);
|
|
101
101
|
await clockOutAllForOpRun(resolved.opRun.id, userId);
|
|
102
|
-
const cost = await
|
|
102
|
+
const { cost, tokens } = await sumLaborTicketMetrics(resolved.opRun.id);
|
|
103
103
|
const opRun = await transitionStatus(resolved.opRun.id, "complete", OperationRunStatus.in_progress, OperationRunStatus.completed, userId, {
|
|
104
104
|
completedAt: new Date(),
|
|
105
105
|
cost,
|
|
106
|
+
tokens,
|
|
106
107
|
statusNote: note ?? null,
|
|
107
108
|
});
|
|
108
109
|
await unblockSuccessors(resolved.run.id, resolved.opRun.operationId, userId);
|
|
@@ -153,8 +154,12 @@ export default function operationRunTransitionRoutes(fastify) {
|
|
|
153
154
|
if (resolved.opRun.status === OperationRunStatus.in_progress) {
|
|
154
155
|
await clockOutAllForOpRun(resolved.opRun.id, userId);
|
|
155
156
|
}
|
|
156
|
-
const cost = await
|
|
157
|
-
const opRun = await transitionStatus(resolved.opRun.id, "skip", resolved.opRun.status, OperationRunStatus.skipped, userId, {
|
|
157
|
+
const { cost, tokens } = await sumLaborTicketMetrics(resolved.opRun.id);
|
|
158
|
+
const opRun = await transitionStatus(resolved.opRun.id, "skip", resolved.opRun.status, OperationRunStatus.skipped, userId, {
|
|
159
|
+
...(cost > 0 ? { cost } : undefined),
|
|
160
|
+
...(tokens > 0 ? { tokens } : undefined),
|
|
161
|
+
statusNote: note ?? null,
|
|
162
|
+
});
|
|
158
163
|
await unblockSuccessors(resolved.run.id, resolved.opRun.operationId, userId);
|
|
159
164
|
const full = await formatOpRunTransition(orderKey, runNo, request.erpUser, opRun);
|
|
160
165
|
return mutationResult(request, reply, full, {
|
|
@@ -196,8 +201,12 @@ export default function operationRunTransitionRoutes(fastify) {
|
|
|
196
201
|
if (statusErr)
|
|
197
202
|
return conflict(reply, statusErr);
|
|
198
203
|
await clockOutAllForOpRun(resolved.opRun.id, userId);
|
|
199
|
-
const cost = await
|
|
200
|
-
const opRun = await transitionStatus(resolved.opRun.id, "fail", OperationRunStatus.in_progress, OperationRunStatus.failed, userId, {
|
|
204
|
+
const { cost, tokens } = await sumLaborTicketMetrics(resolved.opRun.id);
|
|
205
|
+
const opRun = await transitionStatus(resolved.opRun.id, "fail", OperationRunStatus.in_progress, OperationRunStatus.failed, userId, {
|
|
206
|
+
...(cost > 0 ? { cost } : undefined),
|
|
207
|
+
...(tokens > 0 ? { tokens } : undefined),
|
|
208
|
+
statusNote: note ?? null,
|
|
209
|
+
});
|
|
201
210
|
const full = await formatOpRunTransition(orderKey, runNo, request.erpUser, opRun);
|
|
202
211
|
return mutationResult(request, reply, full, {
|
|
203
212
|
status: opRun.status,
|
|
@@ -5,16 +5,25 @@ import { API_PREFIX, selfLink } from "../../hateoas.js";
|
|
|
5
5
|
import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
|
|
6
6
|
import { checkOrderRunStarted, checkWorkCenterAccess, childItemLinks, formatAuditFields, formatDate, resolveActions, resolveOpRun, resolveOrderRun, useFullSerializer, wantsFullResponse, } from "../../route-helpers.js";
|
|
7
7
|
import { checkStepsComplete, getOpRun, getOpRunFieldRefSummary, getOpRunStepSummary, listOpRuns, updateOpRun, validateStatusFor, } from "../../services/operations/operation-run-service.js";
|
|
8
|
+
import { isUserClockedIn } from "../../services/production/labor-ticket-service.js";
|
|
8
9
|
function opRunResource(orderKey, runNo) {
|
|
9
10
|
return `orders/${orderKey}/runs/${runNo}/ops`;
|
|
10
11
|
}
|
|
11
12
|
async function opRunItemActions(orderKey, runNo, seqNo, opRunId, operationId, status, user) {
|
|
12
13
|
const href = `${API_PREFIX}/${opRunResource(orderKey, runNo)}/${seqNo}`;
|
|
13
14
|
const isExecutor = hasPermission(user, "order_executor");
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
: null;
|
|
15
|
+
const isInProgress = status === OperationRunStatus.in_progress;
|
|
16
|
+
const stepsErr = isExecutor && isInProgress ? await checkStepsComplete(opRunId) : null;
|
|
17
17
|
const wcErr = user ? await checkWorkCenterAccess(operationId, user) : null;
|
|
18
|
+
// Completing an op requires the caller to be clocked in (enforced by the
|
|
19
|
+
// /complete transition). Surface clock-in and the requirement here so an
|
|
20
|
+
// agent inspecting the op run finds it without following the `labor` link.
|
|
21
|
+
const userClockedIn = isExecutor && isInProgress && user
|
|
22
|
+
? await isUserClockedIn(opRunId, user.id)
|
|
23
|
+
: false;
|
|
24
|
+
const notClockedInErr = isExecutor && isInProgress && !userClockedIn
|
|
25
|
+
? "You must be clocked in to complete an operation"
|
|
26
|
+
: null;
|
|
18
27
|
return resolveActions([
|
|
19
28
|
{
|
|
20
29
|
rel: "assign",
|
|
@@ -50,6 +59,18 @@ async function opRunItemActions(orderKey, runNo, seqNo, opRunId, operationId, st
|
|
|
50
59
|
? "Operation is blocked by incomplete predecessors"
|
|
51
60
|
: null),
|
|
52
61
|
},
|
|
62
|
+
{
|
|
63
|
+
rel: "clock-in",
|
|
64
|
+
path: "/labor/clock-in",
|
|
65
|
+
method: "POST",
|
|
66
|
+
title: "Clock In",
|
|
67
|
+
permission: "order_executor",
|
|
68
|
+
statuses: [OperationRunStatus.in_progress],
|
|
69
|
+
disabledWhen: () => wcErr ??
|
|
70
|
+
(userClockedIn
|
|
71
|
+
? "You are already clocked in to this operation"
|
|
72
|
+
: null),
|
|
73
|
+
},
|
|
53
74
|
{
|
|
54
75
|
rel: "update",
|
|
55
76
|
method: "PUT",
|
|
@@ -68,7 +89,7 @@ async function opRunItemActions(orderKey, runNo, seqNo, opRunId, operationId, st
|
|
|
68
89
|
body: { note: "" },
|
|
69
90
|
permission: "order_executor",
|
|
70
91
|
statuses: [OperationRunStatus.in_progress],
|
|
71
|
-
disabledWhen: () => wcErr ?? stepsErr,
|
|
92
|
+
disabledWhen: () => wcErr ?? notClockedInErr ?? stepsErr,
|
|
72
93
|
},
|
|
73
94
|
{
|
|
74
95
|
rel: "skip",
|
|
@@ -136,6 +157,7 @@ export async function formatOpRun(orderKey, runNo, user, opRun) {
|
|
|
136
157
|
status: opRun.status,
|
|
137
158
|
assignedTo: opRun.assignedTo?.username ?? null,
|
|
138
159
|
cost: opRun.cost,
|
|
160
|
+
tokens: opRun.tokens,
|
|
139
161
|
note: opRun.statusNote ?? null,
|
|
140
162
|
completedAt: formatDate(opRun.completedAt),
|
|
141
163
|
stepSummary: stepSummaryRows.map((sr) => ({
|
|
@@ -182,6 +204,7 @@ export async function formatOpRunTransition(orderKey, runNo, user, opRun) {
|
|
|
182
204
|
status: opRun.status,
|
|
183
205
|
assignedTo: opRun.assignedTo?.username ?? null,
|
|
184
206
|
cost: opRun.cost,
|
|
207
|
+
tokens: opRun.tokens,
|
|
185
208
|
note: opRun.statusNote ?? null,
|
|
186
209
|
completedAt: formatDate(opRun.completedAt),
|
|
187
210
|
...formatAuditFields(opRun),
|
|
@@ -203,6 +226,7 @@ function formatListOpRun(opRun) {
|
|
|
203
226
|
status: opRun.status,
|
|
204
227
|
assignedTo: opRun.assignedTo?.username ?? null,
|
|
205
228
|
cost: opRun.cost,
|
|
229
|
+
tokens: opRun.tokens,
|
|
206
230
|
note: opRun.statusNote ?? null,
|
|
207
231
|
completedAt: formatDate(opRun.completedAt),
|
|
208
232
|
...formatAuditFields(opRun),
|
|
@@ -92,6 +92,7 @@ function formatLaborTicket(orderKey, runNo, seqNo, ticket) {
|
|
|
92
92
|
clockIn: ticket.clockIn.toISOString(),
|
|
93
93
|
clockOut: formatDate(ticket.clockOut),
|
|
94
94
|
cost: ticket.cost,
|
|
95
|
+
tokens: ticket.tokens,
|
|
95
96
|
...formatAuditFields(ticket),
|
|
96
97
|
_links: [
|
|
97
98
|
selfLink(`/${laborResource(orderKey, runNo, seqNo)}/${ticket.id}`),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { keyBy } from "@naisys/common";
|
|
1
2
|
import { OperationRunStatus as OperationRunStatusValues, } from "@naisys/erp-shared";
|
|
2
3
|
import { fieldTypeString, getValueFormatHint, } from "@naisys/erp-shared";
|
|
3
4
|
import erpDb from "../../database/erpDb.js";
|
|
@@ -153,7 +154,7 @@ export async function getOpRunFieldRefSummary(operationId, orderRunId, orderKey,
|
|
|
153
154
|
},
|
|
154
155
|
},
|
|
155
156
|
});
|
|
156
|
-
const stepRunMap =
|
|
157
|
+
const stepRunMap = keyBy(stepRuns, (sr) => sr.stepId);
|
|
157
158
|
return fieldRefs.map((ref) => {
|
|
158
159
|
const sr = stepRunMap.get(ref.sourceStep.id);
|
|
159
160
|
const storedFieldValues = sr?.fieldRecord?.fieldValues ?? [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mapDefined } from "@naisys/common";
|
|
1
2
|
import { RevisionStatus as RevisionStatusValues, } from "@naisys/erp-shared";
|
|
2
3
|
import erpDb from "../../database/erpDb.js";
|
|
3
4
|
import { includeUsers } from "../../route-helpers.js";
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { keyBy, mapDefined } from "@naisys/common";
|
|
1
2
|
import { OperationRunStatus as OperationRunStatusValues, OrderRunStatus as OrderRunStatusValues, } from "@naisys/erp-shared";
|
|
2
3
|
import erpDb from "../../database/erpDb.js";
|
|
3
4
|
import { writeAuditEntry } from "../audit.js";
|
|
@@ -188,9 +189,7 @@ export async function deleteOrderRun(id) {
|
|
|
188
189
|
where: { operationRunId: { in: opRunIds } },
|
|
189
190
|
select: { fieldRecordId: true },
|
|
190
191
|
});
|
|
191
|
-
const fieldRecordIds = stepRuns
|
|
192
|
-
.map((s) => s.fieldRecordId)
|
|
193
|
-
.filter((id) => id !== null);
|
|
192
|
+
const fieldRecordIds = mapDefined(stepRuns, (s) => s.fieldRecordId);
|
|
194
193
|
if (fieldRecordIds.length > 0) {
|
|
195
194
|
await tx.fieldValue.deleteMany({
|
|
196
195
|
where: { fieldRecordId: { in: fieldRecordIds } },
|
|
@@ -294,8 +293,8 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
|
|
|
294
293
|
};
|
|
295
294
|
}
|
|
296
295
|
const itemFields = order.item.fieldSet?.fields ?? [];
|
|
297
|
-
const fieldsBySeqNo =
|
|
298
|
-
const fieldsById =
|
|
296
|
+
const fieldsBySeqNo = keyBy(itemFields, (f) => f.seqNo);
|
|
297
|
+
const fieldsById = keyBy(itemFields, (f) => f.id);
|
|
299
298
|
// Validate caller-supplied fieldSeqNos exist on the item.
|
|
300
299
|
const callerValues = data.fieldValues ?? [];
|
|
301
300
|
for (const fv of callerValues) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { keyBy, unique } from "@naisys/common";
|
|
1
2
|
import erpDb from "../../database/erpDb.js";
|
|
2
3
|
// --- Prisma deep include for full revision tree ---
|
|
3
4
|
const includeFullTree = {
|
|
@@ -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);
|
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import { getLatestRunInfoByUuid,
|
|
1
|
+
import { getLatestRunInfoByUuid, sumAgentMetricsByUuid, } from "@naisys/hub-database";
|
|
2
2
|
import erpDb from "../../database/erpDb.js";
|
|
3
3
|
import { writeAuditEntry } from "../audit.js";
|
|
4
4
|
// --- Prisma include & result type ---
|
|
@@ -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) => {
|