@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.
- 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-C9uuPHLH.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,256 @@
|
|
|
1
|
+
import { formatFileSize } from "@naisys/common";
|
|
2
|
+
import { FieldType, } from "@naisys/erp-shared";
|
|
3
|
+
import erpDb from "../erpDb.js";
|
|
4
|
+
// --- Lookups ---
|
|
5
|
+
export async function findStepRunWithField(id, opRunId, fieldSeqNo) {
|
|
6
|
+
const stepRun = await erpDb.stepRun.findUnique({
|
|
7
|
+
where: { id },
|
|
8
|
+
include: {
|
|
9
|
+
step: {
|
|
10
|
+
select: {
|
|
11
|
+
multiSet: true,
|
|
12
|
+
fieldSet: {
|
|
13
|
+
select: {
|
|
14
|
+
fields: {
|
|
15
|
+
where: { seqNo: fieldSeqNo },
|
|
16
|
+
select: {
|
|
17
|
+
id: true,
|
|
18
|
+
fieldSetId: true,
|
|
19
|
+
seqNo: true,
|
|
20
|
+
label: true,
|
|
21
|
+
type: true,
|
|
22
|
+
isArray: true,
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
if (!stepRun || stepRun.operationRunId !== opRunId)
|
|
33
|
+
return null;
|
|
34
|
+
return stepRun;
|
|
35
|
+
}
|
|
36
|
+
// --- Serialization ---
|
|
37
|
+
/**
|
|
38
|
+
* Serialize a field value for DB storage.
|
|
39
|
+
* - Scalar (string): stored as-is
|
|
40
|
+
* - Array (string[]): stored as JSON array string
|
|
41
|
+
*/
|
|
42
|
+
export function serializeFieldValue(value) {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Deserialize a DB-stored value back to the API shape.
|
|
50
|
+
* - isArray fields: parse JSON array, falling back to comma-split for legacy data
|
|
51
|
+
* - Scalar fields: return as-is
|
|
52
|
+
*/
|
|
53
|
+
export function deserializeFieldValue(dbValue, isArray) {
|
|
54
|
+
if (!isArray)
|
|
55
|
+
return dbValue;
|
|
56
|
+
if (!dbValue)
|
|
57
|
+
return [];
|
|
58
|
+
// Try JSON array first (new format)
|
|
59
|
+
if (dbValue.startsWith("[")) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(dbValue);
|
|
62
|
+
if (Array.isArray(parsed))
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// fall through to legacy
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Legacy: comma-separated string — migrate on read
|
|
70
|
+
return dbValue.split(",").map((v) => v.trim());
|
|
71
|
+
}
|
|
72
|
+
// --- Validation ---
|
|
73
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
74
|
+
const DATETIME_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?$/;
|
|
75
|
+
function validateSingleValue(type, value) {
|
|
76
|
+
const v = value.trim();
|
|
77
|
+
if (!v)
|
|
78
|
+
return null;
|
|
79
|
+
switch (type) {
|
|
80
|
+
case FieldType.number:
|
|
81
|
+
if (isNaN(Number(v)))
|
|
82
|
+
return "Must be a number";
|
|
83
|
+
break;
|
|
84
|
+
case FieldType.date:
|
|
85
|
+
if (!DATE_RE.test(v) || isNaN(Date.parse(v)))
|
|
86
|
+
return "Must be a valid date (YYYY-MM-DD)";
|
|
87
|
+
break;
|
|
88
|
+
case FieldType.datetime:
|
|
89
|
+
if (!DATETIME_RE.test(v) || isNaN(Date.parse(v)))
|
|
90
|
+
return "Must be a valid date/time (YYYY-MM-DDTHH:mm)";
|
|
91
|
+
break;
|
|
92
|
+
case FieldType.yesNo:
|
|
93
|
+
if (v !== "Yes" && v !== "No")
|
|
94
|
+
return 'Must be "Yes" or "No"';
|
|
95
|
+
break;
|
|
96
|
+
case FieldType.checkbox:
|
|
97
|
+
if (v !== "checked")
|
|
98
|
+
return "Invalid checkbox value";
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check that the value shape matches the field's isArray flag.
|
|
105
|
+
* Returns an error string if mismatched, or null if OK.
|
|
106
|
+
*/
|
|
107
|
+
export function checkFieldValueShape(label, type, isArray, value) {
|
|
108
|
+
if (isArray && !Array.isArray(value)) {
|
|
109
|
+
return `Field "${label}" is an array field (type: ${type}[]) — value must be a JSON array, e.g. ["value1", "value2"]`;
|
|
110
|
+
}
|
|
111
|
+
if (!isArray && Array.isArray(value)) {
|
|
112
|
+
return `Field "${label}" is not an array field — value must be a string, not an array`;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
export function validateFieldValue(type, isArray, required, value) {
|
|
117
|
+
const shapeErr = checkFieldValueShape("field", type, isArray, value);
|
|
118
|
+
if (shapeErr)
|
|
119
|
+
return { valid: false, error: shapeErr };
|
|
120
|
+
if (isArray) {
|
|
121
|
+
const items = value;
|
|
122
|
+
if (required && items.every((v) => !v.trim())) {
|
|
123
|
+
return { valid: false, error: "Required" };
|
|
124
|
+
}
|
|
125
|
+
for (let i = 0; i < items.length; i++) {
|
|
126
|
+
const err = validateSingleValue(type, items[i]);
|
|
127
|
+
if (err) {
|
|
128
|
+
return { valid: false, error: `Item ${i + 1}: ${err}` };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const v = value;
|
|
134
|
+
if (required && !v.trim()) {
|
|
135
|
+
return { valid: false, error: "Required" };
|
|
136
|
+
}
|
|
137
|
+
const err = validateSingleValue(type, v);
|
|
138
|
+
if (err) {
|
|
139
|
+
return { valid: false, error: err };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { valid: true };
|
|
143
|
+
}
|
|
144
|
+
function fieldValueKey(fieldId, setIndex) {
|
|
145
|
+
return `${fieldId}_${setIndex}`;
|
|
146
|
+
}
|
|
147
|
+
export function validateCompletionFields(existing) {
|
|
148
|
+
const existingFieldValues = existing.fieldRecord?.fieldValues ?? [];
|
|
149
|
+
// Build a map of field definitions keyed by id for isArray lookup
|
|
150
|
+
const fieldDefs = new Map((existing.step.fieldSet?.fields ?? []).map((f) => [f.id, f]));
|
|
151
|
+
const storedMap = new Map(existingFieldValues.map((fv) => {
|
|
152
|
+
const def = fieldDefs.get(fv.fieldId);
|
|
153
|
+
return [
|
|
154
|
+
fieldValueKey(fv.fieldId, fv.setIndex),
|
|
155
|
+
deserializeFieldValue(fv.value, def?.isArray ?? false),
|
|
156
|
+
];
|
|
157
|
+
}));
|
|
158
|
+
// Determine how many sets exist
|
|
159
|
+
const allSetIndexes = new Set();
|
|
160
|
+
for (const fv of existingFieldValues)
|
|
161
|
+
allSetIndexes.add(fv.setIndex);
|
|
162
|
+
if (allSetIndexes.size === 0)
|
|
163
|
+
allSetIndexes.add(0);
|
|
164
|
+
const errors = [];
|
|
165
|
+
for (const si of [...allSetIndexes].sort((a, b) => a - b)) {
|
|
166
|
+
for (const field of existing.step.fieldSet?.fields ?? []) {
|
|
167
|
+
const key = fieldValueKey(field.id, si);
|
|
168
|
+
const value = storedMap.get(key) ?? "";
|
|
169
|
+
const result = validateFieldValue(field.type, field.isArray, field.required, value);
|
|
170
|
+
if (!result.valid) {
|
|
171
|
+
const prefix = existing.step.multiSet ? `Set ${si + 1} / ` : "";
|
|
172
|
+
errors.push(`${prefix}${field.label}: ${result.error}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (errors.length > 0) {
|
|
177
|
+
return `Cannot complete step:\n${errors.join("\n")}`;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
// --- Attachment value helpers ---
|
|
182
|
+
export function formatAttachmentLabel(filename, fileSize) {
|
|
183
|
+
return `${filename} (${formatFileSize(fileSize)})`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Query current attachments for a field value and rebuild the stored value
|
|
187
|
+
* to reflect them. Returns the new API-shape value.
|
|
188
|
+
*/
|
|
189
|
+
export async function rebuildAttachmentFieldValue(fieldRecordId, fieldId, setIndex, isArray, userId) {
|
|
190
|
+
const fieldValue = await erpDb.fieldValue.findUnique({
|
|
191
|
+
where: {
|
|
192
|
+
fieldRecordId_fieldId_setIndex: { fieldRecordId, fieldId, setIndex },
|
|
193
|
+
},
|
|
194
|
+
include: {
|
|
195
|
+
fieldAttachments: {
|
|
196
|
+
include: {
|
|
197
|
+
attachment: { select: { filename: true, fileSize: true } },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const labels = (fieldValue?.fieldAttachments ?? []).map((fa) => formatAttachmentLabel(fa.attachment.filename, fa.attachment.fileSize));
|
|
203
|
+
const value = labels.length === 0 ? (isArray ? [] : "") : isArray ? labels : labels[0];
|
|
204
|
+
await upsertFieldValue(fieldRecordId, fieldId, setIndex, value, userId);
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Clear an attachment field: delete all FieldAttachment links and set value to empty.
|
|
209
|
+
*/
|
|
210
|
+
export async function clearAttachmentFieldValue(fieldRecordId, fieldId, setIndex, userId) {
|
|
211
|
+
const fieldValue = await erpDb.fieldValue.findUnique({
|
|
212
|
+
where: {
|
|
213
|
+
fieldRecordId_fieldId_setIndex: { fieldRecordId, fieldId, setIndex },
|
|
214
|
+
},
|
|
215
|
+
select: { id: true },
|
|
216
|
+
});
|
|
217
|
+
if (fieldValue) {
|
|
218
|
+
await erpDb.fieldAttachment.deleteMany({
|
|
219
|
+
where: { fieldValueId: fieldValue.id },
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const empty = "";
|
|
223
|
+
await upsertFieldValue(fieldRecordId, fieldId, setIndex, empty, userId);
|
|
224
|
+
}
|
|
225
|
+
// --- Mutations ---
|
|
226
|
+
export async function upsertFieldValue(fieldRecordId, fieldId, setIndex, value, userId) {
|
|
227
|
+
const dbValue = serializeFieldValue(value);
|
|
228
|
+
await erpDb.fieldValue.upsert({
|
|
229
|
+
where: {
|
|
230
|
+
fieldRecordId_fieldId_setIndex: { fieldRecordId, fieldId, setIndex },
|
|
231
|
+
},
|
|
232
|
+
create: {
|
|
233
|
+
fieldRecordId,
|
|
234
|
+
fieldId,
|
|
235
|
+
setIndex,
|
|
236
|
+
value: dbValue,
|
|
237
|
+
createdById: userId,
|
|
238
|
+
updatedById: userId,
|
|
239
|
+
},
|
|
240
|
+
update: {
|
|
241
|
+
value: dbValue,
|
|
242
|
+
updatedById: userId,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
export async function deleteFieldValueSet(fieldRecordId, setIndex) {
|
|
247
|
+
await erpDb.$transaction(async (erpTx) => {
|
|
248
|
+
// Delete all field values for this set
|
|
249
|
+
await erpTx.fieldValue.deleteMany({
|
|
250
|
+
where: { fieldRecordId, setIndex },
|
|
251
|
+
});
|
|
252
|
+
// Re-index higher sets to fill the gap
|
|
253
|
+
await erpTx.$executeRawUnsafe(`UPDATE field_values SET set_index = set_index - 1 WHERE field_record_id = ? AND set_index > ?`, fieldRecordId, setIndex);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=field-value-service.js.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import erpDb from "../erpDb.js";
|
|
2
|
+
import { includeUsers } from "../route-helpers.js";
|
|
3
|
+
// --- Prisma include & result type ---
|
|
4
|
+
export const includeItemInstanceRelations = {
|
|
5
|
+
...includeUsers,
|
|
6
|
+
item: {
|
|
7
|
+
select: {
|
|
8
|
+
key: true,
|
|
9
|
+
fieldSet: {
|
|
10
|
+
select: {
|
|
11
|
+
id: true,
|
|
12
|
+
fields: {
|
|
13
|
+
select: {
|
|
14
|
+
id: true,
|
|
15
|
+
seqNo: true,
|
|
16
|
+
label: true,
|
|
17
|
+
type: true,
|
|
18
|
+
isArray: true,
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
orderBy: { seqNo: "asc" },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
orderRun: {
|
|
28
|
+
select: {
|
|
29
|
+
runNo: true,
|
|
30
|
+
order: { select: { key: true } },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
fieldRecord: {
|
|
34
|
+
include: {
|
|
35
|
+
fieldValues: {
|
|
36
|
+
select: {
|
|
37
|
+
id: true,
|
|
38
|
+
fieldId: true,
|
|
39
|
+
setIndex: true,
|
|
40
|
+
value: true,
|
|
41
|
+
fieldAttachments: {
|
|
42
|
+
select: {
|
|
43
|
+
attachment: {
|
|
44
|
+
select: { publicId: true, filename: true, fileSize: true },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
orderBy: { setIndex: "asc" },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
// --- Lookups ---
|
|
55
|
+
export async function listItemInstances(where, page, pageSize) {
|
|
56
|
+
return Promise.all([
|
|
57
|
+
erpDb.itemInstance.findMany({
|
|
58
|
+
where,
|
|
59
|
+
include: includeItemInstanceRelations,
|
|
60
|
+
skip: (page - 1) * pageSize,
|
|
61
|
+
take: pageSize,
|
|
62
|
+
orderBy: { createdAt: "desc" },
|
|
63
|
+
}),
|
|
64
|
+
erpDb.itemInstance.count({ where }),
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
export async function findItemInstance(id) {
|
|
68
|
+
return erpDb.itemInstance.findUnique({
|
|
69
|
+
where: { id },
|
|
70
|
+
include: includeItemInstanceRelations,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function findItemInstanceByItemAndKey(itemId, key) {
|
|
74
|
+
return erpDb.itemInstance.findUnique({
|
|
75
|
+
where: { itemId_key: { itemId, key } },
|
|
76
|
+
include: includeItemInstanceRelations,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export async function findItemInstanceWithField(id, fieldSeqNo) {
|
|
80
|
+
return erpDb.itemInstance.findUnique({
|
|
81
|
+
where: { id },
|
|
82
|
+
include: {
|
|
83
|
+
item: {
|
|
84
|
+
select: {
|
|
85
|
+
fieldSet: {
|
|
86
|
+
select: {
|
|
87
|
+
id: true,
|
|
88
|
+
fields: {
|
|
89
|
+
where: { seqNo: fieldSeqNo },
|
|
90
|
+
select: {
|
|
91
|
+
id: true,
|
|
92
|
+
seqNo: true,
|
|
93
|
+
label: true,
|
|
94
|
+
type: true,
|
|
95
|
+
isArray: true,
|
|
96
|
+
required: true,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// --- Mutations ---
|
|
107
|
+
export async function createItemInstance(itemId, key, quantity, orderRunId, userId) {
|
|
108
|
+
return erpDb.itemInstance.create({
|
|
109
|
+
data: {
|
|
110
|
+
itemId,
|
|
111
|
+
key,
|
|
112
|
+
quantity: quantity ?? null,
|
|
113
|
+
orderRunId: orderRunId ?? null,
|
|
114
|
+
createdById: userId,
|
|
115
|
+
updatedById: userId,
|
|
116
|
+
},
|
|
117
|
+
include: includeItemInstanceRelations,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export async function updateItemInstance(id, data, userId) {
|
|
121
|
+
return erpDb.itemInstance.update({
|
|
122
|
+
where: { id },
|
|
123
|
+
data: { ...data, updatedById: userId },
|
|
124
|
+
include: includeItemInstanceRelations,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
export async function deleteItemInstance(id) {
|
|
128
|
+
await erpDb.itemInstance.delete({ where: { id } });
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get or create a FieldRecord for an ItemInstance, linking it back.
|
|
132
|
+
* Returns the fieldRecordId, or null if the item has no fieldSet.
|
|
133
|
+
*/
|
|
134
|
+
export async function ensureItemInstanceFieldRecord(instanceId, userId) {
|
|
135
|
+
const inst = await erpDb.itemInstance.findUniqueOrThrow({
|
|
136
|
+
where: { id: instanceId },
|
|
137
|
+
select: {
|
|
138
|
+
fieldRecordId: true,
|
|
139
|
+
item: { select: { fieldSetId: true } },
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (inst.fieldRecordId)
|
|
143
|
+
return inst.fieldRecordId;
|
|
144
|
+
if (!inst.item.fieldSetId)
|
|
145
|
+
return null;
|
|
146
|
+
const fr = await erpDb.fieldRecord.create({
|
|
147
|
+
data: { fieldSetId: inst.item.fieldSetId, createdById: userId },
|
|
148
|
+
});
|
|
149
|
+
await erpDb.itemInstance.update({
|
|
150
|
+
where: { id: instanceId },
|
|
151
|
+
data: { fieldRecordId: fr.id },
|
|
152
|
+
});
|
|
153
|
+
return fr.id;
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=item-instance-service.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import erpDb from "../erpDb.js";
|
|
2
|
+
import { includeUsers } from "../route-helpers.js";
|
|
3
|
+
// --- Prisma include & result type ---
|
|
4
|
+
export const includeUsersAndFieldSet = {
|
|
5
|
+
...includeUsers,
|
|
6
|
+
fieldSet: {
|
|
7
|
+
include: {
|
|
8
|
+
fields: {
|
|
9
|
+
include: includeUsers,
|
|
10
|
+
orderBy: { seqNo: "asc" },
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
// --- Lookups ---
|
|
16
|
+
export async function listItems(where, page, pageSize) {
|
|
17
|
+
return Promise.all([
|
|
18
|
+
erpDb.item.findMany({
|
|
19
|
+
where,
|
|
20
|
+
include: includeUsersAndFieldSet,
|
|
21
|
+
skip: (page - 1) * pageSize,
|
|
22
|
+
take: pageSize,
|
|
23
|
+
orderBy: { createdAt: "desc" },
|
|
24
|
+
}),
|
|
25
|
+
erpDb.item.count({ where }),
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
export async function findExisting(key) {
|
|
29
|
+
return erpDb.item.findUnique({
|
|
30
|
+
where: { key },
|
|
31
|
+
include: includeUsersAndFieldSet,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// --- Mutations ---
|
|
35
|
+
export async function createItem(key, description, userId) {
|
|
36
|
+
return erpDb.item.create({
|
|
37
|
+
data: {
|
|
38
|
+
key,
|
|
39
|
+
description,
|
|
40
|
+
createdById: userId,
|
|
41
|
+
updatedById: userId,
|
|
42
|
+
},
|
|
43
|
+
include: includeUsersAndFieldSet,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export async function updateItem(key, data, userId) {
|
|
47
|
+
return erpDb.item.update({
|
|
48
|
+
where: { key },
|
|
49
|
+
data: { ...data, updatedById: userId },
|
|
50
|
+
include: includeUsersAndFieldSet,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export async function deleteItem(key) {
|
|
54
|
+
await erpDb.item.delete({ where: { key } });
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=item-service.js.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { getLatestRunInfoByUuid, sumCostsByUuid } from "@naisys/hub-database";
|
|
2
|
+
import { writeAuditEntry } from "../audit.js";
|
|
3
|
+
import erpDb from "../erpDb.js";
|
|
4
|
+
// --- Prisma include & result type ---
|
|
5
|
+
export const includeLaborTicket = {
|
|
6
|
+
user: { select: { username: true } },
|
|
7
|
+
createdBy: { select: { username: true } },
|
|
8
|
+
updatedBy: { select: { username: true } },
|
|
9
|
+
};
|
|
10
|
+
// --- Helpers ---
|
|
11
|
+
/**
|
|
12
|
+
* Compute the cost for a labor ticket at clock-out time.
|
|
13
|
+
* Agents: sum of hub cost entries for the user within the clock-in/out window.
|
|
14
|
+
* Non-agents: 0.
|
|
15
|
+
*/
|
|
16
|
+
async function computeCost(userId, clockIn, clockOut) {
|
|
17
|
+
const user = await erpDb.user.findUnique({
|
|
18
|
+
where: { id: userId },
|
|
19
|
+
select: { isAgent: true, uuid: true },
|
|
20
|
+
});
|
|
21
|
+
if (!user?.isAgent)
|
|
22
|
+
return 0;
|
|
23
|
+
const cost = await sumCostsByUuid(user.uuid, clockIn, clockOut);
|
|
24
|
+
return Math.round(cost * 100) / 100;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get the current hub run info (run_id + session start) for an agent user.
|
|
28
|
+
*/
|
|
29
|
+
async function getAgentRunInfo(userId) {
|
|
30
|
+
const user = await erpDb.user.findUnique({
|
|
31
|
+
where: { id: userId },
|
|
32
|
+
select: { isAgent: true, uuid: true },
|
|
33
|
+
});
|
|
34
|
+
if (!user?.isAgent)
|
|
35
|
+
return null;
|
|
36
|
+
return getLatestRunInfoByUuid(user.uuid);
|
|
37
|
+
}
|
|
38
|
+
// --- Lookups ---
|
|
39
|
+
export async function isUserClockedIn(operationRunId, userId) {
|
|
40
|
+
const ticket = await erpDb.laborTicket.findFirst({
|
|
41
|
+
where: { operationRunId, userId, clockOut: null },
|
|
42
|
+
});
|
|
43
|
+
return !!ticket;
|
|
44
|
+
}
|
|
45
|
+
export async function listLaborTickets(operationRunId) {
|
|
46
|
+
return erpDb.laborTicket.findMany({
|
|
47
|
+
where: { operationRunId },
|
|
48
|
+
include: includeLaborTicket,
|
|
49
|
+
orderBy: { clockIn: "desc" },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// --- Mutations ---
|
|
53
|
+
export async function clockIn(operationRunId, userId, actorId) {
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const runInfo = await getAgentRunInfo(userId);
|
|
56
|
+
const runId = runInfo?.runId ?? null;
|
|
57
|
+
// If already clocked into this op run, just return the existing ticket
|
|
58
|
+
const existing = await erpDb.laborTicket.findFirst({
|
|
59
|
+
where: { operationRunId, userId, clockOut: null },
|
|
60
|
+
include: includeLaborTicket,
|
|
61
|
+
});
|
|
62
|
+
if (existing)
|
|
63
|
+
return existing;
|
|
64
|
+
return erpDb.$transaction(async (tx) => {
|
|
65
|
+
// Auto clock-out all open tickets for this user (globally)
|
|
66
|
+
const openTickets = await tx.laborTicket.findMany({
|
|
67
|
+
where: { userId, clockOut: null },
|
|
68
|
+
});
|
|
69
|
+
for (const ticket of openTickets) {
|
|
70
|
+
const cost = await computeCost(userId, ticket.clockIn, now);
|
|
71
|
+
await tx.laborTicket.update({
|
|
72
|
+
where: { id: ticket.id },
|
|
73
|
+
data: { clockOut: now, cost, updatedById: actorId },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// If no tickets were auto-closed and this is the first ticket for the
|
|
77
|
+
// current run/session, backdate clockIn to the session start so that
|
|
78
|
+
// startup costs (before the agent called clock-in) are captured.
|
|
79
|
+
let clockInTime = now;
|
|
80
|
+
if (runInfo && openTickets.length === 0) {
|
|
81
|
+
const existingForRun = await tx.laborTicket.findFirst({
|
|
82
|
+
where: { userId, runId },
|
|
83
|
+
});
|
|
84
|
+
if (!existingForRun) {
|
|
85
|
+
clockInTime = runInfo.sessionStart;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Create new ticket
|
|
89
|
+
return tx.laborTicket.create({
|
|
90
|
+
data: {
|
|
91
|
+
operationRunId,
|
|
92
|
+
userId,
|
|
93
|
+
runId,
|
|
94
|
+
clockIn: clockInTime,
|
|
95
|
+
createdById: actorId,
|
|
96
|
+
updatedById: actorId,
|
|
97
|
+
updatedAt: now,
|
|
98
|
+
},
|
|
99
|
+
include: includeLaborTicket,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export async function clockOut(operationRunId, opts, actorId) {
|
|
104
|
+
const now = new Date();
|
|
105
|
+
return erpDb.$transaction(async (tx) => {
|
|
106
|
+
// Build where clause based on opts
|
|
107
|
+
const where = {
|
|
108
|
+
operationRunId,
|
|
109
|
+
clockOut: null,
|
|
110
|
+
};
|
|
111
|
+
if (opts.ticketId) {
|
|
112
|
+
where.id = opts.ticketId;
|
|
113
|
+
}
|
|
114
|
+
else if (opts.userId) {
|
|
115
|
+
where.userId = opts.userId;
|
|
116
|
+
}
|
|
117
|
+
// If neither specified, clocks out ALL open tickets for the op run
|
|
118
|
+
const openTickets = await tx.laborTicket.findMany({ where });
|
|
119
|
+
const updated = [];
|
|
120
|
+
for (const ticket of openTickets) {
|
|
121
|
+
const cost = await computeCost(ticket.userId, ticket.clockIn, now);
|
|
122
|
+
const result = await tx.laborTicket.update({
|
|
123
|
+
where: { id: ticket.id },
|
|
124
|
+
data: { clockOut: now, cost, updatedById: actorId },
|
|
125
|
+
include: includeLaborTicket,
|
|
126
|
+
});
|
|
127
|
+
updated.push(result);
|
|
128
|
+
}
|
|
129
|
+
return updated;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export async function clockOutAllForOpRun(operationRunId, actorId) {
|
|
133
|
+
await clockOut(operationRunId, {}, actorId);
|
|
134
|
+
}
|
|
135
|
+
export async function sumLaborTicketCosts(operationRunId) {
|
|
136
|
+
const result = await erpDb.laborTicket.aggregate({
|
|
137
|
+
where: { operationRunId },
|
|
138
|
+
_sum: { cost: true },
|
|
139
|
+
});
|
|
140
|
+
return Math.round((result._sum.cost ?? 0) * 100) / 100;
|
|
141
|
+
}
|
|
142
|
+
export async function deleteLaborTicket(ticketId, actorId) {
|
|
143
|
+
await erpDb.$transaction(async (tx) => {
|
|
144
|
+
await tx.laborTicket.delete({ where: { id: ticketId } });
|
|
145
|
+
await writeAuditEntry(tx, "LaborTicket", ticketId, "delete", "id", String(ticketId), null, actorId);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=labor-ticket-service.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { tailLogFile } from "@naisys/common-node";
|
|
3
|
+
export { tailLogFile };
|
|
4
|
+
export function getErpLogPath() {
|
|
5
|
+
const naisysFolder = process.env.NAISYS_FOLDER;
|
|
6
|
+
if (!naisysFolder) {
|
|
7
|
+
throw new Error("NAISYS_FOLDER environment variable is not set.");
|
|
8
|
+
}
|
|
9
|
+
return path.join(naisysFolder, "logs", "erp.log");
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=log-file-service.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import erpDb from "../erpDb.js";
|
|
2
|
+
const depInclude = {
|
|
3
|
+
predecessor: { select: { seqNo: true, title: true } },
|
|
4
|
+
createdBy: { select: { username: true } },
|
|
5
|
+
};
|
|
6
|
+
export async function listDependencies(operationId) {
|
|
7
|
+
return erpDb.operationDependency.findMany({
|
|
8
|
+
where: { successorId: operationId },
|
|
9
|
+
include: depInclude,
|
|
10
|
+
orderBy: { predecessor: { seqNo: "asc" } },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export async function createDependency(successorId, predecessorId, userId) {
|
|
14
|
+
return erpDb.operationDependency.create({
|
|
15
|
+
data: {
|
|
16
|
+
successorId,
|
|
17
|
+
predecessorId,
|
|
18
|
+
createdById: userId,
|
|
19
|
+
},
|
|
20
|
+
include: depInclude,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export async function deleteDependency(successorId, predecessorId) {
|
|
24
|
+
await erpDb.operationDependency.delete({
|
|
25
|
+
where: {
|
|
26
|
+
successorId_predecessorId: { successorId, predecessorId },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=operation-dependency-service.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import erpDb from "../erpDb.js";
|
|
2
|
+
// --- Prisma include & result type ---
|
|
3
|
+
export const includeComment = {
|
|
4
|
+
createdBy: { select: { username: true } },
|
|
5
|
+
};
|
|
6
|
+
// --- Lookups ---
|
|
7
|
+
export async function listComments(operationRunId) {
|
|
8
|
+
return erpDb.operationRunComment.findMany({
|
|
9
|
+
where: { operationRunId },
|
|
10
|
+
include: includeComment,
|
|
11
|
+
orderBy: { createdAt: "asc" },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
// --- Mutations ---
|
|
15
|
+
export async function createComment(operationRunId, type, body, userId) {
|
|
16
|
+
return erpDb.operationRunComment.create({
|
|
17
|
+
data: {
|
|
18
|
+
operationRunId,
|
|
19
|
+
type,
|
|
20
|
+
body,
|
|
21
|
+
createdById: userId,
|
|
22
|
+
},
|
|
23
|
+
include: includeComment,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=operation-run-comment-service.js.map
|