@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,268 @@
|
|
|
1
|
+
import { ClockOutLaborTicketSchema, CreateResponseSchema, ErrorResponseSchema, LaborTicketListResponseSchema, MutateResponseSchema, OperationRunStatus, } from "@naisys/erp-shared";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { hasPermission, requirePermission } from "../auth-middleware.js";
|
|
4
|
+
import { notFound } from "../error-handler.js";
|
|
5
|
+
import { API_PREFIX, selfLink } from "../hateoas.js";
|
|
6
|
+
import { checkOpRunInProgress, formatAuditFields, formatDate, mutationResult, permGate, resolveOpRun, } from "../route-helpers.js";
|
|
7
|
+
import { clockIn, clockOut, deleteLaborTicket, listLaborTickets, } from "../services/labor-ticket-service.js";
|
|
8
|
+
function laborResource(orderKey, runNo, seqNo) {
|
|
9
|
+
return `orders/${orderKey}/runs/${runNo}/ops/${seqNo}/labor`;
|
|
10
|
+
}
|
|
11
|
+
function laborTicketListActions(orderKey, runNo, seqNo, opRunStatus, user, tickets) {
|
|
12
|
+
const actions = [];
|
|
13
|
+
const base = `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}`;
|
|
14
|
+
const isInProgress = opRunStatus === OperationRunStatus.in_progress;
|
|
15
|
+
const isExecutor = hasPermission(user, "order_executor");
|
|
16
|
+
const executorGate = permGate(isExecutor, "order_executor");
|
|
17
|
+
const userHasOpenTicket = user != null && tickets.some((t) => t.userId === user.id && !t.clockOut);
|
|
18
|
+
const anyOpenTickets = tickets.some((t) => !t.clockOut);
|
|
19
|
+
// clock-in: show for non-executors (disabled for permission), or executors with status logic
|
|
20
|
+
if (!isExecutor) {
|
|
21
|
+
actions.push({
|
|
22
|
+
rel: "clock-in",
|
|
23
|
+
href: `${base}/clock-in`,
|
|
24
|
+
method: "POST",
|
|
25
|
+
title: "Clock In",
|
|
26
|
+
...executorGate,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (!isInProgress) {
|
|
30
|
+
actions.push({
|
|
31
|
+
rel: "clock-in",
|
|
32
|
+
href: `${base}/clock-in`,
|
|
33
|
+
method: "POST",
|
|
34
|
+
title: "Clock In",
|
|
35
|
+
disabled: true,
|
|
36
|
+
disabledReason: "Operation must be in progress to clock in",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (!userHasOpenTicket) {
|
|
40
|
+
actions.push({
|
|
41
|
+
rel: "clock-in",
|
|
42
|
+
href: `${base}/clock-in`,
|
|
43
|
+
method: "POST",
|
|
44
|
+
title: "Clock In",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// clock-out for executor
|
|
48
|
+
if (isExecutor && isInProgress && userHasOpenTicket) {
|
|
49
|
+
actions.push({
|
|
50
|
+
rel: "clock-out",
|
|
51
|
+
href: `${base}/clock-out`,
|
|
52
|
+
method: "POST",
|
|
53
|
+
title: "Clock Out",
|
|
54
|
+
schema: `${API_PREFIX}/schemas/ClockOutLaborTicket`,
|
|
55
|
+
body: {},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// manager clock-out: for non-executor managers only
|
|
59
|
+
if (!isExecutor &&
|
|
60
|
+
hasPermission(user, "order_manager") &&
|
|
61
|
+
isInProgress &&
|
|
62
|
+
anyOpenTickets) {
|
|
63
|
+
actions.push({
|
|
64
|
+
rel: "clock-out",
|
|
65
|
+
href: `${base}/clock-out`,
|
|
66
|
+
method: "POST",
|
|
67
|
+
title: "Clock Out",
|
|
68
|
+
schema: `${API_PREFIX}/schemas/ClockOutLaborTicket`,
|
|
69
|
+
body: { userId: 0, ticketId: 0 },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return actions;
|
|
73
|
+
}
|
|
74
|
+
function laborTicketActionTemplates(orderKey, runNo, seqNo, user) {
|
|
75
|
+
if (!hasPermission(user, "order_manager"))
|
|
76
|
+
return [];
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
rel: "deleteTicket",
|
|
80
|
+
hrefTemplate: `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}/{ticketId}`,
|
|
81
|
+
method: "DELETE",
|
|
82
|
+
title: "Delete Ticket",
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
function formatLaborTicket(orderKey, runNo, seqNo, ticket) {
|
|
87
|
+
return {
|
|
88
|
+
id: ticket.id,
|
|
89
|
+
operationRunId: ticket.operationRunId,
|
|
90
|
+
userId: ticket.userId,
|
|
91
|
+
username: ticket.user.username,
|
|
92
|
+
runId: ticket.runId,
|
|
93
|
+
clockIn: ticket.clockIn.toISOString(),
|
|
94
|
+
clockOut: formatDate(ticket.clockOut),
|
|
95
|
+
cost: ticket.cost,
|
|
96
|
+
...formatAuditFields(ticket),
|
|
97
|
+
_links: [
|
|
98
|
+
selfLink(`/${laborResource(orderKey, runNo, seqNo)}/${ticket.id}`),
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const LaborParamsSchema = z.object({
|
|
103
|
+
orderKey: z.string(),
|
|
104
|
+
runNo: z.coerce.number().int(),
|
|
105
|
+
seqNo: z.coerce.number().int(),
|
|
106
|
+
});
|
|
107
|
+
const LaborTicketParamsSchema = z.object({
|
|
108
|
+
orderKey: z.string(),
|
|
109
|
+
runNo: z.coerce.number().int(),
|
|
110
|
+
seqNo: z.coerce.number().int(),
|
|
111
|
+
ticketId: z.coerce.number().int(),
|
|
112
|
+
});
|
|
113
|
+
export default function laborTicketRoutes(fastify) {
|
|
114
|
+
const app = fastify.withTypeProvider();
|
|
115
|
+
// LIST
|
|
116
|
+
app.get("/", {
|
|
117
|
+
schema: {
|
|
118
|
+
description: "List labor tickets for an operation run",
|
|
119
|
+
tags: ["Labor Tickets"],
|
|
120
|
+
params: LaborParamsSchema,
|
|
121
|
+
response: {
|
|
122
|
+
200: LaborTicketListResponseSchema,
|
|
123
|
+
404: ErrorResponseSchema,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
handler: async (request, reply) => {
|
|
127
|
+
const { orderKey, runNo, seqNo } = request.params;
|
|
128
|
+
const resolved = await resolveOpRun(orderKey, runNo, seqNo);
|
|
129
|
+
if (!resolved) {
|
|
130
|
+
return notFound(reply, "Operation run not found");
|
|
131
|
+
}
|
|
132
|
+
const items = await listLaborTickets(resolved.opRun.id);
|
|
133
|
+
return {
|
|
134
|
+
items: items.map((ticket) => {
|
|
135
|
+
const { _links, ...rest } = formatLaborTicket(orderKey, runNo, seqNo, ticket);
|
|
136
|
+
return rest;
|
|
137
|
+
}),
|
|
138
|
+
total: items.length,
|
|
139
|
+
_links: [selfLink(`/${laborResource(orderKey, runNo, seqNo)}`)],
|
|
140
|
+
_linkTemplates: [
|
|
141
|
+
{
|
|
142
|
+
rel: "item",
|
|
143
|
+
hrefTemplate: `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}/{id}`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
_actions: laborTicketListActions(orderKey, runNo, seqNo, resolved.opRun.status, request.erpUser, items),
|
|
147
|
+
_actionTemplates: laborTicketActionTemplates(orderKey, runNo, seqNo, request.erpUser),
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
// CLOCK IN
|
|
152
|
+
app.post("/clock-in", {
|
|
153
|
+
schema: {
|
|
154
|
+
description: "Clock in to an operation run (auto clocks out any open tickets for this user)",
|
|
155
|
+
tags: ["Labor Tickets"],
|
|
156
|
+
params: LaborParamsSchema,
|
|
157
|
+
response: {
|
|
158
|
+
200: CreateResponseSchema,
|
|
159
|
+
404: ErrorResponseSchema,
|
|
160
|
+
409: ErrorResponseSchema,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
preHandler: requirePermission("order_executor"),
|
|
164
|
+
handler: async (request, reply) => {
|
|
165
|
+
const { orderKey, runNo, seqNo } = request.params;
|
|
166
|
+
const userId = request.erpUser.id;
|
|
167
|
+
const resolved = await resolveOpRun(orderKey, runNo, seqNo);
|
|
168
|
+
if (!resolved)
|
|
169
|
+
return notFound(reply, "Operation run not found");
|
|
170
|
+
const opErr = checkOpRunInProgress(resolved.opRun.status);
|
|
171
|
+
if (opErr) {
|
|
172
|
+
reply.status(409);
|
|
173
|
+
return { statusCode: 409, error: "Conflict", message: opErr };
|
|
174
|
+
}
|
|
175
|
+
const ticket = await clockIn(resolved.opRun.id, userId, userId);
|
|
176
|
+
const full = formatLaborTicket(orderKey, runNo, seqNo, ticket);
|
|
177
|
+
return mutationResult(request, reply, full, {
|
|
178
|
+
id: full.id,
|
|
179
|
+
_links: full._links,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
// CLOCK OUT
|
|
184
|
+
app.post("/clock-out", {
|
|
185
|
+
schema: {
|
|
186
|
+
description: "Clock out of an operation run",
|
|
187
|
+
tags: ["Labor Tickets"],
|
|
188
|
+
params: LaborParamsSchema,
|
|
189
|
+
body: ClockOutLaborTicketSchema,
|
|
190
|
+
response: {
|
|
191
|
+
200: MutateResponseSchema,
|
|
192
|
+
404: ErrorResponseSchema,
|
|
193
|
+
409: ErrorResponseSchema,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
preHandler: requirePermission("order_executor"),
|
|
197
|
+
handler: async (request, reply) => {
|
|
198
|
+
const { orderKey, runNo, seqNo } = request.params;
|
|
199
|
+
const userId = request.erpUser.id;
|
|
200
|
+
const body = request.body;
|
|
201
|
+
const resolved = await resolveOpRun(orderKey, runNo, seqNo);
|
|
202
|
+
if (!resolved)
|
|
203
|
+
return notFound(reply, "Operation run not found");
|
|
204
|
+
const opErr = checkOpRunInProgress(resolved.opRun.status);
|
|
205
|
+
if (opErr) {
|
|
206
|
+
reply.status(409);
|
|
207
|
+
return { statusCode: 409, error: "Conflict", message: opErr };
|
|
208
|
+
}
|
|
209
|
+
// Non-managers can only clock out their own tickets
|
|
210
|
+
const isManager = hasPermission(request.erpUser, "order_manager");
|
|
211
|
+
const opts = {
|
|
212
|
+
userId: body.userId ?? (isManager ? undefined : userId),
|
|
213
|
+
ticketId: body.ticketId,
|
|
214
|
+
};
|
|
215
|
+
// Non-managers cannot specify another user's ID
|
|
216
|
+
if (!isManager && body.userId && body.userId !== userId) {
|
|
217
|
+
reply.status(409);
|
|
218
|
+
return {
|
|
219
|
+
statusCode: 409,
|
|
220
|
+
error: "Conflict",
|
|
221
|
+
message: "Non-managers can only clock out their own tickets",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
await clockOut(resolved.opRun.id, opts, userId);
|
|
225
|
+
// Return full list after clock-out
|
|
226
|
+
const items = await listLaborTickets(resolved.opRun.id);
|
|
227
|
+
const full = {
|
|
228
|
+
items: items.map((ticket) => {
|
|
229
|
+
const { _links, ...rest } = formatLaborTicket(orderKey, runNo, seqNo, ticket);
|
|
230
|
+
return rest;
|
|
231
|
+
}),
|
|
232
|
+
total: items.length,
|
|
233
|
+
_links: [selfLink(`/${laborResource(orderKey, runNo, seqNo)}`)],
|
|
234
|
+
_linkTemplates: [
|
|
235
|
+
{
|
|
236
|
+
rel: "item",
|
|
237
|
+
hrefTemplate: `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}/{id}`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
_actions: laborTicketListActions(orderKey, runNo, seqNo, resolved.opRun.status, request.erpUser, items),
|
|
241
|
+
_actionTemplates: laborTicketActionTemplates(orderKey, runNo, seqNo, request.erpUser),
|
|
242
|
+
};
|
|
243
|
+
return mutationResult(request, reply, full, {
|
|
244
|
+
_actions: full._actions,
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
// DELETE
|
|
249
|
+
app.delete("/:ticketId", {
|
|
250
|
+
schema: {
|
|
251
|
+
description: "Delete a labor ticket",
|
|
252
|
+
tags: ["Labor Tickets"],
|
|
253
|
+
params: LaborTicketParamsSchema,
|
|
254
|
+
response: {
|
|
255
|
+
204: z.void(),
|
|
256
|
+
404: ErrorResponseSchema,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
preHandler: requirePermission("order_manager"),
|
|
260
|
+
handler: async (request, reply) => {
|
|
261
|
+
const { ticketId } = request.params;
|
|
262
|
+
const userId = request.erpUser.id;
|
|
263
|
+
await deleteLaborTicket(ticketId, userId);
|
|
264
|
+
reply.status(204);
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=labor-tickets.js.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { CreateOperationDependencySchema, CreateResponseSchema, ErrorResponseSchema, OperationDependencyListResponseSchema, RevisionStatus, } from "@naisys/erp-shared";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { hasPermission, requirePermission } from "../auth-middleware.js";
|
|
4
|
+
import { conflict, notFound } from "../error-handler.js";
|
|
5
|
+
import { API_PREFIX, selfLink } from "../hateoas.js";
|
|
6
|
+
import { mutationResult, resolveRevision } from "../route-helpers.js";
|
|
7
|
+
import { createDependency, deleteDependency, listDependencies, } from "../services/operation-dependency-service.js";
|
|
8
|
+
import { findExisting } from "../services/operation-service.js";
|
|
9
|
+
const ParamsSchema = z.object({
|
|
10
|
+
orderKey: z.string(),
|
|
11
|
+
revNo: z.coerce.number().int(),
|
|
12
|
+
seqNo: z.coerce.number().int(),
|
|
13
|
+
});
|
|
14
|
+
const DepParamsSchema = z.object({
|
|
15
|
+
orderKey: z.string(),
|
|
16
|
+
revNo: z.coerce.number().int(),
|
|
17
|
+
seqNo: z.coerce.number().int(),
|
|
18
|
+
predecessorSeqNo: z.coerce.number().int(),
|
|
19
|
+
});
|
|
20
|
+
function depBasePath(orderKey, revNo, seqNo) {
|
|
21
|
+
return `/orders/${orderKey}/revs/${revNo}/ops/${seqNo}/deps`;
|
|
22
|
+
}
|
|
23
|
+
function formatDependency(dep) {
|
|
24
|
+
return {
|
|
25
|
+
id: dep.id,
|
|
26
|
+
predecessorSeqNo: dep.predecessor.seqNo,
|
|
27
|
+
predecessorTitle: dep.predecessor.title,
|
|
28
|
+
createdAt: dep.createdAt.toISOString(),
|
|
29
|
+
createdBy: dep.createdBy.username,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function depActionTemplates(basePath, revStatus, user) {
|
|
33
|
+
if (!hasPermission(user, "order_planner") ||
|
|
34
|
+
revStatus !== RevisionStatus.draft)
|
|
35
|
+
return [];
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
rel: "deleteDependency",
|
|
39
|
+
hrefTemplate: `${API_PREFIX}${basePath}/{predecessorSeqNo}`,
|
|
40
|
+
method: "DELETE",
|
|
41
|
+
title: "Remove Dependency",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
export default function operationDependencyRoutes(fastify) {
|
|
46
|
+
const app = fastify.withTypeProvider();
|
|
47
|
+
// LIST dependencies (predecessors) for an operation
|
|
48
|
+
app.get("/", {
|
|
49
|
+
schema: {
|
|
50
|
+
description: "List predecessor dependencies for an operation",
|
|
51
|
+
tags: ["Operation Dependencies"],
|
|
52
|
+
params: ParamsSchema,
|
|
53
|
+
response: {
|
|
54
|
+
200: OperationDependencyListResponseSchema,
|
|
55
|
+
404: ErrorResponseSchema,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
handler: async (request, reply) => {
|
|
59
|
+
const { orderKey, revNo, seqNo } = request.params;
|
|
60
|
+
const resolved = await resolveRevision(orderKey, revNo);
|
|
61
|
+
if (!resolved) {
|
|
62
|
+
return notFound(reply, "Revision not found");
|
|
63
|
+
}
|
|
64
|
+
const operation = await findExisting(resolved.rev.id, seqNo);
|
|
65
|
+
if (!operation) {
|
|
66
|
+
return notFound(reply, `Operation ${seqNo} not found`);
|
|
67
|
+
}
|
|
68
|
+
const items = await listDependencies(operation.id);
|
|
69
|
+
const user = request.erpUser;
|
|
70
|
+
const base = depBasePath(orderKey, revNo, seqNo);
|
|
71
|
+
return {
|
|
72
|
+
items: items.map((dep) => formatDependency(dep)),
|
|
73
|
+
total: items.length,
|
|
74
|
+
_links: [selfLink(base)],
|
|
75
|
+
_actions: hasPermission(user, "order_planner") &&
|
|
76
|
+
resolved.rev.status === RevisionStatus.draft
|
|
77
|
+
? [
|
|
78
|
+
{
|
|
79
|
+
rel: "create",
|
|
80
|
+
href: `${API_PREFIX}${base}`,
|
|
81
|
+
method: "POST",
|
|
82
|
+
title: "Add Dependency",
|
|
83
|
+
schema: `${API_PREFIX}/schemas/CreateOperationDependency`,
|
|
84
|
+
},
|
|
85
|
+
]
|
|
86
|
+
: [],
|
|
87
|
+
_actionTemplates: depActionTemplates(base, resolved.rev.status, user),
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
// CREATE a dependency
|
|
92
|
+
app.post("/", {
|
|
93
|
+
schema: {
|
|
94
|
+
description: "Add a predecessor dependency to an operation",
|
|
95
|
+
tags: ["Operation Dependencies"],
|
|
96
|
+
params: ParamsSchema,
|
|
97
|
+
body: CreateOperationDependencySchema,
|
|
98
|
+
response: {
|
|
99
|
+
201: CreateResponseSchema,
|
|
100
|
+
404: ErrorResponseSchema,
|
|
101
|
+
409: ErrorResponseSchema,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
preHandler: requirePermission("order_planner"),
|
|
105
|
+
handler: async (request, reply) => {
|
|
106
|
+
const { orderKey, revNo, seqNo } = request.params;
|
|
107
|
+
const { predecessorSeqNo } = request.body;
|
|
108
|
+
const userId = request.erpUser.id;
|
|
109
|
+
const resolved = await resolveRevision(orderKey, revNo);
|
|
110
|
+
if (!resolved) {
|
|
111
|
+
return notFound(reply, "Revision not found");
|
|
112
|
+
}
|
|
113
|
+
if (resolved.rev.status !== RevisionStatus.draft) {
|
|
114
|
+
return conflict(reply, `Cannot modify dependencies on a ${resolved.rev.status} revision`);
|
|
115
|
+
}
|
|
116
|
+
const successor = await findExisting(resolved.rev.id, seqNo);
|
|
117
|
+
if (!successor) {
|
|
118
|
+
return notFound(reply, `Operation ${seqNo} not found`);
|
|
119
|
+
}
|
|
120
|
+
const predecessor = await findExisting(resolved.rev.id, predecessorSeqNo);
|
|
121
|
+
if (!predecessor) {
|
|
122
|
+
return notFound(reply, `Predecessor operation ${predecessorSeqNo} not found`);
|
|
123
|
+
}
|
|
124
|
+
if (successor.id === predecessor.id) {
|
|
125
|
+
return conflict(reply, "An operation cannot depend on itself");
|
|
126
|
+
}
|
|
127
|
+
const dep = await createDependency(successor.id, predecessor.id, userId);
|
|
128
|
+
const full = formatDependency(dep);
|
|
129
|
+
reply.status(201);
|
|
130
|
+
return mutationResult(request, reply, full, {
|
|
131
|
+
id: full.id,
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
// DELETE a dependency
|
|
136
|
+
app.delete("/:predecessorSeqNo", {
|
|
137
|
+
schema: {
|
|
138
|
+
description: "Remove a predecessor dependency from an operation",
|
|
139
|
+
tags: ["Operation Dependencies"],
|
|
140
|
+
params: DepParamsSchema,
|
|
141
|
+
response: {
|
|
142
|
+
204: z.void(),
|
|
143
|
+
404: ErrorResponseSchema,
|
|
144
|
+
409: ErrorResponseSchema,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
preHandler: requirePermission("order_planner"),
|
|
148
|
+
handler: async (request, reply) => {
|
|
149
|
+
const { orderKey, revNo, seqNo, predecessorSeqNo } = request.params;
|
|
150
|
+
const resolved = await resolveRevision(orderKey, revNo);
|
|
151
|
+
if (!resolved) {
|
|
152
|
+
return notFound(reply, "Revision not found");
|
|
153
|
+
}
|
|
154
|
+
if (resolved.rev.status !== RevisionStatus.draft) {
|
|
155
|
+
return conflict(reply, `Cannot modify dependencies on a ${resolved.rev.status} revision`);
|
|
156
|
+
}
|
|
157
|
+
const successor = await findExisting(resolved.rev.id, seqNo);
|
|
158
|
+
if (!successor) {
|
|
159
|
+
return notFound(reply, `Operation ${seqNo} not found`);
|
|
160
|
+
}
|
|
161
|
+
const predecessor = await findExisting(resolved.rev.id, predecessorSeqNo);
|
|
162
|
+
if (!predecessor) {
|
|
163
|
+
return notFound(reply, `Predecessor operation ${predecessorSeqNo} not found`);
|
|
164
|
+
}
|
|
165
|
+
await deleteDependency(successor.id, predecessor.id);
|
|
166
|
+
reply.status(204);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=operation-dependencies.js.map
|