@burdenoff/microfe-movethewheels 2026.1016.12 → 2026.1017.1
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/dist/hooks/useFleet.js +7 -0
- package/dist/hooks/useOrderDispatch.js +93 -0
- package/dist/hooks/useOrders.js +148 -134
- package/dist/movethewheels/hooks/useOrderDispatch.d.ts +26 -0
- package/dist/movethewheels/hooks/useOrders.d.ts +2 -1
- package/dist/movethewheels/utils/graphqlClient.d.ts +2 -1
- package/dist/movethewheels/utils/orderVehicleHold.d.ts +16 -0
- package/dist/pages/OrderDetailsPage.js +269 -241
- package/dist/utils/graphqlClient.js +72 -65
- package/dist/utils/orderVehicleHold.js +38 -0
- package/package.json +3 -2
- package/src/movethewheels/hooks/useFleet.ts +9 -0
- package/src/movethewheels/hooks/useOrderDispatch.ts +196 -0
- package/src/movethewheels/hooks/useOrders.ts +59 -27
- package/src/movethewheels/pages/OrderDetailsPage.tsx +229 -90
- package/src/movethewheels/utils/graphqlClient.ts +36 -7
- package/src/movethewheels/utils/orderVehicleHold.ts +84 -0
|
@@ -1,103 +1,110 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { vehicleHoldRefusal as e } from "./orderVehicleHold.js";
|
|
2
|
+
import { prepareWorkspaceRequest as t } from "@burdenoff/fe-libs/shared/graphql";
|
|
2
3
|
//#region src/movethewheels/utils/graphqlClient.ts
|
|
3
|
-
function
|
|
4
|
+
function n(e) {
|
|
4
5
|
return e ? e.endsWith("/graphql") ? e : `${e}/workspaces/graphql` : "/workspaces/graphql";
|
|
5
6
|
}
|
|
6
|
-
var
|
|
7
|
-
function
|
|
7
|
+
var r = 3, i = 12e3;
|
|
8
|
+
function a(e) {
|
|
8
9
|
return e ? /upstream connect|connection refused|reset before headers|delayed connect|econnrefused|econnreset|socket hang up|service unavailable|bad gateway|gateway timeout|temporarily unavailable|please try again|failed to fetch|networkerror|load failed/i.test(e) : !1;
|
|
9
10
|
}
|
|
10
|
-
function
|
|
11
|
+
function o(e) {
|
|
11
12
|
return e === 429 || e === 502 || e === 503 || e === 504;
|
|
12
13
|
}
|
|
13
|
-
function
|
|
14
|
+
function s(e) {
|
|
14
15
|
return typeof e == "object" && !!e && "name" in e && e.name === "AbortError";
|
|
15
16
|
}
|
|
16
|
-
var
|
|
17
|
-
function
|
|
17
|
+
var c = (e) => new Promise((t) => setTimeout(t, e)), l = 4, u = 0, d = [];
|
|
18
|
+
function f() {
|
|
18
19
|
return new Promise((e) => {
|
|
19
|
-
|
|
20
|
+
u < l ? (u += 1, e()) : d.push(e);
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
|
-
function
|
|
23
|
-
let e =
|
|
24
|
-
e ? e() : --
|
|
23
|
+
function p() {
|
|
24
|
+
let e = d.shift();
|
|
25
|
+
e ? e() : --u;
|
|
25
26
|
}
|
|
26
|
-
async function
|
|
27
|
-
let
|
|
27
|
+
async function m(e, i, a, o = "RETRY_TRANSIENT", s) {
|
|
28
|
+
let c = { ...e }, l = JSON.stringify({
|
|
28
29
|
query: i,
|
|
29
30
|
variables: a
|
|
30
|
-
}),
|
|
31
|
-
await
|
|
31
|
+
}), u = n(c.apiGatewayUrl);
|
|
32
|
+
await f();
|
|
32
33
|
try {
|
|
33
|
-
let
|
|
34
|
-
url: new URL(
|
|
35
|
-
authToken:
|
|
36
|
-
actorId:
|
|
37
|
-
workspaceId:
|
|
38
|
-
tenantId:
|
|
39
|
-
workspaceToken:
|
|
34
|
+
let e = typeof window < "u" ? await t({
|
|
35
|
+
url: new URL(u, typeof document < "u" && document.baseURI || window.location.href).toString(),
|
|
36
|
+
authToken: c.authToken ?? "",
|
|
37
|
+
actorId: c.actorId ?? "",
|
|
38
|
+
workspaceId: c.workspaceId ?? "",
|
|
39
|
+
tenantId: c.tenantId,
|
|
40
|
+
workspaceToken: c.workspaceToken,
|
|
40
41
|
currentQuery: i
|
|
41
|
-
}) : null,
|
|
42
|
-
return await
|
|
42
|
+
}) : null, n = e?.workspaceToken ?? c.workspaceToken;
|
|
43
|
+
return await _(u, {
|
|
43
44
|
"content-type": "application/json",
|
|
44
|
-
...
|
|
45
|
-
...
|
|
46
|
-
...
|
|
47
|
-
...
|
|
48
|
-
...
|
|
49
|
-
...
|
|
50
|
-
},
|
|
45
|
+
...c.authToken ? { Authorization: `Bearer ${c.authToken}` } : {},
|
|
46
|
+
...n ? { "X-Workspace-Authorization": `Bearer ${n}` } : {},
|
|
47
|
+
...c.workspaceId ? { "x-workspace-id": c.workspaceId } : {},
|
|
48
|
+
...c.tenantId ? { "x-tenant-id": c.tenantId } : {},
|
|
49
|
+
...c.actorId ? { "x-actor-id": c.actorId } : {},
|
|
50
|
+
...c.actorId ? { "x-actor-type": "user" } : {}
|
|
51
|
+
}, l, o === "SINGLE_ATTEMPT" ? 0 : r, e?.assertCurrent ?? (() => {}), o === "SINGLE_ATTEMPT" ? s : void 0);
|
|
51
52
|
} finally {
|
|
52
|
-
|
|
53
|
+
p();
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
async function
|
|
56
|
+
async function h(t, n) {
|
|
56
57
|
try {
|
|
57
|
-
let
|
|
58
|
-
|
|
58
|
+
let r = await t.json(), i = e(r, t.status, n);
|
|
59
|
+
if (i) return i;
|
|
60
|
+
let a = (r.errors ?? []).map((e) => e.message).filter((e) => typeof e == "string" && e.length > 0);
|
|
61
|
+
return a.length > 0 ? Error(a.join("; ")) : null;
|
|
59
62
|
} catch {
|
|
60
63
|
return null;
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
function g(e, t) {
|
|
67
|
+
return typeof e != "object" || !e || Array.isArray(e) || "errors" in e && (!Array.isArray(e.errors) || e.errors.length !== 0) || !("data" in e) || typeof e.data != "object" || e.data === null || Array.isArray(e.data) ? !1 : Object.keys(e.data).length === 1 && Object.hasOwn(e.data, t);
|
|
68
|
+
}
|
|
69
|
+
async function _(t, n, r, l, u, d) {
|
|
70
|
+
let f = /* @__PURE__ */ Error("GraphQL request failed");
|
|
71
|
+
for (let p = 0; p <= l; p += 1) {
|
|
72
|
+
let m = p < l;
|
|
73
|
+
p > 0 && await c(Math.min(250 * 2 ** (p - 1), 2e3) + Math.floor(Math.random() * 150)), u();
|
|
74
|
+
let _ = new AbortController(), v = setTimeout(() => _.abort(), i);
|
|
69
75
|
try {
|
|
70
|
-
let
|
|
76
|
+
let i = await fetch(t, {
|
|
71
77
|
method: "POST",
|
|
72
|
-
headers:
|
|
73
|
-
body:
|
|
74
|
-
signal:
|
|
78
|
+
headers: n,
|
|
79
|
+
body: r,
|
|
80
|
+
signal: _.signal
|
|
75
81
|
});
|
|
76
|
-
if (!
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
throw u;
|
|
82
|
+
if (!i.ok) {
|
|
83
|
+
if (f = (o(i.status) ? null : await h(i, d)) ?? /* @__PURE__ */ Error(`GraphQL request failed with status ${i.status}`), m && o(i.status)) continue;
|
|
84
|
+
throw f;
|
|
80
85
|
}
|
|
81
|
-
let
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
let s = await i.json(), c = e(s, i.status, d);
|
|
87
|
+
if (c) throw c;
|
|
88
|
+
if (d && !g(s, d)) throw Error("The order write response could not be confirmed.");
|
|
89
|
+
if (s.errors?.length) {
|
|
90
|
+
let e = s.errors.map((e) => e.message ?? "Unknown GraphQL error").join("; ");
|
|
91
|
+
if (f = Error(e), m && s.errors.some((e) => a(e.message))) continue;
|
|
92
|
+
throw f;
|
|
86
93
|
}
|
|
87
|
-
if (!
|
|
88
|
-
if (
|
|
89
|
-
throw
|
|
94
|
+
if (!s.data) {
|
|
95
|
+
if (f = /* @__PURE__ */ Error("GraphQL response did not include data"), m) continue;
|
|
96
|
+
throw f;
|
|
90
97
|
}
|
|
91
|
-
return
|
|
98
|
+
return s.data;
|
|
92
99
|
} catch (e) {
|
|
93
|
-
let t =
|
|
94
|
-
if (
|
|
95
|
-
throw
|
|
100
|
+
let t = s(e), n = t ? `GraphQL request timed out after ${i}ms` : e instanceof Error ? e.message : String(e);
|
|
101
|
+
if (f = !t && e instanceof Error ? e : Error(n), m && (t || a(n) || e instanceof TypeError)) continue;
|
|
102
|
+
throw f;
|
|
96
103
|
} finally {
|
|
97
|
-
clearTimeout(
|
|
104
|
+
clearTimeout(v);
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
|
-
throw
|
|
107
|
+
throw f;
|
|
101
108
|
}
|
|
102
109
|
//#endregion
|
|
103
|
-
export {
|
|
110
|
+
export { m as executeGraphQL, n as resolveGraphqlUrl };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/movethewheels/utils/orderVehicleHold.ts
|
|
2
|
+
var e = class extends Error {
|
|
3
|
+
operation;
|
|
4
|
+
kind;
|
|
5
|
+
constructor(e, t = "vehicle") {
|
|
6
|
+
super("The recorded eligibility prevents this assignment or movement."), this.operation = e, this.kind = t, this.name = "VehicleHoldRefusal";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function t(e) {
|
|
10
|
+
return typeof e == "object" && !!e && !Array.isArray(e);
|
|
11
|
+
}
|
|
12
|
+
function n(n, r, i) {
|
|
13
|
+
if (!i || r !== 200 && r !== 400 || !t(n) || !Array.isArray(n.errors) || n.errors.length !== 1) return null;
|
|
14
|
+
let a = n.errors[0];
|
|
15
|
+
if (!t(a) || typeof a.message != "string" || !a.message || !Array.isArray(a.path) || a.path.length !== 1 || a.path[0] !== i || !t(a.extensions) || a.extensions.code !== "BAD_USER_INPUT") return null;
|
|
16
|
+
let o = a.extensions.i18nKey, s = o === "route.errors.vehicleNotAssignable" ? "vehicle" : o === "route.errors.driverInactive" || o === "route.errors.driverLicenseUnknown" || o === "route.errors.driverLicenseExpired" ? "driver" : null;
|
|
17
|
+
return !s || n.data !== void 0 && n.data !== null && (!t(n.data) || Object.keys(n.data).length !== 1 || n.data[i] !== null) ? null : new e(i, s);
|
|
18
|
+
}
|
|
19
|
+
var r = new Set([
|
|
20
|
+
"PENDING",
|
|
21
|
+
"CONFIRMED",
|
|
22
|
+
"ASSIGNED",
|
|
23
|
+
"PICKED_UP",
|
|
24
|
+
"IN_TRANSIT",
|
|
25
|
+
"OUT_FOR_DELIVERY",
|
|
26
|
+
"DELIVERED",
|
|
27
|
+
"RETURNED",
|
|
28
|
+
"CANCELLED",
|
|
29
|
+
"FAILED"
|
|
30
|
+
]);
|
|
31
|
+
function i(e, n) {
|
|
32
|
+
if (!t(e) || e.id !== n.id || typeof e.status != "string" || !r.has(e.status) || n.vehicleId !== void 0 && e.vehicleId !== n.vehicleId || n.driverId !== void 0 && e.driverId !== n.driverId || n.status !== void 0 && e.status !== n.status) throw Error("The order write could not be confirmed.");
|
|
33
|
+
}
|
|
34
|
+
function a(e) {
|
|
35
|
+
return e === "available" || e === "assigned";
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { e as VehicleHoldRefusal, i as assertOrderAcknowledgement, a as isVehicleAssignable, n as vehicleHoldRefusal };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-movethewheels",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1017.1",
|
|
4
4
|
"description": "Burdenoff MoveTheWheels Microfrontend - Logistics & Supply Chain Management Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"main": "./dist/index.js",
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"scripts": {
|
|
29
|
+
"verify:order-vehicle-hold": "bun test scripts/check-order-vehicle-hold.test.ts && bun scripts/verify-order-dispatch.mjs",
|
|
29
30
|
"verify:workspace-predispatch": "bun test scripts/verify-workspace-predispatch.test.ts",
|
|
30
31
|
"verify:multimodal-single-attempt": "bun scripts/verify-multimodal-single-attempt.mjs",
|
|
31
32
|
"verify:live-logistics-contract": "bun test scripts/check-logistics-live-contract.test.mjs scripts/fetch-logistics-live-schema.test.mjs",
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
37
38
|
"type:check": "bash scripts/build-limits.sh tsc --noEmit",
|
|
38
39
|
"validate:schema-drift": "graphql-schema-drift",
|
|
39
|
-
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run validate:schema-drift && bun run lint:sanity && bun run format && bun run type:check && bun run verify:live-logistics-contract && bun run verify:empty-reuse-retry && bun run verify:empty-reuse-recovery && bun run verify:trip-utilization-input && bun run verify:trip-draft-context && bun run verify:stock-binding-input && bun run verify:stock-observation-input && bun run verify:pending-pick-input && bun run verify:pick-review-ack && bun run verify:driver-license-expiry && bun run verify:freight-input && bun run verify:freight-recovery && bun run verify:freight-commercial && bun run verify:freight-invoice && bun run verify:procedure-input && bun run verify:consolidation && bun run verify:multimodal-single-attempt && bun run verify:workspace-predispatch && bun run verify:tracking-quality-outcomes && bun run verify:shipment-status-authority && bun run verify:free-time-permissions && bun run verify:core-codegen && bun run build && bun run build-storybook'",
|
|
40
|
+
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run validate:schema-drift && bun run lint:sanity && bun run format && bun run type:check && bun run verify:live-logistics-contract && bun run verify:empty-reuse-retry && bun run verify:empty-reuse-recovery && bun run verify:trip-utilization-input && bun run verify:trip-draft-context && bun run verify:stock-binding-input && bun run verify:stock-observation-input && bun run verify:pending-pick-input && bun run verify:pick-review-ack && bun run verify:driver-license-expiry && bun run verify:freight-input && bun run verify:freight-recovery && bun run verify:freight-commercial && bun run verify:freight-invoice && bun run verify:procedure-input && bun run verify:consolidation && bun run verify:multimodal-single-attempt && bun run verify:workspace-predispatch && bun run verify:tracking-quality-outcomes && bun run verify:shipment-status-authority && bun run verify:free-time-permissions && bun run verify:core-codegen && bun run verify:order-vehicle-hold && bun run build && bun run build-storybook'",
|
|
40
41
|
"storybook": "storybook dev -p 6006",
|
|
41
42
|
"build-storybook": "storybook build",
|
|
42
43
|
"test:storybook": "vitest run --project=storybook",
|
|
@@ -1161,6 +1161,15 @@ export function useVehicle(vehicleId: string | undefined) {
|
|
|
1161
1161
|
id: vehicleId,
|
|
1162
1162
|
}
|
|
1163
1163
|
);
|
|
1164
|
+
if (
|
|
1165
|
+
data.vehicle &&
|
|
1166
|
+
(data.vehicle.id !== vehicleId ||
|
|
1167
|
+
!['ACTIVE', 'IN_TRANSIT', 'MAINTENANCE', 'INACTIVE', 'DECOMMISSIONED'].includes(
|
|
1168
|
+
data.vehicle.status
|
|
1169
|
+
))
|
|
1170
|
+
) {
|
|
1171
|
+
throw new Error('Vehicle status could not be verified.');
|
|
1172
|
+
}
|
|
1164
1173
|
setVehicle(data.vehicle ? mapVehicle(data.vehicle) : null);
|
|
1165
1174
|
} catch (fetchError) {
|
|
1166
1175
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load vehicle'));
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { Order, OrderStatus } from '../types';
|
|
3
|
+
import type { OrderWriteInput } from './useOrders';
|
|
4
|
+
import { VehicleHoldRefusal } from '../utils/orderVehicleHold';
|
|
5
|
+
import { useTr } from '../shared/hooks/useTr';
|
|
6
|
+
|
|
7
|
+
interface DispatchOptions {
|
|
8
|
+
order: Order | null;
|
|
9
|
+
contextKey: string;
|
|
10
|
+
ready: boolean;
|
|
11
|
+
assignmentReady: boolean;
|
|
12
|
+
movementReady: boolean;
|
|
13
|
+
updateOrder: (
|
|
14
|
+
id: string,
|
|
15
|
+
input: OrderWriteInput,
|
|
16
|
+
attemptPolicy: 'SINGLE_ATTEMPT'
|
|
17
|
+
) => Promise<Order>;
|
|
18
|
+
updateOrderStatus: (id: string, status: OrderStatus) => Promise<Order>;
|
|
19
|
+
refresh: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One order/context only. Storage retains uncertainty, never a replayable request or token. */
|
|
23
|
+
export function useOrderDispatch(options: DispatchOptions) {
|
|
24
|
+
const tr = useTr();
|
|
25
|
+
const { order, contextKey, updateOrder, updateOrderStatus, refresh } = options;
|
|
26
|
+
const storageKey = `movethewheels.order-dispatch.unconfirmed.v1:${contextKey}`;
|
|
27
|
+
const [blocked, setBlocked] = useState(() => {
|
|
28
|
+
try {
|
|
29
|
+
return sessionStorage.getItem(storageKey) !== null;
|
|
30
|
+
} catch {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const [busy, setBusy] = useState(false);
|
|
35
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
36
|
+
const [knownAssignment, setKnownAssignment] = useState<{
|
|
37
|
+
orderId: string;
|
|
38
|
+
vehicleId: string;
|
|
39
|
+
driverId: string;
|
|
40
|
+
} | null>(null);
|
|
41
|
+
const running = useRef(false);
|
|
42
|
+
const active = useRef(true);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
active.current = true;
|
|
45
|
+
return () => {
|
|
46
|
+
active.current = false;
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
const uncertain = (assignmentSaved: boolean) =>
|
|
50
|
+
tr(
|
|
51
|
+
assignmentSaved
|
|
52
|
+
? 'movethewheels.order.movementUnknown'
|
|
53
|
+
: 'movethewheels.order.assignmentUnknown',
|
|
54
|
+
assignmentSaved
|
|
55
|
+
? 'Assignment saved. Movement could not be confirmed. Further changes are blocked.'
|
|
56
|
+
: 'Assignment could not be confirmed. Further changes are blocked. Check the order before taking another action.'
|
|
57
|
+
);
|
|
58
|
+
const retain = () => {
|
|
59
|
+
// Recheck storage synchronously so a second mounted view cannot bypass a pending write.
|
|
60
|
+
if (sessionStorage.getItem(storageKey) !== null) throw new Error('Unconfirmed order write');
|
|
61
|
+
sessionStorage.setItem(storageKey, 'unconfirmed');
|
|
62
|
+
};
|
|
63
|
+
const confirm = () => sessionStorage.removeItem(storageKey);
|
|
64
|
+
const begin = (allowed: boolean) => {
|
|
65
|
+
if (running.current || blocked || !active.current || !options.ready || !allowed || !order)
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
retain();
|
|
69
|
+
} catch {
|
|
70
|
+
setBlocked(true);
|
|
71
|
+
setMessage(
|
|
72
|
+
tr(
|
|
73
|
+
'movethewheels.order.retentionUnavailable',
|
|
74
|
+
'Further changes are blocked because this browser cannot safely retain the order write.'
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
running.current = true;
|
|
80
|
+
setBusy(true);
|
|
81
|
+
setMessage(null);
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
84
|
+
const finish = () => {
|
|
85
|
+
running.current = false;
|
|
86
|
+
if (active.current) setBusy(false);
|
|
87
|
+
};
|
|
88
|
+
const assign = async (vehicleId: string, driverId: string) => {
|
|
89
|
+
if (
|
|
90
|
+
!vehicleId ||
|
|
91
|
+
!driverId ||
|
|
92
|
+
!order ||
|
|
93
|
+
(order.vehicleId === vehicleId && order.driverId === driverId) ||
|
|
94
|
+
!begin(options.assignmentReady)
|
|
95
|
+
)
|
|
96
|
+
return;
|
|
97
|
+
let assignmentSaved = false;
|
|
98
|
+
try {
|
|
99
|
+
await updateOrder(order.id, { vehicleId, driverId }, 'SINGLE_ATTEMPT');
|
|
100
|
+
assignmentSaved = true;
|
|
101
|
+
if (active.current) setKnownAssignment({ orderId: order.id, vehicleId, driverId });
|
|
102
|
+
confirm();
|
|
103
|
+
if (!active.current) return;
|
|
104
|
+
const advance = !(order.vehicleId && order.driverId) && order.status === 'confirmed';
|
|
105
|
+
if (advance) {
|
|
106
|
+
retain();
|
|
107
|
+
await updateOrderStatus(order.id, 'assigned');
|
|
108
|
+
confirm();
|
|
109
|
+
}
|
|
110
|
+
if (!active.current) return;
|
|
111
|
+
setMessage(tr('movethewheels.order.assignmentSaved', 'Assignment saved.'));
|
|
112
|
+
refresh();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// The opt-in transport classifier is the only known pre-write refusal.
|
|
115
|
+
if (
|
|
116
|
+
error instanceof VehicleHoldRefusal &&
|
|
117
|
+
error.operation === (assignmentSaved ? 'updateDeliveryJobStatus' : 'updateDeliveryJob')
|
|
118
|
+
) {
|
|
119
|
+
try {
|
|
120
|
+
confirm();
|
|
121
|
+
} catch {
|
|
122
|
+
if (active.current) setBlocked(true);
|
|
123
|
+
}
|
|
124
|
+
if (!active.current) return;
|
|
125
|
+
setMessage(
|
|
126
|
+
tr(
|
|
127
|
+
error.kind === 'driver'
|
|
128
|
+
? assignmentSaved
|
|
129
|
+
? 'movethewheels.order.driverMovementBlocked'
|
|
130
|
+
: 'movethewheels.order.driverAssignmentBlocked'
|
|
131
|
+
: assignmentSaved
|
|
132
|
+
? 'movethewheels.order.movementBlocked'
|
|
133
|
+
: 'movethewheels.order.assignmentBlocked',
|
|
134
|
+
error.kind === 'driver'
|
|
135
|
+
? assignmentSaved
|
|
136
|
+
? 'Assignment saved. Driver eligibility blocked movement.'
|
|
137
|
+
: 'Assignment blocked by driver eligibility. Choose another driver.'
|
|
138
|
+
: assignmentSaved
|
|
139
|
+
? 'Assignment saved. Movement blocked.'
|
|
140
|
+
: 'Assignment blocked by the recorded vehicle status. Choose another vehicle.'
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
refresh();
|
|
144
|
+
} else if (active.current) {
|
|
145
|
+
setBlocked(true);
|
|
146
|
+
setMessage(uncertain(assignmentSaved));
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
finish();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const changeStatus = async (status: OrderStatus) => {
|
|
153
|
+
if (!order || ['delivered', 'returned', 'cancelled', 'failed'].includes(order.status)) return;
|
|
154
|
+
if (status === 'assigned' && (!order.vehicleId || !order.driverId)) return;
|
|
155
|
+
const movement = ['assigned', 'picked_up', 'in_transit', 'out_for_delivery'].includes(status);
|
|
156
|
+
if (!begin(!movement || options.movementReady) || !order) return;
|
|
157
|
+
try {
|
|
158
|
+
await updateOrderStatus(order.id, status);
|
|
159
|
+
confirm();
|
|
160
|
+
if (!active.current) return;
|
|
161
|
+
setMessage(tr('movethewheels.order.statusSaved', 'Order status updated.'));
|
|
162
|
+
refresh();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error instanceof VehicleHoldRefusal && error.operation === 'updateDeliveryJobStatus') {
|
|
165
|
+
try {
|
|
166
|
+
confirm();
|
|
167
|
+
} catch {
|
|
168
|
+
if (active.current) setBlocked(true);
|
|
169
|
+
}
|
|
170
|
+
if (!active.current) return;
|
|
171
|
+
setMessage(
|
|
172
|
+
tr(
|
|
173
|
+
error.kind === 'driver'
|
|
174
|
+
? 'movethewheels.order.driverStatusBlocked'
|
|
175
|
+
: 'movethewheels.order.statusBlocked',
|
|
176
|
+
error.kind === 'driver'
|
|
177
|
+
? 'Movement blocked by driver eligibility. Choose another driver.'
|
|
178
|
+
: 'Movement blocked by the recorded vehicle status. Choose another vehicle.'
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
refresh();
|
|
182
|
+
} else if (active.current) {
|
|
183
|
+
setBlocked(true);
|
|
184
|
+
setMessage(
|
|
185
|
+
tr(
|
|
186
|
+
'movethewheels.order.statusUnknown',
|
|
187
|
+
'Order status could not be confirmed. Further changes are blocked.'
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
finish();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
return { assign, changeStatus, busy, blocked, message, knownAssignment };
|
|
196
|
+
}
|
|
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
2
2
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
3
3
|
import type { Order, OrderFilter, OrderStatus } from '../types';
|
|
4
4
|
import { mapRoute } from './useRoutes';
|
|
5
|
-
import { executeGraphQL } from '../utils/graphqlClient';
|
|
5
|
+
import { executeGraphQL, type GraphQLAttemptPolicy } from '../utils/graphqlClient';
|
|
6
|
+
import { assertOrderAcknowledgement } from '../utils/orderVehicleHold';
|
|
6
7
|
|
|
7
8
|
interface UseOrdersOptions {
|
|
8
9
|
filters?: OrderFilter;
|
|
@@ -30,7 +31,11 @@ interface UseOrdersResult {
|
|
|
30
31
|
error: Error | null;
|
|
31
32
|
refetch: () => void;
|
|
32
33
|
createOrder: (order: OrderWriteInput) => Promise<Order>;
|
|
33
|
-
updateOrder: (
|
|
34
|
+
updateOrder: (
|
|
35
|
+
id: string,
|
|
36
|
+
updates: OrderWriteInput,
|
|
37
|
+
attemptPolicy?: GraphQLAttemptPolicy
|
|
38
|
+
) => Promise<Order>;
|
|
34
39
|
deleteOrder: (id: string) => Promise<void>;
|
|
35
40
|
updateOrderStatus: (id: string, status: OrderStatus) => Promise<Order>;
|
|
36
41
|
}
|
|
@@ -462,30 +467,47 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
|
462
467
|
);
|
|
463
468
|
|
|
464
469
|
const updateOrder = useCallback(
|
|
465
|
-
async (
|
|
470
|
+
async (
|
|
471
|
+
id: string,
|
|
472
|
+
updates: OrderWriteInput,
|
|
473
|
+
attemptPolicy: GraphQLAttemptPolicy = 'RETRY_TRANSIENT'
|
|
474
|
+
): Promise<Order> => {
|
|
466
475
|
const data = await executeGraphQL<{
|
|
467
476
|
updateDeliveryJob: ApiOrder;
|
|
468
|
-
}>(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
477
|
+
}>(
|
|
478
|
+
graphQLOptions,
|
|
479
|
+
UPDATE_ORDER_MUTATION,
|
|
480
|
+
{
|
|
481
|
+
id,
|
|
482
|
+
input: {
|
|
483
|
+
status: updates.status ? toApiStatus(updates.status) : undefined,
|
|
484
|
+
priority: updates.priority ? toApiPriority(updates.priority) : undefined,
|
|
485
|
+
customerId: updates.customerId,
|
|
486
|
+
pickupAddress: updates.pickupAddress,
|
|
487
|
+
deliveryAddress: updates.deliveryAddress,
|
|
488
|
+
cargoItems: updates.items,
|
|
489
|
+
pricing: updates.pricing,
|
|
490
|
+
specialInstructions: updates.specialInstructions,
|
|
491
|
+
trackingNumber: updates.trackingNumber,
|
|
492
|
+
// `null` clears the booked slot; `undefined` leaves it untouched.
|
|
493
|
+
scheduledDelivery:
|
|
494
|
+
updates.scheduledDelivery === null ? null : updates.scheduledDelivery?.toISOString(),
|
|
495
|
+
vehicleId: updates.vehicleId,
|
|
496
|
+
driverId: updates.driverId,
|
|
497
|
+
routeId: updates.routeId,
|
|
498
|
+
},
|
|
486
499
|
},
|
|
487
|
-
|
|
500
|
+
attemptPolicy,
|
|
501
|
+
attemptPolicy === 'SINGLE_ATTEMPT' ? 'updateDeliveryJob' : undefined
|
|
502
|
+
);
|
|
488
503
|
|
|
504
|
+
if (attemptPolicy === 'SINGLE_ATTEMPT')
|
|
505
|
+
assertOrderAcknowledgement(data.updateDeliveryJob, {
|
|
506
|
+
id,
|
|
507
|
+
vehicleId: updates.vehicleId,
|
|
508
|
+
driverId: updates.driverId,
|
|
509
|
+
status: updates.status ? toApiStatus(updates.status) : undefined,
|
|
510
|
+
});
|
|
489
511
|
const updated = mapOrder(data.updateDeliveryJob);
|
|
490
512
|
setOrders((previous) => previous.map((item) => (item.id === id ? updated : item)));
|
|
491
513
|
return updated;
|
|
@@ -507,13 +529,20 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
|
507
529
|
async (id: string, status: OrderStatus): Promise<Order> => {
|
|
508
530
|
const data = await executeGraphQL<{
|
|
509
531
|
updateDeliveryJobStatus: ApiOrder;
|
|
510
|
-
}>(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
532
|
+
}>(
|
|
533
|
+
graphQLOptions,
|
|
534
|
+
UPDATE_ORDER_STATUS_MUTATION,
|
|
535
|
+
{
|
|
536
|
+
id,
|
|
537
|
+
input: {
|
|
538
|
+
status: toApiStatus(status),
|
|
539
|
+
},
|
|
514
540
|
},
|
|
515
|
-
|
|
541
|
+
'SINGLE_ATTEMPT',
|
|
542
|
+
'updateDeliveryJobStatus'
|
|
543
|
+
);
|
|
516
544
|
|
|
545
|
+
assertOrderAcknowledgement(data.updateDeliveryJobStatus, { id, status: toApiStatus(status) });
|
|
517
546
|
const updated = mapOrder(data.updateDeliveryJobStatus);
|
|
518
547
|
setOrders((previous) => previous.map((item) => (item.id === id ? updated : item)));
|
|
519
548
|
return updated;
|
|
@@ -567,6 +596,9 @@ export function useOrder(orderId: string | undefined) {
|
|
|
567
596
|
deliveryJob: ApiOrder | null;
|
|
568
597
|
}>(graphQLOptions, GET_ORDER_QUERY, { id: orderId });
|
|
569
598
|
|
|
599
|
+
if (data.deliveryJob && data.deliveryJob.id !== orderId) {
|
|
600
|
+
throw new Error('The requested order could not be verified.');
|
|
601
|
+
}
|
|
570
602
|
setOrder(data.deliveryJob ? mapOrder(data.deliveryJob) : null);
|
|
571
603
|
} catch (fetchError) {
|
|
572
604
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load order'));
|