@burdenoff/microfe-movethewheels 2026.921.1 → 2026.924.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/components/CustomerPicker.js +94 -101
- package/dist/components/EntityCombobox.js +105 -0
- package/dist/components/ui.js +2 -2
- package/dist/generated/wspace-types.d.ts +1676 -3
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useFleet.js +57 -51
- package/dist/hooks/useOrders.js +89 -84
- package/dist/hooks/useRoutes.js +1 -1
- package/dist/index.js +27 -27
- package/dist/movethewheels/components/CustomerPicker.d.ts +3 -2
- package/dist/movethewheels/components/EntityCombobox.d.ts +40 -0
- package/dist/movethewheels/components/ui.d.ts +1 -1
- package/dist/movethewheels/hooks/useFleet.d.ts +4 -0
- package/dist/movethewheels/hooks/useOrders.d.ts +15 -0
- package/dist/movethewheels/hooks/useRoutes.d.ts +16 -0
- package/dist/movethewheels/types/index.d.ts +5 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/chevrons-up-down.js +11 -0
- package/dist/pages/CreateOrderPage.js +196 -134
- package/dist/pages/OrderDetailsPage.js +328 -263
- package/dist/pages/RouteDetailsPage.js +277 -266
- package/package.json +1 -1
- package/src/generated/wspace-types.ts +1948 -3
- package/src/movethewheels/components/CustomerPicker.tsx +26 -36
- package/src/movethewheels/components/EntityCombobox.tsx +237 -0
- package/src/movethewheels/components/ui.tsx +9 -0
- package/src/movethewheels/hooks/useFleet.ts +19 -6
- package/src/movethewheels/hooks/useOrders.ts +35 -0
- package/src/movethewheels/hooks/useRoutes.ts +1 -1
- package/src/movethewheels/pages/CreateOrderPage.tsx +108 -5
- package/src/movethewheels/pages/OrderDetailsPage.tsx +130 -60
- package/src/movethewheels/pages/RouteDetailsPage.tsx +57 -47
- package/src/movethewheels/types/index.ts +5 -0
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
SelectTrigger,
|
|
17
17
|
SelectValue,
|
|
18
18
|
} from './ui';
|
|
19
|
+
import { EntityCombobox } from './EntityCombobox';
|
|
19
20
|
import type { Customer } from '../types';
|
|
20
21
|
import { FormError } from './FormError';
|
|
21
22
|
|
|
@@ -30,7 +31,8 @@ interface CustomerPickerProps {
|
|
|
30
31
|
onRetry: () => void;
|
|
31
32
|
onOpenCustomers?: () => void;
|
|
32
33
|
selectedCustomer?: Customer | null;
|
|
33
|
-
|
|
34
|
+
/** Called with the (already debounced) query text as the user types in the
|
|
35
|
+
* combobox's search box, so the caller can re-fetch `customers` server-side. */
|
|
34
36
|
onSearchQueryChange?: (query: string) => void;
|
|
35
37
|
disabled?: boolean;
|
|
36
38
|
}
|
|
@@ -51,13 +53,6 @@ const EMPTY_FORM: CustomerFormState = {
|
|
|
51
53
|
company: '',
|
|
52
54
|
};
|
|
53
55
|
|
|
54
|
-
function mergeCustomers(customers: Customer[], selectedCustomer?: Customer | null): Customer[] {
|
|
55
|
-
if (!selectedCustomer || customers.some((customer) => customer.id === selectedCustomer.id)) {
|
|
56
|
-
return customers;
|
|
57
|
-
}
|
|
58
|
-
return [selectedCustomer, ...customers];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
56
|
export function CustomerPicker({
|
|
62
57
|
customers,
|
|
63
58
|
value,
|
|
@@ -69,7 +64,6 @@ export function CustomerPicker({
|
|
|
69
64
|
onRetry,
|
|
70
65
|
onOpenCustomers,
|
|
71
66
|
selectedCustomer,
|
|
72
|
-
searchQuery = '',
|
|
73
67
|
onSearchQueryChange,
|
|
74
68
|
disabled = false,
|
|
75
69
|
}: CustomerPickerProps) {
|
|
@@ -77,7 +71,6 @@ export function CustomerPicker({
|
|
|
77
71
|
const [form, setForm] = useState<CustomerFormState>(EMPTY_FORM);
|
|
78
72
|
const [isCreating, setIsCreating] = useState(false);
|
|
79
73
|
const [createError, setCreateError] = useState<string | null>(null);
|
|
80
|
-
const options = mergeCustomers(customers, selectedCustomer);
|
|
81
74
|
|
|
82
75
|
const openCreateDialog = () => {
|
|
83
76
|
setCreateError(null);
|
|
@@ -139,32 +132,29 @@ export function CustomerPicker({
|
|
|
139
132
|
</Button>
|
|
140
133
|
</div>
|
|
141
134
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-start">
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
value={searchQuery}
|
|
145
|
-
onChange={(event) => onSearchQueryChange(event.target.value)}
|
|
146
|
-
placeholder="Search customers"
|
|
147
|
-
aria-label="Search customers"
|
|
148
|
-
disabled={disabled}
|
|
149
|
-
className="sm:max-w-56"
|
|
150
|
-
/>
|
|
151
|
-
)}
|
|
152
|
-
<Select
|
|
135
|
+
<EntityCombobox
|
|
136
|
+
id="customer"
|
|
153
137
|
value={value}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
138
|
+
onChange={onValueChange}
|
|
139
|
+
options={customers}
|
|
140
|
+
loading={isLoading}
|
|
141
|
+
getId={(customer) => customer.id}
|
|
142
|
+
getLabel={(customer) => `${customer.name}${customer.email ? ` (${customer.email})` : ''}`}
|
|
143
|
+
renderOption={(customer) => (
|
|
144
|
+
<>
|
|
145
|
+
{customer.name} {customer.email ? `(${customer.email})` : ''}
|
|
146
|
+
</>
|
|
147
|
+
)}
|
|
148
|
+
selectedOption={selectedCustomer}
|
|
149
|
+
required
|
|
150
|
+
placeholder={isLoading ? 'Loading customers...' : 'Select customer'}
|
|
151
|
+
searchPlaceholder="Search customers"
|
|
152
|
+
emptyText="No customers found."
|
|
153
|
+
disabled={disabled || Boolean(error)}
|
|
154
|
+
error={error ? 'Customers could not be loaded' : undefined}
|
|
155
|
+
onSearchChange={onSearchQueryChange}
|
|
156
|
+
className="flex-1"
|
|
157
|
+
/>
|
|
168
158
|
{error && (
|
|
169
159
|
<Button type="button" variant="outline" size="sm" onClick={onRetry}>
|
|
170
160
|
<RefreshCw size={14} className="mr-1" />
|
|
@@ -183,7 +173,7 @@ export function CustomerPicker({
|
|
|
183
173
|
</Button>
|
|
184
174
|
)}
|
|
185
175
|
</div>
|
|
186
|
-
) : !isLoading &&
|
|
176
|
+
) : !isLoading && customers.length === 0 ? (
|
|
187
177
|
<div className="rounded-lg border border-border-seam bg-bg-sunken/40 p-3">
|
|
188
178
|
<div className="flex items-start gap-2">
|
|
189
179
|
<UserPlus size={16} className="mt-0.5 shrink-0 text-text-secondary" />
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EntityCombobox — searchable single-select for backend-record lists (routes,
|
|
3
|
+
* customers, vehicles, drivers, …).
|
|
4
|
+
*
|
|
5
|
+
* A button trigger shows the current selection's label (or a placeholder) and
|
|
6
|
+
* opens a Popover with a cmdk Command. Built entirely on existing fe-libs `ui`
|
|
7
|
+
* primitives (Command + Popover) — no new dependency, following the pattern
|
|
8
|
+
* already proven by `microfe-product`'s `ProductCombobox`.
|
|
9
|
+
*
|
|
10
|
+
* Every list this feeds (`useRoutes`, `useCustomers`, `useVehicles`,
|
|
11
|
+
* `useDrivers`) is capped to a small server page (`limit`, default 20) so the
|
|
12
|
+
* option list is never "fetch everything" — a workspace with thousands of
|
|
13
|
+
* records would otherwise either hang the request or silently truncate. Two
|
|
14
|
+
* search modes cover both call shapes in this app:
|
|
15
|
+
*
|
|
16
|
+
* - **Internal search** (pass `onSearchChange`): the combobox renders its own
|
|
17
|
+
* debounced `CommandInput` and calls back with the debounced query so the
|
|
18
|
+
* caller's hook re-fetches server-side. Used where each field owns its own
|
|
19
|
+
* search (saved route, customer).
|
|
20
|
+
* - **External search** (omit `onSearchChange`): no input is rendered inside
|
|
21
|
+
* the popover; the caller is expected to already be filtering `options` via
|
|
22
|
+
* a search control elsewhere on the page. Used where one shared search box
|
|
23
|
+
* drives two comboboxes at once (vehicle + driver assignment), so a second,
|
|
24
|
+
* redundant input isn't stacked on top of it.
|
|
25
|
+
*
|
|
26
|
+
* In both modes, the current selection's label is resolved independently via
|
|
27
|
+
* `selectedOption`/`selectedLoading` (typically a single-record lookup hook
|
|
28
|
+
* like `useRoute(value)`), so it keeps rendering correctly even once it falls
|
|
29
|
+
* outside the current search results or page.
|
|
30
|
+
*/
|
|
31
|
+
import { useEffect, useState } from 'react';
|
|
32
|
+
import { Check, ChevronsUpDown } from 'lucide-react';
|
|
33
|
+
import {
|
|
34
|
+
Command,
|
|
35
|
+
CommandEmpty,
|
|
36
|
+
CommandInput,
|
|
37
|
+
CommandItem,
|
|
38
|
+
CommandList,
|
|
39
|
+
Popover,
|
|
40
|
+
PopoverContent,
|
|
41
|
+
PopoverTrigger,
|
|
42
|
+
} from './ui';
|
|
43
|
+
import { cn } from '../utils/cn';
|
|
44
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
45
|
+
|
|
46
|
+
interface EntityComboboxProps<T> {
|
|
47
|
+
/** Selected record id (controlled). Empty string = nothing selected. */
|
|
48
|
+
value: string;
|
|
49
|
+
/** Called with the selected record id ('' when cleared). */
|
|
50
|
+
onChange: (id: string) => void;
|
|
51
|
+
/** The current (already fetched/filtered) page of selectable records. */
|
|
52
|
+
options: T[];
|
|
53
|
+
/** True while `options` is (re)loading. */
|
|
54
|
+
loading?: boolean;
|
|
55
|
+
getId: (option: T) => string;
|
|
56
|
+
/** Rich content for a list row. */
|
|
57
|
+
renderOption: (option: T) => React.ReactNode;
|
|
58
|
+
/** Plain-text label for the trigger button (must be a string, not JSX). */
|
|
59
|
+
getLabel: (option: T) => string;
|
|
60
|
+
/** Independently-loaded current selection, so its label still renders even
|
|
61
|
+
* when it isn't present in `options` (filtered out or paginated away). */
|
|
62
|
+
selectedOption?: T | null;
|
|
63
|
+
selectedLoading?: boolean;
|
|
64
|
+
/** Renders a clearable "None" row when true (hidden when `required`). */
|
|
65
|
+
required?: boolean;
|
|
66
|
+
placeholder?: string;
|
|
67
|
+
searchPlaceholder?: string;
|
|
68
|
+
emptyText?: string;
|
|
69
|
+
noneLabel?: string;
|
|
70
|
+
disabled?: boolean;
|
|
71
|
+
id?: string;
|
|
72
|
+
/** Toggles the invalid border + aria-invalid. */
|
|
73
|
+
error?: string;
|
|
74
|
+
/** Extra classes merged onto the trigger button. */
|
|
75
|
+
className?: string;
|
|
76
|
+
/** Server-side search callback (debounced). Omit to hide the built-in search
|
|
77
|
+
* input when a caller already provides one elsewhere for the same list. */
|
|
78
|
+
onSearchChange?: (value: string) => void;
|
|
79
|
+
/** True when the source hook's page doesn't hold every matching record —
|
|
80
|
+
* paired with `totalCount` to render a "keep typing to narrow" hint. */
|
|
81
|
+
hasMore?: boolean;
|
|
82
|
+
totalCount?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function EntityCombobox<T>({
|
|
86
|
+
value,
|
|
87
|
+
onChange,
|
|
88
|
+
options,
|
|
89
|
+
loading = false,
|
|
90
|
+
getId,
|
|
91
|
+
renderOption,
|
|
92
|
+
getLabel,
|
|
93
|
+
selectedOption = null,
|
|
94
|
+
selectedLoading = false,
|
|
95
|
+
required = false,
|
|
96
|
+
placeholder = 'Select…',
|
|
97
|
+
searchPlaceholder = 'Search…',
|
|
98
|
+
emptyText = 'No results found.',
|
|
99
|
+
noneLabel = 'None',
|
|
100
|
+
disabled = false,
|
|
101
|
+
id,
|
|
102
|
+
error,
|
|
103
|
+
className,
|
|
104
|
+
onSearchChange,
|
|
105
|
+
hasMore = false,
|
|
106
|
+
totalCount,
|
|
107
|
+
}: EntityComboboxProps<T>) {
|
|
108
|
+
const [open, setOpen] = useState(false);
|
|
109
|
+
const [search, setSearch] = useState('');
|
|
110
|
+
const debouncedSearch = useDebouncedValue(search.trim(), 200);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (onSearchChange) {
|
|
114
|
+
onSearchChange(debouncedSearch);
|
|
115
|
+
}
|
|
116
|
+
// Only internal-search mode reacts to keystrokes; external-search callers
|
|
117
|
+
// manage their own query state and don't pass `onSearchChange`.
|
|
118
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
+
}, [debouncedSearch]);
|
|
120
|
+
|
|
121
|
+
const triggerLabel = selectedOption
|
|
122
|
+
? getLabel(selectedOption)
|
|
123
|
+
: value
|
|
124
|
+
? selectedLoading
|
|
125
|
+
? 'Loading…'
|
|
126
|
+
: value
|
|
127
|
+
: placeholder;
|
|
128
|
+
|
|
129
|
+
const triggerClassName = cn(
|
|
130
|
+
'w-full flex items-center justify-between gap-2 px-3 py-2',
|
|
131
|
+
'bg-surface',
|
|
132
|
+
'border border-default',
|
|
133
|
+
'rounded-lg',
|
|
134
|
+
'text-left text-sm text-text-primary',
|
|
135
|
+
'focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]',
|
|
136
|
+
'disabled:bg-bg-sunken disabled:cursor-not-allowed',
|
|
137
|
+
'transition-all duration-200',
|
|
138
|
+
error && 'border-[var(--color-status-error)]',
|
|
139
|
+
!selectedOption && 'text-text-tertiary',
|
|
140
|
+
className
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const showMoreHint = hasMore && typeof totalCount === 'number' && totalCount > options.length;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Popover
|
|
147
|
+
open={open}
|
|
148
|
+
onOpenChange={(next: boolean) => {
|
|
149
|
+
setOpen(next);
|
|
150
|
+
// Reset the query on close so reopening starts fresh, not pre-filtered.
|
|
151
|
+
if (!next) setSearch('');
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<PopoverTrigger asChild>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
id={id}
|
|
158
|
+
role="combobox"
|
|
159
|
+
aria-expanded={open}
|
|
160
|
+
aria-required={required}
|
|
161
|
+
aria-invalid={error ? true : undefined}
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
className={triggerClassName}
|
|
164
|
+
>
|
|
165
|
+
<span className="truncate">{triggerLabel}</span>
|
|
166
|
+
<ChevronsUpDown size={16} className="shrink-0 text-text-tertiary" aria-hidden="true" />
|
|
167
|
+
</button>
|
|
168
|
+
</PopoverTrigger>
|
|
169
|
+
<PopoverContent
|
|
170
|
+
align="start"
|
|
171
|
+
className="w-[var(--radix-popover-trigger-width)] p-0 border border-default bg-surface"
|
|
172
|
+
>
|
|
173
|
+
{/* Search is server-side (or driven externally) → disable cmdk's
|
|
174
|
+
client filter so we render exactly what was fetched. */}
|
|
175
|
+
<Command shouldFilter={false} className="bg-surface text-text-primary">
|
|
176
|
+
{onSearchChange && (
|
|
177
|
+
<CommandInput
|
|
178
|
+
value={search}
|
|
179
|
+
onValueChange={setSearch}
|
|
180
|
+
placeholder={searchPlaceholder}
|
|
181
|
+
className="text-text-primary placeholder:text-text-tertiary"
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
<CommandList>
|
|
185
|
+
<CommandEmpty className="text-text-tertiary">
|
|
186
|
+
{loading ? 'Searching…' : emptyText}
|
|
187
|
+
</CommandEmpty>
|
|
188
|
+
{!required && (
|
|
189
|
+
<CommandItem
|
|
190
|
+
value="__none__"
|
|
191
|
+
onSelect={() => {
|
|
192
|
+
onChange('');
|
|
193
|
+
setOpen(false);
|
|
194
|
+
}}
|
|
195
|
+
className="text-text-tertiary"
|
|
196
|
+
>
|
|
197
|
+
<Check
|
|
198
|
+
size={16}
|
|
199
|
+
className={cn('shrink-0', value === '' ? 'opacity-100' : 'opacity-0')}
|
|
200
|
+
aria-hidden="true"
|
|
201
|
+
/>
|
|
202
|
+
<span className="truncate italic">{noneLabel}</span>
|
|
203
|
+
</CommandItem>
|
|
204
|
+
)}
|
|
205
|
+
{options.map((option) => {
|
|
206
|
+
const optionId = getId(option);
|
|
207
|
+
return (
|
|
208
|
+
<CommandItem
|
|
209
|
+
key={optionId}
|
|
210
|
+
value={optionId}
|
|
211
|
+
onSelect={() => {
|
|
212
|
+
onChange(optionId);
|
|
213
|
+
setOpen(false);
|
|
214
|
+
}}
|
|
215
|
+
className="text-text-primary"
|
|
216
|
+
>
|
|
217
|
+
<Check
|
|
218
|
+
size={16}
|
|
219
|
+
className={cn('shrink-0', optionId === value ? 'opacity-100' : 'opacity-0')}
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
/>
|
|
222
|
+
<span className="truncate">{renderOption(option)}</span>
|
|
223
|
+
</CommandItem>
|
|
224
|
+
);
|
|
225
|
+
})}
|
|
226
|
+
{showMoreHint && (
|
|
227
|
+
<div className="px-2 py-1.5 text-xs text-text-tertiary border-t border-border-seam">
|
|
228
|
+
Showing {options.length} of {totalCount}
|
|
229
|
+
{onSearchChange ? ' — keep typing to narrow results.' : '.'}
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
</CommandList>
|
|
233
|
+
</Command>
|
|
234
|
+
</PopoverContent>
|
|
235
|
+
</Popover>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
@@ -533,7 +533,13 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
533
533
|
// refetched) every render when the caller passes an inline filters object.
|
|
534
534
|
const filters = useMemo(
|
|
535
535
|
() => rawFilters,
|
|
536
|
-
[
|
|
536
|
+
[
|
|
537
|
+
rawFilters?.driverId,
|
|
538
|
+
rawFilters?.search,
|
|
539
|
+
rawFilters?.status,
|
|
540
|
+
rawFilters?.vehicleId,
|
|
541
|
+
rawFilters?.vehicleType,
|
|
542
|
+
]
|
|
537
543
|
);
|
|
538
544
|
const graphQLOptions = useGraphqlOptions();
|
|
539
545
|
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
|
@@ -551,13 +557,14 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
551
557
|
search: filters?.search,
|
|
552
558
|
type: filters?.vehicleType?.toUpperCase(),
|
|
553
559
|
status: filters?.status ? toApiVehicleStatus(filters.status) : undefined,
|
|
560
|
+
driverId: filters?.driverId,
|
|
554
561
|
},
|
|
555
562
|
pagination: { limit, offset: 0 },
|
|
556
563
|
}),
|
|
557
564
|
executeGraphQL<{ drivers: ApiDriver[] }>(graphQLOptions, LIST_DRIVERS_QUERY, {
|
|
558
565
|
filter: {
|
|
559
566
|
search: filters?.search,
|
|
560
|
-
vehicleId: filters?.
|
|
567
|
+
vehicleId: filters?.vehicleId,
|
|
561
568
|
},
|
|
562
569
|
pagination: { limit, offset: 0 },
|
|
563
570
|
}),
|
|
@@ -695,6 +702,8 @@ interface UseVehiclesOptions extends UseListOptions {
|
|
|
695
702
|
search?: string;
|
|
696
703
|
status?: VehicleStatus;
|
|
697
704
|
vehicleType?: Vehicle['type'];
|
|
705
|
+
/** Narrows to vehicles linked to this driver server-side (VehicleFilterInput.driverId). */
|
|
706
|
+
driverId?: string;
|
|
698
707
|
}
|
|
699
708
|
|
|
700
709
|
interface UseVehiclesResult {
|
|
@@ -710,7 +719,7 @@ interface UseVehiclesResult {
|
|
|
710
719
|
}
|
|
711
720
|
|
|
712
721
|
export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult {
|
|
713
|
-
const { limit = 20, offset = 0, search, status, vehicleType } = options;
|
|
722
|
+
const { limit = 20, offset = 0, search, status, vehicleType, driverId } = options;
|
|
714
723
|
const graphQLOptions = useGraphqlOptions();
|
|
715
724
|
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
|
716
725
|
const [totalVehicles, setTotalVehicles] = useState(0);
|
|
@@ -730,6 +739,7 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
|
|
|
730
739
|
search: search || undefined,
|
|
731
740
|
status: status ? toApiVehicleStatus(status) : undefined,
|
|
732
741
|
type: vehicleType?.toUpperCase(),
|
|
742
|
+
driverId: driverId || undefined,
|
|
733
743
|
},
|
|
734
744
|
pagination: { limit, offset },
|
|
735
745
|
});
|
|
@@ -741,7 +751,7 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
|
|
|
741
751
|
} finally {
|
|
742
752
|
setIsLoading(false);
|
|
743
753
|
}
|
|
744
|
-
}, [graphQLOptions, limit, offset, search, status, vehicleType]);
|
|
754
|
+
}, [graphQLOptions, limit, offset, search, status, vehicleType, driverId]);
|
|
745
755
|
|
|
746
756
|
useEffect(() => {
|
|
747
757
|
void load();
|
|
@@ -859,10 +869,12 @@ interface UseDriversOptions extends UseListOptions {
|
|
|
859
869
|
status?: DriverStatus;
|
|
860
870
|
search?: string;
|
|
861
871
|
licenseClass?: string;
|
|
872
|
+
/** Narrows to drivers linked to this vehicle server-side (DriverFilterInput.vehicleId). */
|
|
873
|
+
vehicleId?: string;
|
|
862
874
|
}
|
|
863
875
|
|
|
864
876
|
export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
|
|
865
|
-
const { limit = 20, offset = 0, status, search, licenseClass } = options;
|
|
877
|
+
const { limit = 20, offset = 0, status, search, licenseClass, vehicleId } = options;
|
|
866
878
|
const graphQLOptions = useGraphqlOptions();
|
|
867
879
|
const [drivers, setDrivers] = useState<Driver[]>([]);
|
|
868
880
|
const [totalDrivers, setTotalDrivers] = useState(0);
|
|
@@ -882,6 +894,7 @@ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
|
|
|
882
894
|
status: status ? toApiDriverStatus(status) : undefined,
|
|
883
895
|
search: search || undefined,
|
|
884
896
|
licenseClass: licenseClass || undefined,
|
|
897
|
+
vehicleId: vehicleId || undefined,
|
|
885
898
|
},
|
|
886
899
|
pagination: { limit, offset },
|
|
887
900
|
});
|
|
@@ -893,7 +906,7 @@ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
|
|
|
893
906
|
} finally {
|
|
894
907
|
setIsLoading(false);
|
|
895
908
|
}
|
|
896
|
-
}, [graphQLOptions, limit, offset, status, search, licenseClass]);
|
|
909
|
+
}, [graphQLOptions, limit, offset, status, search, licenseClass, vehicleId]);
|
|
897
910
|
|
|
898
911
|
useEffect(() => {
|
|
899
912
|
void load();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
3
3
|
import type { Order, OrderFilter, OrderStatus } from '../types';
|
|
4
|
+
import { mapRoute } from './useRoutes';
|
|
4
5
|
import { executeGraphQL } from '../utils/graphqlClient';
|
|
5
6
|
|
|
6
7
|
interface UseOrdersOptions {
|
|
@@ -53,6 +54,21 @@ export interface ApiOrder {
|
|
|
53
54
|
scheduledDelivery?: string | null;
|
|
54
55
|
vehicleId?: string | null;
|
|
55
56
|
driverId?: string | null;
|
|
57
|
+
routeId?: string | null;
|
|
58
|
+
route?: {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
status: string;
|
|
62
|
+
vehicleId?: string | null;
|
|
63
|
+
driverId?: string | null;
|
|
64
|
+
waypoints?: unknown;
|
|
65
|
+
estimatedDistance?: number | null;
|
|
66
|
+
estimatedDuration?: number | null;
|
|
67
|
+
fuelCost?: number | null;
|
|
68
|
+
actualDistance?: number | null;
|
|
69
|
+
actualDuration?: number | null;
|
|
70
|
+
createdAt: string;
|
|
71
|
+
} | null;
|
|
56
72
|
createdAt: string;
|
|
57
73
|
updatedAt: string;
|
|
58
74
|
customer?: {
|
|
@@ -85,6 +101,21 @@ const DELIVERY_JOB_FIELDS = `
|
|
|
85
101
|
scheduledDelivery
|
|
86
102
|
vehicleId
|
|
87
103
|
driverId
|
|
104
|
+
routeId
|
|
105
|
+
route {
|
|
106
|
+
id
|
|
107
|
+
name
|
|
108
|
+
status
|
|
109
|
+
vehicleId
|
|
110
|
+
driverId
|
|
111
|
+
waypoints
|
|
112
|
+
estimatedDistance
|
|
113
|
+
estimatedDuration
|
|
114
|
+
fuelCost
|
|
115
|
+
actualDistance
|
|
116
|
+
actualDuration
|
|
117
|
+
createdAt
|
|
118
|
+
}
|
|
88
119
|
createdAt
|
|
89
120
|
updatedAt
|
|
90
121
|
customer {
|
|
@@ -302,6 +333,8 @@ export function mapOrder(order: ApiOrder): Order {
|
|
|
302
333
|
scheduledDelivery: order.scheduledDelivery ? new Date(order.scheduledDelivery) : undefined,
|
|
303
334
|
vehicleId: order.vehicleId ?? undefined,
|
|
304
335
|
driverId: order.driverId ?? undefined,
|
|
336
|
+
routeId: order.routeId ?? undefined,
|
|
337
|
+
route: order.route ? mapRoute(order.route) : undefined,
|
|
305
338
|
createdAt: new Date(order.createdAt),
|
|
306
339
|
updatedAt: new Date(order.updatedAt),
|
|
307
340
|
};
|
|
@@ -409,6 +442,7 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
|
409
442
|
scheduledDelivery: orderData.scheduledDelivery?.toISOString(),
|
|
410
443
|
vehicleId: orderData.vehicleId,
|
|
411
444
|
driverId: orderData.driverId,
|
|
445
|
+
routeId: orderData.routeId,
|
|
412
446
|
},
|
|
413
447
|
});
|
|
414
448
|
|
|
@@ -440,6 +474,7 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
|
440
474
|
updates.scheduledDelivery === null ? null : updates.scheduledDelivery?.toISOString(),
|
|
441
475
|
vehicleId: updates.vehicleId,
|
|
442
476
|
driverId: updates.driverId,
|
|
477
|
+
routeId: updates.routeId,
|
|
443
478
|
},
|
|
444
479
|
});
|
|
445
480
|
|