@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.
@@ -1,142 +1,195 @@
1
1
  import { TriangleAlert as e } from "../node_modules/lucide-react/dist/esm/icons/triangle-alert.js";
2
- import { Card as t, CardContent as n, CardHeader as r, CardTitle as i, Table as a, TableBody as o, TableCell as s, TableHead as c, TableHeader as l, TableRow as u } from "./ui.js";
3
- import { StatusBadge as d } from "./StatusBadge.js";
4
- import { formatCurrency as f } from "../utils/formatters.js";
5
- import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
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 g = {
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 v(e, t) {
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 y(e, t) {
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 b({ quote: e, t }) {
23
- return /* @__PURE__ */ h(p, { children: [
24
- e.isCheapest && /* @__PURE__ */ m(d, {
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: g,
27
+ customColors: _,
27
28
  size: "sm",
28
29
  showDot: !1
29
30
  }),
30
- e.isFastest && /* @__PURE__ */ m(d, {
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__ */ m(d, {
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 x({ result: d, t: g }) {
45
- let _ = d.providers.filter((e) => !e.ok);
46
- return /* @__PURE__ */ h("div", {
47
- className: "space-y-4",
48
- children: [_.map((t) => /* @__PURE__ */ h("div", {
49
- role: "alert",
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
- }), /* @__PURE__ */ m("span", { children: t.errorMessage })]
56
- }, t.provider)), /* @__PURE__ */ h(t, { children: [/* @__PURE__ */ h(r, { children: [/* @__PURE__ */ h(i, { children: [
57
- g("movethewheels.shipping.results.title", "Courier options"),
58
- " (",
59
- d.quotes.length,
60
- ")"
61
- ] }), /* @__PURE__ */ h("p", {
62
- className: "text-sm text-text-secondary",
63
- children: [
64
- g("movethewheels.shipping.results.chargeableWeight", "Charged on"),
65
- " ",
66
- d.chargeableWeightKg,
67
- " kg"
68
- ]
69
- })] }), /* @__PURE__ */ m(n, {
70
- className: "p-0",
71
- children: d.quotes.length === 0 ? /* @__PURE__ */ m("p", {
72
- className: "px-6 pb-6 text-sm text-text-secondary",
73
- children: _.length > 0 ? g("movethewheels.shipping.results.allFailed", "No rates could be fetched — see the message above.") : g("movethewheels.shipping.results.noServiceable", "No courier serves this pincode pair.")
74
- }) : /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m("ul", {
75
- className: "divide-y md:hidden",
76
- "aria-label": g("movethewheels.shipping.results.title", "Courier options"),
77
- children: d.quotes.map((e) => /* @__PURE__ */ h("li", {
78
- className: "space-y-2 px-4 py-4",
79
- children: [
80
- /* @__PURE__ */ h("div", {
81
- className: "flex items-start justify-between gap-3",
82
- children: [/* @__PURE__ */ m("span", {
83
- className: "min-w-0 font-medium",
84
- children: e.courierName
85
- }), /* @__PURE__ */ m("span", {
86
- className: "shrink-0 text-lg font-semibold",
87
- children: f(e.price, e.currency)
88
- })]
89
- }),
90
- /* @__PURE__ */ m("div", {
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__ */ m(b, {
173
+ children: [/* @__PURE__ */ h("span", {
174
+ className: "font-medium",
175
+ children: e.courierName
176
+ }), /* @__PURE__ */ h(x, {
93
177
  quote: e,
94
- t: g
95
- })
96
- }),
97
- /* @__PURE__ */ m("p", {
98
- className: "text-sm text-text-secondary",
99
- children: [
100
- y(e, g),
101
- v(e, g),
102
- e.rating === null ? null : `${e.rating.toFixed(1)} ★`
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
- }, `${e.provider}:${e.courierId}`))
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 { x as ShippingQuoteResults };
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(!1), [L, R] = m(null), [z, B] = m(null), V = async (e) => {
18
- e.preventDefault(), R(null);
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
- R(y("movethewheels.shipping.form.postcodeError", "Enter valid 6-digit pickup and delivery postcodes"));
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
- R(y("movethewheels.shipping.form.weightError", "Enter the package weight in kg"));
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
- R(y("movethewheels.shipping.form.dimensionsError", "Enter length, width and height together, or leave all three empty"));
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
- I(!0);
62
+ H(!0);
49
63
  try {
50
- B(await b(i));
64
+ K(await b(c));
51
65
  } catch (e) {
52
- B(null), R(e instanceof Error ? e.message : y("movethewheels.shipping.form.failed", "Could not fetch rates"));
66
+ K(null), W(e instanceof Error ? e.message : y("movethewheels.shipping.form.failed", "Could not fetch rates"));
53
67
  } finally {
54
- I(!1);
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 V(e),
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: L }),
230
+ /* @__PURE__ */ h(d, { message: U }),
163
231
  /* @__PURE__ */ h(n, {
164
232
  type: "submit",
165
- disabled: F,
166
- children: F ? y("movethewheels.shipping.form.comparing", "Comparing…") : y("movethewheels.shipping.form.compare", "Compare rates")
233
+ disabled: V,
234
+ children: V ? y("movethewheels.shipping.form.comparing", "Comparing…") : y("movethewheels.shipping.form.compare", "Compare rates")
167
235
  })
168
236
  ]
169
- }) })] }), z && /* @__PURE__ */ h(f, {
170
- result: z,
237
+ }) })] }), G && /* @__PURE__ */ h(f, {
238
+ result: G,
171
239
  t: y
172
240
  })]
173
241
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-movethewheels",
3
- "version": "2026.920.1",
3
+ "version": "2026.920.2",
4
4
  "description": "Burdenoff MoveTheWheels Microfrontend - Logistics & Supply Chain Management Platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -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 { ShippingQuote, ShippingQuoteResult } from '../hooks/useShippingCarriers';
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
  '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
14
14
 
15
15
  export type MapMarkerStatus =
16
- 'active' | 'in_transit' | 'idle' | 'delayed' | 'delivered' | 'origin' | 'destination' | 'default';
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
- 'PLANNED' | 'BOOKED' | 'IN_TRANSIT' | 'ARRIVED' | 'DELIVERED' | 'CANCELLED';
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">