@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.
- package/dist/MoveTheWheelsRoutes.js +7 -3
- package/dist/components/ShippingQuoteResults.js +142 -0
- package/dist/hooks/useShippingCarriers.js +29 -0
- package/dist/movethewheels/components/ShippingQuoteResults.d.ts +8 -0
- package/dist/movethewheels/hooks/useShippingCarriers.d.ts +43 -0
- package/dist/movethewheels/pages/ShippingRatesPage.d.ts +9 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/scale.js +26 -0
- package/dist/pages/ShippingRatesPage.js +177 -0
- package/package.json +1 -1
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
- package/src/movethewheels/components/ShippingQuoteResults.tsx +199 -0
- package/src/movethewheels/hooks/useShippingCarriers.ts +110 -0
- package/src/movethewheels/pages/ShippingRatesPage.tsx +216 -0
|
@@ -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
|
|
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 {
|
|
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
|
@@ -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
|
+
}
|