@naisys/erp 3.0.0-beta.10

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.
Files changed (150) hide show
  1. package/bin/naisys-erp +2 -0
  2. package/client-dist/android-chrome-192x192.png +0 -0
  3. package/client-dist/android-chrome-512x512.png +0 -0
  4. package/client-dist/apple-touch-icon.png +0 -0
  5. package/client-dist/assets/index-45dVo30p.css +1 -0
  6. package/client-dist/assets/index-C9uuPHLH.js +168 -0
  7. package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
  8. package/client-dist/favicon.ico +0 -0
  9. package/client-dist/index.html +42 -0
  10. package/client-dist/site.webmanifest +22 -0
  11. package/dist/api-reference.js +101 -0
  12. package/dist/audit.js +14 -0
  13. package/dist/auth-middleware.js +203 -0
  14. package/dist/dbConfig.js +10 -0
  15. package/dist/erpDb.js +34 -0
  16. package/dist/erpServer.js +321 -0
  17. package/dist/error-handler.js +17 -0
  18. package/dist/generated/prisma/client.js +35 -0
  19. package/dist/generated/prisma/commonInputTypes.js +11 -0
  20. package/dist/generated/prisma/enums.js +60 -0
  21. package/dist/generated/prisma/internal/class.js +50 -0
  22. package/dist/generated/prisma/internal/prismaNamespace.js +366 -0
  23. package/dist/generated/prisma/models/Attachment.js +2 -0
  24. package/dist/generated/prisma/models/AuditLog.js +2 -0
  25. package/dist/generated/prisma/models/Field.js +2 -0
  26. package/dist/generated/prisma/models/FieldAttachment.js +2 -0
  27. package/dist/generated/prisma/models/FieldRecord.js +2 -0
  28. package/dist/generated/prisma/models/FieldSet.js +2 -0
  29. package/dist/generated/prisma/models/FieldValue.js +2 -0
  30. package/dist/generated/prisma/models/Item.js +2 -0
  31. package/dist/generated/prisma/models/ItemInstance.js +2 -0
  32. package/dist/generated/prisma/models/LaborTicket.js +2 -0
  33. package/dist/generated/prisma/models/Operation.js +2 -0
  34. package/dist/generated/prisma/models/OperationDependency.js +2 -0
  35. package/dist/generated/prisma/models/OperationFieldRef.js +2 -0
  36. package/dist/generated/prisma/models/OperationRun.js +2 -0
  37. package/dist/generated/prisma/models/OperationRunComment.js +2 -0
  38. package/dist/generated/prisma/models/Order.js +2 -0
  39. package/dist/generated/prisma/models/OrderRevision.js +2 -0
  40. package/dist/generated/prisma/models/OrderRun.js +2 -0
  41. package/dist/generated/prisma/models/SchemaVersion.js +2 -0
  42. package/dist/generated/prisma/models/Session.js +2 -0
  43. package/dist/generated/prisma/models/Step.js +2 -0
  44. package/dist/generated/prisma/models/StepRun.js +2 -0
  45. package/dist/generated/prisma/models/User.js +2 -0
  46. package/dist/generated/prisma/models/UserPermission.js +2 -0
  47. package/dist/generated/prisma/models/WorkCenter.js +2 -0
  48. package/dist/generated/prisma/models/WorkCenterUser.js +2 -0
  49. package/dist/generated/prisma/models.js +2 -0
  50. package/dist/hateoas.js +61 -0
  51. package/dist/route-helpers.js +220 -0
  52. package/dist/routes/admin.js +147 -0
  53. package/dist/routes/audit.js +36 -0
  54. package/dist/routes/auth.js +112 -0
  55. package/dist/routes/dispatch.js +174 -0
  56. package/dist/routes/inventory.js +70 -0
  57. package/dist/routes/item-fields.js +220 -0
  58. package/dist/routes/item-instances.js +426 -0
  59. package/dist/routes/items.js +252 -0
  60. package/dist/routes/labor-tickets.js +268 -0
  61. package/dist/routes/operation-dependencies.js +170 -0
  62. package/dist/routes/operation-field-refs.js +263 -0
  63. package/dist/routes/operation-run-comments.js +108 -0
  64. package/dist/routes/operation-run-transitions.js +249 -0
  65. package/dist/routes/operation-runs.js +299 -0
  66. package/dist/routes/operations.js +283 -0
  67. package/dist/routes/order-revision-transitions.js +86 -0
  68. package/dist/routes/order-revisions.js +327 -0
  69. package/dist/routes/order-run-transitions.js +215 -0
  70. package/dist/routes/order-runs.js +335 -0
  71. package/dist/routes/orders.js +262 -0
  72. package/dist/routes/root.js +123 -0
  73. package/dist/routes/schemas.js +31 -0
  74. package/dist/routes/step-field-attachments.js +231 -0
  75. package/dist/routes/step-fields.js +315 -0
  76. package/dist/routes/step-run-fields.js +438 -0
  77. package/dist/routes/step-run-transitions.js +113 -0
  78. package/dist/routes/step-runs.js +324 -0
  79. package/dist/routes/steps.js +283 -0
  80. package/dist/routes/user-permissions.js +100 -0
  81. package/dist/routes/users.js +381 -0
  82. package/dist/routes/work-centers.js +280 -0
  83. package/dist/schema-registry.js +45 -0
  84. package/dist/services/attachment-service.js +118 -0
  85. package/dist/services/field-ref-service.js +74 -0
  86. package/dist/services/field-service.js +114 -0
  87. package/dist/services/field-value-service.js +256 -0
  88. package/dist/services/item-instance-service.js +155 -0
  89. package/dist/services/item-service.js +56 -0
  90. package/dist/services/labor-ticket-service.js +148 -0
  91. package/dist/services/log-file-service.js +11 -0
  92. package/dist/services/operation-dependency-service.js +30 -0
  93. package/dist/services/operation-run-comment-service.js +26 -0
  94. package/dist/services/operation-run-service.js +347 -0
  95. package/dist/services/operation-service.js +132 -0
  96. package/dist/services/order-revision-service.js +264 -0
  97. package/dist/services/order-run-service.js +356 -0
  98. package/dist/services/order-service.js +68 -0
  99. package/dist/services/revision-diff-service.js +194 -0
  100. package/dist/services/step-run-service.js +106 -0
  101. package/dist/services/step-service.js +89 -0
  102. package/dist/services/user-service.js +132 -0
  103. package/dist/services/work-center-service.js +106 -0
  104. package/dist/supervisorAuth.js +16 -0
  105. package/dist/userService.js +118 -0
  106. package/package.json +75 -0
  107. package/prisma/migrations/20260212170352_init/migration.sql +125 -0
  108. package/prisma/migrations/20260308000000_multi_session/migration.sql +23 -0
  109. package/prisma/migrations/20260309000000_add_user_api_key/migration.sql +5 -0
  110. package/prisma/migrations/20260309010000_add_plan_operations/migration.sql +21 -0
  111. package/prisma/migrations/20260309020000_rename_exec_orders_to_order_runs/migration.sql +13 -0
  112. package/prisma/migrations/20260310000000_rename_plan_order_revs_to_order_revisions/migration.sql +22 -0
  113. package/prisma/migrations/20260310100000_rename_plan_orders_to_orders/migration.sql +23 -0
  114. package/prisma/migrations/20260310200000_rename_order_no_to_run_no/migration.sql +3 -0
  115. package/prisma/migrations/20260312000000_add_user_permissions/migration.sql +16 -0
  116. package/prisma/migrations/20260313000000_rename_plan_operations_to_operations/migration.sql +2 -0
  117. package/prisma/migrations/20260313100000_add_steps/migration.sql +20 -0
  118. package/prisma/migrations/20260314000000_add_step_fields/migration.sql +22 -0
  119. package/prisma/migrations/20260315000000_add_operation_runs/migration.sql +24 -0
  120. package/prisma/migrations/20260315100000_add_step_runs/migration.sql +40 -0
  121. package/prisma/migrations/20260316000000_drop_order_name/migration.sql +12 -0
  122. package/prisma/migrations/20260317000000_add_attachments/migration.sql +28 -0
  123. package/prisma/migrations/20260317000000_add_items/migration.sql +21 -0
  124. package/prisma/migrations/20260317100000_add_order_item_id/migration.sql +8 -0
  125. package/prisma/migrations/20260318000000_add_labor_tickets/migration.sql +27 -0
  126. package/prisma/migrations/20260319000000_add_operation_dependencies/migration.sql +17 -0
  127. package/prisma/migrations/20260320000000_step_field_is_array/migration.sql +5 -0
  128. package/prisma/migrations/20260320100000_rename_is_array_to_multi_value/migration.sql +2 -0
  129. package/prisma/migrations/20260320200000_add_field_types/migration.sql +2 -0
  130. package/prisma/migrations/20260321000000_add_multi_set/migration.sql +13 -0
  131. package/prisma/migrations/20260322000000_add_field_sets/migration.sql +90 -0
  132. package/prisma/migrations/20260323000000_add_item_instances/migration.sql +18 -0
  133. package/prisma/migrations/20260324000000_add_field_records/migration.sql +77 -0
  134. package/prisma/migrations/20260325000000_add_run_costs/migration.sql +3 -0
  135. package/prisma/migrations/20260326000000_add_comments/migration.sql +16 -0
  136. package/prisma/migrations/20260327000000_move_assigned_to_op_run/migration.sql +3 -0
  137. package/prisma/migrations/20260328000000_add_step_completion_note/migration.sql +2 -0
  138. package/prisma/migrations/20260328000000_add_step_title/migration.sql +2 -0
  139. package/prisma/migrations/20260329000000_rename_notes_to_release_note/migration.sql +2 -0
  140. package/prisma/migrations/20260329000000_simplify_order_run_dates/migration.sql +5 -0
  141. package/prisma/migrations/20260330000000_add_operation_run_completion_note/migration.sql +2 -0
  142. package/prisma/migrations/20260331000000_add_work_centers/migration.sql +30 -0
  143. package/prisma/migrations/20260401000000_fix_field_values_column_shift/migration.sql +26 -0
  144. package/prisma/migrations/20260402000000_rename_completion_note_to_status_note/migration.sql +5 -0
  145. package/prisma/migrations/20260403000000_add_operation_field_refs/migration.sql +16 -0
  146. package/prisma/migrations/20260404000000_rename_multi_value_to_is_array/migration.sql +2 -0
  147. package/prisma/migrations/20260404100000_add_attachment_public_id/migration.sql +8 -0
  148. package/prisma/migrations/migration_lock.toml +3 -0
  149. package/prisma/schema.prisma +595 -0
  150. 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