@burdenoff/microfe-movethewheels 2026.918.1 → 2026.920.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.
@@ -28,8 +28,8 @@ function d() {
28
28
  })
29
29
  });
30
30
  }
31
- var f = r(() => import("./pages/DashboardPage.js")), p = r(() => import("./pages/OrdersListPage.js")), m = r(() => import("./pages/OrderDetailsPage.js")), h = r(() => import("./pages/CreateOrderPage.js")), g = r(() => import("./pages/TrackingPage.js")), _ = r(() => import("./pages/LiveTrackingPage.js")), v = r(() => import("./pages/FleetPage.js")), y = r(() => import("./pages/VehiclesPage.js")), b = r(() => import("./pages/VehicleDetailsPage.js")), x = r(() => import("./pages/DriversPage.js")), S = r(() => import("./pages/DriverDetailsPage.js")), C = r(() => import("./pages/InventoryPage.js")), w = r(() => import("./pages/ProductsPage.js")), T = r(() => import("./pages/ProductDetailsPage.js")), E = r(() => import("./pages/WarehousesPage.js")), D = r(() => import("./pages/WarehouseDetailsPage.js")), O = r(() => import("./pages/CustomersPage.js")), k = r(() => import("./pages/CustomerDetailsPage.js")), A = r(() => import("./pages/RoutesPage.js")), j = r(() => import("./pages/RouteDetailsPage.js")), M = r(() => import("./pages/AnalyticsPage.js")), N = r(() => import("./pages/FinancePage.js")), P = r(() => import("./pages/MarketplacePage.js")), F = r(() => import("./pages/ImportExportPage.js")), I = r(() => import("./pages/ReportsPage.js")), L = r(() => import("./pages/SettingsPage.js")), R = r(() => import("./pages/AIAssistantPage.js")), z = r(() => import("./pages/DeliveryCapturePage.js")), B = r(() => import("./pages/MultiModalShipmentPage.js"));
32
- function V() {
31
+ var f = r(() => import("./pages/DashboardPage.js")), p = r(() => import("./pages/OrdersListPage.js")), m = r(() => import("./pages/OrderDetailsPage.js")), h = r(() => import("./pages/CreateOrderPage.js")), g = r(() => import("./pages/TrackingPage.js")), _ = r(() => import("./pages/LiveTrackingPage.js")), v = r(() => import("./pages/FleetPage.js")), y = r(() => import("./pages/VehiclesPage.js")), b = r(() => import("./pages/VehicleDetailsPage.js")), x = r(() => import("./pages/DriversPage.js")), S = r(() => import("./pages/DriverDetailsPage.js")), C = r(() => import("./pages/InventoryPage.js")), w = r(() => import("./pages/ProductsPage.js")), T = r(() => import("./pages/ProductDetailsPage.js")), E = r(() => import("./pages/WarehousesPage.js")), D = r(() => import("./pages/WarehouseDetailsPage.js")), O = r(() => import("./pages/CustomersPage.js")), k = r(() => import("./pages/CustomerDetailsPage.js")), A = r(() => import("./pages/RoutesPage.js")), j = r(() => import("./pages/RouteDetailsPage.js")), M = r(() => import("./pages/AnalyticsPage.js")), N = r(() => import("./pages/FinancePage.js")), P = r(() => import("./pages/MarketplacePage.js")), F = r(() => import("./pages/ImportExportPage.js")), I = r(() => import("./pages/ReportsPage.js")), L = r(() => import("./pages/SettingsPage.js")), R = r(() => import("./pages/AIAssistantPage.js")), z = r(() => import("./pages/DeliveryCapturePage.js")), B = r(() => import("./pages/MultiModalShipmentPage.js")), V = r(() => import("./pages/ShippingRatesPage.js"));
32
+ function H() {
33
33
  return /* @__PURE__ */ s(n, {
34
34
  fallback: /* @__PURE__ */ s(d, {}),
35
35
  children: /* @__PURE__ */ c(a, { children: [
@@ -69,6 +69,10 @@ function V() {
69
69
  path: "shipments/multi-modal",
70
70
  element: /* @__PURE__ */ s(B, {})
71
71
  }),
72
+ /* @__PURE__ */ s(i, {
73
+ path: "shipments/rates",
74
+ element: /* @__PURE__ */ s(V, {})
75
+ }),
72
76
  /* @__PURE__ */ s(i, {
73
77
  path: "fleet",
74
78
  element: /* @__PURE__ */ s(v, {})
@@ -205,4 +209,4 @@ function V() {
205
209
  });
206
210
  }
207
211
  //#endregion
208
- export { V as MoveTheWheelsRoutes };
212
+ export { H as MoveTheWheelsRoutes };
@@ -0,0 +1,142 @@
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";
6
+ //#region src/movethewheels/components/ShippingQuoteResults.tsx
7
+ var g = {
8
+ bg: "bg-status-success-bg-subtle",
9
+ text: "text-status-success-text"
10
+ }, _ = {
11
+ bg: "bg-status-info-bg-subtle",
12
+ text: "text-status-info-text"
13
+ };
14
+ function v(e, t) {
15
+ if (e.estimatedDeliveryDays === null) return e.estimatedDeliveryDate ?? "—";
16
+ let n = e.estimatedDeliveryDays === 1 ? t("movethewheels.shipping.results.day", "day") : t("movethewheels.shipping.results.days", "days"), r = `${e.estimatedDeliveryDays} ${n}`;
17
+ return e.estimatedDeliveryDate ? `${r} · ${e.estimatedDeliveryDate}` : r;
18
+ }
19
+ function y(e, t) {
20
+ return e.mode === "AIR" ? t("movethewheels.shipping.results.air", "Air") : e.mode === "SURFACE" ? t("movethewheels.shipping.results.surface", "Surface") : "—";
21
+ }
22
+ function b({ quote: e, t }) {
23
+ return /* @__PURE__ */ h(p, { children: [
24
+ e.isCheapest && /* @__PURE__ */ m(d, {
25
+ status: t("movethewheels.shipping.results.cheapest", "Cheapest"),
26
+ customColors: g,
27
+ size: "sm",
28
+ showDot: !1
29
+ }),
30
+ e.isFastest && /* @__PURE__ */ m(d, {
31
+ status: t("movethewheels.shipping.results.fastest", "Fastest"),
32
+ customColors: _,
33
+ size: "sm",
34
+ showDot: !1
35
+ }),
36
+ e.isRecommended && /* @__PURE__ */ m(d, {
37
+ status: t("movethewheels.shipping.results.recommended", "Recommended"),
38
+ customColors: _,
39
+ size: "sm",
40
+ showDot: !1
41
+ })
42
+ ] });
43
+ }
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, {
52
+ size: 18,
53
+ className: "mt-0.5 shrink-0",
54
+ "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", {
91
+ className: "flex flex-wrap items-center gap-2",
92
+ children: /* @__PURE__ */ m(b, {
93
+ 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(" · ")
104
+ })
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}`)) })] })
137
+ })] })
138
+ })] })]
139
+ });
140
+ }
141
+ //#endregion
142
+ export { x as ShippingQuoteResults };
@@ -0,0 +1,29 @@
1
+ import { useMoveTheWheels as e } from "../providers/MoveTheWheelsProvider.js";
2
+ import { executeGraphQL as t } from "../utils/graphqlClient.js";
3
+ import { useCallback as n, useMemo as r } from "react";
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";
6
+ function a() {
7
+ let { apiGatewayUrl: t, authToken: n, workspaceToken: i, workspaceId: a, tenantId: o, currentUser: s } = e();
8
+ return r(() => ({
9
+ apiGatewayUrl: t,
10
+ authToken: n,
11
+ workspaceToken: i,
12
+ workspaceId: a,
13
+ tenantId: o,
14
+ actorId: s?.id
15
+ }), [
16
+ t,
17
+ n,
18
+ i,
19
+ a,
20
+ o,
21
+ s?.id
22
+ ]);
23
+ }
24
+ function o() {
25
+ let e = a();
26
+ return n(async (n) => (await t(e, i, { input: n })).shippingQuotes, [e]);
27
+ }
28
+ //#endregion
29
+ export { o as useShippingQuotes };
@@ -0,0 +1,8 @@
1
+ import { ShippingQuoteResult } from '../hooks/useShippingCarriers';
2
+
3
+ interface ShippingQuoteResultsProps {
4
+ result: ShippingQuoteResult;
5
+ t: (key: string, fallback: string) => string;
6
+ }
7
+ export declare function ShippingQuoteResults({ result, t }: ShippingQuoteResultsProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shipping rate comparison — every carrier's price for one pincode pair, on top
3
+ * of wspace-movethewheels-svc's carrier module. The carrier accounts belong to
4
+ * the platform, so there is nothing for a workspace to configure.
5
+ */
6
+ export type CarrierProvider = 'SHIPROCKET';
7
+ export type CarrierServiceMode = 'SURFACE' | 'AIR';
8
+ export interface ShippingQuoteInput {
9
+ pickupPostcode: string;
10
+ deliveryPostcode: string;
11
+ weightKg: number;
12
+ lengthCm?: number;
13
+ widthCm?: number;
14
+ heightCm?: number;
15
+ cod?: boolean;
16
+ }
17
+ export interface ShippingQuote {
18
+ provider: CarrierProvider;
19
+ courierId: string;
20
+ courierName: string;
21
+ price: number;
22
+ currency: string;
23
+ estimatedDeliveryDays: number | null;
24
+ estimatedDeliveryDate: string | null;
25
+ rating: number | null;
26
+ mode: CarrierServiceMode | null;
27
+ isRecommended: boolean;
28
+ isCheapest: boolean;
29
+ isFastest: boolean;
30
+ }
31
+ export interface CarrierOutcome {
32
+ provider: CarrierProvider;
33
+ ok: boolean;
34
+ quoteCount: number;
35
+ errorMessage: string | null;
36
+ }
37
+ export interface ShippingQuoteResult {
38
+ chargeableWeightKg: number;
39
+ quotes: ShippingQuote[];
40
+ providers: CarrierOutcome[];
41
+ }
42
+ /** Imperative: a comparison is something the operator asks for, not page-load data. */
43
+ export declare function useShippingQuotes(): (input: ShippingQuoteInput) => Promise<ShippingQuoteResult>;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shipping Rates Page
3
+ *
4
+ * Compare live courier rates between two pincodes across the platform's
5
+ * carriers (Shiprocket first). Nothing to set up: the carrier accounts belong
6
+ * to the platform. Read-only — comparing never books a shipment or spends
7
+ * carrier credit.
8
+ */
9
+ export default function ShippingRatesPage(): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { createLucideIcon as e } from "../createLucideIcon.js";
2
+ //#region node_modules/lucide-react/dist/esm/icons/scale.js
3
+ var t = e("Scale", [
4
+ ["path", {
5
+ d: "m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z",
6
+ key: "7g6ntu"
7
+ }],
8
+ ["path", {
9
+ d: "m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z",
10
+ key: "ijws7r"
11
+ }],
12
+ ["path", {
13
+ d: "M7 21h10",
14
+ key: "1b0cd5"
15
+ }],
16
+ ["path", {
17
+ d: "M12 3v18",
18
+ key: "108xh3"
19
+ }],
20
+ ["path", {
21
+ d: "M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2",
22
+ key: "3gwbw2"
23
+ }]
24
+ ]);
25
+ //#endregion
26
+ export { t as Scale };
@@ -0,0 +1,177 @@
1
+ import { Scale as e } from "../node_modules/lucide-react/dist/esm/icons/scale.js";
2
+ import { useTr as t } from "../shared/hooks/useTr.js";
3
+ import "../hooks/index.js";
4
+ import { Button as n, Card as r, CardContent as i, CardHeader as a, CardTitle as o, Input as s, Label as c, Switch as l } from "../components/ui.js";
5
+ import { PageLayout as u } from "../components/PageLayout.js";
6
+ import { FormError as d } from "../components/FormError.js";
7
+ import { ShippingQuoteResults as f } from "../components/ShippingQuoteResults.js";
8
+ import { useShippingQuotes as p } from "../hooks/useShippingCarriers.js";
9
+ import { useState as m } from "react";
10
+ import { jsx as h, jsxs as g } from "react/jsx-runtime";
11
+ //#region src/movethewheels/pages/ShippingRatesPage.tsx
12
+ var _ = /^[1-9]\d{5}$/, v = (e) => {
13
+ let t = Number(e);
14
+ return e.trim() !== "" && Number.isFinite(t) ? t : void 0;
15
+ };
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);
19
+ let t = v(T);
20
+ if (!_.test(x) || !_.test(C)) {
21
+ R(y("movethewheels.shipping.form.postcodeError", "Enter valid 6-digit pickup and delivery postcodes"));
22
+ return;
23
+ }
24
+ if (t === void 0 || t <= 0) {
25
+ R(y("movethewheels.shipping.form.weightError", "Enter the package weight in kg"));
26
+ return;
27
+ }
28
+ let n = [
29
+ v(D),
30
+ v(k),
31
+ v(j)
32
+ ], r = n.filter((e) => e !== void 0).length;
33
+ if (r !== 0 && r !== 3) {
34
+ R(y("movethewheels.shipping.form.dimensionsError", "Enter length, width and height together, or leave all three empty"));
35
+ return;
36
+ }
37
+ let i = {
38
+ pickupPostcode: x,
39
+ deliveryPostcode: C,
40
+ weightKg: t,
41
+ cod: N,
42
+ ...r === 3 ? {
43
+ lengthCm: n[0],
44
+ widthCm: n[1],
45
+ heightCm: n[2]
46
+ } : {}
47
+ };
48
+ I(!0);
49
+ try {
50
+ B(await b(i));
51
+ } catch (e) {
52
+ B(null), R(e instanceof Error ? e.message : y("movethewheels.shipping.form.failed", "Could not fetch rates"));
53
+ } finally {
54
+ I(!1);
55
+ }
56
+ };
57
+ return /* @__PURE__ */ h(u, {
58
+ title: y("movethewheels.shipping.title", "Compare Rates"),
59
+ subtitle: y("movethewheels.shipping.subtitle", "Enter a pickup and a delivery pincode to see every courier available for that route, with price and delivery time."),
60
+ icon: /* @__PURE__ */ h(e, { size: 24 }),
61
+ children: /* @__PURE__ */ g("div", {
62
+ className: "space-y-6",
63
+ 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),
65
+ className: "space-y-4",
66
+ children: [
67
+ /* @__PURE__ */ g("div", {
68
+ className: "grid gap-3 sm:grid-cols-3",
69
+ children: [
70
+ /* @__PURE__ */ g("div", {
71
+ className: "space-y-1",
72
+ children: [/* @__PURE__ */ h(c, {
73
+ htmlFor: "rate-pickup",
74
+ children: y("movethewheels.shipping.form.pickup", "Pickup postcode")
75
+ }), /* @__PURE__ */ h(s, {
76
+ id: "rate-pickup",
77
+ inputMode: "numeric",
78
+ maxLength: 6,
79
+ value: x,
80
+ onChange: (e) => S(e.target.value.trim())
81
+ })]
82
+ }),
83
+ /* @__PURE__ */ g("div", {
84
+ className: "space-y-1",
85
+ children: [/* @__PURE__ */ h(c, {
86
+ htmlFor: "rate-delivery",
87
+ children: y("movethewheels.shipping.form.delivery", "Delivery postcode")
88
+ }), /* @__PURE__ */ h(s, {
89
+ id: "rate-delivery",
90
+ inputMode: "numeric",
91
+ maxLength: 6,
92
+ value: C,
93
+ onChange: (e) => w(e.target.value.trim())
94
+ })]
95
+ }),
96
+ /* @__PURE__ */ g("div", {
97
+ className: "space-y-1",
98
+ children: [/* @__PURE__ */ h(c, {
99
+ htmlFor: "rate-weight",
100
+ children: y("movethewheels.shipping.form.weight", "Weight (kg)")
101
+ }), /* @__PURE__ */ h(s, {
102
+ id: "rate-weight",
103
+ inputMode: "decimal",
104
+ value: T,
105
+ onChange: (e) => E(e.target.value)
106
+ })]
107
+ })
108
+ ]
109
+ }),
110
+ /* @__PURE__ */ g("div", {
111
+ className: "grid gap-3 sm:grid-cols-3",
112
+ children: [
113
+ /* @__PURE__ */ g("div", {
114
+ className: "space-y-1",
115
+ children: [/* @__PURE__ */ h(c, {
116
+ htmlFor: "rate-length",
117
+ children: y("movethewheels.shipping.form.length", "Length (cm, optional)")
118
+ }), /* @__PURE__ */ h(s, {
119
+ id: "rate-length",
120
+ inputMode: "decimal",
121
+ value: D,
122
+ onChange: (e) => O(e.target.value)
123
+ })]
124
+ }),
125
+ /* @__PURE__ */ g("div", {
126
+ className: "space-y-1",
127
+ children: [/* @__PURE__ */ h(c, {
128
+ htmlFor: "rate-width",
129
+ children: y("movethewheels.shipping.form.width", "Width (cm, optional)")
130
+ }), /* @__PURE__ */ h(s, {
131
+ id: "rate-width",
132
+ inputMode: "decimal",
133
+ value: k,
134
+ onChange: (e) => A(e.target.value)
135
+ })]
136
+ }),
137
+ /* @__PURE__ */ g("div", {
138
+ className: "space-y-1",
139
+ children: [/* @__PURE__ */ h(c, {
140
+ htmlFor: "rate-height",
141
+ children: y("movethewheels.shipping.form.height", "Height (cm, optional)")
142
+ }), /* @__PURE__ */ h(s, {
143
+ id: "rate-height",
144
+ inputMode: "decimal",
145
+ value: j,
146
+ onChange: (e) => M(e.target.value)
147
+ })]
148
+ })
149
+ ]
150
+ }),
151
+ /* @__PURE__ */ g("div", {
152
+ className: "flex items-center gap-2",
153
+ children: [/* @__PURE__ */ h(l, {
154
+ id: "rate-cod",
155
+ checked: N,
156
+ onCheckedChange: P
157
+ }), /* @__PURE__ */ h(c, {
158
+ htmlFor: "rate-cod",
159
+ children: y("movethewheels.shipping.form.cod", "Cash on delivery")
160
+ })]
161
+ }),
162
+ /* @__PURE__ */ h(d, { message: L }),
163
+ /* @__PURE__ */ h(n, {
164
+ type: "submit",
165
+ disabled: F,
166
+ children: F ? y("movethewheels.shipping.form.comparing", "Comparing…") : y("movethewheels.shipping.form.compare", "Compare rates")
167
+ })
168
+ ]
169
+ }) })] }), z && /* @__PURE__ */ h(f, {
170
+ result: z,
171
+ t: y
172
+ })]
173
+ })
174
+ });
175
+ }
176
+ //#endregion
177
+ export { y as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-movethewheels",
3
- "version": "2026.918.1",
3
+ "version": "2026.920.1",
4
4
  "description": "Burdenoff MoveTheWheels Microfrontend - Logistics & Supply Chain Management Platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -79,6 +79,7 @@ const SettingsPage = lazy(() => import('./pages/SettingsPage'));
79
79
  const AIAssistantPage = lazy(() => import('./pages/AIAssistantPage'));
80
80
  const DeliveryCapturePage = lazy(() => import('./pages/DeliveryCapturePage'));
81
81
  const MultiModalShipmentPage = lazy(() => import('./pages/MultiModalShipmentPage'));
82
+ const ShippingRatesPage = lazy(() => import('./pages/ShippingRatesPage'));
82
83
 
83
84
  export function MoveTheWheelsRoutes() {
84
85
  return (
@@ -102,6 +103,7 @@ export function MoveTheWheelsRoutes() {
102
103
 
103
104
  {/* Multi-modal freight execution (BOFF-4398) */}
104
105
  <Route path="shipments/multi-modal" element={<MultiModalShipmentPage />} />
106
+ <Route path="shipments/rates" element={<ShippingRatesPage />} />
105
107
 
106
108
  {/* Fleet */}
107
109
  <Route path="fleet" element={<FleetPage />} />
@@ -0,0 +1,199 @@
1
+ /**
2
+ * ShippingQuoteResults
3
+ *
4
+ * Every courier option from every carrier for one pincode pair, cheapest first,
5
+ * with the cheapest and fastest called out. A carrier that failed is listed
6
+ * with its reason instead of silently missing from the comparison.
7
+ *
8
+ * Wide screens get a table. On a phone a table pushes the price — the one
9
+ * number people compare on — off-screen, so each courier is a card instead.
10
+ */
11
+
12
+ import { AlertTriangle } from 'lucide-react';
13
+
14
+ import { StatusBadge } from './StatusBadge';
15
+ import {
16
+ Card,
17
+ CardContent,
18
+ CardHeader,
19
+ CardTitle,
20
+ Table,
21
+ TableBody,
22
+ TableCell,
23
+ TableHead,
24
+ TableHeader,
25
+ TableRow,
26
+ } from './ui';
27
+
28
+ import type { ShippingQuote, ShippingQuoteResult } from '../hooks/useShippingCarriers';
29
+ import { formatCurrency } from '../utils/formatters';
30
+
31
+ interface ShippingQuoteResultsProps {
32
+ result: ShippingQuoteResult;
33
+ t: (key: string, fallback: string) => string;
34
+ }
35
+
36
+ const HIGHLIGHT = { bg: 'bg-status-success-bg-subtle', text: 'text-status-success-text' };
37
+ const INFO = { bg: 'bg-status-info-bg-subtle', text: 'text-status-info-text' };
38
+
39
+ function transit(quote: ShippingQuote, t: ShippingQuoteResultsProps['t']): string {
40
+ if (quote.estimatedDeliveryDays === null) return quote.estimatedDeliveryDate ?? '—';
41
+ const unit =
42
+ quote.estimatedDeliveryDays === 1
43
+ ? t('movethewheels.shipping.results.day', 'day')
44
+ : t('movethewheels.shipping.results.days', 'days');
45
+ const days = `${quote.estimatedDeliveryDays} ${unit}`;
46
+ return quote.estimatedDeliveryDate ? `${days} · ${quote.estimatedDeliveryDate}` : days;
47
+ }
48
+
49
+ function modeLabel(quote: ShippingQuote, t: ShippingQuoteResultsProps['t']): string {
50
+ if (quote.mode === 'AIR') return t('movethewheels.shipping.results.air', 'Air');
51
+ if (quote.mode === 'SURFACE') return t('movethewheels.shipping.results.surface', 'Surface');
52
+ return '—';
53
+ }
54
+
55
+ function QuoteBadges({ quote, t }: { quote: ShippingQuote; t: ShippingQuoteResultsProps['t'] }) {
56
+ return (
57
+ <>
58
+ {quote.isCheapest && (
59
+ <StatusBadge
60
+ status={t('movethewheels.shipping.results.cheapest', 'Cheapest')}
61
+ customColors={HIGHLIGHT}
62
+ size="sm"
63
+ showDot={false}
64
+ />
65
+ )}
66
+ {quote.isFastest && (
67
+ <StatusBadge
68
+ status={t('movethewheels.shipping.results.fastest', 'Fastest')}
69
+ customColors={INFO}
70
+ size="sm"
71
+ showDot={false}
72
+ />
73
+ )}
74
+ {quote.isRecommended && (
75
+ <StatusBadge
76
+ status={t('movethewheels.shipping.results.recommended', 'Recommended')}
77
+ customColors={INFO}
78
+ size="sm"
79
+ showDot={false}
80
+ />
81
+ )}
82
+ </>
83
+ );
84
+ }
85
+
86
+ export function ShippingQuoteResults({ result, t }: ShippingQuoteResultsProps) {
87
+ const failed = result.providers.filter((provider) => !provider.ok);
88
+
89
+ return (
90
+ <div className="space-y-4">
91
+ {failed.map((provider) => (
92
+ <div
93
+ key={provider.provider}
94
+ role="alert"
95
+ 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"
96
+ >
97
+ <AlertTriangle size={18} className="mt-0.5 shrink-0" aria-hidden="true" />
98
+ <span>{provider.errorMessage}</span>
99
+ </div>
100
+ ))}
101
+
102
+ <Card>
103
+ <CardHeader>
104
+ <CardTitle>
105
+ {t('movethewheels.shipping.results.title', 'Courier options')} ({result.quotes.length})
106
+ </CardTitle>
107
+ <p className="text-sm text-text-secondary">
108
+ {t('movethewheels.shipping.results.chargeableWeight', 'Charged on')}{' '}
109
+ {result.chargeableWeightKg} kg
110
+ </p>
111
+ </CardHeader>
112
+ <CardContent className="p-0">
113
+ {result.quotes.length === 0 ? (
114
+ <p className="px-6 pb-6 text-sm text-text-secondary">
115
+ {failed.length > 0
116
+ ? t(
117
+ 'movethewheels.shipping.results.allFailed',
118
+ 'No rates could be fetched — see the message above.'
119
+ )
120
+ : t(
121
+ 'movethewheels.shipping.results.noServiceable',
122
+ 'No courier serves this pincode pair.'
123
+ )}
124
+ </p>
125
+ ) : (
126
+ <>
127
+ <ul
128
+ className="divide-y md:hidden"
129
+ aria-label={t('movethewheels.shipping.results.title', 'Courier options')}
130
+ >
131
+ {result.quotes.map((quote) => (
132
+ <li key={`${quote.provider}:${quote.courierId}`} className="space-y-2 px-4 py-4">
133
+ <div className="flex items-start justify-between gap-3">
134
+ <span className="min-w-0 font-medium">{quote.courierName}</span>
135
+ <span className="shrink-0 text-lg font-semibold">
136
+ {formatCurrency(quote.price, quote.currency)}
137
+ </span>
138
+ </div>
139
+ <div className="flex flex-wrap items-center gap-2">
140
+ <QuoteBadges quote={quote} t={t} />
141
+ </div>
142
+ <p className="text-sm text-text-secondary">
143
+ {[
144
+ modeLabel(quote, t),
145
+ transit(quote, t),
146
+ quote.rating === null ? null : `${quote.rating.toFixed(1)} ★`,
147
+ ]
148
+ .filter((part) => part !== '—' && part !== null)
149
+ .join(' · ')}
150
+ </p>
151
+ </li>
152
+ ))}
153
+ </ul>
154
+ <div className="hidden md:block">
155
+ <Table>
156
+ <TableHeader>
157
+ <TableRow>
158
+ <TableHead>
159
+ {t('movethewheels.shipping.results.courier', 'Courier')}
160
+ </TableHead>
161
+ <TableHead>{t('movethewheels.shipping.results.mode', 'Mode')}</TableHead>
162
+ <TableHead>
163
+ {t('movethewheels.shipping.results.transit', 'Estimated delivery')}
164
+ </TableHead>
165
+ <TableHead>{t('movethewheels.shipping.results.rating', 'Rating')}</TableHead>
166
+ <TableHead className="text-right">
167
+ {t('movethewheels.shipping.results.price', 'Price')}
168
+ </TableHead>
169
+ </TableRow>
170
+ </TableHeader>
171
+ <TableBody>
172
+ {result.quotes.map((quote) => (
173
+ <TableRow key={`${quote.provider}:${quote.courierId}`}>
174
+ <TableCell>
175
+ <div className="flex flex-wrap items-center gap-2">
176
+ <span className="font-medium">{quote.courierName}</span>
177
+ <QuoteBadges quote={quote} t={t} />
178
+ </div>
179
+ </TableCell>
180
+ <TableCell>{modeLabel(quote, t)}</TableCell>
181
+ <TableCell>{transit(quote, t)}</TableCell>
182
+ <TableCell>
183
+ {quote.rating === null ? '—' : quote.rating.toFixed(1)}
184
+ </TableCell>
185
+ <TableCell className="text-right font-medium">
186
+ {formatCurrency(quote.price, quote.currency)}
187
+ </TableCell>
188
+ </TableRow>
189
+ ))}
190
+ </TableBody>
191
+ </Table>
192
+ </div>
193
+ </>
194
+ )}
195
+ </CardContent>
196
+ </Card>
197
+ </div>
198
+ );
199
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Shipping rate comparison — every carrier's price for one pincode pair, on top
3
+ * of wspace-movethewheels-svc's carrier module. The carrier accounts belong to
4
+ * the platform, so there is nothing for a workspace to configure.
5
+ */
6
+
7
+ import { useCallback, useMemo } from 'react';
8
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
9
+ import { executeGraphQL } from '../utils/graphqlClient';
10
+
11
+ export type CarrierProvider = 'SHIPROCKET';
12
+ export type CarrierServiceMode = 'SURFACE' | 'AIR';
13
+
14
+ export interface ShippingQuoteInput {
15
+ pickupPostcode: string;
16
+ deliveryPostcode: string;
17
+ weightKg: number;
18
+ lengthCm?: number;
19
+ widthCm?: number;
20
+ heightCm?: number;
21
+ cod?: boolean;
22
+ }
23
+
24
+ export interface ShippingQuote {
25
+ provider: CarrierProvider;
26
+ courierId: string;
27
+ courierName: string;
28
+ price: number;
29
+ currency: string;
30
+ estimatedDeliveryDays: number | null;
31
+ estimatedDeliveryDate: string | null;
32
+ rating: number | null;
33
+ mode: CarrierServiceMode | null;
34
+ isRecommended: boolean;
35
+ isCheapest: boolean;
36
+ isFastest: boolean;
37
+ }
38
+
39
+ export interface CarrierOutcome {
40
+ provider: CarrierProvider;
41
+ ok: boolean;
42
+ quoteCount: number;
43
+ errorMessage: string | null;
44
+ }
45
+
46
+ export interface ShippingQuoteResult {
47
+ chargeableWeightKg: number;
48
+ quotes: ShippingQuote[];
49
+ providers: CarrierOutcome[];
50
+ }
51
+
52
+ const SHIPPING_QUOTES_QUERY = `
53
+ query ShippingQuotes($input: ShippingQuoteInput!) {
54
+ shippingQuotes(input: $input) {
55
+ chargeableWeightKg
56
+ quotes {
57
+ provider
58
+ courierId
59
+ courierName
60
+ price
61
+ currency
62
+ estimatedDeliveryDays
63
+ estimatedDeliveryDate
64
+ rating
65
+ mode
66
+ isRecommended
67
+ isCheapest
68
+ isFastest
69
+ }
70
+ providers {
71
+ provider
72
+ ok
73
+ quoteCount
74
+ errorMessage
75
+ }
76
+ }
77
+ }
78
+ `;
79
+
80
+ function useGraphqlOptions() {
81
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
82
+ useMoveTheWheels();
83
+ return useMemo(
84
+ () => ({
85
+ apiGatewayUrl,
86
+ authToken,
87
+ workspaceToken,
88
+ workspaceId,
89
+ tenantId,
90
+ actorId: currentUser?.id,
91
+ }),
92
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
93
+ );
94
+ }
95
+
96
+ /** Imperative: a comparison is something the operator asks for, not page-load data. */
97
+ export function useShippingQuotes() {
98
+ const graphQLOptions = useGraphqlOptions();
99
+ return useCallback(
100
+ async (input: ShippingQuoteInput): Promise<ShippingQuoteResult> => {
101
+ const result = await executeGraphQL<{ shippingQuotes: ShippingQuoteResult }>(
102
+ graphQLOptions,
103
+ SHIPPING_QUOTES_QUERY,
104
+ { input }
105
+ );
106
+ return result.shippingQuotes;
107
+ },
108
+ [graphQLOptions]
109
+ );
110
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Shipping Rates Page
3
+ *
4
+ * Compare live courier rates between two pincodes across the platform's
5
+ * carriers (Shiprocket first). Nothing to set up: the carrier accounts belong
6
+ * to the platform. Read-only — comparing never books a shipment or spends
7
+ * carrier credit.
8
+ */
9
+
10
+ import { useState, type FormEvent } from 'react';
11
+ import { Scale } from 'lucide-react';
12
+
13
+ import { FormError } from '../components/FormError';
14
+ import { PageLayout } from '../components/PageLayout';
15
+ import { ShippingQuoteResults } from '../components/ShippingQuoteResults';
16
+ import {
17
+ Button,
18
+ Card,
19
+ CardContent,
20
+ CardHeader,
21
+ CardTitle,
22
+ Input,
23
+ Label,
24
+ Switch,
25
+ } from '../components/ui';
26
+ import { useTr } from '../hooks';
27
+ import {
28
+ useShippingQuotes,
29
+ type ShippingQuoteInput,
30
+ type ShippingQuoteResult,
31
+ } from '../hooks/useShippingCarriers';
32
+
33
+ const POSTCODE = /^[1-9]\d{5}$/;
34
+
35
+ const optionalNumber = (value: string): number | undefined => {
36
+ const parsed = Number(value);
37
+ return value.trim() !== '' && Number.isFinite(parsed) ? parsed : undefined;
38
+ };
39
+
40
+ export default function ShippingRatesPage() {
41
+ const tr = useTr();
42
+ const fetchQuotes = useShippingQuotes();
43
+
44
+ const [pickup, setPickup] = useState('');
45
+ const [delivery, setDelivery] = useState('');
46
+ const [weight, setWeight] = useState('');
47
+ const [length, setLength] = useState('');
48
+ const [width, setWidth] = useState('');
49
+ const [height, setHeight] = useState('');
50
+ const [cod, setCod] = useState(false);
51
+ const [isLoading, setIsLoading] = useState(false);
52
+ const [error, setError] = useState<string | null>(null);
53
+ const [result, setResult] = useState<ShippingQuoteResult | null>(null);
54
+
55
+ const handleCompare = async (event: FormEvent) => {
56
+ event.preventDefault();
57
+ setError(null);
58
+
59
+ const weightKg = optionalNumber(weight);
60
+ if (!POSTCODE.test(pickup) || !POSTCODE.test(delivery)) {
61
+ setError(
62
+ tr(
63
+ 'movethewheels.shipping.form.postcodeError',
64
+ 'Enter valid 6-digit pickup and delivery postcodes'
65
+ )
66
+ );
67
+ return;
68
+ }
69
+ if (weightKg === undefined || weightKg <= 0) {
70
+ setError(tr('movethewheels.shipping.form.weightError', 'Enter the package weight in kg'));
71
+ return;
72
+ }
73
+ const dims = [optionalNumber(length), optionalNumber(width), optionalNumber(height)];
74
+ const given = dims.filter((d) => d !== undefined).length;
75
+ if (given !== 0 && given !== 3) {
76
+ setError(
77
+ tr(
78
+ 'movethewheels.shipping.form.dimensionsError',
79
+ 'Enter length, width and height together, or leave all three empty'
80
+ )
81
+ );
82
+ return;
83
+ }
84
+
85
+ const input: ShippingQuoteInput = {
86
+ pickupPostcode: pickup,
87
+ deliveryPostcode: delivery,
88
+ weightKg,
89
+ cod,
90
+ ...(given === 3 ? { lengthCm: dims[0], widthCm: dims[1], heightCm: dims[2] } : {}),
91
+ };
92
+
93
+ setIsLoading(true);
94
+ try {
95
+ setResult(await fetchQuotes(input));
96
+ } catch (e: unknown) {
97
+ setResult(null);
98
+ setError(
99
+ e instanceof Error
100
+ ? e.message
101
+ : tr('movethewheels.shipping.form.failed', 'Could not fetch rates')
102
+ );
103
+ } finally {
104
+ setIsLoading(false);
105
+ }
106
+ };
107
+
108
+ return (
109
+ <PageLayout
110
+ title={tr('movethewheels.shipping.title', 'Compare Rates')}
111
+ subtitle={tr(
112
+ 'movethewheels.shipping.subtitle',
113
+ 'Enter a pickup and a delivery pincode to see every courier available for that route, with price and delivery time.'
114
+ )}
115
+ icon={<Scale size={24} />}
116
+ >
117
+ <div className="space-y-6">
118
+ <Card>
119
+ <CardHeader>
120
+ <CardTitle>{tr('movethewheels.shipping.form.title', 'Shipment details')}</CardTitle>
121
+ </CardHeader>
122
+ <CardContent>
123
+ <form onSubmit={(event) => void handleCompare(event)} className="space-y-4">
124
+ <div className="grid gap-3 sm:grid-cols-3">
125
+ <div className="space-y-1">
126
+ <Label htmlFor="rate-pickup">
127
+ {tr('movethewheels.shipping.form.pickup', 'Pickup postcode')}
128
+ </Label>
129
+ <Input
130
+ id="rate-pickup"
131
+ inputMode="numeric"
132
+ maxLength={6}
133
+ value={pickup}
134
+ onChange={(e) => setPickup(e.target.value.trim())}
135
+ />
136
+ </div>
137
+ <div className="space-y-1">
138
+ <Label htmlFor="rate-delivery">
139
+ {tr('movethewheels.shipping.form.delivery', 'Delivery postcode')}
140
+ </Label>
141
+ <Input
142
+ id="rate-delivery"
143
+ inputMode="numeric"
144
+ maxLength={6}
145
+ value={delivery}
146
+ onChange={(e) => setDelivery(e.target.value.trim())}
147
+ />
148
+ </div>
149
+ <div className="space-y-1">
150
+ <Label htmlFor="rate-weight">
151
+ {tr('movethewheels.shipping.form.weight', 'Weight (kg)')}
152
+ </Label>
153
+ <Input
154
+ id="rate-weight"
155
+ inputMode="decimal"
156
+ value={weight}
157
+ onChange={(e) => setWeight(e.target.value)}
158
+ />
159
+ </div>
160
+ </div>
161
+ <div className="grid gap-3 sm:grid-cols-3">
162
+ <div className="space-y-1">
163
+ <Label htmlFor="rate-length">
164
+ {tr('movethewheels.shipping.form.length', 'Length (cm, optional)')}
165
+ </Label>
166
+ <Input
167
+ id="rate-length"
168
+ inputMode="decimal"
169
+ value={length}
170
+ onChange={(e) => setLength(e.target.value)}
171
+ />
172
+ </div>
173
+ <div className="space-y-1">
174
+ <Label htmlFor="rate-width">
175
+ {tr('movethewheels.shipping.form.width', 'Width (cm, optional)')}
176
+ </Label>
177
+ <Input
178
+ id="rate-width"
179
+ inputMode="decimal"
180
+ value={width}
181
+ onChange={(e) => setWidth(e.target.value)}
182
+ />
183
+ </div>
184
+ <div className="space-y-1">
185
+ <Label htmlFor="rate-height">
186
+ {tr('movethewheels.shipping.form.height', 'Height (cm, optional)')}
187
+ </Label>
188
+ <Input
189
+ id="rate-height"
190
+ inputMode="decimal"
191
+ value={height}
192
+ onChange={(e) => setHeight(e.target.value)}
193
+ />
194
+ </div>
195
+ </div>
196
+ <div className="flex items-center gap-2">
197
+ <Switch id="rate-cod" checked={cod} onCheckedChange={setCod} />
198
+ <Label htmlFor="rate-cod">
199
+ {tr('movethewheels.shipping.form.cod', 'Cash on delivery')}
200
+ </Label>
201
+ </div>
202
+ <FormError message={error} />
203
+ <Button type="submit" disabled={isLoading}>
204
+ {isLoading
205
+ ? tr('movethewheels.shipping.form.comparing', 'Comparing…')
206
+ : tr('movethewheels.shipping.form.compare', 'Compare rates')}
207
+ </Button>
208
+ </form>
209
+ </CardContent>
210
+ </Card>
211
+
212
+ {result && <ShippingQuoteResults result={result} t={tr} />}
213
+ </div>
214
+ </PageLayout>
215
+ );
216
+ }