@burdenoff/microfe-movethewheels 2026.929.4 → 2026.930.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CustomerPicker.js +52 -48
- package/dist/components/EntityCombobox.js +25 -23
- package/dist/components/OrderItemRow.js +123 -78
- package/dist/hooks/useCustomers.js +52 -17
- package/dist/hooks/useFleet.js +126 -44
- package/dist/hooks/useInventory.js +43 -7
- package/dist/hooks/useMultiModalShipments.js +30 -26
- package/dist/hooks/useRefetchOnLeaveEdit.js +10 -0
- package/dist/hooks/useRoutes.js +55 -15
- package/dist/movethewheels/components/CustomerPicker.d.ts +5 -1
- package/dist/movethewheels/components/EntityCombobox.d.ts +6 -3
- package/dist/movethewheels/components/OrderItemRow.d.ts +7 -1
- package/dist/movethewheels/hooks/useCustomers.d.ts +3 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +6 -0
- package/dist/movethewheels/hooks/useInventory.d.ts +2 -0
- package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +1 -0
- package/dist/movethewheels/hooks/useRefetchOnLeaveEdit.d.ts +7 -0
- package/dist/movethewheels/hooks/useRoutes.d.ts +3 -0
- package/dist/movethewheels/utils/orderValidation.d.ts +11 -0
- package/dist/pages/CreateOrderPage.js +349 -297
- package/dist/pages/DriverDetailsPage.js +258 -251
- package/dist/pages/DriversPage.js +3 -1
- package/dist/pages/MultiModalShipmentPage.js +139 -125
- package/dist/pages/OrderDetailsPage.js +132 -140
- package/dist/pages/ProductDetailsPage.js +22 -19
- package/dist/pages/RouteDetailsPage.js +284 -281
- package/dist/pages/TrackingPage.js +2 -1
- package/dist/pages/VehicleDetailsPage.js +28 -25
- package/dist/utils/orderValidation.js +18 -0
- package/package.json +1 -1
- package/src/movethewheels/components/CustomerPicker.tsx +12 -0
- package/src/movethewheels/components/EntityCombobox.tsx +30 -11
- package/src/movethewheels/components/OrderItemRow.tsx +44 -0
- package/src/movethewheels/hooks/useCustomers.ts +38 -0
- package/src/movethewheels/hooks/useFleet.ts +125 -5
- package/src/movethewheels/hooks/useInventory.ts +42 -0
- package/src/movethewheels/hooks/useMultiModalShipments.ts +9 -7
- package/src/movethewheels/hooks/useRefetchOnLeaveEdit.ts +18 -0
- package/src/movethewheels/hooks/useRoutes.ts +51 -0
- package/src/movethewheels/pages/CreateOrderPage.tsx +66 -2
- package/src/movethewheels/pages/DriverDetailsPage.tsx +14 -1
- package/src/movethewheels/pages/DriversPage.tsx +7 -1
- package/src/movethewheels/pages/MultiModalShipmentPage.tsx +15 -1
- package/src/movethewheels/pages/OrderDetailsPage.tsx +25 -25
- package/src/movethewheels/pages/ProductDetailsPage.tsx +2 -0
- package/src/movethewheels/pages/RouteDetailsPage.tsx +10 -4
- package/src/movethewheels/pages/TrackingPage.tsx +7 -2
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +2 -0
- package/src/movethewheels/utils/orderValidation.ts +53 -0
|
@@ -82,7 +82,7 @@ function N() {
|
|
|
82
82
|
className: "p-6",
|
|
83
83
|
children: /* @__PURE__ */ k("form", {
|
|
84
84
|
onSubmit: q,
|
|
85
|
-
className: "flex gap-4",
|
|
85
|
+
className: "flex flex-col gap-4 sm:flex-row",
|
|
86
86
|
children: [/* @__PURE__ */ k("div", {
|
|
87
87
|
className: "flex-1 relative",
|
|
88
88
|
children: [/* @__PURE__ */ O(a, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-secondary" }), /* @__PURE__ */ O(h, {
|
|
@@ -94,6 +94,7 @@ function N() {
|
|
|
94
94
|
}), /* @__PURE__ */ O(u, {
|
|
95
95
|
type: "submit",
|
|
96
96
|
size: "lg",
|
|
97
|
+
className: "w-full sm:w-auto",
|
|
97
98
|
disabled: !z || G,
|
|
98
99
|
children: G ? P("movethewheels.tracking.search.searching", "Searching...") : P("movethewheels.tracking.search.track", "Track")
|
|
99
100
|
})]
|
|
@@ -19,15 +19,16 @@ import { FormError as L } from "../components/FormError.js";
|
|
|
19
19
|
import { Notice as R, useNotice as z } from "../components/Notice.js";
|
|
20
20
|
import { formatDate as B, getInitials as V } from "../utils/formatters.js";
|
|
21
21
|
import { DocumentsCard as H } from "../components/DocumentsCard.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { useRefetchOnLeaveEdit as U } from "../hooks/useRefetchOnLeaveEdit.js";
|
|
23
|
+
import { useRecordVehicleService as W } from "../hooks/useFleetIntelligence.js";
|
|
24
|
+
import { VehicleServiceCard as ee } from "../components/VehicleServiceCard.js";
|
|
24
25
|
import { useEffect as G, useState as K } from "react";
|
|
25
|
-
import { useLocation as
|
|
26
|
+
import { useLocation as te, useParams as ne } from "react-router";
|
|
26
27
|
import { jsx as q, jsxs as J } from "react/jsx-runtime";
|
|
27
|
-
import { useI18n as
|
|
28
|
-
import { EmphasisPanel as Y, NextSteps as
|
|
28
|
+
import { useI18n as re } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
29
|
+
import { EmphasisPanel as Y, NextSteps as ie, PagePurpose as X, ResponsiveDialog as Z, ResponsiveDialogContent as Q, ResponsiveDialogDescription as $, ResponsiveDialogFooter as ae, ResponsiveDialogHeader as oe, ResponsiveDialogTitle as se } from "@burdenoff/fe-libs/ui";
|
|
29
30
|
//#region src/movethewheels/pages/VehicleDetailsPage.tsx
|
|
30
|
-
var
|
|
31
|
+
var ce = [
|
|
31
32
|
{
|
|
32
33
|
value: "TRUCK",
|
|
33
34
|
label: "Truck"
|
|
@@ -60,10 +61,10 @@ var se = [
|
|
|
60
61
|
value: "DRONE",
|
|
61
62
|
label: "Drone"
|
|
62
63
|
}
|
|
63
|
-
],
|
|
64
|
+
], le = Object.entries(P).map(([e, t]) => ({
|
|
64
65
|
value: e,
|
|
65
66
|
label: t
|
|
66
|
-
})),
|
|
67
|
+
})), ue = [
|
|
67
68
|
{
|
|
68
69
|
value: "DIESEL",
|
|
69
70
|
label: "Diesel"
|
|
@@ -84,7 +85,7 @@ var se = [
|
|
|
84
85
|
value: "HYBRID",
|
|
85
86
|
label: "Hybrid"
|
|
86
87
|
}
|
|
87
|
-
],
|
|
88
|
+
], de = [
|
|
88
89
|
{
|
|
89
90
|
value: "owned",
|
|
90
91
|
label: "Owned"
|
|
@@ -98,7 +99,7 @@ var se = [
|
|
|
98
99
|
label: "Rented"
|
|
99
100
|
}
|
|
100
101
|
];
|
|
101
|
-
function
|
|
102
|
+
function fe({ editId: t }) {
|
|
102
103
|
let { basePath: n, navigate: r } = d(), { createVehicle: i, updateVehicle: o, deleteVehicle: l } = p(), { vehicle: h } = m(t), g = !!t, [_, T] = K({
|
|
103
104
|
vehicleNumber: "",
|
|
104
105
|
type: "VAN",
|
|
@@ -240,7 +241,7 @@ function de({ editId: t }) {
|
|
|
240
241
|
children: [/* @__PURE__ */ q(k, {
|
|
241
242
|
id: "type",
|
|
242
243
|
children: /* @__PURE__ */ q(A, {})
|
|
243
|
-
}), /* @__PURE__ */ q(D, { children:
|
|
244
|
+
}), /* @__PURE__ */ q(D, { children: ce.map((e) => /* @__PURE__ */ q(O, {
|
|
244
245
|
value: e.value,
|
|
245
246
|
children: e.label
|
|
246
247
|
}, e.value)) })]
|
|
@@ -314,7 +315,7 @@ function de({ editId: t }) {
|
|
|
314
315
|
children: [/* @__PURE__ */ q(k, {
|
|
315
316
|
id: "ownershipType",
|
|
316
317
|
children: /* @__PURE__ */ q(A, { placeholder: "Select ownership" })
|
|
317
|
-
}), /* @__PURE__ */ q(D, { children:
|
|
318
|
+
}), /* @__PURE__ */ q(D, { children: de.map((e) => /* @__PURE__ */ q(O, {
|
|
318
319
|
value: e.value,
|
|
319
320
|
children: e.label
|
|
320
321
|
}, e.value)) })]
|
|
@@ -352,7 +353,7 @@ function de({ editId: t }) {
|
|
|
352
353
|
children: [/* @__PURE__ */ q(k, {
|
|
353
354
|
id: "vehicleStatus",
|
|
354
355
|
children: /* @__PURE__ */ q(A, {})
|
|
355
|
-
}), /* @__PURE__ */ q(D, { children:
|
|
356
|
+
}), /* @__PURE__ */ q(D, { children: le.map((e) => /* @__PURE__ */ q(O, {
|
|
356
357
|
value: e.value,
|
|
357
358
|
children: e.label
|
|
358
359
|
}, e.value)) })]
|
|
@@ -370,7 +371,7 @@ function de({ editId: t }) {
|
|
|
370
371
|
}), /* @__PURE__ */ J(E, {
|
|
371
372
|
value: _.fuelType || void 0,
|
|
372
373
|
onValueChange: (e) => {
|
|
373
|
-
let t =
|
|
374
|
+
let t = ue.find((t) => t.value === e)?.value;
|
|
374
375
|
T((e) => ({
|
|
375
376
|
...e,
|
|
376
377
|
fuelType: t ?? ""
|
|
@@ -379,7 +380,7 @@ function de({ editId: t }) {
|
|
|
379
380
|
children: [/* @__PURE__ */ q(k, {
|
|
380
381
|
id: "fuelType",
|
|
381
382
|
children: /* @__PURE__ */ q(A, { placeholder: "Select fuel type" })
|
|
382
|
-
}), /* @__PURE__ */ q(D, { children:
|
|
383
|
+
}), /* @__PURE__ */ q(D, { children: ue.map((e) => /* @__PURE__ */ q(O, {
|
|
383
384
|
value: e.value,
|
|
384
385
|
children: e.label
|
|
385
386
|
}, e.value)) })]
|
|
@@ -484,7 +485,7 @@ function de({ editId: t }) {
|
|
|
484
485
|
open: I,
|
|
485
486
|
onOpenChange: R,
|
|
486
487
|
children: /* @__PURE__ */ J(Q, { children: [
|
|
487
|
-
/* @__PURE__ */ J(
|
|
488
|
+
/* @__PURE__ */ J(oe, { children: [/* @__PURE__ */ q(se, { children: "Delete this vehicle?" }), /* @__PURE__ */ J($, { children: [
|
|
488
489
|
"This removes ",
|
|
489
490
|
_.vehicleNumber || "this vehicle",
|
|
490
491
|
" from the active fleet. It cannot be undone from here."
|
|
@@ -493,7 +494,7 @@ function de({ editId: t }) {
|
|
|
493
494
|
className: "px-1",
|
|
494
495
|
children: /* @__PURE__ */ q(L, { message: V })
|
|
495
496
|
}),
|
|
496
|
-
/* @__PURE__ */ J(
|
|
497
|
+
/* @__PURE__ */ J(ae, { children: [/* @__PURE__ */ q(v, {
|
|
497
498
|
variant: "outline",
|
|
498
499
|
onClick: () => R(!1),
|
|
499
500
|
disabled: z,
|
|
@@ -520,12 +521,14 @@ function de({ editId: t }) {
|
|
|
520
521
|
]
|
|
521
522
|
});
|
|
522
523
|
}
|
|
523
|
-
function
|
|
524
|
-
let { vehicleId: a } =
|
|
524
|
+
function pe() {
|
|
525
|
+
let { vehicleId: a } = ne(), { basePath: s, navigate: f } = d(), { vehicle: p, isLoading: C, error: w, refetch: E } = m(a), D = W(), { message: O, notify: k } = z(), { t: A } = re(), P = (e, t) => {
|
|
525
526
|
let n = A(e);
|
|
526
527
|
return n === e ? t : n;
|
|
527
|
-
}, L =
|
|
528
|
-
|
|
528
|
+
}, L = te().pathname.endsWith("/edit");
|
|
529
|
+
U(L, E);
|
|
530
|
+
let { drivers: G, isLoading: K, error: Z } = h(L ? void 0 : a), Q = () => f?.(u(s, "fleet/vehicles")), $ = (e) => f?.(u(s, e));
|
|
531
|
+
return a ? L ? /* @__PURE__ */ q(fe, { editId: a }) : C ? /* @__PURE__ */ q(M, { message: "Loading vehicle details..." }) : w || !p ? /* @__PURE__ */ q(j, {
|
|
529
532
|
title: "Vehicle not found",
|
|
530
533
|
onRetry: E
|
|
531
534
|
}) : /* @__PURE__ */ J(N, {
|
|
@@ -555,7 +558,7 @@ function fe() {
|
|
|
555
558
|
icon: /* @__PURE__ */ q(c, { size: 18 }),
|
|
556
559
|
children: "The full record for this vehicle — specs, capacity, fuel and mileage, who is driving it and where it is right now. Use it to confirm a vehicle is fuelled, crewed and the right size before dispatching it on a delivery."
|
|
557
560
|
}),
|
|
558
|
-
/* @__PURE__ */ q(
|
|
561
|
+
/* @__PURE__ */ q(ie, {
|
|
559
562
|
storageKey: "movethewheels-vehicle-details",
|
|
560
563
|
steps: [{
|
|
561
564
|
id: "assign",
|
|
@@ -773,7 +776,7 @@ function fe() {
|
|
|
773
776
|
})]
|
|
774
777
|
}, e.id))
|
|
775
778
|
})] }),
|
|
776
|
-
/* @__PURE__ */ q(
|
|
779
|
+
/* @__PURE__ */ q(ee, {
|
|
777
780
|
vehicle: p,
|
|
778
781
|
t: P,
|
|
779
782
|
onRecord: async (e) => {
|
|
@@ -794,7 +797,7 @@ function fe() {
|
|
|
794
797
|
})]
|
|
795
798
|
})
|
|
796
799
|
]
|
|
797
|
-
}) : /* @__PURE__ */ q(
|
|
800
|
+
}) : /* @__PURE__ */ q(fe, {});
|
|
798
801
|
}
|
|
799
802
|
//#endregion
|
|
800
|
-
export {
|
|
803
|
+
export { pe as default };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/movethewheels/utils/orderValidation.ts
|
|
2
|
+
var e = (e) => Number.isFinite(e) && e >= 0;
|
|
3
|
+
function t(t) {
|
|
4
|
+
let n = {}, r = t.productId.trim().length > 0, i = t.name.trim().length > 0;
|
|
5
|
+
!r && !i && (n.name = "Enter an item name or select a catalog product."), t.sku.trim().length === 0 && (n.sku = "SKU is required for every shipment item."), (!Number.isFinite(t.quantity) || t.quantity <= 0) && (n.quantity = "Quantity must be a finite number greater than 0."), e(t.weight) || (n.weight = "Weight must be a finite number of 0 or more."), e(t.value) || (n.value = "Value must be a finite number of 0 or more.");
|
|
6
|
+
let a = t.dimensions;
|
|
7
|
+
for (let t of [
|
|
8
|
+
"length",
|
|
9
|
+
"width",
|
|
10
|
+
"height"
|
|
11
|
+
]) (!a || !e(a[t])) && (n[t] = `${t[0].toUpperCase()}${t.slice(1)} must be a finite number of 0 or more.`);
|
|
12
|
+
return n;
|
|
13
|
+
}
|
|
14
|
+
function n(e) {
|
|
15
|
+
return Object.fromEntries(e.map((e) => [e.id, t(e)]).filter(([, e]) => Object.keys(e).length > 0));
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { n as validateOrderItems };
|
package/package.json
CHANGED
|
@@ -27,8 +27,12 @@ interface CustomerPickerProps {
|
|
|
27
27
|
onCreated: (customer: Customer) => void;
|
|
28
28
|
createCustomer: (customer: Partial<Customer>) => Promise<Customer>;
|
|
29
29
|
isLoading: boolean;
|
|
30
|
+
isLoadingMore?: boolean;
|
|
30
31
|
error: Error | null;
|
|
31
32
|
onRetry: () => void;
|
|
33
|
+
onLoadMore?: () => void | Promise<void>;
|
|
34
|
+
hasMore?: boolean;
|
|
35
|
+
totalCount?: number;
|
|
32
36
|
onOpenCustomers?: () => void;
|
|
33
37
|
selectedCustomer?: Customer | null;
|
|
34
38
|
/** Called with the (already debounced) query text as the user types in the
|
|
@@ -60,8 +64,12 @@ export function CustomerPicker({
|
|
|
60
64
|
onCreated,
|
|
61
65
|
createCustomer,
|
|
62
66
|
isLoading,
|
|
67
|
+
isLoadingMore = false,
|
|
63
68
|
error,
|
|
64
69
|
onRetry,
|
|
70
|
+
onLoadMore,
|
|
71
|
+
hasMore = false,
|
|
72
|
+
totalCount,
|
|
65
73
|
onOpenCustomers,
|
|
66
74
|
selectedCustomer,
|
|
67
75
|
onSearchQueryChange,
|
|
@@ -153,6 +161,10 @@ export function CustomerPicker({
|
|
|
153
161
|
disabled={disabled || Boolean(error)}
|
|
154
162
|
error={error ? 'Customers could not be loaded' : undefined}
|
|
155
163
|
onSearchChange={onSearchQueryChange}
|
|
164
|
+
hasMore={hasMore}
|
|
165
|
+
totalCount={totalCount}
|
|
166
|
+
onLoadMore={onLoadMore}
|
|
167
|
+
loadingMore={isLoadingMore}
|
|
156
168
|
className="flex-1"
|
|
157
169
|
/>
|
|
158
170
|
{error && (
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* already proven by `microfe-product`'s `ProductCombobox`.
|
|
9
9
|
*
|
|
10
10
|
* Every list this feeds (`useRoutes`, `useCustomers`, `useVehicles`,
|
|
11
|
-
* `useDrivers
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* `useDrivers`, and product pickers) is capped to a small server page (`limit`,
|
|
12
|
+
* default 20). When the source exposes `onLoadMore`, the accessible Load more
|
|
13
|
+
* control requests the next server page and appends it without losing the
|
|
14
|
+
* current selection.
|
|
15
15
|
*
|
|
16
16
|
* - **Internal search** (pass `onSearchChange`): the combobox renders its own
|
|
17
17
|
* debounced `CommandInput` and calls back with the debounced query so the
|
|
@@ -76,10 +76,13 @@ interface EntityComboboxProps<T> {
|
|
|
76
76
|
/** Server-side search callback (debounced). Omit to hide the built-in search
|
|
77
77
|
* input when a caller already provides one elsewhere for the same list. */
|
|
78
78
|
onSearchChange?: (value: string) => void;
|
|
79
|
-
/** True when the source hook
|
|
80
|
-
* paired with `totalCount` to render a "keep typing to narrow" hint. */
|
|
79
|
+
/** True when the source hook has another server page available. */
|
|
81
80
|
hasMore?: boolean;
|
|
82
81
|
totalCount?: number;
|
|
82
|
+
/** Fetches and appends the next server page without replacing the current options. */
|
|
83
|
+
onLoadMore?: () => void | Promise<void>;
|
|
84
|
+
/** True while the next server page is being fetched. */
|
|
85
|
+
loadingMore?: boolean;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
export function EntityCombobox<T>({
|
|
@@ -104,6 +107,8 @@ export function EntityCombobox<T>({
|
|
|
104
107
|
onSearchChange,
|
|
105
108
|
hasMore = false,
|
|
106
109
|
totalCount,
|
|
110
|
+
onLoadMore,
|
|
111
|
+
loadingMore = false,
|
|
107
112
|
}: EntityComboboxProps<T>) {
|
|
108
113
|
const [open, setOpen] = useState(false);
|
|
109
114
|
const [search, setSearch] = useState('');
|
|
@@ -140,7 +145,7 @@ export function EntityCombobox<T>({
|
|
|
140
145
|
className
|
|
141
146
|
);
|
|
142
147
|
|
|
143
|
-
const
|
|
148
|
+
const canLoadMore = Boolean(onLoadMore && hasMore);
|
|
144
149
|
|
|
145
150
|
return (
|
|
146
151
|
<Popover
|
|
@@ -223,10 +228,24 @@ export function EntityCombobox<T>({
|
|
|
223
228
|
</CommandItem>
|
|
224
229
|
);
|
|
225
230
|
})}
|
|
226
|
-
{
|
|
227
|
-
<div className="
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
{canLoadMore && (
|
|
232
|
+
<div className="border-t border-border-seam p-2">
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
className="w-full rounded-md px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/10 focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:opacity-60"
|
|
236
|
+
onClick={() => void onLoadMore?.()}
|
|
237
|
+
disabled={loading || loadingMore}
|
|
238
|
+
aria-busy={loadingMore}
|
|
239
|
+
aria-label={
|
|
240
|
+
loadingMore
|
|
241
|
+
? 'Loading more options'
|
|
242
|
+
: `Load more options${typeof totalCount === 'number' ? ` (${options.length} of ${totalCount} shown)` : ''}`
|
|
243
|
+
}
|
|
244
|
+
>
|
|
245
|
+
{loadingMore
|
|
246
|
+
? 'Loading more…'
|
|
247
|
+
: `Load more${typeof totalCount === 'number' ? ` (${options.length} of ${totalCount})` : ''}`}
|
|
248
|
+
</button>
|
|
230
249
|
</div>
|
|
231
250
|
)}
|
|
232
251
|
</CommandList>
|
|
@@ -12,12 +12,18 @@ import { Button, Input, Label } from './ui';
|
|
|
12
12
|
import { EntityCombobox } from './EntityCombobox';
|
|
13
13
|
import { useProduct, useProductStockLevels } from '../hooks/useInventory';
|
|
14
14
|
import type { Dimensions, OrderItem, Product } from '../types';
|
|
15
|
+
import type { OrderItemFieldErrors } from '../utils/orderValidation';
|
|
15
16
|
|
|
16
17
|
interface OrderItemRowProps {
|
|
17
18
|
item: OrderItem;
|
|
18
19
|
index: number;
|
|
19
20
|
products: Product[];
|
|
20
21
|
productsLoading: boolean;
|
|
22
|
+
productsLoadingMore?: boolean;
|
|
23
|
+
productsHasMore?: boolean;
|
|
24
|
+
productsTotalCount?: number;
|
|
25
|
+
onLoadMoreProducts?: () => void | Promise<void>;
|
|
26
|
+
errors?: OrderItemFieldErrors;
|
|
21
27
|
onUpdate: (index: number, field: string, value: unknown) => void;
|
|
22
28
|
onRemove: (index: number) => void;
|
|
23
29
|
}
|
|
@@ -37,6 +43,11 @@ export function OrderItemRow({
|
|
|
37
43
|
index,
|
|
38
44
|
products,
|
|
39
45
|
productsLoading,
|
|
46
|
+
productsLoadingMore = false,
|
|
47
|
+
productsHasMore = false,
|
|
48
|
+
productsTotalCount,
|
|
49
|
+
onLoadMoreProducts,
|
|
50
|
+
errors = {},
|
|
40
51
|
onUpdate,
|
|
41
52
|
onRemove,
|
|
42
53
|
}: OrderItemRowProps) {
|
|
@@ -80,6 +91,17 @@ export function OrderItemRow({
|
|
|
80
91
|
});
|
|
81
92
|
};
|
|
82
93
|
|
|
94
|
+
const fieldError = (field: keyof OrderItemFieldErrors) =>
|
|
95
|
+
errors[field] ? (
|
|
96
|
+
<p
|
|
97
|
+
id={`item-${field}-${item.id}-error`}
|
|
98
|
+
className="text-xs text-status-error-text"
|
|
99
|
+
role="alert"
|
|
100
|
+
>
|
|
101
|
+
{errors[field]}
|
|
102
|
+
</p>
|
|
103
|
+
) : null;
|
|
104
|
+
|
|
83
105
|
return (
|
|
84
106
|
<div
|
|
85
107
|
className="rounded-card border border-border-subtle bg-bg-surface p-4 shadow-elevation-1 sm:p-5"
|
|
@@ -131,6 +153,10 @@ export function OrderItemRow({
|
|
|
131
153
|
placeholder="Pick from your product catalog"
|
|
132
154
|
emptyText="No products found."
|
|
133
155
|
noneLabel="Enter details manually"
|
|
156
|
+
hasMore={productsHasMore}
|
|
157
|
+
totalCount={productsTotalCount}
|
|
158
|
+
onLoadMore={onLoadMoreProducts}
|
|
159
|
+
loadingMore={productsLoadingMore}
|
|
134
160
|
/>
|
|
135
161
|
{item.productId && !stockLoading && (
|
|
136
162
|
<p className="mt-2 text-xs text-text-secondary" role="status">
|
|
@@ -149,7 +175,10 @@ export function OrderItemRow({
|
|
|
149
175
|
onUpdate(index, 'name', e.target.value)
|
|
150
176
|
}
|
|
151
177
|
placeholder="Product name"
|
|
178
|
+
aria-invalid={Boolean(errors.name)}
|
|
179
|
+
aria-describedby={errors.name ? `item-name-${item.id}-error` : undefined}
|
|
152
180
|
/>
|
|
181
|
+
{fieldError('name')}
|
|
153
182
|
</div>
|
|
154
183
|
<div className="space-y-2 lg:col-span-3">
|
|
155
184
|
<Label htmlFor={`item-sku-${item.id}`}>SKU</Label>
|
|
@@ -160,7 +189,10 @@ export function OrderItemRow({
|
|
|
160
189
|
onUpdate(index, 'sku', e.target.value)
|
|
161
190
|
}
|
|
162
191
|
placeholder="SKU-001"
|
|
192
|
+
aria-invalid={Boolean(errors.sku)}
|
|
193
|
+
aria-describedby={errors.sku ? `item-sku-${item.id}-error` : undefined}
|
|
163
194
|
/>
|
|
195
|
+
{fieldError('sku')}
|
|
164
196
|
</div>
|
|
165
197
|
<div className="space-y-2 lg:col-span-2">
|
|
166
198
|
<Label htmlFor={`item-quantity-${item.id}`}>Quantity</Label>
|
|
@@ -172,7 +204,10 @@ export function OrderItemRow({
|
|
|
172
204
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
173
205
|
onUpdate(index, 'quantity', parseInt(e.target.value, 10) || 1)
|
|
174
206
|
}
|
|
207
|
+
aria-invalid={Boolean(errors.quantity)}
|
|
208
|
+
aria-describedby={errors.quantity ? `item-quantity-${item.id}-error` : undefined}
|
|
175
209
|
/>
|
|
210
|
+
{fieldError('quantity')}
|
|
176
211
|
</div>
|
|
177
212
|
<div className="space-y-2 lg:col-span-2">
|
|
178
213
|
<Label htmlFor={`item-weight-${item.id}`}>Weight (kg)</Label>
|
|
@@ -185,7 +220,10 @@ export function OrderItemRow({
|
|
|
185
220
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
186
221
|
onUpdate(index, 'weight', parseFloat(e.target.value) || 0)
|
|
187
222
|
}
|
|
223
|
+
aria-invalid={Boolean(errors.weight)}
|
|
224
|
+
aria-describedby={errors.weight ? `item-weight-${item.id}-error` : undefined}
|
|
188
225
|
/>
|
|
226
|
+
{fieldError('weight')}
|
|
189
227
|
</div>
|
|
190
228
|
<div className="space-y-2 lg:col-span-1">
|
|
191
229
|
<Label htmlFor={`item-value-${item.id}`}>Value ($)</Label>
|
|
@@ -198,7 +236,10 @@ export function OrderItemRow({
|
|
|
198
236
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
199
237
|
onUpdate(index, 'value', parseFloat(e.target.value) || 0)
|
|
200
238
|
}
|
|
239
|
+
aria-invalid={Boolean(errors.value)}
|
|
240
|
+
aria-describedby={errors.value ? `item-value-${item.id}-error` : undefined}
|
|
201
241
|
/>
|
|
242
|
+
{fieldError('value')}
|
|
202
243
|
</div>
|
|
203
244
|
</div>
|
|
204
245
|
|
|
@@ -221,7 +262,10 @@ export function OrderItemRow({
|
|
|
221
262
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
222
263
|
updateDimension(field, e.target.value)
|
|
223
264
|
}
|
|
265
|
+
aria-invalid={Boolean(errors[field])}
|
|
266
|
+
aria-describedby={errors[field] ? `item-${field}-${item.id}-error` : undefined}
|
|
224
267
|
/>
|
|
268
|
+
{fieldError(field)}
|
|
225
269
|
</div>
|
|
226
270
|
))}
|
|
227
271
|
</div>
|
|
@@ -14,8 +14,11 @@ interface UseCustomersResult {
|
|
|
14
14
|
totalCustomers: number;
|
|
15
15
|
hasMore: boolean;
|
|
16
16
|
isLoading: boolean;
|
|
17
|
+
/** True while an additional server page is appended. */
|
|
18
|
+
isLoadingMore: boolean;
|
|
17
19
|
error: Error | null;
|
|
18
20
|
refetch: () => void;
|
|
21
|
+
loadMore: () => Promise<void>;
|
|
19
22
|
createCustomer: (customer: Partial<Customer>) => Promise<Customer>;
|
|
20
23
|
updateCustomer: (id: string, updates: Partial<Customer>) => Promise<Customer>;
|
|
21
24
|
deleteCustomer: (id: string) => Promise<void>;
|
|
@@ -163,6 +166,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
163
166
|
const [totalCustomers, setTotalCustomers] = useState(0);
|
|
164
167
|
const [hasMore, setHasMore] = useState(false);
|
|
165
168
|
const [isLoading, setIsLoading] = useState(false);
|
|
169
|
+
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
166
170
|
const [error, setError] = useState<Error | null>(null);
|
|
167
171
|
|
|
168
172
|
const graphQLOptions = useMemo(
|
|
@@ -204,6 +208,38 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
204
208
|
}
|
|
205
209
|
}, [filters, graphQLOptions, limit, offset]);
|
|
206
210
|
|
|
211
|
+
const loadMore = useCallback(async () => {
|
|
212
|
+
if (!hasMore || isLoading || isLoadingMore) return;
|
|
213
|
+
const nextOffset = offset + customers.length;
|
|
214
|
+
setIsLoadingMore(true);
|
|
215
|
+
setError(null);
|
|
216
|
+
try {
|
|
217
|
+
const data = await executeGraphQL<{
|
|
218
|
+
customers: ApiCustomer[];
|
|
219
|
+
customersCount: number;
|
|
220
|
+
}>(graphQLOptions, LIST_CUSTOMERS_QUERY, {
|
|
221
|
+
filter: {
|
|
222
|
+
type: filters?.type,
|
|
223
|
+
search: filters?.search,
|
|
224
|
+
},
|
|
225
|
+
pagination: { limit, offset: nextOffset },
|
|
226
|
+
});
|
|
227
|
+
const mapped = data.customers.map(mapCustomer);
|
|
228
|
+
setTotalCustomers(data.customersCount);
|
|
229
|
+
setHasMore(nextOffset + mapped.length < data.customersCount);
|
|
230
|
+
setCustomers((previous) => {
|
|
231
|
+
const existingIds = new Set(previous.map((customer) => customer.id));
|
|
232
|
+
return [...previous, ...mapped.filter((customer) => !existingIds.has(customer.id))];
|
|
233
|
+
});
|
|
234
|
+
} catch (fetchError) {
|
|
235
|
+
setError(
|
|
236
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load more customers')
|
|
237
|
+
);
|
|
238
|
+
} finally {
|
|
239
|
+
setIsLoadingMore(false);
|
|
240
|
+
}
|
|
241
|
+
}, [customers.length, filters, graphQLOptions, hasMore, isLoading, isLoadingMore, limit, offset]);
|
|
242
|
+
|
|
207
243
|
useEffect(() => {
|
|
208
244
|
void loadCustomers();
|
|
209
245
|
}, [loadCustomers]);
|
|
@@ -278,8 +314,10 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
278
314
|
totalCustomers,
|
|
279
315
|
hasMore,
|
|
280
316
|
isLoading,
|
|
317
|
+
isLoadingMore,
|
|
281
318
|
error,
|
|
282
319
|
refetch,
|
|
320
|
+
loadMore,
|
|
283
321
|
createCustomer,
|
|
284
322
|
updateCustomer,
|
|
285
323
|
deleteCustomer,
|