@burdenoff/microfe-movethewheels 2026.703.6 → 2026.703.8
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/{AIAssistantPage-BmILdHgO.js → AIAssistantPage-MWJLAMN_.js} +5 -5
- package/dist/{AnalyticsPage-BSlsiSDE.js → AnalyticsPage-900jpV2T.js} +9 -9
- package/dist/{CreateOrderPage-DaCgVaO3.js → CreateOrderPage-V8aXfcpN.js} +11 -11
- package/dist/{CustomerDetailsPage-DErpbafW.js → CustomerDetailsPage-CHzg75Q5.js} +16 -16
- package/dist/{CustomersPage-C1lRU6xs.js → CustomersPage-CwtVspuq.js} +24 -22
- package/dist/{DashboardPage-Q5-5EKAU.js → DashboardPage-DiD-cWwD.js} +17 -17
- package/dist/DataTable-CmdZd6iV.js +369 -0
- package/dist/{DocumentsCard-T9y6cv4-.js → DocumentsCard-y2ybnAlu.js} +4 -4
- package/dist/{DriverDetailsPage-DnHGkDC7.js → DriverDetailsPage-DjaXaATQ.js} +14 -14
- package/dist/{DriversPage-C0ujWVOn.js → DriversPage-DW-FGPFW.js} +26 -24
- package/dist/{FinancePage-CA2XEgsQ.js → FinancePage-DYadOUzC.js} +9 -9
- package/dist/{FleetPage-DrvXW7wK.js → FleetPage-P9k4zZqP.js} +10 -10
- package/dist/{FormError-CRsMP7TQ.js → FormError-D4T5FWZA.js} +1 -1
- package/dist/{ImportExportPage-Cac9HjFx.js → ImportExportPage-BhWGfbH3.js} +6 -6
- package/dist/{InventoryPage-Ezf4gqKI.js → InventoryPage-BWikjZMW.js} +10 -10
- package/dist/{LiveTrackingPage-CzbVwDqK.js → LiveTrackingPage-UQBWlthJ.js} +10 -10
- package/dist/{MarketplacePage-BHDmSQmo.js → MarketplacePage-DMH0HNDH.js} +9 -9
- package/dist/{MetricCard-DQlDNqmK.js → MetricCard-CqU5WsyW.js} +2 -2
- package/dist/{Notice-GmA7JDR3.js → Notice-BrXJ85dA.js} +2 -2
- package/dist/{OrderDetailsPage-CR0Wc1aN.js → OrderDetailsPage-2J4ua0wd.js} +28 -24
- package/dist/OrdersListPage-DvICveRv.js +388 -0
- package/dist/{PageLayout-DX1Ou6V6.js → PageLayout-j9Zw3wBN.js} +542 -517
- package/dist/{ProductDetailsPage-B_ns2MOe.js → ProductDetailsPage-BSjWLrty.js} +9 -9
- package/dist/{ProductsPage-c92BsEoA.js → ProductsPage-Bn1U2jTj.js} +21 -19
- package/dist/{ReportsPage-CPLXtLT2.js → ReportsPage-MU8F9DQC.js} +9 -9
- package/dist/{RouteDetailsPage-DOfcRiFv.js → RouteDetailsPage-CgSY1CTd.js} +18 -18
- package/dist/{RoutesPage-BZGublby.js → RoutesPage-Dqd5Don6.js} +19 -17
- package/dist/{SettingsPage-5cweX_xh.js → SettingsPage-2a05Sq2k.js} +4 -4
- package/dist/{Sparkline-zSXvG6vA.js → Sparkline-DTGdlxUH.js} +1 -1
- package/dist/{StatusBadge-oZjownr1.js → StatusBadge-BfiF-nNI.js} +1 -1
- package/dist/{TrackingPage-Ln9qVflK.js → TrackingPage-SnjW6qeD.js} +11 -11
- package/dist/{VehicleDetailsPage-CrkzE0dN.js → VehicleDetailsPage-CvRMJ2zC.js} +12 -12
- package/dist/{VehiclesPage-CTQWH7af.js → VehiclesPage-qxf-1C1B.js} +26 -24
- package/dist/{WarehouseDetailsPage-CwDLaG65.js → WarehouseDetailsPage-D0Wyyj1Y.js} +11 -11
- package/dist/{WarehousesPage-KW02DvZg.js → WarehousesPage-44Wj7AGT.js} +19 -17
- package/dist/{arrow-left-DJDw_Yqv.js → arrow-left-1dVWv2dg.js} +1 -1
- package/dist/{arrow-right-Cb2fLTPJ.js → arrow-right-PUCNATnk.js} +1 -1
- package/dist/{arrow-up-PxaOIyiT.js → arrow-up-VQuGMABN.js} +1 -1
- package/dist/{box-hSJVtvGf.js → box-DW1KNP8j.js} +1 -1
- package/dist/{boxes-DDnxEkT5.js → boxes-D6I00XqA.js} +1 -1
- package/dist/{chart-column-xXgAZvRl.js → chart-column-BwQ5H9ol.js} +1 -1
- package/dist/{charts-CfCwQg6o.js → charts-Dv_zxP7L.js} +2 -2
- package/dist/{chevron-left-dhEGJYdh.js → chevron-left-DzNxoaBG.js} +1 -1
- package/dist/{chevron-right-Cud2NDvm.js → chevron-right-CKm3ZC1T.js} +1 -1
- package/dist/{circle-alert-Rx0p3D5w.js → circle-alert-CtBm6oIb.js} +1 -1
- package/dist/{circle-check-BCxRFdFo.js → circle-check-B_Y7FDyR.js} +1 -1
- package/dist/{circle-check-big-BcIJ8xjx.js → circle-check-big-H5pLiMBW.js} +1 -1
- package/dist/{clock-B2_YxJgB.js → clock-B4-qjGuB.js} +1 -1
- package/dist/{dollar-sign-CQmAICv4.js → dollar-sign-CYGHy2oS.js} +1 -1
- package/dist/{download-CG4Tponq.js → download-BJonq2ty.js} +1 -1
- package/dist/{file-text-CzH-zMub.js → file-text-C-cZx58r.js} +1 -1
- package/dist/{filter-C0u2Lwfn.js → filter-B_S6WOqr.js} +1 -1
- package/dist/{fuel-C2KELcM-.js → fuel-Cjt6IxKr.js} +1 -1
- package/dist/index.js +33 -33
- package/dist/{map-FuPu5aTw.js → map-CO0aWUae.js} +1 -1
- package/dist/{map-pin-czpLv9uy.js → map-pin-BISLzLwb.js} +1 -1
- package/dist/{maps-D9m7PKrs.js → maps-CtfuCAmh.js} +1 -1
- package/dist/{navigation-D5_pdCSS.js → navigation-BFki5UJz.js} +1 -1
- package/dist/{package-B0sdi6lU.js → package-UyaWiALC.js} +1 -1
- package/dist/{phone-V2nuzPu9.js → phone-Bav_V7rR.js} +1 -1
- package/dist/{plus-BqQxkfoU.js → plus-Bk_G-lFC.js} +1 -1
- package/dist/{refresh-cw-CEOAPEcV.js → refresh-cw-C4-jecjy.js} +1 -1
- package/dist/{route-CZWcW-Dw.js → route-WpCP6F1x.js} +1 -1
- package/dist/{save-DDFvrxWl.js → save-CcfoIn2J.js} +1 -1
- package/dist/{search-D7Ya6jxj.js → search-C3YRxW9e.js} +1 -1
- package/dist/{square-pen-DoZZS4Lw.js → square-pen-Dr1X-P2s.js} +1 -1
- package/dist/{star-B7PgVu5j.js → star-1Ixfuzai.js} +1 -1
- package/dist/{store-YVzcIG4A.js → store-BwU935v5.js} +1 -1
- package/dist/{trash-2-BegnBzwI.js → trash-2-CC4KugEJ.js} +1 -1
- package/dist/{trending-up-DBd3XNdb.js → trending-up-1brglj1E.js} +1 -1
- package/dist/{triangle-alert-Bxk3Kgs6.js → triangle-alert-Dh56Yu3-.js} +1 -1
- package/dist/{truck-Clrn0oHp.js → truck-DxhtLfkG.js} +1 -1
- package/dist/{upload-Df5JmMwH.js → upload-CLf0JLXW.js} +1 -1
- package/dist/{useAssistant-Cb8nnIVC.js → useAssistant-B9qBtRKm.js} +1 -1
- package/dist/{useCustomers-Dq2IR2HQ.js → useCustomers-BQ4AIEAT.js} +1 -1
- package/dist/{useFleet-CapnWBcA.js → useFleet-BXSocBD_.js} +1 -1
- package/dist/{useImportExport-BoGzrlJy.js → useImportExport-7-O8noRJ.js} +1 -1
- package/dist/{useInventory-Ci6M2d9f.js → useInventory-DmZficcp.js} +1 -1
- package/dist/{useOrders-BuQU1OHX.js → useOrders-B5FV0kcz.js} +1 -1
- package/dist/{useReports-DIx8W7CO.js → useReports-hUXb8ryJ.js} +1 -1
- package/dist/{useRoutes-D7EvI_pm.js → useRoutes-C_ZCYJ_h.js} +1 -1
- package/dist/{useSettings-BhX251UQ.js → useSettings-CteNasBE.js} +1 -1
- package/dist/{useTracking-C40jmZ6W.js → useTracking-B7ttexRe.js} +3 -3
- package/dist/{user-BlcJrbtE.js → user-B1IjWJtn.js} +1 -1
- package/dist/{user-check-DtnQmimP.js → user-check-Cx6fHHll.js} +1 -1
- package/dist/{users-Cvwko-Dx.js → users-BKmsb79k.js} +1 -1
- package/dist/{warehouse-LWLVTk6G.js → warehouse-DE1irFyJ.js} +1 -1
- package/dist/{wrench-Cme0RbyR.js → wrench-DTJGFs17.js} +1 -1
- package/dist/{x-D8C9Pg5V.js → x-CvsUGJy1.js} +1 -1
- package/package.json +1 -1
- package/src/movethewheels/components/DataTable.tsx +40 -0
- package/src/movethewheels/components/GlassPanel.tsx +1 -3
- package/src/movethewheels/components/MetricCard.tsx +16 -3
- package/src/movethewheels/components/charts/StatusDonut.tsx +2 -6
- package/src/movethewheels/components/charts/TrendChart.tsx +2 -6
- package/src/movethewheels/components/maps/LogisticsMap.tsx +1 -4
- package/src/movethewheels/components/maps/RouteMap.tsx +3 -5
- package/src/movethewheels/components/maps/TrackingMap.tsx +11 -2
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +3 -2
- package/src/movethewheels/pages/CustomersPage.tsx +3 -1
- package/src/movethewheels/pages/DriverDetailsPage.tsx +2 -2
- package/src/movethewheels/pages/DriversPage.tsx +3 -1
- package/src/movethewheels/pages/OrderDetailsPage.tsx +2 -1
- package/src/movethewheels/pages/OrdersListPage.tsx +3 -1
- package/src/movethewheels/pages/ProductsPage.tsx +3 -1
- package/src/movethewheels/pages/RoutesPage.tsx +3 -1
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +2 -2
- package/src/movethewheels/pages/VehiclesPage.tsx +12 -2
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +3 -1
- package/src/movethewheels/pages/WarehousesPage.tsx +3 -1
- package/src/movethewheels/utils/graphqlClient.ts +103 -8
- package/dist/DataTable-KjzbwnRL.js +0 -340
- package/dist/OrdersListPage-C8Bu6wXM.js +0 -386
|
@@ -78,7 +78,13 @@ const accentChipClasses: Record<MetricAccent, string> = {
|
|
|
78
78
|
function ArrowIcon({ direction }: { direction: 'up' | 'down' | 'flat' }) {
|
|
79
79
|
if (direction === 'flat') {
|
|
80
80
|
return (
|
|
81
|
-
<svg
|
|
81
|
+
<svg
|
|
82
|
+
className="h-3.5 w-3.5"
|
|
83
|
+
fill="none"
|
|
84
|
+
viewBox="0 0 24 24"
|
|
85
|
+
stroke="currentColor"
|
|
86
|
+
aria-hidden
|
|
87
|
+
>
|
|
82
88
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 12h14" />
|
|
83
89
|
</svg>
|
|
84
90
|
);
|
|
@@ -91,7 +97,12 @@ function ArrowIcon({ direction }: { direction: 'up' | 'down' | 'flat' }) {
|
|
|
91
97
|
stroke="currentColor"
|
|
92
98
|
aria-hidden
|
|
93
99
|
>
|
|
94
|
-
<path
|
|
100
|
+
<path
|
|
101
|
+
strokeLinecap="round"
|
|
102
|
+
strokeLinejoin="round"
|
|
103
|
+
strokeWidth={2.5}
|
|
104
|
+
d="M12 19V5m0 0l-6 6m6-6l6 6"
|
|
105
|
+
/>
|
|
95
106
|
</svg>
|
|
96
107
|
);
|
|
97
108
|
}
|
|
@@ -278,7 +289,9 @@ export function MetricCardSkeleton({ size = 'md' }: { size?: 'sm' | 'md' | 'lg'
|
|
|
278
289
|
<div className="h-8 w-32 rounded bg-bg-sunken motion-safe:animate-vc-shimmer" />
|
|
279
290
|
<div className="h-3 w-20 rounded bg-bg-sunken motion-safe:animate-vc-shimmer" />
|
|
280
291
|
</div>
|
|
281
|
-
<div
|
|
292
|
+
<div
|
|
293
|
+
className={cn('rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer', styles.icon)}
|
|
294
|
+
/>
|
|
282
295
|
</div>
|
|
283
296
|
</div>
|
|
284
297
|
</GlassCard>
|
|
@@ -67,9 +67,7 @@ export function StatusDonut({
|
|
|
67
67
|
<Tooltip
|
|
68
68
|
content={
|
|
69
69
|
<ChartTooltip
|
|
70
|
-
formatter={(v) =>
|
|
71
|
-
valueFormatter ? valueFormatter(Number(v)) : String(v)
|
|
72
|
-
}
|
|
70
|
+
formatter={(v) => (valueFormatter ? valueFormatter(Number(v)) : String(v))}
|
|
73
71
|
/>
|
|
74
72
|
}
|
|
75
73
|
/>
|
|
@@ -77,9 +75,7 @@ export function StatusDonut({
|
|
|
77
75
|
</ResponsiveContainer>
|
|
78
76
|
{centerLabel != null && (
|
|
79
77
|
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center">
|
|
80
|
-
<span className="text-2xl font-bold tabular-nums text-text-primary">
|
|
81
|
-
{centerLabel}
|
|
82
|
-
</span>
|
|
78
|
+
<span className="text-2xl font-bold tabular-nums text-text-primary">{centerLabel}</span>
|
|
83
79
|
{centerSubLabel && (
|
|
84
80
|
<span className="text-xs text-text-secondary">{centerSubLabel}</span>
|
|
85
81
|
)}
|
|
@@ -99,9 +99,7 @@ export function TrendChart({
|
|
|
99
99
|
tickLine={false}
|
|
100
100
|
axisLine={false}
|
|
101
101
|
width={44}
|
|
102
|
-
tickFormatter={
|
|
103
|
-
valueFormatter ? (v: number) => valueFormatter(v, '') : undefined
|
|
104
|
-
}
|
|
102
|
+
tickFormatter={valueFormatter ? (v: number) => valueFormatter(v, '') : undefined}
|
|
105
103
|
/>
|
|
106
104
|
<Tooltip
|
|
107
105
|
cursor={{ stroke: chartTokens.axis, strokeOpacity: 0.3 }}
|
|
@@ -144,9 +142,7 @@ export function TrendChart({
|
|
|
144
142
|
tickLine={false}
|
|
145
143
|
axisLine={false}
|
|
146
144
|
width={44}
|
|
147
|
-
tickFormatter={
|
|
148
|
-
valueFormatter ? (v: number) => valueFormatter(v, '') : undefined
|
|
149
|
-
}
|
|
145
|
+
tickFormatter={valueFormatter ? (v: number) => valueFormatter(v, '') : undefined}
|
|
150
146
|
/>
|
|
151
147
|
<Tooltip
|
|
152
148
|
cursor={{ stroke: chartTokens.axis, strokeOpacity: 0.3 }}
|
|
@@ -40,10 +40,7 @@ export function LogisticsMap({
|
|
|
40
40
|
fitToMarkers = true,
|
|
41
41
|
emptyMessage = 'No live locations yet',
|
|
42
42
|
}: LogisticsMapProps) {
|
|
43
|
-
const validMarkers = useMemo(
|
|
44
|
-
() => markers.filter((m) => isRealCoord(m.lat, m.lng)),
|
|
45
|
-
[markers]
|
|
46
|
-
);
|
|
43
|
+
const validMarkers = useMemo(() => markers.filter((m) => isRealCoord(m.lat, m.lng)), [markers]);
|
|
47
44
|
|
|
48
45
|
const validRoutes = useMemo(
|
|
49
46
|
() =>
|
|
@@ -31,7 +31,8 @@ export function RouteMap({
|
|
|
31
31
|
emptyMessage = 'No stops with coordinates yet',
|
|
32
32
|
}: RouteMapProps) {
|
|
33
33
|
const ordered = useMemo(
|
|
34
|
-
() =>
|
|
34
|
+
() =>
|
|
35
|
+
[...stops].filter((s) => isRealCoord(s.lat, s.lng)).sort((a, b) => a.sequence - b.sequence),
|
|
35
36
|
[stops]
|
|
36
37
|
);
|
|
37
38
|
|
|
@@ -48,10 +49,7 @@ export function RouteMap({
|
|
|
48
49
|
emptyState={<p className="text-sm text-text-secondary">{emptyMessage}</p>}
|
|
49
50
|
>
|
|
50
51
|
{line.length >= 2 && (
|
|
51
|
-
<Polyline
|
|
52
|
-
positions={line}
|
|
53
|
-
pathOptions={{ color: routeColor, weight: 4, opacity: 0.85 }}
|
|
54
|
-
/>
|
|
52
|
+
<Polyline positions={line} pathOptions={{ color: routeColor, weight: 4, opacity: 0.85 }} />
|
|
55
53
|
)}
|
|
56
54
|
{ordered.map((s) => (
|
|
57
55
|
<Marker
|
|
@@ -70,11 +70,20 @@ export function TrackingMap({
|
|
|
70
70
|
{trailLine.length >= 2 && (
|
|
71
71
|
<Polyline
|
|
72
72
|
positions={trailLine}
|
|
73
|
-
pathOptions={{
|
|
73
|
+
pathOptions={{
|
|
74
|
+
color: trailColor,
|
|
75
|
+
weight: 4,
|
|
76
|
+
opacity: 0.85,
|
|
77
|
+
dashArray: '1 8',
|
|
78
|
+
lineCap: 'round',
|
|
79
|
+
}}
|
|
74
80
|
/>
|
|
75
81
|
)}
|
|
76
82
|
{originOk && (
|
|
77
|
-
<Marker
|
|
83
|
+
<Marker
|
|
84
|
+
position={[originOk.lat, originOk.lng]}
|
|
85
|
+
icon={createPinIcon('origin', { glyph: false })}
|
|
86
|
+
>
|
|
78
87
|
<Popup>
|
|
79
88
|
<span className="font-semibold">{originOk.label ?? 'Origin'}</span>
|
|
80
89
|
</Popup>
|
|
@@ -116,8 +116,9 @@ function NewCustomerForm() {
|
|
|
116
116
|
}
|
|
117
117
|
>
|
|
118
118
|
<PagePurpose icon={<Users size={18} />}>
|
|
119
|
-
Add a customer once and their contact details auto-fill every future order you book for
|
|
120
|
-
Only a name is required — add email, phone and company to make deliveries easier to
|
|
119
|
+
Add a customer once and their contact details auto-fill every future order you book for
|
|
120
|
+
them. Only a name is required — add email, phone and company to make deliveries easier to
|
|
121
|
+
arrange.
|
|
121
122
|
</PagePurpose>
|
|
122
123
|
|
|
123
124
|
<form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
|
|
@@ -20,7 +20,7 @@ export default function CustomersPage() {
|
|
|
20
20
|
const { basePath, navigate } = useMoveTheWheels();
|
|
21
21
|
const [page, setPage] = useState(0);
|
|
22
22
|
const [pageSize, setPageSize] = useState(25);
|
|
23
|
-
const { customers, hasMore, isLoading, refetch } = useCustomers({
|
|
23
|
+
const { customers, hasMore, isLoading, error, refetch } = useCustomers({
|
|
24
24
|
limit: pageSize,
|
|
25
25
|
offset: page * pageSize,
|
|
26
26
|
});
|
|
@@ -151,6 +151,8 @@ export default function CustomersPage() {
|
|
|
151
151
|
columns={columns}
|
|
152
152
|
data={customers}
|
|
153
153
|
isLoading={isLoading}
|
|
154
|
+
error={error}
|
|
155
|
+
onRetry={refetch}
|
|
154
156
|
searchable
|
|
155
157
|
searchPlaceholder="Search customers..."
|
|
156
158
|
searchFields={['name', 'email', 'company'] as (keyof Customer)[]}
|
|
@@ -94,8 +94,8 @@ function NewDriverForm() {
|
|
|
94
94
|
}
|
|
95
95
|
>
|
|
96
96
|
<PagePurpose icon={<Users size={18} />}>
|
|
97
|
-
Add a driver so they can be assigned to orders and routes. An employee ID and name get them
|
|
98
|
-
the roster — add contact details and license number to keep the record complete.
|
|
97
|
+
Add a driver so they can be assigned to orders and routes. An employee ID and name get them
|
|
98
|
+
on the roster — add contact details and license number to keep the record complete.
|
|
99
99
|
</PagePurpose>
|
|
100
100
|
|
|
101
101
|
<form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
|
|
@@ -20,7 +20,7 @@ export default function DriversPage() {
|
|
|
20
20
|
const { basePath, navigate } = useMoveTheWheels();
|
|
21
21
|
const [page, setPage] = useState(0);
|
|
22
22
|
const [pageSize, setPageSize] = useState(25);
|
|
23
|
-
const { drivers, isLoading, refetch } = useDrivers({
|
|
23
|
+
const { drivers, isLoading, error, refetch } = useDrivers({
|
|
24
24
|
limit: pageSize,
|
|
25
25
|
offset: page * pageSize,
|
|
26
26
|
});
|
|
@@ -161,6 +161,8 @@ export default function DriversPage() {
|
|
|
161
161
|
columns={columns}
|
|
162
162
|
data={drivers}
|
|
163
163
|
isLoading={isLoading}
|
|
164
|
+
error={error}
|
|
165
|
+
onRetry={refetch}
|
|
164
166
|
searchable
|
|
165
167
|
searchPlaceholder="Search drivers..."
|
|
166
168
|
searchFields={['fullName', 'employeeId'] as (keyof Driver)[]}
|
|
@@ -548,7 +548,8 @@ export default function OrderDetailsPage() {
|
|
|
548
548
|
|
|
549
549
|
<div className="space-y-2">
|
|
550
550
|
<p className="text-sm text-text-secondary">
|
|
551
|
-
Current status:
|
|
551
|
+
Current status:{' '}
|
|
552
|
+
<span className="font-medium">{STATUS_LABELS[order.status]}</span>
|
|
552
553
|
</p>
|
|
553
554
|
{(() => {
|
|
554
555
|
const upcoming = nextStatus(order.status);
|
|
@@ -55,7 +55,7 @@ export default function OrdersListPage() {
|
|
|
55
55
|
|
|
56
56
|
const filtersActive = statusFilter !== 'all' || typeFilter !== 'all' || unassignedOnly;
|
|
57
57
|
|
|
58
|
-
const { orders, hasMore, isLoading, refetch } = useOrders({
|
|
58
|
+
const { orders, hasMore, isLoading, error, refetch } = useOrders({
|
|
59
59
|
filters: {
|
|
60
60
|
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
61
61
|
orderType: typeFilter === 'all' ? undefined : typeFilter,
|
|
@@ -346,6 +346,8 @@ export default function OrdersListPage() {
|
|
|
346
346
|
columns={columns}
|
|
347
347
|
data={orders}
|
|
348
348
|
isLoading={isLoading}
|
|
349
|
+
error={error}
|
|
350
|
+
onRetry={refetch}
|
|
349
351
|
searchable
|
|
350
352
|
searchPlaceholder={t('movethewheels.orders.searchPlaceholder', 'Search orders...')}
|
|
351
353
|
searchFields={['orderNumber', 'customerInfo'] as (keyof Order)[]}
|
|
@@ -19,7 +19,7 @@ export default function ProductsPage() {
|
|
|
19
19
|
const { basePath, navigate } = useMoveTheWheels();
|
|
20
20
|
const [page, setPage] = useState(0);
|
|
21
21
|
const [pageSize, setPageSize] = useState(25);
|
|
22
|
-
const { products, hasMore, isLoading, refetch } = useProducts({
|
|
22
|
+
const { products, hasMore, isLoading, error, refetch } = useProducts({
|
|
23
23
|
limit: pageSize,
|
|
24
24
|
offset: page * pageSize,
|
|
25
25
|
});
|
|
@@ -159,6 +159,8 @@ export default function ProductsPage() {
|
|
|
159
159
|
columns={columns}
|
|
160
160
|
data={products}
|
|
161
161
|
isLoading={isLoading}
|
|
162
|
+
error={error}
|
|
163
|
+
onRetry={refetch}
|
|
162
164
|
searchable
|
|
163
165
|
searchPlaceholder="Search products..."
|
|
164
166
|
searchFields={['name', 'sku', 'category'] as (keyof Product)[]}
|
|
@@ -20,7 +20,7 @@ export default function RoutesPage() {
|
|
|
20
20
|
const { basePath, navigate } = useMoveTheWheels();
|
|
21
21
|
const [page, setPage] = useState(0);
|
|
22
22
|
const [pageSize, setPageSize] = useState(25);
|
|
23
|
-
const { routes, hasMore, isLoading, refetch } = useRoutes({
|
|
23
|
+
const { routes, hasMore, isLoading, error, refetch } = useRoutes({
|
|
24
24
|
limit: pageSize,
|
|
25
25
|
offset: page * pageSize,
|
|
26
26
|
});
|
|
@@ -126,6 +126,8 @@ export default function RoutesPage() {
|
|
|
126
126
|
columns={columns}
|
|
127
127
|
data={routes}
|
|
128
128
|
isLoading={isLoading}
|
|
129
|
+
error={error}
|
|
130
|
+
onRetry={refetch}
|
|
129
131
|
searchable
|
|
130
132
|
searchPlaceholder="Search routes..."
|
|
131
133
|
searchFields={['name'] as (keyof Route)[]}
|
|
@@ -97,8 +97,8 @@ function NewVehicleForm() {
|
|
|
97
97
|
}
|
|
98
98
|
>
|
|
99
99
|
<PagePurpose icon={<Truck size={18} />}>
|
|
100
|
-
Add a vehicle so it can be assigned to deliveries and routes. Give it a fleet number and
|
|
101
|
-
to start — make, model and plate help identify it on the road.
|
|
100
|
+
Add a vehicle so it can be assigned to deliveries and routes. Give it a fleet number and
|
|
101
|
+
type to start — make, model and plate help identify it on the road.
|
|
102
102
|
</PagePurpose>
|
|
103
103
|
|
|
104
104
|
<form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
|
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useState } from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Truck,
|
|
8
|
+
Plus,
|
|
9
|
+
RefreshCw,
|
|
10
|
+
Download,
|
|
11
|
+
CircleCheck,
|
|
12
|
+
Route as RouteIcon,
|
|
13
|
+
Wrench,
|
|
14
|
+
} from 'lucide-react';
|
|
7
15
|
import { PageLayout } from '../components/PageLayout';
|
|
8
16
|
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
9
17
|
import { DataTable, type Column } from '../components/DataTable';
|
|
@@ -19,7 +27,7 @@ export default function VehiclesPage() {
|
|
|
19
27
|
const { basePath, navigate } = useMoveTheWheels();
|
|
20
28
|
const [page, setPage] = useState(0);
|
|
21
29
|
const [pageSize, setPageSize] = useState(25);
|
|
22
|
-
const { vehicles, isLoading, refetch } = useVehicles({
|
|
30
|
+
const { vehicles, isLoading, error, refetch } = useVehicles({
|
|
23
31
|
limit: pageSize,
|
|
24
32
|
offset: page * pageSize,
|
|
25
33
|
});
|
|
@@ -163,6 +171,8 @@ export default function VehiclesPage() {
|
|
|
163
171
|
columns={columns}
|
|
164
172
|
data={vehicles}
|
|
165
173
|
isLoading={isLoading}
|
|
174
|
+
error={error}
|
|
175
|
+
onRetry={refetch}
|
|
166
176
|
searchable
|
|
167
177
|
searchPlaceholder="Search vehicles..."
|
|
168
178
|
searchFields={['vehicleNumber', 'make', 'model'] as (keyof Vehicle)[]}
|
|
@@ -90,7 +90,9 @@ function NewWarehouseForm() {
|
|
|
90
90
|
navigate?.(joinPath(basePath, `inventory/warehouses/${created.id}`));
|
|
91
91
|
} catch (err) {
|
|
92
92
|
console.error('Failed to create warehouse:', err);
|
|
93
|
-
setError(
|
|
93
|
+
setError(
|
|
94
|
+
err instanceof Error ? err.message : 'Failed to create warehouse. Please try again.'
|
|
95
|
+
);
|
|
94
96
|
setSubmitting(false);
|
|
95
97
|
}
|
|
96
98
|
};
|
|
@@ -18,7 +18,7 @@ export default function WarehousesPage() {
|
|
|
18
18
|
const { basePath, navigate } = useMoveTheWheels();
|
|
19
19
|
const [page, setPage] = useState(0);
|
|
20
20
|
const [pageSize, setPageSize] = useState(25);
|
|
21
|
-
const { warehouses, hasMore, isLoading, refetch } = useWarehouses({
|
|
21
|
+
const { warehouses, hasMore, isLoading, error, refetch } = useWarehouses({
|
|
22
22
|
limit: pageSize,
|
|
23
23
|
offset: page * pageSize,
|
|
24
24
|
});
|
|
@@ -152,6 +152,8 @@ export default function WarehousesPage() {
|
|
|
152
152
|
columns={columns}
|
|
153
153
|
data={warehouses}
|
|
154
154
|
isLoading={isLoading}
|
|
155
|
+
error={error}
|
|
156
|
+
onRetry={refetch}
|
|
155
157
|
searchable
|
|
156
158
|
searchPlaceholder="Search warehouses..."
|
|
157
159
|
searchFields={['name', 'code'] as (keyof WarehouseType)[]}
|
|
@@ -21,12 +21,25 @@ export function resolveGraphqlUrl(apiGatewayUrl?: string): string {
|
|
|
21
21
|
|
|
22
22
|
const MAX_RETRIES = 3;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Per-attempt hard timeout. The workspace service runs a lean single-replica
|
|
26
|
+
* pool with no PgBouncer, so under a burst of concurrent queries a connection
|
|
27
|
+
* can be accepted by the gateway but never answered — the request then hangs
|
|
28
|
+
* indefinitely. `fetch` has no built-in timeout, so without this an in-flight
|
|
29
|
+
* request never settles and the caller's `isLoading` sticks true forever
|
|
30
|
+
* (perpetual skeletons). A bounded abort turns a hang into a transient error
|
|
31
|
+
* that the retry loop can recover from on a fresh connection. Real queries
|
|
32
|
+
* return in well under a second, so 12s is generous headroom.
|
|
33
|
+
*/
|
|
34
|
+
const REQUEST_TIMEOUT_MS = 12000;
|
|
35
|
+
|
|
24
36
|
/**
|
|
25
37
|
* A transient failure is a momentary gateway↔service connectivity blip (the
|
|
26
38
|
* workspace service runs a lean replica pool with no PgBouncer, so a burst of
|
|
27
39
|
* concurrent dashboard queries can briefly exhaust connections and the gateway
|
|
28
|
-
* returns an upstream-connect / 5xx / reset error
|
|
29
|
-
* genuine validation / auth /
|
|
40
|
+
* returns an upstream-connect / 5xx / reset error, or the request simply hangs
|
|
41
|
+
* until we abort it). These are safe to retry; genuine validation / auth /
|
|
42
|
+
* business errors are not.
|
|
30
43
|
*/
|
|
31
44
|
function isTransientMessage(message?: string): boolean {
|
|
32
45
|
if (!message) return false;
|
|
@@ -40,8 +53,56 @@ function isTransientStatus(status: number): boolean {
|
|
|
40
53
|
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A fetch aborted by our timeout rejects with an `AbortError`. It arrives as a
|
|
58
|
+
* `DOMException`, which is not reliably an `instanceof Error` across engines, so
|
|
59
|
+
* we duck-type the `name` field rather than relying on the prototype chain.
|
|
60
|
+
*/
|
|
61
|
+
function isAbortError(error: unknown): boolean {
|
|
62
|
+
return (
|
|
63
|
+
typeof error === 'object' &&
|
|
64
|
+
error !== null &&
|
|
65
|
+
'name' in error &&
|
|
66
|
+
(error as { name?: unknown }).name === 'AbortError'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Global in-flight cap across every GraphQL call this MFE makes. The workspace
|
|
74
|
+
* service runs a single lean replica (no PgBouncer), and a page like the
|
|
75
|
+
* dashboard fans out ~7 queries at once; multiplied by the retry loop below a
|
|
76
|
+
* transient blip could balloon into dozens of simultaneous requests and tip the
|
|
77
|
+
* replica over. A small semaphore keeps the fan-out polite — requests beyond the
|
|
78
|
+
* limit queue for a moment instead of stampeding the service. 4 is high enough
|
|
79
|
+
* that normal navigation feels instant, low enough that a burst never floods.
|
|
80
|
+
*/
|
|
81
|
+
const MAX_CONCURRENT = 4;
|
|
82
|
+
let activeRequests = 0;
|
|
83
|
+
const pendingAcquirers: Array<() => void> = [];
|
|
84
|
+
|
|
85
|
+
function acquireSlot(): Promise<void> {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
if (activeRequests < MAX_CONCURRENT) {
|
|
88
|
+
activeRequests += 1;
|
|
89
|
+
resolve();
|
|
90
|
+
} else {
|
|
91
|
+
pendingAcquirers.push(resolve);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function releaseSlot(): void {
|
|
97
|
+
const next = pendingAcquirers.shift();
|
|
98
|
+
if (next) {
|
|
99
|
+
// Hand the slot straight to the next waiter — activeRequests stays put.
|
|
100
|
+
next();
|
|
101
|
+
} else {
|
|
102
|
+
activeRequests -= 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
45
106
|
export async function executeGraphQL<TData>(
|
|
46
107
|
options: GraphQLRequestOptions,
|
|
47
108
|
query: string,
|
|
@@ -61,15 +122,38 @@ export async function executeGraphQL<TData>(
|
|
|
61
122
|
};
|
|
62
123
|
const body = JSON.stringify({ query, variables });
|
|
63
124
|
|
|
125
|
+
// Serialize past the global concurrency cap before doing any network work,
|
|
126
|
+
// and always release the slot when this call settles (success or failure).
|
|
127
|
+
await acquireSlot();
|
|
128
|
+
try {
|
|
129
|
+
return await runWithRetries<TData>(url, headers, body);
|
|
130
|
+
} finally {
|
|
131
|
+
releaseSlot();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runWithRetries<TData>(
|
|
136
|
+
url: string,
|
|
137
|
+
headers: Record<string, string>,
|
|
138
|
+
body: string
|
|
139
|
+
): Promise<TData> {
|
|
64
140
|
let lastError: Error = new Error('GraphQL request failed');
|
|
65
141
|
|
|
66
142
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
|
|
67
143
|
const canRetry = attempt < MAX_RETRIES;
|
|
68
144
|
// Exponential backoff with jitter before each retry (not the first attempt).
|
|
69
|
-
if (attempt > 0)
|
|
145
|
+
if (attempt > 0)
|
|
146
|
+
await sleep(Math.min(250 * 2 ** (attempt - 1), 2000) + Math.floor(Math.random() * 150));
|
|
70
147
|
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
71
150
|
try {
|
|
72
|
-
const response = await fetch(url, {
|
|
151
|
+
const response = await fetch(url, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers,
|
|
154
|
+
body,
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
});
|
|
73
157
|
|
|
74
158
|
if (!response.ok) {
|
|
75
159
|
lastError = new Error(`GraphQL request failed with status ${response.status}`);
|
|
@@ -101,11 +185,22 @@ export async function executeGraphQL<TData>(
|
|
|
101
185
|
|
|
102
186
|
return payload.data;
|
|
103
187
|
} catch (error) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
188
|
+
const aborted = isAbortError(error);
|
|
189
|
+
// A thrown fetch is a network error (TypeError "Failed to fetch") or our
|
|
190
|
+
// own timeout abort — both transient and safe to retry on a fresh
|
|
191
|
+
// connection.
|
|
192
|
+
const message = aborted
|
|
193
|
+
? `GraphQL request timed out after ${REQUEST_TIMEOUT_MS}ms`
|
|
194
|
+
: error instanceof Error
|
|
195
|
+
? error.message
|
|
196
|
+
: String(error);
|
|
197
|
+
lastError = !aborted && error instanceof Error ? error : new Error(message);
|
|
198
|
+
if (canRetry && (aborted || isTransientMessage(message) || error instanceof TypeError)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
108
201
|
throw lastError;
|
|
202
|
+
} finally {
|
|
203
|
+
clearTimeout(timeoutId);
|
|
109
204
|
}
|
|
110
205
|
}
|
|
111
206
|
|