@burdenoff/microfe-movethewheels 2026.920.1 → 2026.920.2
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/components/ShippingQuoteResults.js +159 -106
- package/dist/hooks/useShippingCarriers.js +1 -1
- package/dist/movethewheels/hooks/useShippingCarriers.d.ts +17 -0
- package/dist/pages/ShippingRatesPage.js +84 -16
- package/package.json +1 -1
- package/src/movethewheels/components/ShippingQuoteResults.tsx +62 -2
- package/src/movethewheels/components/maps/leaflet-setup.ts +8 -1
- package/src/movethewheels/hooks/useMultiModalShipments.ts +6 -1
- package/src/movethewheels/hooks/useShippingCarriers.ts +28 -0
- package/src/movethewheels/pages/ShippingRatesPage.tsx +85 -0
|
@@ -1,142 +1,195 @@
|
|
|
1
1
|
import { TriangleAlert as e } from "../node_modules/lucide-react/dist/esm/icons/triangle-alert.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { Truck as t } from "../node_modules/lucide-react/dist/esm/icons/truck.js";
|
|
3
|
+
import { Card as n, CardContent as r, CardHeader as i, CardTitle as a, Table as o, TableBody as s, TableCell as c, TableHead as l, TableHeader as u, TableRow as d } from "./ui.js";
|
|
4
|
+
import { StatusBadge as f } from "./StatusBadge.js";
|
|
5
|
+
import { formatCurrency as p } from "../utils/formatters.js";
|
|
6
|
+
import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
6
7
|
//#region src/movethewheels/components/ShippingQuoteResults.tsx
|
|
7
|
-
var
|
|
8
|
+
var _ = {
|
|
8
9
|
bg: "bg-status-success-bg-subtle",
|
|
9
10
|
text: "text-status-success-text"
|
|
10
|
-
},
|
|
11
|
+
}, v = {
|
|
11
12
|
bg: "bg-status-info-bg-subtle",
|
|
12
13
|
text: "text-status-info-text"
|
|
13
14
|
};
|
|
14
|
-
function
|
|
15
|
+
function y(e, t) {
|
|
15
16
|
if (e.estimatedDeliveryDays === null) return e.estimatedDeliveryDate ?? "—";
|
|
16
17
|
let n = e.estimatedDeliveryDays === 1 ? t("movethewheels.shipping.results.day", "day") : t("movethewheels.shipping.results.days", "days"), r = `${e.estimatedDeliveryDays} ${n}`;
|
|
17
18
|
return e.estimatedDeliveryDate ? `${r} · ${e.estimatedDeliveryDate}` : r;
|
|
18
19
|
}
|
|
19
|
-
function
|
|
20
|
+
function b(e, t) {
|
|
20
21
|
return e.mode === "AIR" ? t("movethewheels.shipping.results.air", "Air") : e.mode === "SURFACE" ? t("movethewheels.shipping.results.surface", "Surface") : "—";
|
|
21
22
|
}
|
|
22
|
-
function
|
|
23
|
-
return /* @__PURE__ */
|
|
24
|
-
e.isCheapest && /* @__PURE__ */
|
|
23
|
+
function x({ quote: e, t }) {
|
|
24
|
+
return /* @__PURE__ */ g(m, { children: [
|
|
25
|
+
e.isCheapest && /* @__PURE__ */ h(f, {
|
|
25
26
|
status: t("movethewheels.shipping.results.cheapest", "Cheapest"),
|
|
26
|
-
customColors:
|
|
27
|
+
customColors: _,
|
|
27
28
|
size: "sm",
|
|
28
29
|
showDot: !1
|
|
29
30
|
}),
|
|
30
|
-
e.isFastest && /* @__PURE__ */
|
|
31
|
+
e.isFastest && /* @__PURE__ */ h(f, {
|
|
31
32
|
status: t("movethewheels.shipping.results.fastest", "Fastest"),
|
|
32
|
-
customColors:
|
|
33
|
+
customColors: v,
|
|
33
34
|
size: "sm",
|
|
34
35
|
showDot: !1
|
|
35
36
|
}),
|
|
36
|
-
e.isRecommended && /* @__PURE__ */
|
|
37
|
+
e.isRecommended && /* @__PURE__ */ h(f, {
|
|
37
38
|
status: t("movethewheels.shipping.results.recommended", "Recommended"),
|
|
38
|
-
customColors:
|
|
39
|
+
customColors: v,
|
|
39
40
|
size: "sm",
|
|
40
41
|
showDot: !1
|
|
41
42
|
})
|
|
42
43
|
] });
|
|
43
44
|
}
|
|
44
|
-
function
|
|
45
|
-
let
|
|
46
|
-
return /* @__PURE__ */ h(
|
|
47
|
-
className: "
|
|
48
|
-
children: [
|
|
49
|
-
|
|
50
|
-
className: "flex items-start gap-2 rounded-lg border border-status-warning-border bg-status-warning-bg-subtle px-4 py-3 text-sm text-status-warning-text",
|
|
51
|
-
children: [/* @__PURE__ */ m(e, {
|
|
45
|
+
function S({ estimate: e, t: o }) {
|
|
46
|
+
let s = e.savingsVsCheapestCourier;
|
|
47
|
+
return /* @__PURE__ */ g(n, { children: [/* @__PURE__ */ h(i, { children: /* @__PURE__ */ g(a, {
|
|
48
|
+
className: "flex items-center gap-2",
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ h(t, {
|
|
52
51
|
size: 18,
|
|
53
|
-
className: "mt-0.5 shrink-0",
|
|
54
52
|
"aria-hidden": "true"
|
|
55
|
-
}),
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
className: "
|
|
73
|
-
children:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
53
|
+
}),
|
|
54
|
+
o("movethewheels.shipping.ownFleet.title", "Your own vehicle"),
|
|
55
|
+
/* @__PURE__ */ h(f, {
|
|
56
|
+
status: o("movethewheels.shipping.ownFleet.estimate", "Your estimate"),
|
|
57
|
+
customColors: v,
|
|
58
|
+
size: "sm",
|
|
59
|
+
showDot: !1
|
|
60
|
+
})
|
|
61
|
+
]
|
|
62
|
+
}) }), /* @__PURE__ */ g(r, {
|
|
63
|
+
className: "space-y-2",
|
|
64
|
+
children: [
|
|
65
|
+
/* @__PURE__ */ h("p", {
|
|
66
|
+
className: "text-2xl font-semibold",
|
|
67
|
+
children: p(e.cost, e.currency)
|
|
68
|
+
}),
|
|
69
|
+
/* @__PURE__ */ g("p", {
|
|
70
|
+
className: "text-sm text-text-secondary",
|
|
71
|
+
children: [
|
|
72
|
+
e.distanceKm,
|
|
73
|
+
" km × ",
|
|
74
|
+
p(e.costPerKm, e.currency),
|
|
75
|
+
"/km",
|
|
76
|
+
e.fixedCost > 0 && ` + ${p(e.fixedCost, e.currency)}`
|
|
77
|
+
]
|
|
78
|
+
}),
|
|
79
|
+
s !== null && /* @__PURE__ */ h("p", {
|
|
80
|
+
className: `text-sm font-medium ${e.isCheaperThanCouriers ? "text-status-success-text" : "text-status-warning-text"}`,
|
|
81
|
+
children: e.isCheaperThanCouriers ? `${o("movethewheels.shipping.ownFleet.cheaper", "Your own vehicle is cheaper than the cheapest courier by")} ${p(s, e.currency)}` : s === 0 ? o("movethewheels.shipping.ownFleet.same", "Same cost as the cheapest courier") : `${o("movethewheels.shipping.ownFleet.dearer", "The cheapest courier is cheaper by")} ${p(-s, e.currency)}`
|
|
82
|
+
}),
|
|
83
|
+
/* @__PURE__ */ h("p", {
|
|
84
|
+
className: "text-xs text-text-secondary",
|
|
85
|
+
children: o("movethewheels.shipping.ownFleet.note", "Based only on the distance and cost per km you entered — not a quote.")
|
|
86
|
+
})
|
|
87
|
+
]
|
|
88
|
+
})] });
|
|
89
|
+
}
|
|
90
|
+
function C({ result: t, t: f }) {
|
|
91
|
+
let _ = t.providers.filter((e) => !e.ok);
|
|
92
|
+
return /* @__PURE__ */ g("div", {
|
|
93
|
+
className: "space-y-4",
|
|
94
|
+
children: [
|
|
95
|
+
_.map((t) => /* @__PURE__ */ g("div", {
|
|
96
|
+
role: "alert",
|
|
97
|
+
className: "flex items-start gap-2 rounded-lg border border-status-warning-border bg-status-warning-bg-subtle px-4 py-3 text-sm text-status-warning-text",
|
|
98
|
+
children: [/* @__PURE__ */ h(e, {
|
|
99
|
+
size: 18,
|
|
100
|
+
className: "mt-0.5 shrink-0",
|
|
101
|
+
"aria-hidden": "true"
|
|
102
|
+
}), /* @__PURE__ */ h("span", { children: t.errorMessage })]
|
|
103
|
+
}, t.provider)),
|
|
104
|
+
t.ownFleet && /* @__PURE__ */ h(S, {
|
|
105
|
+
estimate: t.ownFleet,
|
|
106
|
+
t: f
|
|
107
|
+
}),
|
|
108
|
+
/* @__PURE__ */ g(n, { children: [/* @__PURE__ */ g(i, { children: [/* @__PURE__ */ g(a, { children: [
|
|
109
|
+
f("movethewheels.shipping.results.title", "Courier options"),
|
|
110
|
+
" (",
|
|
111
|
+
t.quotes.length,
|
|
112
|
+
")"
|
|
113
|
+
] }), /* @__PURE__ */ g("p", {
|
|
114
|
+
className: "text-sm text-text-secondary",
|
|
115
|
+
children: [
|
|
116
|
+
f("movethewheels.shipping.results.chargeableWeight", "Charged on"),
|
|
117
|
+
" ",
|
|
118
|
+
t.chargeableWeightKg,
|
|
119
|
+
" kg"
|
|
120
|
+
]
|
|
121
|
+
})] }), /* @__PURE__ */ h(r, {
|
|
122
|
+
className: "p-0",
|
|
123
|
+
children: t.quotes.length === 0 ? /* @__PURE__ */ h("p", {
|
|
124
|
+
className: "px-6 pb-6 text-sm text-text-secondary",
|
|
125
|
+
children: _.length > 0 ? f("movethewheels.shipping.results.allFailed", "No rates could be fetched — see the message above.") : f("movethewheels.shipping.results.noServiceable", "No courier serves this pincode pair.")
|
|
126
|
+
}) : /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h("ul", {
|
|
127
|
+
className: "divide-y md:hidden",
|
|
128
|
+
"aria-label": f("movethewheels.shipping.results.title", "Courier options"),
|
|
129
|
+
children: t.quotes.map((e) => /* @__PURE__ */ g("li", {
|
|
130
|
+
className: "space-y-2 px-4 py-4",
|
|
131
|
+
children: [
|
|
132
|
+
/* @__PURE__ */ g("div", {
|
|
133
|
+
className: "flex items-start justify-between gap-3",
|
|
134
|
+
children: [/* @__PURE__ */ h("span", {
|
|
135
|
+
className: "min-w-0 font-medium",
|
|
136
|
+
children: e.courierName
|
|
137
|
+
}), /* @__PURE__ */ h("span", {
|
|
138
|
+
className: "shrink-0 text-lg font-semibold",
|
|
139
|
+
children: p(e.price, e.currency)
|
|
140
|
+
})]
|
|
141
|
+
}),
|
|
142
|
+
/* @__PURE__ */ h("div", {
|
|
143
|
+
className: "flex flex-wrap items-center gap-2",
|
|
144
|
+
children: /* @__PURE__ */ h(x, {
|
|
145
|
+
quote: e,
|
|
146
|
+
t: f
|
|
147
|
+
})
|
|
148
|
+
}),
|
|
149
|
+
/* @__PURE__ */ h("p", {
|
|
150
|
+
className: "text-sm text-text-secondary",
|
|
151
|
+
children: [
|
|
152
|
+
b(e, f),
|
|
153
|
+
y(e, f),
|
|
154
|
+
e.rating === null ? null : `${e.rating.toFixed(1)} ★`
|
|
155
|
+
].filter((e) => e !== "—" && e !== null).join(" · ")
|
|
156
|
+
})
|
|
157
|
+
]
|
|
158
|
+
}, `${e.provider}:${e.courierId}`))
|
|
159
|
+
}), /* @__PURE__ */ h("div", {
|
|
160
|
+
className: "hidden md:block",
|
|
161
|
+
children: /* @__PURE__ */ g(o, { children: [/* @__PURE__ */ h(u, { children: /* @__PURE__ */ g(d, { children: [
|
|
162
|
+
/* @__PURE__ */ h(l, { children: f("movethewheels.shipping.results.courier", "Courier") }),
|
|
163
|
+
/* @__PURE__ */ h(l, { children: f("movethewheels.shipping.results.mode", "Mode") }),
|
|
164
|
+
/* @__PURE__ */ h(l, { children: f("movethewheels.shipping.results.transit", "Estimated delivery") }),
|
|
165
|
+
/* @__PURE__ */ h(l, { children: f("movethewheels.shipping.results.rating", "Rating") }),
|
|
166
|
+
/* @__PURE__ */ h(l, {
|
|
167
|
+
className: "text-right",
|
|
168
|
+
children: f("movethewheels.shipping.results.price", "Price")
|
|
169
|
+
})
|
|
170
|
+
] }) }), /* @__PURE__ */ h(s, { children: t.quotes.map((e) => /* @__PURE__ */ g(d, { children: [
|
|
171
|
+
/* @__PURE__ */ h(c, { children: /* @__PURE__ */ g("div", {
|
|
91
172
|
className: "flex flex-wrap items-center gap-2",
|
|
92
|
-
children: /* @__PURE__ */
|
|
173
|
+
children: [/* @__PURE__ */ h("span", {
|
|
174
|
+
className: "font-medium",
|
|
175
|
+
children: e.courierName
|
|
176
|
+
}), /* @__PURE__ */ h(x, {
|
|
93
177
|
quote: e,
|
|
94
|
-
t:
|
|
95
|
-
})
|
|
96
|
-
}),
|
|
97
|
-
/* @__PURE__ */
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
].filter((e) => e !== "—" && e !== null).join(" · ")
|
|
178
|
+
t: f
|
|
179
|
+
})]
|
|
180
|
+
}) }),
|
|
181
|
+
/* @__PURE__ */ h(c, { children: b(e, f) }),
|
|
182
|
+
/* @__PURE__ */ h(c, { children: y(e, f) }),
|
|
183
|
+
/* @__PURE__ */ h(c, { children: e.rating === null ? "—" : e.rating.toFixed(1) }),
|
|
184
|
+
/* @__PURE__ */ h(c, {
|
|
185
|
+
className: "text-right font-medium",
|
|
186
|
+
children: p(e.price, e.currency)
|
|
104
187
|
})
|
|
105
|
-
]
|
|
106
|
-
}
|
|
107
|
-
}), /* @__PURE__ */ m("div", {
|
|
108
|
-
className: "hidden md:block",
|
|
109
|
-
children: /* @__PURE__ */ h(a, { children: [/* @__PURE__ */ m(l, { children: /* @__PURE__ */ h(u, { children: [
|
|
110
|
-
/* @__PURE__ */ m(c, { children: g("movethewheels.shipping.results.courier", "Courier") }),
|
|
111
|
-
/* @__PURE__ */ m(c, { children: g("movethewheels.shipping.results.mode", "Mode") }),
|
|
112
|
-
/* @__PURE__ */ m(c, { children: g("movethewheels.shipping.results.transit", "Estimated delivery") }),
|
|
113
|
-
/* @__PURE__ */ m(c, { children: g("movethewheels.shipping.results.rating", "Rating") }),
|
|
114
|
-
/* @__PURE__ */ m(c, {
|
|
115
|
-
className: "text-right",
|
|
116
|
-
children: g("movethewheels.shipping.results.price", "Price")
|
|
117
|
-
})
|
|
118
|
-
] }) }), /* @__PURE__ */ m(o, { children: d.quotes.map((e) => /* @__PURE__ */ h(u, { children: [
|
|
119
|
-
/* @__PURE__ */ m(s, { children: /* @__PURE__ */ h("div", {
|
|
120
|
-
className: "flex flex-wrap items-center gap-2",
|
|
121
|
-
children: [/* @__PURE__ */ m("span", {
|
|
122
|
-
className: "font-medium",
|
|
123
|
-
children: e.courierName
|
|
124
|
-
}), /* @__PURE__ */ m(b, {
|
|
125
|
-
quote: e,
|
|
126
|
-
t: g
|
|
127
|
-
})]
|
|
128
|
-
}) }),
|
|
129
|
-
/* @__PURE__ */ m(s, { children: y(e, g) }),
|
|
130
|
-
/* @__PURE__ */ m(s, { children: v(e, g) }),
|
|
131
|
-
/* @__PURE__ */ m(s, { children: e.rating === null ? "—" : e.rating.toFixed(1) }),
|
|
132
|
-
/* @__PURE__ */ m(s, {
|
|
133
|
-
className: "text-right font-medium",
|
|
134
|
-
children: f(e.price, e.currency)
|
|
135
|
-
})
|
|
136
|
-
] }, `${e.provider}:${e.courierId}`)) })] })
|
|
188
|
+
] }, `${e.provider}:${e.courierId}`)) })] })
|
|
189
|
+
})] })
|
|
137
190
|
})] })
|
|
138
|
-
|
|
191
|
+
]
|
|
139
192
|
});
|
|
140
193
|
}
|
|
141
194
|
//#endregion
|
|
142
|
-
export {
|
|
195
|
+
export { C as ShippingQuoteResults };
|
|
@@ -2,7 +2,7 @@ import { useMoveTheWheels as e } from "../providers/MoveTheWheelsProvider.js";
|
|
|
2
2
|
import { executeGraphQL as t } from "../utils/graphqlClient.js";
|
|
3
3
|
import { useCallback as n, useMemo as r } from "react";
|
|
4
4
|
//#region src/movethewheels/hooks/useShippingCarriers.ts
|
|
5
|
-
var i = "\n query ShippingQuotes($input: ShippingQuoteInput!) {\n shippingQuotes(input: $input) {\n chargeableWeightKg\n quotes {\n provider\n courierId\n courierName\n price\n currency\n estimatedDeliveryDays\n estimatedDeliveryDate\n rating\n mode\n isRecommended\n isCheapest\n isFastest\n }\n providers {\n provider\n ok\n quoteCount\n errorMessage\n }\n }\n }\n";
|
|
5
|
+
var i = "\n query ShippingQuotes($input: ShippingQuoteInput!) {\n shippingQuotes(input: $input) {\n chargeableWeightKg\n ownFleet {\n distanceKm\n costPerKm\n fixedCost\n cost\n currency\n savingsVsCheapestCourier\n isCheaperThanCouriers\n }\n quotes {\n provider\n courierId\n courierName\n price\n currency\n estimatedDeliveryDays\n estimatedDeliveryDate\n rating\n mode\n isRecommended\n isCheapest\n isFastest\n }\n providers {\n provider\n ok\n quoteCount\n errorMessage\n }\n }\n }\n";
|
|
6
6
|
function a() {
|
|
7
7
|
let { apiGatewayUrl: t, authToken: n, workspaceToken: i, workspaceId: a, tenantId: o, currentUser: s } = e();
|
|
8
8
|
return r(() => ({
|
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export type CarrierProvider = 'SHIPROCKET';
|
|
7
7
|
export type CarrierServiceMode = 'SURFACE' | 'AIR';
|
|
8
|
+
export interface OwnFleetInput {
|
|
9
|
+
distanceKm: number;
|
|
10
|
+
costPerKm: number;
|
|
11
|
+
fixedCost?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface OwnFleetEstimate {
|
|
14
|
+
distanceKm: number;
|
|
15
|
+
costPerKm: number;
|
|
16
|
+
fixedCost: number;
|
|
17
|
+
cost: number;
|
|
18
|
+
currency: string;
|
|
19
|
+
/** Cheapest courier price minus this estimate; positive = your own vehicle is cheaper. */
|
|
20
|
+
savingsVsCheapestCourier: number | null;
|
|
21
|
+
isCheaperThanCouriers: boolean;
|
|
22
|
+
}
|
|
8
23
|
export interface ShippingQuoteInput {
|
|
9
24
|
pickupPostcode: string;
|
|
10
25
|
deliveryPostcode: string;
|
|
@@ -13,6 +28,7 @@ export interface ShippingQuoteInput {
|
|
|
13
28
|
widthCm?: number;
|
|
14
29
|
heightCm?: number;
|
|
15
30
|
cod?: boolean;
|
|
31
|
+
ownFleet?: OwnFleetInput;
|
|
16
32
|
}
|
|
17
33
|
export interface ShippingQuote {
|
|
18
34
|
provider: CarrierProvider;
|
|
@@ -36,6 +52,7 @@ export interface CarrierOutcome {
|
|
|
36
52
|
}
|
|
37
53
|
export interface ShippingQuoteResult {
|
|
38
54
|
chargeableWeightKg: number;
|
|
55
|
+
ownFleet: OwnFleetEstimate | null;
|
|
39
56
|
quotes: ShippingQuote[];
|
|
40
57
|
providers: CarrierOutcome[];
|
|
41
58
|
}
|
|
@@ -14,15 +14,15 @@ var _ = /^[1-9]\d{5}$/, v = (e) => {
|
|
|
14
14
|
return e.trim() !== "" && Number.isFinite(t) ? t : void 0;
|
|
15
15
|
};
|
|
16
16
|
function y() {
|
|
17
|
-
let y = t(), b = p(), [x, S] = m(""), [C, w] = m(""), [T, E] = m(""), [D, O] = m(""), [k, A] = m(""), [j, M] = m(""), [N, P] = m(!1), [F, I] = m(
|
|
18
|
-
e.preventDefault(),
|
|
17
|
+
let y = t(), b = p(), [x, S] = m(""), [C, w] = m(""), [T, E] = m(""), [D, O] = m(""), [k, A] = m(""), [j, M] = m(""), [N, P] = m(!1), [F, I] = m(""), [L, R] = m(""), [z, B] = m(""), [V, H] = m(!1), [U, W] = m(null), [G, K] = m(null), q = async (e) => {
|
|
18
|
+
e.preventDefault(), W(null);
|
|
19
19
|
let t = v(T);
|
|
20
20
|
if (!_.test(x) || !_.test(C)) {
|
|
21
|
-
|
|
21
|
+
W(y("movethewheels.shipping.form.postcodeError", "Enter valid 6-digit pickup and delivery postcodes"));
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
if (t === void 0 || t <= 0) {
|
|
25
|
-
|
|
25
|
+
W(y("movethewheels.shipping.form.weightError", "Enter the package weight in kg"));
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
let n = [
|
|
@@ -31,27 +31,41 @@ function y() {
|
|
|
31
31
|
v(j)
|
|
32
32
|
], r = n.filter((e) => e !== void 0).length;
|
|
33
33
|
if (r !== 0 && r !== 3) {
|
|
34
|
-
|
|
34
|
+
W(y("movethewheels.shipping.form.dimensionsError", "Enter length, width and height together, or leave all three empty"));
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
let i =
|
|
37
|
+
let i = v(F), a = v(L), o = v(z), s = i !== void 0 || a !== void 0;
|
|
38
|
+
if (s && (!i || i <= 0 || !a || a <= 0)) {
|
|
39
|
+
W(y("movethewheels.shipping.form.ownFleetError", "To compare with your own vehicle, enter both the distance and your cost per km"));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (o !== void 0 && o < 0) {
|
|
43
|
+
W(y("movethewheels.shipping.form.fixedCostError", "Fixed cost cannot be negative"));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let c = {
|
|
38
47
|
pickupPostcode: x,
|
|
39
48
|
deliveryPostcode: C,
|
|
40
49
|
weightKg: t,
|
|
41
50
|
cod: N,
|
|
51
|
+
...s && i && a ? { ownFleet: {
|
|
52
|
+
distanceKm: i,
|
|
53
|
+
costPerKm: a,
|
|
54
|
+
...o ? { fixedCost: o } : {}
|
|
55
|
+
} } : {},
|
|
42
56
|
...r === 3 ? {
|
|
43
57
|
lengthCm: n[0],
|
|
44
58
|
widthCm: n[1],
|
|
45
59
|
heightCm: n[2]
|
|
46
60
|
} : {}
|
|
47
61
|
};
|
|
48
|
-
|
|
62
|
+
H(!0);
|
|
49
63
|
try {
|
|
50
|
-
|
|
64
|
+
K(await b(c));
|
|
51
65
|
} catch (e) {
|
|
52
|
-
|
|
66
|
+
K(null), W(e instanceof Error ? e.message : y("movethewheels.shipping.form.failed", "Could not fetch rates"));
|
|
53
67
|
} finally {
|
|
54
|
-
|
|
68
|
+
H(!1);
|
|
55
69
|
}
|
|
56
70
|
};
|
|
57
71
|
return /* @__PURE__ */ h(u, {
|
|
@@ -61,7 +75,7 @@ function y() {
|
|
|
61
75
|
children: /* @__PURE__ */ g("div", {
|
|
62
76
|
className: "space-y-6",
|
|
63
77
|
children: [/* @__PURE__ */ g(r, { children: [/* @__PURE__ */ h(a, { children: /* @__PURE__ */ h(o, { children: y("movethewheels.shipping.form.title", "Shipment details") }) }), /* @__PURE__ */ h(i, { children: /* @__PURE__ */ g("form", {
|
|
64
|
-
onSubmit: (e) => void
|
|
78
|
+
onSubmit: (e) => void q(e),
|
|
65
79
|
className: "space-y-4",
|
|
66
80
|
children: [
|
|
67
81
|
/* @__PURE__ */ g("div", {
|
|
@@ -148,6 +162,60 @@ function y() {
|
|
|
148
162
|
})
|
|
149
163
|
]
|
|
150
164
|
}),
|
|
165
|
+
/* @__PURE__ */ g("fieldset", {
|
|
166
|
+
className: "space-y-3 rounded-lg border p-4",
|
|
167
|
+
children: [
|
|
168
|
+
/* @__PURE__ */ h("legend", {
|
|
169
|
+
className: "px-1 text-sm font-medium",
|
|
170
|
+
children: y("movethewheels.shipping.form.ownFleetTitle", "Compare with your own vehicle (optional)")
|
|
171
|
+
}),
|
|
172
|
+
/* @__PURE__ */ h("p", {
|
|
173
|
+
className: "text-sm text-text-secondary",
|
|
174
|
+
children: y("movethewheels.shipping.form.ownFleetHelp", "Enter the road distance and what it costs you to run a vehicle per km, and we will show it next to the courier prices.")
|
|
175
|
+
}),
|
|
176
|
+
/* @__PURE__ */ g("div", {
|
|
177
|
+
className: "grid gap-3 sm:grid-cols-3",
|
|
178
|
+
children: [
|
|
179
|
+
/* @__PURE__ */ g("div", {
|
|
180
|
+
className: "space-y-1",
|
|
181
|
+
children: [/* @__PURE__ */ h(c, {
|
|
182
|
+
htmlFor: "rate-distance",
|
|
183
|
+
children: y("movethewheels.shipping.form.distance", "Distance (km)")
|
|
184
|
+
}), /* @__PURE__ */ h(s, {
|
|
185
|
+
id: "rate-distance",
|
|
186
|
+
inputMode: "decimal",
|
|
187
|
+
value: F,
|
|
188
|
+
onChange: (e) => I(e.target.value)
|
|
189
|
+
})]
|
|
190
|
+
}),
|
|
191
|
+
/* @__PURE__ */ g("div", {
|
|
192
|
+
className: "space-y-1",
|
|
193
|
+
children: [/* @__PURE__ */ h(c, {
|
|
194
|
+
htmlFor: "rate-cost-per-km",
|
|
195
|
+
children: y("movethewheels.shipping.form.costPerKm", "Your cost per km (₹)")
|
|
196
|
+
}), /* @__PURE__ */ h(s, {
|
|
197
|
+
id: "rate-cost-per-km",
|
|
198
|
+
inputMode: "decimal",
|
|
199
|
+
value: L,
|
|
200
|
+
onChange: (e) => R(e.target.value)
|
|
201
|
+
})]
|
|
202
|
+
}),
|
|
203
|
+
/* @__PURE__ */ g("div", {
|
|
204
|
+
className: "space-y-1",
|
|
205
|
+
children: [/* @__PURE__ */ h(c, {
|
|
206
|
+
htmlFor: "rate-fixed-cost",
|
|
207
|
+
children: y("movethewheels.shipping.form.fixedCost", "Fixed cost per trip (₹, optional)")
|
|
208
|
+
}), /* @__PURE__ */ h(s, {
|
|
209
|
+
id: "rate-fixed-cost",
|
|
210
|
+
inputMode: "decimal",
|
|
211
|
+
value: z,
|
|
212
|
+
onChange: (e) => B(e.target.value)
|
|
213
|
+
})]
|
|
214
|
+
})
|
|
215
|
+
]
|
|
216
|
+
})
|
|
217
|
+
]
|
|
218
|
+
}),
|
|
151
219
|
/* @__PURE__ */ g("div", {
|
|
152
220
|
className: "flex items-center gap-2",
|
|
153
221
|
children: [/* @__PURE__ */ h(l, {
|
|
@@ -159,15 +227,15 @@ function y() {
|
|
|
159
227
|
children: y("movethewheels.shipping.form.cod", "Cash on delivery")
|
|
160
228
|
})]
|
|
161
229
|
}),
|
|
162
|
-
/* @__PURE__ */ h(d, { message:
|
|
230
|
+
/* @__PURE__ */ h(d, { message: U }),
|
|
163
231
|
/* @__PURE__ */ h(n, {
|
|
164
232
|
type: "submit",
|
|
165
|
-
disabled:
|
|
166
|
-
children:
|
|
233
|
+
disabled: V,
|
|
234
|
+
children: V ? y("movethewheels.shipping.form.comparing", "Comparing…") : y("movethewheels.shipping.form.compare", "Compare rates")
|
|
167
235
|
})
|
|
168
236
|
]
|
|
169
|
-
}) })] }),
|
|
170
|
-
result:
|
|
237
|
+
}) })] }), G && /* @__PURE__ */ h(f, {
|
|
238
|
+
result: G,
|
|
171
239
|
t: y
|
|
172
240
|
})]
|
|
173
241
|
})
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* number people compare on — off-screen, so each courier is a card instead.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { AlertTriangle } from 'lucide-react';
|
|
12
|
+
import { AlertTriangle, Truck } from 'lucide-react';
|
|
13
13
|
|
|
14
14
|
import { StatusBadge } from './StatusBadge';
|
|
15
15
|
import {
|
|
@@ -25,7 +25,11 @@ import {
|
|
|
25
25
|
TableRow,
|
|
26
26
|
} from './ui';
|
|
27
27
|
|
|
28
|
-
import type {
|
|
28
|
+
import type {
|
|
29
|
+
OwnFleetEstimate,
|
|
30
|
+
ShippingQuote,
|
|
31
|
+
ShippingQuoteResult,
|
|
32
|
+
} from '../hooks/useShippingCarriers';
|
|
29
33
|
import { formatCurrency } from '../utils/formatters';
|
|
30
34
|
|
|
31
35
|
interface ShippingQuoteResultsProps {
|
|
@@ -83,6 +87,60 @@ function QuoteBadges({ quote, t }: { quote: ShippingQuote; t: ShippingQuoteResul
|
|
|
83
87
|
);
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
function OwnFleetCard({
|
|
91
|
+
estimate,
|
|
92
|
+
t,
|
|
93
|
+
}: {
|
|
94
|
+
estimate: OwnFleetEstimate;
|
|
95
|
+
t: ShippingQuoteResultsProps['t'];
|
|
96
|
+
}) {
|
|
97
|
+
const savings = estimate.savingsVsCheapestCourier;
|
|
98
|
+
return (
|
|
99
|
+
<Card>
|
|
100
|
+
<CardHeader>
|
|
101
|
+
<CardTitle className="flex items-center gap-2">
|
|
102
|
+
<Truck size={18} aria-hidden="true" />
|
|
103
|
+
{t('movethewheels.shipping.ownFleet.title', 'Your own vehicle')}
|
|
104
|
+
<StatusBadge
|
|
105
|
+
status={t('movethewheels.shipping.ownFleet.estimate', 'Your estimate')}
|
|
106
|
+
customColors={INFO}
|
|
107
|
+
size="sm"
|
|
108
|
+
showDot={false}
|
|
109
|
+
/>
|
|
110
|
+
</CardTitle>
|
|
111
|
+
</CardHeader>
|
|
112
|
+
<CardContent className="space-y-2">
|
|
113
|
+
<p className="text-2xl font-semibold">{formatCurrency(estimate.cost, estimate.currency)}</p>
|
|
114
|
+
<p className="text-sm text-text-secondary">
|
|
115
|
+
{estimate.distanceKm} km × {formatCurrency(estimate.costPerKm, estimate.currency)}/km
|
|
116
|
+
{estimate.fixedCost > 0 && ` + ${formatCurrency(estimate.fixedCost, estimate.currency)}`}
|
|
117
|
+
</p>
|
|
118
|
+
{savings !== null && (
|
|
119
|
+
<p
|
|
120
|
+
className={`text-sm font-medium ${
|
|
121
|
+
estimate.isCheaperThanCouriers
|
|
122
|
+
? 'text-status-success-text'
|
|
123
|
+
: 'text-status-warning-text'
|
|
124
|
+
}`}
|
|
125
|
+
>
|
|
126
|
+
{estimate.isCheaperThanCouriers
|
|
127
|
+
? `${t('movethewheels.shipping.ownFleet.cheaper', 'Your own vehicle is cheaper than the cheapest courier by')} ${formatCurrency(savings, estimate.currency)}`
|
|
128
|
+
: savings === 0
|
|
129
|
+
? t('movethewheels.shipping.ownFleet.same', 'Same cost as the cheapest courier')
|
|
130
|
+
: `${t('movethewheels.shipping.ownFleet.dearer', 'The cheapest courier is cheaper by')} ${formatCurrency(-savings, estimate.currency)}`}
|
|
131
|
+
</p>
|
|
132
|
+
)}
|
|
133
|
+
<p className="text-xs text-text-secondary">
|
|
134
|
+
{t(
|
|
135
|
+
'movethewheels.shipping.ownFleet.note',
|
|
136
|
+
'Based only on the distance and cost per km you entered — not a quote.'
|
|
137
|
+
)}
|
|
138
|
+
</p>
|
|
139
|
+
</CardContent>
|
|
140
|
+
</Card>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
86
144
|
export function ShippingQuoteResults({ result, t }: ShippingQuoteResultsProps) {
|
|
87
145
|
const failed = result.providers.filter((provider) => !provider.ok);
|
|
88
146
|
|
|
@@ -99,6 +157,8 @@ export function ShippingQuoteResults({ result, t }: ShippingQuoteResultsProps) {
|
|
|
99
157
|
</div>
|
|
100
158
|
))}
|
|
101
159
|
|
|
160
|
+
{result.ownFleet && <OwnFleetCard estimate={result.ownFleet} t={t} />}
|
|
161
|
+
|
|
102
162
|
<Card>
|
|
103
163
|
<CardHeader>
|
|
104
164
|
<CardTitle>
|
|
@@ -13,7 +13,14 @@ export const OSM_ATTRIBUTION =
|
|
|
13
13
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
|
14
14
|
|
|
15
15
|
export type MapMarkerStatus =
|
|
16
|
-
|
|
16
|
+
| 'active'
|
|
17
|
+
| 'in_transit'
|
|
18
|
+
| 'idle'
|
|
19
|
+
| 'delayed'
|
|
20
|
+
| 'delivered'
|
|
21
|
+
| 'origin'
|
|
22
|
+
| 'destination'
|
|
23
|
+
| 'default';
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
26
|
* A coordinate is "real" only when it is finite and not the null-island (0,0)
|
|
@@ -12,7 +12,12 @@ import { executeGraphQL } from '../utils/graphqlClient';
|
|
|
12
12
|
|
|
13
13
|
export type TransportMode = 'ROAD' | 'AIR' | 'OCEAN' | 'RAIL';
|
|
14
14
|
export type ShipmentLegStatus =
|
|
15
|
-
|
|
15
|
+
| 'PLANNED'
|
|
16
|
+
| 'BOOKED'
|
|
17
|
+
| 'IN_TRANSIT'
|
|
18
|
+
| 'ARRIVED'
|
|
19
|
+
| 'DELIVERED'
|
|
20
|
+
| 'CANCELLED';
|
|
16
21
|
export type OceanContainerType = 'LCL' | 'FCL_20FT' | 'FCL_40FT' | 'FCL_40FT_HC';
|
|
17
22
|
|
|
18
23
|
export interface FreightRateBreakdownLine {
|
|
@@ -11,6 +11,23 @@ import { executeGraphQL } from '../utils/graphqlClient';
|
|
|
11
11
|
export type CarrierProvider = 'SHIPROCKET';
|
|
12
12
|
export type CarrierServiceMode = 'SURFACE' | 'AIR';
|
|
13
13
|
|
|
14
|
+
export interface OwnFleetInput {
|
|
15
|
+
distanceKm: number;
|
|
16
|
+
costPerKm: number;
|
|
17
|
+
fixedCost?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OwnFleetEstimate {
|
|
21
|
+
distanceKm: number;
|
|
22
|
+
costPerKm: number;
|
|
23
|
+
fixedCost: number;
|
|
24
|
+
cost: number;
|
|
25
|
+
currency: string;
|
|
26
|
+
/** Cheapest courier price minus this estimate; positive = your own vehicle is cheaper. */
|
|
27
|
+
savingsVsCheapestCourier: number | null;
|
|
28
|
+
isCheaperThanCouriers: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
export interface ShippingQuoteInput {
|
|
15
32
|
pickupPostcode: string;
|
|
16
33
|
deliveryPostcode: string;
|
|
@@ -19,6 +36,7 @@ export interface ShippingQuoteInput {
|
|
|
19
36
|
widthCm?: number;
|
|
20
37
|
heightCm?: number;
|
|
21
38
|
cod?: boolean;
|
|
39
|
+
ownFleet?: OwnFleetInput;
|
|
22
40
|
}
|
|
23
41
|
|
|
24
42
|
export interface ShippingQuote {
|
|
@@ -45,6 +63,7 @@ export interface CarrierOutcome {
|
|
|
45
63
|
|
|
46
64
|
export interface ShippingQuoteResult {
|
|
47
65
|
chargeableWeightKg: number;
|
|
66
|
+
ownFleet: OwnFleetEstimate | null;
|
|
48
67
|
quotes: ShippingQuote[];
|
|
49
68
|
providers: CarrierOutcome[];
|
|
50
69
|
}
|
|
@@ -53,6 +72,15 @@ const SHIPPING_QUOTES_QUERY = `
|
|
|
53
72
|
query ShippingQuotes($input: ShippingQuoteInput!) {
|
|
54
73
|
shippingQuotes(input: $input) {
|
|
55
74
|
chargeableWeightKg
|
|
75
|
+
ownFleet {
|
|
76
|
+
distanceKm
|
|
77
|
+
costPerKm
|
|
78
|
+
fixedCost
|
|
79
|
+
cost
|
|
80
|
+
currency
|
|
81
|
+
savingsVsCheapestCourier
|
|
82
|
+
isCheaperThanCouriers
|
|
83
|
+
}
|
|
56
84
|
quotes {
|
|
57
85
|
provider
|
|
58
86
|
courierId
|
|
@@ -48,6 +48,9 @@ export default function ShippingRatesPage() {
|
|
|
48
48
|
const [width, setWidth] = useState('');
|
|
49
49
|
const [height, setHeight] = useState('');
|
|
50
50
|
const [cod, setCod] = useState(false);
|
|
51
|
+
const [distance, setDistance] = useState('');
|
|
52
|
+
const [costPerKm, setCostPerKm] = useState('');
|
|
53
|
+
const [fixedCost, setFixedCost] = useState('');
|
|
51
54
|
const [isLoading, setIsLoading] = useState(false);
|
|
52
55
|
const [error, setError] = useState<string | null>(null);
|
|
53
56
|
const [result, setResult] = useState<ShippingQuoteResult | null>(null);
|
|
@@ -82,11 +85,41 @@ export default function ShippingRatesPage() {
|
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
const fleetDistance = optionalNumber(distance);
|
|
89
|
+
const fleetCostPerKm = optionalNumber(costPerKm);
|
|
90
|
+
const fleetFixed = optionalNumber(fixedCost);
|
|
91
|
+
const fleetGiven = fleetDistance !== undefined || fleetCostPerKm !== undefined;
|
|
92
|
+
if (
|
|
93
|
+
fleetGiven &&
|
|
94
|
+
(!fleetDistance || fleetDistance <= 0 || !fleetCostPerKm || fleetCostPerKm <= 0)
|
|
95
|
+
) {
|
|
96
|
+
setError(
|
|
97
|
+
tr(
|
|
98
|
+
'movethewheels.shipping.form.ownFleetError',
|
|
99
|
+
'To compare with your own vehicle, enter both the distance and your cost per km'
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (fleetFixed !== undefined && fleetFixed < 0) {
|
|
105
|
+
setError(tr('movethewheels.shipping.form.fixedCostError', 'Fixed cost cannot be negative'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
const input: ShippingQuoteInput = {
|
|
86
110
|
pickupPostcode: pickup,
|
|
87
111
|
deliveryPostcode: delivery,
|
|
88
112
|
weightKg,
|
|
89
113
|
cod,
|
|
114
|
+
...(fleetGiven && fleetDistance && fleetCostPerKm
|
|
115
|
+
? {
|
|
116
|
+
ownFleet: {
|
|
117
|
+
distanceKm: fleetDistance,
|
|
118
|
+
costPerKm: fleetCostPerKm,
|
|
119
|
+
...(fleetFixed ? { fixedCost: fleetFixed } : {}),
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
: {}),
|
|
90
123
|
...(given === 3 ? { lengthCm: dims[0], widthCm: dims[1], heightCm: dims[2] } : {}),
|
|
91
124
|
};
|
|
92
125
|
|
|
@@ -193,6 +226,58 @@ export default function ShippingRatesPage() {
|
|
|
193
226
|
/>
|
|
194
227
|
</div>
|
|
195
228
|
</div>
|
|
229
|
+
<fieldset className="space-y-3 rounded-lg border p-4">
|
|
230
|
+
<legend className="px-1 text-sm font-medium">
|
|
231
|
+
{tr(
|
|
232
|
+
'movethewheels.shipping.form.ownFleetTitle',
|
|
233
|
+
'Compare with your own vehicle (optional)'
|
|
234
|
+
)}
|
|
235
|
+
</legend>
|
|
236
|
+
<p className="text-sm text-text-secondary">
|
|
237
|
+
{tr(
|
|
238
|
+
'movethewheels.shipping.form.ownFleetHelp',
|
|
239
|
+
'Enter the road distance and what it costs you to run a vehicle per km, and we will show it next to the courier prices.'
|
|
240
|
+
)}
|
|
241
|
+
</p>
|
|
242
|
+
<div className="grid gap-3 sm:grid-cols-3">
|
|
243
|
+
<div className="space-y-1">
|
|
244
|
+
<Label htmlFor="rate-distance">
|
|
245
|
+
{tr('movethewheels.shipping.form.distance', 'Distance (km)')}
|
|
246
|
+
</Label>
|
|
247
|
+
<Input
|
|
248
|
+
id="rate-distance"
|
|
249
|
+
inputMode="decimal"
|
|
250
|
+
value={distance}
|
|
251
|
+
onChange={(e) => setDistance(e.target.value)}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
<div className="space-y-1">
|
|
255
|
+
<Label htmlFor="rate-cost-per-km">
|
|
256
|
+
{tr('movethewheels.shipping.form.costPerKm', 'Your cost per km (₹)')}
|
|
257
|
+
</Label>
|
|
258
|
+
<Input
|
|
259
|
+
id="rate-cost-per-km"
|
|
260
|
+
inputMode="decimal"
|
|
261
|
+
value={costPerKm}
|
|
262
|
+
onChange={(e) => setCostPerKm(e.target.value)}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
<div className="space-y-1">
|
|
266
|
+
<Label htmlFor="rate-fixed-cost">
|
|
267
|
+
{tr(
|
|
268
|
+
'movethewheels.shipping.form.fixedCost',
|
|
269
|
+
'Fixed cost per trip (₹, optional)'
|
|
270
|
+
)}
|
|
271
|
+
</Label>
|
|
272
|
+
<Input
|
|
273
|
+
id="rate-fixed-cost"
|
|
274
|
+
inputMode="decimal"
|
|
275
|
+
value={fixedCost}
|
|
276
|
+
onChange={(e) => setFixedCost(e.target.value)}
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</fieldset>
|
|
196
281
|
<div className="flex items-center gap-2">
|
|
197
282
|
<Switch id="rate-cod" checked={cod} onCheckedChange={setCod} />
|
|
198
283
|
<Label htmlFor="rate-cod">
|