@naisys/erp 3.0.0-beta.6
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/bin/naisys-erp +2 -0
- package/client-dist/android-chrome-192x192.png +0 -0
- package/client-dist/android-chrome-512x512.png +0 -0
- package/client-dist/apple-touch-icon.png +0 -0
- package/client-dist/assets/index-45dVo30p.css +1 -0
- package/client-dist/assets/index-Dffms7F_.js +168 -0
- package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +42 -0
- package/client-dist/site.webmanifest +22 -0
- package/dist/api-reference.js +101 -0
- package/dist/audit.js +14 -0
- package/dist/auth-middleware.js +203 -0
- package/dist/dbConfig.js +10 -0
- package/dist/erpDb.js +34 -0
- package/dist/erpServer.js +321 -0
- package/dist/error-handler.js +17 -0
- package/dist/generated/prisma/client.js +35 -0
- package/dist/generated/prisma/commonInputTypes.js +11 -0
- package/dist/generated/prisma/enums.js +60 -0
- package/dist/generated/prisma/internal/class.js +50 -0
- package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
- package/dist/generated/prisma/models/Attachment.js +2 -0
- package/dist/generated/prisma/models/AuditLog.js +2 -0
- package/dist/generated/prisma/models/Field.js +2 -0
- package/dist/generated/prisma/models/FieldAttachment.js +2 -0
- package/dist/generated/prisma/models/FieldRecord.js +2 -0
- package/dist/generated/prisma/models/FieldSet.js +2 -0
- package/dist/generated/prisma/models/FieldValue.js +2 -0
- package/dist/generated/prisma/models/Item.js +2 -0
- package/dist/generated/prisma/models/ItemInstance.js +2 -0
- package/dist/generated/prisma/models/LaborTicket.js +2 -0
- package/dist/generated/prisma/models/Operation.js +2 -0
- package/dist/generated/prisma/models/OperationDependency.js +2 -0
- package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
- package/dist/generated/prisma/models/OperationRun.js +2 -0
- package/dist/generated/prisma/models/OperationRunComment.js +2 -0
- package/dist/generated/prisma/models/Order.js +2 -0
- package/dist/generated/prisma/models/OrderRevision.js +2 -0
- package/dist/generated/prisma/models/OrderRun.js +2 -0
- package/dist/generated/prisma/models/SchemaVersion.js +2 -0
- package/dist/generated/prisma/models/Session.js +2 -0
- package/dist/generated/prisma/models/Step.js +2 -0
- package/dist/generated/prisma/models/StepRun.js +2 -0
- package/dist/generated/prisma/models/User.js +2 -0
- package/dist/generated/prisma/models/UserPermission.js +2 -0
- package/dist/generated/prisma/models/WorkCenter.js +2 -0
- package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
- package/dist/generated/prisma/models.js +2 -0
- package/dist/hateoas.js +61 -0
- package/dist/route-helpers.js +220 -0
- package/dist/routes/admin.js +147 -0
- package/dist/routes/audit.js +36 -0
- package/dist/routes/auth.js +112 -0
- package/dist/routes/dispatch.js +174 -0
- package/dist/routes/inventory.js +70 -0
- package/dist/routes/item-fields.js +220 -0
- package/dist/routes/item-instances.js +426 -0
- package/dist/routes/items.js +252 -0
- package/dist/routes/labor-tickets.js +268 -0
- package/dist/routes/operation-dependencies.js +170 -0
- package/dist/routes/operation-field-refs.js +263 -0
- package/dist/routes/operation-run-comments.js +108 -0
- package/dist/routes/operation-run-transitions.js +249 -0
- package/dist/routes/operation-runs.js +299 -0
- package/dist/routes/operations.js +283 -0
- package/dist/routes/order-revision-transitions.js +86 -0
- package/dist/routes/order-revisions.js +327 -0
- package/dist/routes/order-run-transitions.js +215 -0
- package/dist/routes/order-runs.js +335 -0
- package/dist/routes/orders.js +262 -0
- package/dist/routes/root.js +123 -0
- package/dist/routes/schemas.js +31 -0
- package/dist/routes/step-field-attachments.js +231 -0
- package/dist/routes/step-fields.js +315 -0
- package/dist/routes/step-run-fields.js +438 -0
- package/dist/routes/step-run-transitions.js +113 -0
- package/dist/routes/step-runs.js +324 -0
- package/dist/routes/steps.js +283 -0
- package/dist/routes/user-permissions.js +100 -0
- package/dist/routes/users.js +381 -0
- package/dist/routes/work-centers.js +280 -0
- package/dist/schema-registry.js +45 -0
- package/dist/services/attachment-service.js +118 -0
- package/dist/services/field-ref-service.js +74 -0
- package/dist/services/field-service.js +114 -0
- package/dist/services/field-value-service.js +256 -0
- package/dist/services/item-instance-service.js +155 -0
- package/dist/services/item-service.js +56 -0
- package/dist/services/labor-ticket-service.js +148 -0
- package/dist/services/log-file-service.js +11 -0
- package/dist/services/operation-dependency-service.js +30 -0
- package/dist/services/operation-run-comment-service.js +26 -0
- package/dist/services/operation-run-service.js +347 -0
- package/dist/services/operation-service.js +132 -0
- package/dist/services/order-revision-service.js +264 -0
- package/dist/services/order-run-service.js +356 -0
- package/dist/services/order-service.js +68 -0
- package/dist/services/revision-diff-service.js +194 -0
- package/dist/services/step-run-service.js +106 -0
- package/dist/services/step-service.js +89 -0
- package/dist/services/user-service.js +132 -0
- package/dist/services/work-center-service.js +106 -0
- package/dist/supervisorAuth.js +16 -0
- package/dist/userService.js +118 -0
- package/package.json +75 -0
- package/prisma/migrations/20260212170352_init/migration.sql +125 -0
- package/prisma/migrations/20260308000000_multi_session/migration.sql +23 -0
- package/prisma/migrations/20260309000000_add_user_api_key/migration.sql +5 -0
- package/prisma/migrations/20260309010000_add_plan_operations/migration.sql +21 -0
- package/prisma/migrations/20260309020000_rename_exec_orders_to_order_runs/migration.sql +13 -0
- package/prisma/migrations/20260310000000_rename_plan_order_revs_to_order_revisions/migration.sql +22 -0
- package/prisma/migrations/20260310100000_rename_plan_orders_to_orders/migration.sql +23 -0
- package/prisma/migrations/20260310200000_rename_order_no_to_run_no/migration.sql +3 -0
- package/prisma/migrations/20260312000000_add_user_permissions/migration.sql +16 -0
- package/prisma/migrations/20260313000000_rename_plan_operations_to_operations/migration.sql +2 -0
- package/prisma/migrations/20260313100000_add_steps/migration.sql +20 -0
- package/prisma/migrations/20260314000000_add_step_fields/migration.sql +22 -0
- package/prisma/migrations/20260315000000_add_operation_runs/migration.sql +24 -0
- package/prisma/migrations/20260315100000_add_step_runs/migration.sql +40 -0
- package/prisma/migrations/20260316000000_drop_order_name/migration.sql +12 -0
- package/prisma/migrations/20260317000000_add_attachments/migration.sql +28 -0
- package/prisma/migrations/20260317000000_add_items/migration.sql +21 -0
- package/prisma/migrations/20260317100000_add_order_item_id/migration.sql +8 -0
- package/prisma/migrations/20260318000000_add_labor_tickets/migration.sql +27 -0
- package/prisma/migrations/20260319000000_add_operation_dependencies/migration.sql +17 -0
- package/prisma/migrations/20260320000000_step_field_is_array/migration.sql +5 -0
- package/prisma/migrations/20260320100000_rename_is_array_to_multi_value/migration.sql +2 -0
- package/prisma/migrations/20260320200000_add_field_types/migration.sql +2 -0
- package/prisma/migrations/20260321000000_add_multi_set/migration.sql +13 -0
- package/prisma/migrations/20260322000000_add_field_sets/migration.sql +90 -0
- package/prisma/migrations/20260323000000_add_item_instances/migration.sql +18 -0
- package/prisma/migrations/20260324000000_add_field_records/migration.sql +77 -0
- package/prisma/migrations/20260325000000_add_run_costs/migration.sql +3 -0
- package/prisma/migrations/20260326000000_add_comments/migration.sql +16 -0
- package/prisma/migrations/20260327000000_move_assigned_to_op_run/migration.sql +3 -0
- package/prisma/migrations/20260328000000_add_step_completion_note/migration.sql +2 -0
- package/prisma/migrations/20260328000000_add_step_title/migration.sql +2 -0
- package/prisma/migrations/20260329000000_rename_notes_to_release_note/migration.sql +2 -0
- package/prisma/migrations/20260329000000_simplify_order_run_dates/migration.sql +5 -0
- package/prisma/migrations/20260330000000_add_operation_run_completion_note/migration.sql +2 -0
- package/prisma/migrations/20260331000000_add_work_centers/migration.sql +30 -0
- package/prisma/migrations/20260401000000_fix_field_values_column_shift/migration.sql +26 -0
- package/prisma/migrations/20260402000000_rename_completion_note_to_status_note/migration.sql +5 -0
- package/prisma/migrations/20260403000000_add_operation_field_refs/migration.sql +16 -0
- package/prisma/migrations/20260404000000_rename_multi_value_to_is_array/migration.sql +2 -0
- package/prisma/migrations/20260404100000_add_attachment_public_id/migration.sql +8 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +595 -0
- package/prisma.config.ts +18 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { RevisionStatus as RevisionStatusValues, } from "@naisys/erp-shared";
|
|
2
|
+
import { writeAuditEntry } from "../audit.js";
|
|
3
|
+
import erpDb from "../erpDb.js";
|
|
4
|
+
import { includeUsers } from "../route-helpers.js";
|
|
5
|
+
// --- Prisma include & result type ---
|
|
6
|
+
const includeRevisionRelations = {
|
|
7
|
+
...includeUsers,
|
|
8
|
+
order: { select: { item: { select: { key: true } } } },
|
|
9
|
+
};
|
|
10
|
+
export async function getRevisionOpSummary(orderRevId) {
|
|
11
|
+
return erpDb.operation.findMany({
|
|
12
|
+
where: { orderRevId },
|
|
13
|
+
select: { seqNo: true, title: true },
|
|
14
|
+
orderBy: { seqNo: "asc" },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
// --- Lookups ---
|
|
18
|
+
export async function listRevisions(orderId, where, page, pageSize) {
|
|
19
|
+
const fullWhere = { orderId, ...where };
|
|
20
|
+
return Promise.all([
|
|
21
|
+
erpDb.orderRevision.findMany({
|
|
22
|
+
where: fullWhere,
|
|
23
|
+
include: includeRevisionRelations,
|
|
24
|
+
skip: (page - 1) * pageSize,
|
|
25
|
+
take: pageSize,
|
|
26
|
+
orderBy: { revNo: "desc" },
|
|
27
|
+
}),
|
|
28
|
+
erpDb.orderRevision.count({ where: fullWhere }),
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
export async function getRevision(orderId, revNo) {
|
|
32
|
+
return erpDb.orderRevision.findFirst({
|
|
33
|
+
where: { orderId, revNo },
|
|
34
|
+
include: includeRevisionRelations,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export async function findExisting(orderId, revNo) {
|
|
38
|
+
return erpDb.orderRevision.findFirst({
|
|
39
|
+
where: { orderId, revNo },
|
|
40
|
+
include: includeRevisionRelations,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// --- Validation ---
|
|
44
|
+
export function validateDraftStatus(status) {
|
|
45
|
+
if (status !== RevisionStatusValues.draft) {
|
|
46
|
+
return `Cannot update revision in ${status} status`;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
export async function checkHasOrderRuns(revisionId) {
|
|
51
|
+
const orderRunCount = await erpDb.orderRun.count({
|
|
52
|
+
where: { orderRevId: revisionId },
|
|
53
|
+
});
|
|
54
|
+
if (orderRunCount > 0) {
|
|
55
|
+
return "Cannot delete revision with existing order runs.";
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// --- Mutations ---
|
|
60
|
+
export async function createRevision(orderId, data, userId) {
|
|
61
|
+
return erpDb.$transaction(async (erpTx) => {
|
|
62
|
+
const prevRev = await erpTx.orderRevision.findFirst({
|
|
63
|
+
where: { orderId },
|
|
64
|
+
orderBy: { revNo: "desc" },
|
|
65
|
+
include: {
|
|
66
|
+
operations: {
|
|
67
|
+
include: {
|
|
68
|
+
steps: {
|
|
69
|
+
include: {
|
|
70
|
+
fieldSet: { include: { fields: true } },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
predecessors: true,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const nextRevNo = (prevRev?.revNo ?? 0) + 1;
|
|
79
|
+
// Seed description: explicit value > previous rev's description > order's description
|
|
80
|
+
let resolvedDescription = data.description;
|
|
81
|
+
if (resolvedDescription === undefined) {
|
|
82
|
+
if (prevRev) {
|
|
83
|
+
resolvedDescription = prevRev.description;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const order = await erpTx.order.findUniqueOrThrow({
|
|
87
|
+
where: { id: orderId },
|
|
88
|
+
});
|
|
89
|
+
resolvedDescription = order.description;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const newRev = await erpTx.orderRevision.create({
|
|
93
|
+
data: {
|
|
94
|
+
orderId,
|
|
95
|
+
revNo: nextRevNo,
|
|
96
|
+
description: resolvedDescription,
|
|
97
|
+
changeSummary: data.changeSummary ?? null,
|
|
98
|
+
createdById: userId,
|
|
99
|
+
updatedById: userId,
|
|
100
|
+
},
|
|
101
|
+
include: includeRevisionRelations,
|
|
102
|
+
});
|
|
103
|
+
// Copy operations, steps, fields, dependencies, and field refs from the previous revision
|
|
104
|
+
if (prevRev) {
|
|
105
|
+
const oldToNewOpId = new Map();
|
|
106
|
+
const oldToNewStepId = new Map();
|
|
107
|
+
for (const op of prevRev.operations) {
|
|
108
|
+
const newOp = await erpTx.operation.create({
|
|
109
|
+
data: {
|
|
110
|
+
orderRevId: newRev.id,
|
|
111
|
+
seqNo: op.seqNo,
|
|
112
|
+
title: op.title,
|
|
113
|
+
description: op.description,
|
|
114
|
+
workCenterId: op.workCenterId,
|
|
115
|
+
createdById: userId,
|
|
116
|
+
updatedById: userId,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
oldToNewOpId.set(op.id, newOp.id);
|
|
120
|
+
for (const step of op.steps) {
|
|
121
|
+
const fields = step.fieldSet?.fields ?? [];
|
|
122
|
+
let newFieldSetId = null;
|
|
123
|
+
if (fields.length > 0) {
|
|
124
|
+
const newFieldSet = await erpTx.fieldSet.create({
|
|
125
|
+
data: { createdById: userId },
|
|
126
|
+
});
|
|
127
|
+
newFieldSetId = newFieldSet.id;
|
|
128
|
+
for (const field of fields) {
|
|
129
|
+
await erpTx.field.create({
|
|
130
|
+
data: {
|
|
131
|
+
fieldSetId: newFieldSet.id,
|
|
132
|
+
seqNo: field.seqNo,
|
|
133
|
+
label: field.label,
|
|
134
|
+
type: field.type,
|
|
135
|
+
isArray: field.isArray,
|
|
136
|
+
required: field.required,
|
|
137
|
+
createdById: userId,
|
|
138
|
+
updatedById: userId,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const newStep = await erpTx.step.create({
|
|
144
|
+
data: {
|
|
145
|
+
operationId: newOp.id,
|
|
146
|
+
seqNo: step.seqNo,
|
|
147
|
+
title: step.title,
|
|
148
|
+
instructions: step.instructions,
|
|
149
|
+
multiSet: step.multiSet,
|
|
150
|
+
fieldSetId: newFieldSetId,
|
|
151
|
+
createdById: userId,
|
|
152
|
+
updatedById: userId,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
oldToNewStepId.set(step.id, newStep.id);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Copy operation dependencies using the old-to-new ID mapping
|
|
159
|
+
for (const op of prevRev.operations) {
|
|
160
|
+
for (const dep of op.predecessors) {
|
|
161
|
+
const newSuccessorId = oldToNewOpId.get(dep.successorId);
|
|
162
|
+
const newPredecessorId = oldToNewOpId.get(dep.predecessorId);
|
|
163
|
+
if (newSuccessorId && newPredecessorId) {
|
|
164
|
+
await erpTx.operationDependency.create({
|
|
165
|
+
data: {
|
|
166
|
+
successorId: newSuccessorId,
|
|
167
|
+
predecessorId: newPredecessorId,
|
|
168
|
+
createdById: userId,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Copy field refs using old-to-new operation and step ID mappings
|
|
175
|
+
const oldFieldRefs = await erpTx.operationFieldRef.findMany({
|
|
176
|
+
where: {
|
|
177
|
+
operationId: {
|
|
178
|
+
in: [...oldToNewOpId.keys()],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
for (const ref of oldFieldRefs) {
|
|
183
|
+
const newOpId = oldToNewOpId.get(ref.operationId);
|
|
184
|
+
const newStepId = oldToNewStepId.get(ref.sourceStepId);
|
|
185
|
+
if (newOpId && newStepId) {
|
|
186
|
+
await erpTx.operationFieldRef.create({
|
|
187
|
+
data: {
|
|
188
|
+
operationId: newOpId,
|
|
189
|
+
seqNo: ref.seqNo,
|
|
190
|
+
title: ref.title,
|
|
191
|
+
sourceStepId: newStepId,
|
|
192
|
+
createdById: userId,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return newRev;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export async function updateRevision(id, data, userId) {
|
|
202
|
+
return erpDb.orderRevision.update({
|
|
203
|
+
where: { id },
|
|
204
|
+
data: {
|
|
205
|
+
...(data.description !== undefined
|
|
206
|
+
? { description: data.description }
|
|
207
|
+
: {}),
|
|
208
|
+
...(data.changeSummary !== undefined
|
|
209
|
+
? { changeSummary: data.changeSummary }
|
|
210
|
+
: {}),
|
|
211
|
+
updatedById: userId,
|
|
212
|
+
},
|
|
213
|
+
include: includeRevisionRelations,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
export async function deleteRevision(id) {
|
|
217
|
+
await erpDb.$transaction(async (erpTx) => {
|
|
218
|
+
// Delete child records bottom-up: step fields → steps → operations → revision
|
|
219
|
+
const operations = await erpTx.operation.findMany({
|
|
220
|
+
where: { orderRevId: id },
|
|
221
|
+
select: { id: true },
|
|
222
|
+
});
|
|
223
|
+
const opIds = operations.map((op) => op.id);
|
|
224
|
+
if (opIds.length > 0) {
|
|
225
|
+
// Delete field refs first (sourceStepId FK is Restrict, so must go before steps)
|
|
226
|
+
await erpTx.operationFieldRef.deleteMany({
|
|
227
|
+
where: { operationId: { in: opIds } },
|
|
228
|
+
});
|
|
229
|
+
const steps = await erpTx.step.findMany({
|
|
230
|
+
where: { operationId: { in: opIds } },
|
|
231
|
+
select: { id: true, fieldSetId: true },
|
|
232
|
+
});
|
|
233
|
+
const fieldSetIds = steps
|
|
234
|
+
.map((s) => s.fieldSetId)
|
|
235
|
+
.filter((id) => id !== null);
|
|
236
|
+
// Steps reference field_sets via FK, so delete steps first
|
|
237
|
+
await erpTx.step.deleteMany({
|
|
238
|
+
where: { operationId: { in: opIds } },
|
|
239
|
+
});
|
|
240
|
+
// Fields cascade-delete from field_sets
|
|
241
|
+
if (fieldSetIds.length > 0) {
|
|
242
|
+
await erpTx.fieldSet.deleteMany({
|
|
243
|
+
where: { id: { in: fieldSetIds } },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
await erpTx.operation.deleteMany({
|
|
247
|
+
where: { orderRevId: id },
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
await erpTx.orderRevision.delete({ where: { id } });
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
export async function transitionStatus(id, action, fromStatus, toStatus, userId) {
|
|
254
|
+
return erpDb.$transaction(async (erpTx) => {
|
|
255
|
+
const updated = await erpTx.orderRevision.update({
|
|
256
|
+
where: { id },
|
|
257
|
+
data: { status: toStatus, updatedById: userId },
|
|
258
|
+
include: includeRevisionRelations,
|
|
259
|
+
});
|
|
260
|
+
await writeAuditEntry(erpTx, "OrderRevision", id, action, "status", fromStatus, toStatus, userId);
|
|
261
|
+
return updated;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=order-revision-service.js.map
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { OperationRunStatus as OperationRunStatusValues, OrderRunStatus as OrderRunStatusValues, } from "@naisys/erp-shared";
|
|
2
|
+
import { writeAuditEntry } from "../audit.js";
|
|
3
|
+
import erpDb from "../erpDb.js";
|
|
4
|
+
// --- Prisma include & result type ---
|
|
5
|
+
export const includeRev = {
|
|
6
|
+
orderRev: { select: { revNo: true } },
|
|
7
|
+
order: { select: { item: { select: { key: true } } } },
|
|
8
|
+
itemInstances: { select: { id: true, key: true }, take: 1 },
|
|
9
|
+
createdBy: { select: { username: true } },
|
|
10
|
+
updatedBy: { select: { username: true } },
|
|
11
|
+
};
|
|
12
|
+
// --- Lookups ---
|
|
13
|
+
export async function listOrderRuns(where, page, pageSize) {
|
|
14
|
+
const [items, total] = await Promise.all([
|
|
15
|
+
erpDb.orderRun.findMany({
|
|
16
|
+
where,
|
|
17
|
+
include: includeRev,
|
|
18
|
+
skip: (page - 1) * pageSize,
|
|
19
|
+
take: pageSize,
|
|
20
|
+
orderBy: { createdAt: "desc" },
|
|
21
|
+
}),
|
|
22
|
+
erpDb.orderRun.count({ where }),
|
|
23
|
+
]);
|
|
24
|
+
return { items, total };
|
|
25
|
+
}
|
|
26
|
+
export async function getOrderRun(id) {
|
|
27
|
+
return erpDb.orderRun.findUnique({
|
|
28
|
+
where: { id },
|
|
29
|
+
include: includeRev,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export async function findExisting(id, orderId) {
|
|
33
|
+
const existing = await erpDb.orderRun.findUnique({ where: { id } });
|
|
34
|
+
if (!existing || existing.orderId !== orderId)
|
|
35
|
+
return null;
|
|
36
|
+
return existing;
|
|
37
|
+
}
|
|
38
|
+
export async function getOrderRunOpSummary(orderRunId) {
|
|
39
|
+
return erpDb.operationRun.findMany({
|
|
40
|
+
where: { orderRunId },
|
|
41
|
+
select: {
|
|
42
|
+
operation: { select: { seqNo: true, title: true } },
|
|
43
|
+
status: true,
|
|
44
|
+
},
|
|
45
|
+
orderBy: { operation: { seqNo: "asc" } },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// --- Validation ---
|
|
49
|
+
export function validateStatusFor(action, currentStatus, allowedStatuses) {
|
|
50
|
+
if (!allowedStatuses.includes(currentStatus)) {
|
|
51
|
+
return `Cannot ${action} order run in ${currentStatus} status`;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
export async function checkOpsComplete(orderRunId) {
|
|
56
|
+
const incompleteOps = await erpDb.operationRun.findMany({
|
|
57
|
+
where: {
|
|
58
|
+
orderRunId,
|
|
59
|
+
status: {
|
|
60
|
+
notIn: [
|
|
61
|
+
OperationRunStatusValues.completed,
|
|
62
|
+
OperationRunStatusValues.skipped,
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
include: { operation: { select: { seqNo: true, title: true } } },
|
|
67
|
+
});
|
|
68
|
+
if (incompleteOps.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const labels = incompleteOps.map((op) => `Op ${op.operation.seqNo} "${op.operation.title}" (${op.status})`);
|
|
71
|
+
return `Cannot close order run: incomplete operations — ${labels.join(", ")}`;
|
|
72
|
+
}
|
|
73
|
+
export async function sumOpRunCosts(orderRunId) {
|
|
74
|
+
const result = await erpDb.operationRun.aggregate({
|
|
75
|
+
where: { orderRunId },
|
|
76
|
+
_sum: { cost: true },
|
|
77
|
+
});
|
|
78
|
+
return Math.round((result._sum.cost ?? 0) * 100) / 100;
|
|
79
|
+
}
|
|
80
|
+
// --- Mutations ---
|
|
81
|
+
export async function createOrderRun(orderId, orderRevId, data, userId) {
|
|
82
|
+
return erpDb.$transaction(async (erpTx) => {
|
|
83
|
+
const maxOrder = await erpTx.orderRun.findFirst({
|
|
84
|
+
where: { orderId },
|
|
85
|
+
orderBy: { runNo: "desc" },
|
|
86
|
+
select: { runNo: true },
|
|
87
|
+
});
|
|
88
|
+
const nextRunNo = (maxOrder?.runNo ?? 0) + 1;
|
|
89
|
+
const orderRun = await erpTx.orderRun.create({
|
|
90
|
+
data: {
|
|
91
|
+
runNo: nextRunNo,
|
|
92
|
+
orderId,
|
|
93
|
+
orderRevId,
|
|
94
|
+
priority: data.priority,
|
|
95
|
+
dueAt: data.dueAt ?? null,
|
|
96
|
+
releaseNote: data.releaseNote ?? null,
|
|
97
|
+
createdById: userId,
|
|
98
|
+
updatedById: userId,
|
|
99
|
+
},
|
|
100
|
+
include: includeRev,
|
|
101
|
+
});
|
|
102
|
+
// Fetch operations -> steps -> fields for this revision
|
|
103
|
+
const operations = await erpTx.operation.findMany({
|
|
104
|
+
where: { orderRevId },
|
|
105
|
+
include: {
|
|
106
|
+
steps: {
|
|
107
|
+
include: { fieldSet: { include: { fields: true } } },
|
|
108
|
+
orderBy: { seqNo: "asc" },
|
|
109
|
+
},
|
|
110
|
+
predecessors: { select: { predecessorId: true } },
|
|
111
|
+
},
|
|
112
|
+
orderBy: { seqNo: "asc" },
|
|
113
|
+
});
|
|
114
|
+
// Create OperationRun -> StepRun -> FieldRecord -> FieldValue rows
|
|
115
|
+
for (const op of operations) {
|
|
116
|
+
// Ops with predecessors start blocked; ops without start pending
|
|
117
|
+
const initialStatus = op.predecessors.length > 0
|
|
118
|
+
? OperationRunStatusValues.blocked
|
|
119
|
+
: OperationRunStatusValues.pending;
|
|
120
|
+
const opRun = await erpTx.operationRun.create({
|
|
121
|
+
data: {
|
|
122
|
+
orderRunId: orderRun.id,
|
|
123
|
+
operationId: op.id,
|
|
124
|
+
status: initialStatus,
|
|
125
|
+
createdById: userId,
|
|
126
|
+
updatedById: userId,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
for (const step of op.steps) {
|
|
130
|
+
const stepRun = await erpTx.stepRun.create({
|
|
131
|
+
data: {
|
|
132
|
+
operationRunId: opRun.id,
|
|
133
|
+
stepId: step.id,
|
|
134
|
+
createdById: userId,
|
|
135
|
+
updatedById: userId,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const fields = step.fieldSet?.fields ?? [];
|
|
139
|
+
if (fields.length > 0 && step.fieldSetId) {
|
|
140
|
+
const fieldRecord = await erpTx.fieldRecord.create({
|
|
141
|
+
data: { fieldSetId: step.fieldSetId, createdById: userId },
|
|
142
|
+
});
|
|
143
|
+
await erpTx.stepRun.update({
|
|
144
|
+
where: { id: stepRun.id },
|
|
145
|
+
data: { fieldRecordId: fieldRecord.id },
|
|
146
|
+
});
|
|
147
|
+
for (const field of fields) {
|
|
148
|
+
await erpTx.fieldValue.create({
|
|
149
|
+
data: {
|
|
150
|
+
fieldRecordId: fieldRecord.id,
|
|
151
|
+
fieldId: field.id,
|
|
152
|
+
value: "",
|
|
153
|
+
createdById: userId,
|
|
154
|
+
updatedById: userId,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return orderRun;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export async function updateOrderRun(id, data, userId) {
|
|
165
|
+
const updateData = { updatedById: userId };
|
|
166
|
+
if (data.priority !== undefined)
|
|
167
|
+
updateData.priority = data.priority;
|
|
168
|
+
if (data.releaseNote !== undefined)
|
|
169
|
+
updateData.releaseNote = data.releaseNote;
|
|
170
|
+
if (data.dueAt !== undefined)
|
|
171
|
+
updateData.dueAt = data.dueAt;
|
|
172
|
+
return erpDb.orderRun.update({
|
|
173
|
+
where: { id },
|
|
174
|
+
data: updateData,
|
|
175
|
+
include: includeRev,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
export async function deleteOrderRun(id) {
|
|
179
|
+
await erpDb.$transaction(async (tx) => {
|
|
180
|
+
const opRuns = await tx.operationRun.findMany({
|
|
181
|
+
where: { orderRunId: id },
|
|
182
|
+
select: { id: true },
|
|
183
|
+
});
|
|
184
|
+
const opRunIds = opRuns.map((r) => r.id);
|
|
185
|
+
if (opRunIds.length > 0) {
|
|
186
|
+
const stepRuns = await tx.stepRun.findMany({
|
|
187
|
+
where: { operationRunId: { in: opRunIds } },
|
|
188
|
+
select: { fieldRecordId: true },
|
|
189
|
+
});
|
|
190
|
+
const fieldRecordIds = stepRuns
|
|
191
|
+
.map((s) => s.fieldRecordId)
|
|
192
|
+
.filter((id) => id !== null);
|
|
193
|
+
if (fieldRecordIds.length > 0) {
|
|
194
|
+
await tx.fieldValue.deleteMany({
|
|
195
|
+
where: { fieldRecordId: { in: fieldRecordIds } },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
await tx.stepRun.deleteMany({
|
|
199
|
+
where: { operationRunId: { in: opRunIds } },
|
|
200
|
+
});
|
|
201
|
+
if (fieldRecordIds.length > 0) {
|
|
202
|
+
await tx.fieldRecord.deleteMany({
|
|
203
|
+
where: { id: { in: fieldRecordIds } },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
await tx.operationRun.deleteMany({ where: { orderRunId: id } });
|
|
207
|
+
}
|
|
208
|
+
await tx.orderRun.delete({ where: { id } });
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export async function transitionStatus(id, action, fromStatus, toStatus, userId, extraData) {
|
|
212
|
+
return erpDb.$transaction(async (erpTx) => {
|
|
213
|
+
const updated = await erpTx.orderRun.update({
|
|
214
|
+
where: { id },
|
|
215
|
+
data: { status: toStatus, updatedById: userId, ...extraData },
|
|
216
|
+
include: includeRev,
|
|
217
|
+
});
|
|
218
|
+
await writeAuditEntry(erpTx, "OrderRun", id, action, "status", fromStatus, toStatus, userId);
|
|
219
|
+
return updated;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
export async function findOrderRevision(orderId, revNo) {
|
|
223
|
+
return erpDb.orderRevision.findUnique({
|
|
224
|
+
where: { orderId_revNo: { orderId, revNo } },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
export async function findLatestApprovedRevision(orderId) {
|
|
228
|
+
return erpDb.orderRevision.findFirst({
|
|
229
|
+
where: { orderId, status: "approved" },
|
|
230
|
+
orderBy: { revNo: "desc" },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
export function getReopenTarget(currentStatus) {
|
|
234
|
+
return currentStatus === OrderRunStatusValues.closed
|
|
235
|
+
? OrderRunStatusValues.started
|
|
236
|
+
: OrderRunStatusValues.released;
|
|
237
|
+
}
|
|
238
|
+
// --- Completion ---
|
|
239
|
+
/**
|
|
240
|
+
* Auto-generate an instance key by finding the last instance for the item,
|
|
241
|
+
* parsing its key as a number, and incrementing. Returns the generated key
|
|
242
|
+
* or an error string if the last key is not numeric.
|
|
243
|
+
*/
|
|
244
|
+
async function autoGenerateInstanceKey(erpTx, itemId) {
|
|
245
|
+
const last = await erpTx.itemInstance.findFirst({
|
|
246
|
+
where: { itemId },
|
|
247
|
+
orderBy: { createdAt: "desc" },
|
|
248
|
+
select: { key: true },
|
|
249
|
+
});
|
|
250
|
+
if (!last)
|
|
251
|
+
return { key: "1" };
|
|
252
|
+
const num = Number(last.key);
|
|
253
|
+
if (isNaN(num)) {
|
|
254
|
+
return {
|
|
255
|
+
error: `Cannot auto-generate instance key: last key "${last.key}" is not numeric. Please provide a key manually.`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { key: String(Math.floor(num) + 1) };
|
|
259
|
+
}
|
|
260
|
+
export async function completeOrderRun(orderRunId, orderId, data, userId) {
|
|
261
|
+
return erpDb.$transaction(async (erpTx) => {
|
|
262
|
+
// Load the order with its item
|
|
263
|
+
const order = await erpTx.order.findUniqueOrThrow({
|
|
264
|
+
where: { id: orderId },
|
|
265
|
+
select: {
|
|
266
|
+
item: {
|
|
267
|
+
select: {
|
|
268
|
+
id: true,
|
|
269
|
+
fieldSetId: true,
|
|
270
|
+
fieldSet: {
|
|
271
|
+
select: {
|
|
272
|
+
fields: {
|
|
273
|
+
select: { id: true },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
if (!order.item) {
|
|
282
|
+
return { error: "Order has no item assigned — cannot complete" };
|
|
283
|
+
}
|
|
284
|
+
// Determine instance key
|
|
285
|
+
let instanceKey = data.instanceKey;
|
|
286
|
+
if (!instanceKey) {
|
|
287
|
+
const result = await autoGenerateInstanceKey(erpTx, order.item.id);
|
|
288
|
+
if ("error" in result)
|
|
289
|
+
return result;
|
|
290
|
+
instanceKey = result.key;
|
|
291
|
+
}
|
|
292
|
+
// Check for duplicate key
|
|
293
|
+
const existing = await erpTx.itemInstance.findUnique({
|
|
294
|
+
where: { itemId_key: { itemId: order.item.id, key: instanceKey } },
|
|
295
|
+
});
|
|
296
|
+
if (existing) {
|
|
297
|
+
return {
|
|
298
|
+
error: `Instance key "${instanceKey}" already exists for this item`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// Create the item instance
|
|
302
|
+
const instance = await erpTx.itemInstance.create({
|
|
303
|
+
data: {
|
|
304
|
+
itemId: order.item.id,
|
|
305
|
+
orderRunId: orderRunId,
|
|
306
|
+
key: instanceKey,
|
|
307
|
+
quantity: data.quantity ?? null,
|
|
308
|
+
createdById: userId,
|
|
309
|
+
updatedById: userId,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
// Create field record and field values if item has a field set
|
|
313
|
+
if (order.item.fieldSetId && (data.fieldValues?.length ?? 0) > 0) {
|
|
314
|
+
const fieldRecord = await erpTx.fieldRecord.create({
|
|
315
|
+
data: {
|
|
316
|
+
fieldSetId: order.item.fieldSetId,
|
|
317
|
+
createdById: userId,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
await erpTx.itemInstance.update({
|
|
321
|
+
where: { id: instance.id },
|
|
322
|
+
data: { fieldRecordId: fieldRecord.id },
|
|
323
|
+
});
|
|
324
|
+
for (const fv of data.fieldValues ?? []) {
|
|
325
|
+
await erpTx.fieldValue.create({
|
|
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
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Sum operation run costs and transition run to closed
|
|
338
|
+
const costResult = await erpTx.operationRun.aggregate({
|
|
339
|
+
where: { orderRunId },
|
|
340
|
+
_sum: { cost: true },
|
|
341
|
+
});
|
|
342
|
+
const cost = Math.round((costResult._sum.cost ?? 0) * 100) / 100;
|
|
343
|
+
const updated = await erpTx.orderRun.update({
|
|
344
|
+
where: { id: orderRunId },
|
|
345
|
+
data: {
|
|
346
|
+
status: OrderRunStatusValues.closed,
|
|
347
|
+
cost,
|
|
348
|
+
updatedById: userId,
|
|
349
|
+
},
|
|
350
|
+
include: includeRev,
|
|
351
|
+
});
|
|
352
|
+
await writeAuditEntry(erpTx, "OrderRun", orderRunId, "complete", "status", OrderRunStatusValues.started, OrderRunStatusValues.closed, userId);
|
|
353
|
+
return { run: updated };
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=order-run-service.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import erpDb from "../erpDb.js";
|
|
2
|
+
import { includeUsers } from "../route-helpers.js";
|
|
3
|
+
// --- Prisma include & result type ---
|
|
4
|
+
const includeOrderRelations = {
|
|
5
|
+
...includeUsers,
|
|
6
|
+
item: { select: { key: true } },
|
|
7
|
+
};
|
|
8
|
+
// --- Lookups ---
|
|
9
|
+
export async function listOrders(where, page, pageSize) {
|
|
10
|
+
return Promise.all([
|
|
11
|
+
erpDb.order.findMany({
|
|
12
|
+
where,
|
|
13
|
+
include: includeOrderRelations,
|
|
14
|
+
skip: (page - 1) * pageSize,
|
|
15
|
+
take: pageSize,
|
|
16
|
+
orderBy: { createdAt: "desc" },
|
|
17
|
+
}),
|
|
18
|
+
erpDb.order.count({ where }),
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
export async function findExisting(key) {
|
|
22
|
+
return erpDb.order.findUnique({
|
|
23
|
+
where: { key },
|
|
24
|
+
include: includeOrderRelations,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// --- Validation ---
|
|
28
|
+
export async function checkHasRevisions(orderId) {
|
|
29
|
+
const revisionCount = await erpDb.orderRevision.count({
|
|
30
|
+
where: { orderId },
|
|
31
|
+
});
|
|
32
|
+
return revisionCount > 0;
|
|
33
|
+
}
|
|
34
|
+
// --- Mutations ---
|
|
35
|
+
export async function resolveItemKey(itemKey) {
|
|
36
|
+
if (!itemKey)
|
|
37
|
+
return null;
|
|
38
|
+
const item = await erpDb.item.findUnique({
|
|
39
|
+
where: { key: itemKey },
|
|
40
|
+
select: { id: true },
|
|
41
|
+
});
|
|
42
|
+
if (!item)
|
|
43
|
+
throw new Error(`Item '${itemKey}' not found`);
|
|
44
|
+
return item.id;
|
|
45
|
+
}
|
|
46
|
+
export async function createOrder(key, description, itemId, userId) {
|
|
47
|
+
return erpDb.order.create({
|
|
48
|
+
data: {
|
|
49
|
+
key,
|
|
50
|
+
description,
|
|
51
|
+
itemId,
|
|
52
|
+
createdById: userId,
|
|
53
|
+
updatedById: userId,
|
|
54
|
+
},
|
|
55
|
+
include: includeOrderRelations,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export async function updateOrder(key, data, userId) {
|
|
59
|
+
return erpDb.order.update({
|
|
60
|
+
where: { key },
|
|
61
|
+
data: { ...data, updatedById: userId },
|
|
62
|
+
include: includeOrderRelations,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export async function deleteOrder(key) {
|
|
66
|
+
await erpDb.order.delete({ where: { key } });
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=order-service.js.map
|