@naisys/erp 3.0.0-beta.50 → 3.0.0-beta.51

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.
@@ -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, sumLaborTicketCosts, } from "../../services/production/labor-ticket-service.js";
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 sumLaborTicketCosts(resolved.opRun.id);
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 sumLaborTicketCosts(resolved.opRun.id);
157
- const opRun = await transitionStatus(resolved.opRun.id, "skip", resolved.opRun.status, OperationRunStatus.skipped, userId, { ...(cost > 0 ? { cost } : undefined), statusNote: note ?? null });
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 sumLaborTicketCosts(resolved.opRun.id);
200
- const opRun = await transitionStatus(resolved.opRun.id, "fail", OperationRunStatus.in_progress, OperationRunStatus.failed, userId, { ...(cost > 0 ? { cost } : undefined), statusNote: note ?? null });
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,
@@ -136,6 +136,7 @@ export async function formatOpRun(orderKey, runNo, user, opRun) {
136
136
  status: opRun.status,
137
137
  assignedTo: opRun.assignedTo?.username ?? null,
138
138
  cost: opRun.cost,
139
+ tokens: opRun.tokens,
139
140
  note: opRun.statusNote ?? null,
140
141
  completedAt: formatDate(opRun.completedAt),
141
142
  stepSummary: stepSummaryRows.map((sr) => ({
@@ -182,6 +183,7 @@ export async function formatOpRunTransition(orderKey, runNo, user, opRun) {
182
183
  status: opRun.status,
183
184
  assignedTo: opRun.assignedTo?.username ?? null,
184
185
  cost: opRun.cost,
186
+ tokens: opRun.tokens,
185
187
  note: opRun.statusNote ?? null,
186
188
  completedAt: formatDate(opRun.completedAt),
187
189
  ...formatAuditFields(opRun),
@@ -203,6 +205,7 @@ function formatListOpRun(opRun) {
203
205
  status: opRun.status,
204
206
  assignedTo: opRun.assignedTo?.username ?? null,
205
207
  cost: opRun.cost,
208
+ tokens: opRun.tokens,
206
209
  note: opRun.statusNote ?? null,
207
210
  completedAt: formatDate(opRun.completedAt),
208
211
  ...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 = new Map(stepRuns.map((sr) => [sr.stepId, sr]));
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 = new Map(itemFields.map((f) => [f.seqNo, f]));
298
- const fieldsById = new Map(itemFields.map((f) => [f.id, f]));
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 = new Map(fromFields.map((f) => [f.seqNo, f]));
45
- const toMap = new Map(toFields.map((f) => [f.seqNo, f]));
46
- const allSeqNos = new Set([...fromMap.keys(), ...toMap.keys()]);
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 = new Map(fromSteps.map((s) => [s.seqNo, s]));
76
- const toMap = new Map(toSteps.map((s) => [s.seqNo, s]));
77
- const allSeqNos = new Set([...fromMap.keys(), ...toMap.keys()]);
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 = new Map(fromOps.map((op) => [op.seqNo, op]));
142
- const toMap = new Map(toOps.map((op) => [op.seqNo, op]));
143
- const allSeqNos = new Set([...fromMap.keys(), ...toMap.keys()]);
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, sumCostsByUuid } from "@naisys/hub-database";
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 the cost for a labor ticket at clock-out time.
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: 0.
14
+ * Non-agents: zeros.
15
15
  */
16
- async function computeCost(userId, clockIn, clockOut) {
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 sumCostsByUuid(user.uuid, clockIn, clockOut);
24
- return Math.round(cost * 100) / 100;
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 computeCost(userId, ticket.clockIn, now);
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 computeCost(ticket.userId, ticket.clockIn, now);
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 sumLaborTicketCosts(operationRunId) {
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 Math.round((result._sum.cost ?? 0) * 100) / 100;
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,12 +1,12 @@
1
1
  {
2
2
  "name": "@naisys/erp",
3
- "version": "3.0.0-beta.50",
3
+ "version": "3.0.0-beta.51",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@naisys/erp",
9
- "version": "3.0.0-beta.50",
9
+ "version": "3.0.0-beta.51",
10
10
  "dependencies": {
11
11
  "@fastify/cookie": "^11.0.2",
12
12
  "@fastify/cors": "^11.2.0",
@@ -14,11 +14,11 @@
14
14
  "@fastify/rate-limit": "^10.3.0",
15
15
  "@fastify/static": "^9.0.0",
16
16
  "@fastify/swagger": "^9.7.0",
17
- "@naisys/common": "3.0.0-beta.50",
18
- "@naisys/common-node": "3.0.0-beta.50",
19
- "@naisys/erp-shared": "3.0.0-beta.50",
20
- "@naisys/hub-database": "3.0.0-beta.50",
21
- "@naisys/supervisor-database": "3.0.0-beta.50",
17
+ "@naisys/common": "3.0.0-beta.51",
18
+ "@naisys/common-node": "3.0.0-beta.51",
19
+ "@naisys/erp-shared": "3.0.0-beta.51",
20
+ "@naisys/hub-database": "3.0.0-beta.51",
21
+ "@naisys/supervisor-database": "3.0.0-beta.51",
22
22
  "@prisma/adapter-better-sqlite3": "^7.5.0",
23
23
  "@prisma/client": "^7.5.0",
24
24
  "@scalar/fastify-api-reference": "^1.48.7",
@@ -394,41 +394,42 @@
394
394
  }
395
395
  },
396
396
  "node_modules/@naisys/common": {
397
- "version": "3.0.0-beta.50",
398
- "resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.50.tgz",
399
- "integrity": "sha512-jbd0+ARSKo+u0qtGosBkmlZgEFRKU+KWJbSjPSQOAEGaJLlvPfi31nT2hjjN+HpaSsOz3CV7q1B51YqV40YN2A==",
397
+ "version": "3.0.0-beta.51",
398
+ "resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.51.tgz",
399
+ "integrity": "sha512-CTv2SNKRMnR0N1rbHM5xeqVOVAQpc16dcnATCwerJS41Uha9kakMaBITlBhoQA8O2PhO/Cc6VbzBYRnSKBsWag==",
400
400
  "dependencies": {
401
+ "cron-parser": "^5.5.0",
401
402
  "semver": "^7.7.4",
402
403
  "zod": "^4.3.6"
403
404
  }
404
405
  },
405
406
  "node_modules/@naisys/common-node": {
406
- "version": "3.0.0-beta.50",
407
- "resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.50.tgz",
408
- "integrity": "sha512-w+C4E06UnMDwxQodeOjE9iB7dTdvGo8ZvvTVzA9USCKzC9TiJYWX1JJ39FIyQ4i7gZwp6Aady/vKuinAZtbWeQ==",
407
+ "version": "3.0.0-beta.51",
408
+ "resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.51.tgz",
409
+ "integrity": "sha512-0G3uDNTKpCu6ZMO5NL/vQOn/0iK4B1HOQbW3U1d8eFIZbbS2YBlLhxL8ee2p4uDlnPiwA7V3nUTXrFRcHpwGJg==",
409
410
  "dependencies": {
410
- "@naisys/common": "3.0.0-beta.50",
411
+ "@naisys/common": "3.0.0-beta.51",
411
412
  "better-sqlite3": "^12.6.2",
412
413
  "js-yaml": "^4.1.1",
413
414
  "pino": "^10.3.1"
414
415
  }
415
416
  },
416
417
  "node_modules/@naisys/erp-shared": {
417
- "version": "3.0.0-beta.50",
418
- "resolved": "https://registry.npmjs.org/@naisys/erp-shared/-/erp-shared-3.0.0-beta.50.tgz",
419
- "integrity": "sha512-yoUEgZFskOyY7gaiz2DN8zk9m5qmFeaR2RBMtkCHKJG/7FDM70HJqtqsEhPg/J0dtUCYfignDFJ6EV4nmuzfyg==",
418
+ "version": "3.0.0-beta.51",
419
+ "resolved": "https://registry.npmjs.org/@naisys/erp-shared/-/erp-shared-3.0.0-beta.51.tgz",
420
+ "integrity": "sha512-6I3ohn19tHpcYwJDFcoNpI70IUPT9Em+xpkRIG11j0G/gxOszGHIlOELsEeOtL/+qDK9MfFfsth6aoFewm11Uw==",
420
421
  "dependencies": {
421
- "@naisys/common": "3.0.0-beta.50",
422
+ "@naisys/common": "3.0.0-beta.51",
422
423
  "zod": "^4.3.6"
423
424
  }
424
425
  },
425
426
  "node_modules/@naisys/hub-database": {
426
- "version": "3.0.0-beta.50",
427
- "resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.50.tgz",
428
- "integrity": "sha512-MnMTWMFLyfuyCHDZDCdFBs3oesPNZHJ1b8oUavSz4Z58fZbXgxYesddOu1RSb2gG2n4fu9JKJAMiS4EUmH0g5g==",
427
+ "version": "3.0.0-beta.51",
428
+ "resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.51.tgz",
429
+ "integrity": "sha512-TG2lOubOjl+guafQ4vKk9Vetw4Z7Vc43geRlYLYJjDBgN1twuwEBlFncRDbGdjD3Ohu81BUxhONOC5LSZiU/0A==",
429
430
  "dependencies": {
430
- "@naisys/common": "3.0.0-beta.50",
431
- "@naisys/common-node": "3.0.0-beta.50",
431
+ "@naisys/common": "3.0.0-beta.51",
432
+ "@naisys/common-node": "3.0.0-beta.51",
432
433
  "@prisma/adapter-better-sqlite3": "^7.5.0",
433
434
  "@prisma/client": "^7.5.0",
434
435
  "better-sqlite3": "^12.6.2",
@@ -436,12 +437,12 @@
436
437
  }
437
438
  },
438
439
  "node_modules/@naisys/supervisor-database": {
439
- "version": "3.0.0-beta.50",
440
- "resolved": "https://registry.npmjs.org/@naisys/supervisor-database/-/supervisor-database-3.0.0-beta.50.tgz",
441
- "integrity": "sha512-Ur1lgpJYj038TCwZ7RG3RSS+4+DoAZFz818o6av1nn1OF/BbU54sRw71X+AM0uvCEEKFQTRnLJIZMQs98xRWWw==",
440
+ "version": "3.0.0-beta.51",
441
+ "resolved": "https://registry.npmjs.org/@naisys/supervisor-database/-/supervisor-database-3.0.0-beta.51.tgz",
442
+ "integrity": "sha512-cSIjScSiSloqgtU1AdkgvXeBIiDy5K4eXYTc4lAsokbx3EJWuRx7tibuu/kLnPrAZfzfC2uii/wPeTv5ryTR3g==",
442
443
  "dependencies": {
443
- "@naisys/common": "3.0.0-beta.50",
444
- "@naisys/common-node": "3.0.0-beta.50",
444
+ "@naisys/common": "3.0.0-beta.51",
445
+ "@naisys/common-node": "3.0.0-beta.51",
445
446
  "@prisma/adapter-better-sqlite3": "^7.5.0",
446
447
  "@prisma/client": "^7.5.0",
447
448
  "bcryptjs": "^3.0.2",
@@ -1267,6 +1268,18 @@
1267
1268
  "url": "https://opencollective.com/express"
1268
1269
  }
1269
1270
  },
1271
+ "node_modules/cron-parser": {
1272
+ "version": "5.5.0",
1273
+ "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.5.0.tgz",
1274
+ "integrity": "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==",
1275
+ "license": "MIT",
1276
+ "dependencies": {
1277
+ "luxon": "^3.7.1"
1278
+ },
1279
+ "engines": {
1280
+ "node": ">=18"
1281
+ }
1282
+ },
1270
1283
  "node_modules/cross-spawn": {
1271
1284
  "version": "7.0.6",
1272
1285
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1991,6 +2004,15 @@
1991
2004
  "url": "https://github.com/sponsors/wellwelwel"
1992
2005
  }
1993
2006
  },
2007
+ "node_modules/luxon": {
2008
+ "version": "3.7.2",
2009
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
2010
+ "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
2011
+ "license": "MIT",
2012
+ "engines": {
2013
+ "node": ">=12"
2014
+ }
2015
+ },
1994
2016
  "node_modules/mime": {
1995
2017
  "version": "3.0.0",
1996
2018
  "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naisys/erp",
3
- "version": "3.0.0-beta.50",
3
+ "version": "3.0.0-beta.51",
4
4
  "description": "NAISYS ERP - Web UI for AI-driven order and work management",
5
5
  "type": "module",
6
6
  "main": "dist/erpServer.js",
@@ -46,11 +46,11 @@
46
46
  "@fastify/rate-limit": "^10.3.0",
47
47
  "@fastify/static": "^9.0.0",
48
48
  "@fastify/swagger": "^9.7.0",
49
- "@naisys/common": "3.0.0-beta.50",
50
- "@naisys/common-node": "3.0.0-beta.50",
51
- "@naisys/erp-shared": "3.0.0-beta.50",
52
- "@naisys/hub-database": "3.0.0-beta.50",
53
- "@naisys/supervisor-database": "3.0.0-beta.50",
49
+ "@naisys/common": "3.0.0-beta.51",
50
+ "@naisys/common-node": "3.0.0-beta.51",
51
+ "@naisys/erp-shared": "3.0.0-beta.51",
52
+ "@naisys/hub-database": "3.0.0-beta.51",
53
+ "@naisys/supervisor-database": "3.0.0-beta.51",
54
54
  "@prisma/adapter-better-sqlite3": "^7.5.0",
55
55
  "@prisma/client": "^7.5.0",
56
56
  "@scalar/fastify-api-reference": "^1.48.7",
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "operation_runs" ADD COLUMN "tokens" INTEGER;
2
+ ALTER TABLE "labor_tickets" ADD COLUMN "tokens" INTEGER;
@@ -307,6 +307,7 @@ model OperationRun {
307
307
  status OperationRunStatus @default(pending)
308
308
  assignedToId Int? @map("assigned_to_id")
309
309
  cost Float?
310
+ tokens Int?
310
311
  statusNote String? @map("status_note")
311
312
  completedAt DateTime? @map("completed_at")
312
313
  createdAt DateTime @default(now()) @map("created_at")
@@ -518,6 +519,7 @@ model LaborTicket {
518
519
  clockIn DateTime @map("clock_in")
519
520
  clockOut DateTime? @map("clock_out")
520
521
  cost Float?
522
+ tokens Int?
521
523
  createdAt DateTime @default(now()) @map("created_at")
522
524
  createdById Int @map("created_by")
523
525
  updatedAt DateTime @updatedAt @map("updated_at")