@burdenoff/microfe-movethewheels 2026.924.1 → 2026.925.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/OrderItemRow.js +210 -0
- package/dist/constants/index.js +1 -0
- package/dist/generated/wspace-operations.d.ts +7 -0
- package/dist/hooks/useOrders.js +8 -4
- package/dist/hooks/useTracking.js +127 -66
- package/dist/movethewheels/components/OrderItemRow.d.ts +12 -0
- package/dist/movethewheels/hooks/useOrders.d.ts +1 -0
- package/dist/movethewheels/hooks/useTracking.d.ts +2 -2
- package/dist/movethewheels/types/index.d.ts +1 -1
- package/dist/pages/CreateOrderPage.js +624 -477
- package/dist/pages/OrderDetailsPage.js +289 -266
- package/dist/pages/OrdersListPage.js +4 -0
- package/dist/pages/TrackingPage.js +205 -195
- package/dist/utils/mapMarkers.js +2 -1
- package/package.json +1 -1
- package/src/generated/wspace-operations.ts +10 -3
- package/src/movethewheels/components/OrderItemRow.tsx +235 -0
- package/src/movethewheels/constants/index.ts +1 -0
- package/src/movethewheels/hooks/useOrders.ts +17 -3
- package/src/movethewheels/hooks/useTracking.ts +208 -27
- package/src/movethewheels/pages/CreateOrderPage.tsx +379 -308
- package/src/movethewheels/pages/OrderDetailsPage.tsx +55 -23
- package/src/movethewheels/pages/OrdersListPage.tsx +1 -0
- package/src/movethewheels/pages/TrackingPage.tsx +44 -14
- package/src/movethewheels/types/index.ts +1 -0
- package/src/movethewheels/utils/mapMarkers.ts +1 -0
- package/src/operations/wspace/movethewheels/queries/dashboard.graphql +5 -0
- package/src/operations/wspace/movethewheels/queries/logistics.graphql +2 -2
|
@@ -4,18 +4,17 @@
|
|
|
4
4
|
* Form for creating or editing orders.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useEffect, useState } from 'react';
|
|
7
|
+
import { useEffect, useState, type ChangeEvent } from 'react';
|
|
8
8
|
import { useParams, useSearchParams } from 'react-router';
|
|
9
9
|
import {
|
|
10
10
|
Package,
|
|
11
11
|
ArrowLeft,
|
|
12
12
|
Save,
|
|
13
13
|
Plus,
|
|
14
|
-
Trash2,
|
|
15
14
|
Users,
|
|
16
15
|
MapPin,
|
|
17
16
|
CalendarClock,
|
|
18
|
-
|
|
17
|
+
ArrowRight,
|
|
19
18
|
} from 'lucide-react';
|
|
20
19
|
import { PageLayout, EmptyState } from '../components/PageLayout';
|
|
21
20
|
import { FormError } from '../components/FormError';
|
|
@@ -24,6 +23,9 @@ import {
|
|
|
24
23
|
CardContent,
|
|
25
24
|
CardHeader,
|
|
26
25
|
CardTitle,
|
|
26
|
+
CardDescription,
|
|
27
|
+
Badge,
|
|
28
|
+
Separator,
|
|
27
29
|
Button,
|
|
28
30
|
Input,
|
|
29
31
|
Label,
|
|
@@ -33,16 +35,18 @@ import {
|
|
|
33
35
|
SelectItem,
|
|
34
36
|
SelectTrigger,
|
|
35
37
|
SelectValue,
|
|
36
|
-
Separator,
|
|
37
38
|
} from '../components/ui';
|
|
38
|
-
import {
|
|
39
|
+
import { FormSection } from '@burdenoff/fe-libs/form';
|
|
40
|
+
import { EmphasisPanel, PagePurpose, Stepper } from '@burdenoff/fe-libs/ui';
|
|
39
41
|
import { useOrder, useOrders } from '../hooks/useOrders';
|
|
40
42
|
import { useCustomer, useCustomers } from '../hooks/useCustomers';
|
|
41
43
|
import { useRoute, useRoutes } from '../hooks/useRoutes';
|
|
44
|
+
import { useProducts } from '../hooks/useInventory';
|
|
42
45
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
43
46
|
import { joinPath, buildCustomerCreatePath } from '../utils/navigation';
|
|
44
47
|
import { CustomerPicker } from '../components/CustomerPicker';
|
|
45
48
|
import { EntityCombobox } from '../components/EntityCombobox';
|
|
49
|
+
import { OrderItemRow } from '../components/OrderItemRow';
|
|
46
50
|
import type { Customer } from '../types';
|
|
47
51
|
|
|
48
52
|
/**
|
|
@@ -92,6 +96,11 @@ export default function CreateOrderPage() {
|
|
|
92
96
|
search: routeSearch || undefined,
|
|
93
97
|
limit: 20,
|
|
94
98
|
});
|
|
99
|
+
const [productSearch, setProductSearch] = useState('');
|
|
100
|
+
const { products, isLoading: productsLoading } = useProducts({
|
|
101
|
+
search: productSearch || undefined,
|
|
102
|
+
limit: 20,
|
|
103
|
+
});
|
|
95
104
|
|
|
96
105
|
const isEditing = !!orderId && !!order;
|
|
97
106
|
|
|
@@ -253,8 +262,12 @@ export default function CreateOrderPage() {
|
|
|
253
262
|
navigate(joinPath(basePath, `orders/${orderId}`));
|
|
254
263
|
}
|
|
255
264
|
} else {
|
|
256
|
-
await createOrder(input);
|
|
257
|
-
|
|
265
|
+
const createdOrder = await createOrder(input);
|
|
266
|
+
if (navigate) {
|
|
267
|
+
navigate(joinPath(basePath, `orders/${createdOrder.id}`));
|
|
268
|
+
} else {
|
|
269
|
+
goBack();
|
|
270
|
+
}
|
|
258
271
|
}
|
|
259
272
|
} catch (error) {
|
|
260
273
|
setFormError(
|
|
@@ -354,18 +367,37 @@ export default function CreateOrderPage() {
|
|
|
354
367
|
navigate(joinPath(basePath, buildCustomerCreatePath(orderPath)));
|
|
355
368
|
};
|
|
356
369
|
|
|
370
|
+
const activeStep = !formData.customerId
|
|
371
|
+
? 0
|
|
372
|
+
: !formData.pickupAddress.street || !formData.deliveryAddress.street
|
|
373
|
+
? 1
|
|
374
|
+
: formData.items.length === 0
|
|
375
|
+
? 2
|
|
376
|
+
: 3;
|
|
377
|
+
|
|
378
|
+
const focusStep = (step: number) => {
|
|
379
|
+
const targetIds = ['customer', 'pickupStreet', 'items-section', 'scheduledDelivery'];
|
|
380
|
+
const target = document.getElementById(targetIds[step]);
|
|
381
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
|
|
382
|
+
target.focus();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
386
|
+
};
|
|
387
|
+
|
|
357
388
|
return (
|
|
358
389
|
<PageLayout
|
|
359
390
|
title={isEditing ? 'Edit Order' : 'Create Order'}
|
|
360
391
|
subtitle={isEditing ? `Editing ${order?.orderNumber}` : 'Create a new delivery order'}
|
|
361
392
|
icon={<Package size={24} />}
|
|
393
|
+
maxWidth="2xl"
|
|
362
394
|
headerActions={
|
|
363
|
-
<div className="flex gap-2">
|
|
364
|
-
<Button variant="outline" onClick={goBack}>
|
|
395
|
+
<div className="flex w-full flex-col-reverse gap-2 sm:w-auto sm:flex-row">
|
|
396
|
+
<Button type="button" variant="outline" onClick={goBack} disabled={isSubmitting}>
|
|
365
397
|
<ArrowLeft size={16} />
|
|
366
398
|
Cancel
|
|
367
399
|
</Button>
|
|
368
|
-
<Button
|
|
400
|
+
<Button type="submit" form="order-form" disabled={isSubmitting || customersLoading}>
|
|
369
401
|
<Save size={16} />
|
|
370
402
|
{isSubmitting ? 'Saving...' : isEditing ? 'Save Changes' : 'Create Order'}
|
|
371
403
|
</Button>
|
|
@@ -374,48 +406,63 @@ export default function CreateOrderPage() {
|
|
|
374
406
|
>
|
|
375
407
|
<PagePurpose icon={<Package size={18} />}>
|
|
376
408
|
{isEditing
|
|
377
|
-
? 'Update
|
|
378
|
-
: '
|
|
409
|
+
? 'Update the customer, delivery plan, shipment contents, or handling details before saving.'
|
|
410
|
+
: 'Set the customer and delivery plan, add what is being shipped, then choose any timing or handling preferences.'}
|
|
379
411
|
</PagePurpose>
|
|
380
412
|
|
|
381
|
-
<
|
|
382
|
-
storageKey="movethewheels-create-order"
|
|
413
|
+
<Stepper
|
|
383
414
|
steps={[
|
|
384
415
|
{
|
|
385
|
-
|
|
386
|
-
label: '
|
|
387
|
-
description: '
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
416
|
+
key: 'customer',
|
|
417
|
+
label: 'Customer & priority',
|
|
418
|
+
description: 'Who is receiving this delivery?',
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
key: 'locations',
|
|
422
|
+
label: 'Route & addresses',
|
|
423
|
+
description: 'Where should it be collected and delivered?',
|
|
391
424
|
},
|
|
392
425
|
{
|
|
393
|
-
|
|
394
|
-
label: '
|
|
395
|
-
description: '
|
|
396
|
-
icon: <MapPin size={16} />,
|
|
397
|
-
done: !!formData.pickupAddress.street && !!formData.deliveryAddress.street,
|
|
398
|
-
onClick: () => document.getElementById('pickupStreet')?.focus(),
|
|
426
|
+
key: 'items',
|
|
427
|
+
label: 'Shipment contents',
|
|
428
|
+
description: 'What is being shipped?',
|
|
399
429
|
},
|
|
400
430
|
{
|
|
401
|
-
|
|
402
|
-
label: '
|
|
403
|
-
description: '
|
|
404
|
-
icon: <Package size={16} />,
|
|
405
|
-
done: formData.items.length > 0,
|
|
406
|
-
onClick: addItem,
|
|
431
|
+
key: 'preferences',
|
|
432
|
+
label: 'Timing & handling',
|
|
433
|
+
description: 'Add optional delivery instructions.',
|
|
407
434
|
},
|
|
408
435
|
]}
|
|
436
|
+
active={activeStep}
|
|
437
|
+
onStepClick={focusStep}
|
|
438
|
+
allowSkipAhead
|
|
409
439
|
/>
|
|
410
440
|
|
|
411
|
-
<form onSubmit={handleSubmit} className="space-y-6">
|
|
441
|
+
<form id="order-form" onSubmit={handleSubmit} className="space-y-6">
|
|
412
442
|
<FormError message={formError} />
|
|
413
443
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
444
|
+
<section aria-labelledby="order-details-title">
|
|
445
|
+
<EmphasisPanel className="space-y-5">
|
|
446
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
447
|
+
<div className="flex items-start gap-3">
|
|
448
|
+
<div className="rounded-xl bg-action-primary-bg/10 p-2 text-action-primary-bg">
|
|
449
|
+
<Users size={18} aria-hidden="true" />
|
|
450
|
+
</div>
|
|
451
|
+
<div>
|
|
452
|
+
<h2
|
|
453
|
+
id="order-details-title"
|
|
454
|
+
className="text-base font-semibold text-text-primary"
|
|
455
|
+
>
|
|
456
|
+
Customer & priority
|
|
457
|
+
</h2>
|
|
458
|
+
<p className="mt-1 text-sm text-text-secondary">
|
|
459
|
+
Choose who this delivery is for and how urgently it should be handled.
|
|
460
|
+
</p>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
<Badge variant="outline">Required</Badge>
|
|
464
|
+
</div>
|
|
465
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
419
466
|
<CustomerPicker
|
|
420
467
|
customers={customers}
|
|
421
468
|
value={formData.customerId}
|
|
@@ -447,7 +494,7 @@ export default function CreateOrderPage() {
|
|
|
447
494
|
<Label htmlFor="priority">Priority</Label>
|
|
448
495
|
<Select
|
|
449
496
|
value={formData.priority}
|
|
450
|
-
onValueChange={(value) => updateField('priority', value)}
|
|
497
|
+
onValueChange={(value: string) => updateField('priority', value)}
|
|
451
498
|
>
|
|
452
499
|
<SelectTrigger id="priority">
|
|
453
500
|
<SelectValue />
|
|
@@ -459,307 +506,331 @@ export default function CreateOrderPage() {
|
|
|
459
506
|
<SelectItem value="urgent">Urgent</SelectItem>
|
|
460
507
|
</SelectContent>
|
|
461
508
|
</Select>
|
|
509
|
+
<p className="text-xs text-text-secondary">Normal is selected by default.</p>
|
|
462
510
|
</div>
|
|
463
511
|
</div>
|
|
464
|
-
</
|
|
465
|
-
</
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
renderOption={(route) => (
|
|
485
|
-
<>
|
|
486
|
-
{route.name} · {route.stops.length} stops
|
|
487
|
-
</>
|
|
488
|
-
)}
|
|
489
|
-
selectedOption={selectedRoute}
|
|
490
|
-
selectedLoading={selectedRouteLoading}
|
|
491
|
-
placeholder="Choose a saved route"
|
|
492
|
-
searchPlaceholder="Search saved routes"
|
|
493
|
-
emptyText="No saved routes found."
|
|
494
|
-
noneLabel="No saved route"
|
|
495
|
-
error={routesError ? 'Saved routes are unavailable' : undefined}
|
|
496
|
-
onSearchChange={setRouteSearch}
|
|
497
|
-
hasMore={routesHaveMore}
|
|
498
|
-
totalCount={totalRoutes}
|
|
499
|
-
/>
|
|
500
|
-
<p className="text-xs text-text-secondary">
|
|
501
|
-
{routesLoading
|
|
502
|
-
? 'Loading planned routes…'
|
|
503
|
-
: routesError
|
|
504
|
-
? 'Saved routes are unavailable; enter pickup and delivery addresses manually.'
|
|
505
|
-
: 'A saved route fills any blank address fields. You can always edit both addresses below.'}
|
|
506
|
-
</p>
|
|
507
|
-
</CardContent>
|
|
508
|
-
</Card>
|
|
509
|
-
|
|
510
|
-
{/* Addresses */}
|
|
511
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
512
|
-
{/* Pickup Address */}
|
|
513
|
-
<Card>
|
|
514
|
-
<CardHeader>
|
|
515
|
-
<CardTitle>Pickup Address</CardTitle>
|
|
516
|
-
</CardHeader>
|
|
517
|
-
<CardContent className="space-y-4">
|
|
518
|
-
<div className="space-y-2">
|
|
519
|
-
<Label htmlFor="pickupStreet">Street Address</Label>
|
|
520
|
-
<Input
|
|
521
|
-
id="pickupStreet"
|
|
522
|
-
value={formData.pickupAddress.street}
|
|
523
|
-
onChange={(e) => updateAddress('pickupAddress', 'street', e.target.value)}
|
|
524
|
-
placeholder="123 Main St"
|
|
525
|
-
/>
|
|
526
|
-
</div>
|
|
527
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
528
|
-
<div className="space-y-2">
|
|
529
|
-
<Label htmlFor="pickupCity">City</Label>
|
|
530
|
-
<Input
|
|
531
|
-
id="pickupCity"
|
|
532
|
-
value={formData.pickupAddress.city}
|
|
533
|
-
onChange={(e) => updateAddress('pickupAddress', 'city', e.target.value)}
|
|
534
|
-
placeholder="San Francisco"
|
|
535
|
-
/>
|
|
536
|
-
</div>
|
|
537
|
-
<div className="space-y-2">
|
|
538
|
-
<Label htmlFor="pickupState">State</Label>
|
|
539
|
-
<Input
|
|
540
|
-
id="pickupState"
|
|
541
|
-
value={formData.pickupAddress.state}
|
|
542
|
-
onChange={(e) => updateAddress('pickupAddress', 'state', e.target.value)}
|
|
543
|
-
placeholder="CA"
|
|
544
|
-
/>
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
547
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
548
|
-
<div className="space-y-2">
|
|
549
|
-
<Label htmlFor="pickupPostal">Postal Code</Label>
|
|
550
|
-
<Input
|
|
551
|
-
id="pickupPostal"
|
|
552
|
-
value={formData.pickupAddress.postalCode}
|
|
553
|
-
onChange={(e) => updateAddress('pickupAddress', 'postalCode', e.target.value)}
|
|
554
|
-
placeholder="94103"
|
|
555
|
-
/>
|
|
556
|
-
</div>
|
|
557
|
-
<div className="space-y-2">
|
|
558
|
-
<Label htmlFor="pickupCountry">Country</Label>
|
|
559
|
-
<Input
|
|
560
|
-
id="pickupCountry"
|
|
561
|
-
value={formData.pickupAddress.country}
|
|
562
|
-
onChange={(e) => updateAddress('pickupAddress', 'country', e.target.value)}
|
|
563
|
-
placeholder="USA"
|
|
564
|
-
/>
|
|
512
|
+
</EmphasisPanel>
|
|
513
|
+
</section>
|
|
514
|
+
|
|
515
|
+
<div id="locations-section">
|
|
516
|
+
<FormSection
|
|
517
|
+
title={
|
|
518
|
+
<span className="flex items-center gap-2">
|
|
519
|
+
<MapPin size={18} aria-hidden="true" />
|
|
520
|
+
Route & addresses
|
|
521
|
+
</span>
|
|
522
|
+
}
|
|
523
|
+
description="Choose an optional saved route, then review or enter the collection and destination details."
|
|
524
|
+
>
|
|
525
|
+
<div className="rounded-card border border-border-subtle bg-bg-sunken/40 p-4">
|
|
526
|
+
<div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
527
|
+
<div>
|
|
528
|
+
<h3 className="text-sm font-semibold text-text-primary">Use a saved route</h3>
|
|
529
|
+
<p className="mt-1 text-xs text-text-secondary">
|
|
530
|
+
Optional — fills only blank address fields; you can always edit them below.
|
|
531
|
+
</p>
|
|
565
532
|
</div>
|
|
533
|
+
<Badge variant="secondary">Optional</Badge>
|
|
566
534
|
</div>
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
value={formData.deliveryAddress.city}
|
|
591
|
-
onChange={(e) => updateAddress('deliveryAddress', 'city', e.target.value)}
|
|
592
|
-
placeholder="Los Angeles"
|
|
593
|
-
/>
|
|
594
|
-
</div>
|
|
595
|
-
<div className="space-y-2">
|
|
596
|
-
<Label htmlFor="deliveryState">State</Label>
|
|
597
|
-
<Input
|
|
598
|
-
id="deliveryState"
|
|
599
|
-
value={formData.deliveryAddress.state}
|
|
600
|
-
onChange={(e) => updateAddress('deliveryAddress', 'state', e.target.value)}
|
|
601
|
-
placeholder="CA"
|
|
602
|
-
/>
|
|
603
|
-
</div>
|
|
604
|
-
</div>
|
|
605
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
606
|
-
<div className="space-y-2">
|
|
607
|
-
<Label htmlFor="deliveryPostal">Postal Code</Label>
|
|
608
|
-
<Input
|
|
609
|
-
id="deliveryPostal"
|
|
610
|
-
value={formData.deliveryAddress.postalCode}
|
|
611
|
-
onChange={(e) => updateAddress('deliveryAddress', 'postalCode', e.target.value)}
|
|
612
|
-
placeholder="90210"
|
|
613
|
-
/>
|
|
614
|
-
</div>
|
|
615
|
-
<div className="space-y-2">
|
|
616
|
-
<Label htmlFor="deliveryCountry">Country</Label>
|
|
617
|
-
<Input
|
|
618
|
-
id="deliveryCountry"
|
|
619
|
-
value={formData.deliveryAddress.country}
|
|
620
|
-
onChange={(e) => updateAddress('deliveryAddress', 'country', e.target.value)}
|
|
621
|
-
placeholder="USA"
|
|
622
|
-
/>
|
|
623
|
-
</div>
|
|
624
|
-
</div>
|
|
625
|
-
</CardContent>
|
|
626
|
-
</Card>
|
|
627
|
-
</div>
|
|
628
|
-
|
|
629
|
-
{/* Order Items */}
|
|
630
|
-
<Card>
|
|
631
|
-
<CardHeader className="flex flex-row items-center justify-between">
|
|
632
|
-
<CardTitle>Order Items</CardTitle>
|
|
633
|
-
<Button type="button" variant="outline" size="sm" onClick={addItem}>
|
|
634
|
-
<Plus size={16} />
|
|
635
|
-
Add Item
|
|
636
|
-
</Button>
|
|
637
|
-
</CardHeader>
|
|
638
|
-
<CardContent>
|
|
639
|
-
{formData.items.length === 0 ? (
|
|
640
|
-
<EmptyState
|
|
641
|
-
illustration="empty-files"
|
|
642
|
-
title="No items added yet"
|
|
643
|
-
description="Add the products this order will ship — each with a name, SKU, quantity and value."
|
|
644
|
-
action={
|
|
645
|
-
<Button type="button" variant="outline" onClick={addItem}>
|
|
646
|
-
<Plus size={16} className="mr-1.5" />
|
|
647
|
-
Add your first item
|
|
648
|
-
</Button>
|
|
649
|
-
}
|
|
535
|
+
<EntityCombobox
|
|
536
|
+
id="saved-route"
|
|
537
|
+
value={formData.routeId}
|
|
538
|
+
onChange={selectSavedRoute}
|
|
539
|
+
options={routes}
|
|
540
|
+
loading={routesLoading}
|
|
541
|
+
getId={(route) => route.id}
|
|
542
|
+
getLabel={(route) => `${route.name} · ${route.stops.length} stops`}
|
|
543
|
+
renderOption={(route) => (
|
|
544
|
+
<>
|
|
545
|
+
{route.name} · {route.stops.length} stops
|
|
546
|
+
</>
|
|
547
|
+
)}
|
|
548
|
+
selectedOption={selectedRoute}
|
|
549
|
+
selectedLoading={selectedRouteLoading}
|
|
550
|
+
placeholder="Choose a saved route"
|
|
551
|
+
searchPlaceholder="Search saved routes"
|
|
552
|
+
emptyText="No saved routes found."
|
|
553
|
+
noneLabel="No saved route"
|
|
554
|
+
error={routesError ? 'Saved routes are unavailable' : undefined}
|
|
555
|
+
onSearchChange={setRouteSearch}
|
|
556
|
+
hasMore={routesHaveMore}
|
|
557
|
+
totalCount={totalRoutes}
|
|
650
558
|
/>
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
559
|
+
<p className="mt-2 text-xs text-text-secondary" role="status">
|
|
560
|
+
{routesLoading
|
|
561
|
+
? 'Loading planned routes…'
|
|
562
|
+
: routesError
|
|
563
|
+
? 'Saved routes are unavailable; enter pickup and delivery addresses manually.'
|
|
564
|
+
: 'Manual values always remain authoritative when a route is selected.'}
|
|
565
|
+
</p>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
<Separator />
|
|
569
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] lg:items-stretch">
|
|
570
|
+
<Card className="border-border-seam shadow-none">
|
|
571
|
+
<CardHeader>
|
|
572
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
573
|
+
<MapPin size={17} className="text-text-secondary" aria-hidden="true" />
|
|
574
|
+
Pickup address <span className="text-status-error-text">*</span>
|
|
575
|
+
</CardTitle>
|
|
576
|
+
<CardDescription>Where the shipment is collected.</CardDescription>
|
|
577
|
+
</CardHeader>
|
|
578
|
+
<CardContent className="space-y-4">
|
|
579
|
+
<fieldset className="space-y-4">
|
|
580
|
+
<legend className="sr-only">Pickup address details</legend>
|
|
581
|
+
<div className="space-y-2">
|
|
582
|
+
<Label htmlFor="pickupStreet">
|
|
583
|
+
Street address <span className="text-status-error-text">*</span>
|
|
584
|
+
</Label>
|
|
585
|
+
<Input
|
|
586
|
+
id="pickupStreet"
|
|
587
|
+
value={formData.pickupAddress.street}
|
|
588
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
589
|
+
updateAddress('pickupAddress', 'street', e.target.value)
|
|
590
|
+
}
|
|
591
|
+
placeholder="123 Main St"
|
|
592
|
+
aria-required="true"
|
|
593
|
+
/>
|
|
594
|
+
</div>
|
|
595
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
596
|
+
<div className="space-y-2">
|
|
597
|
+
<Label htmlFor="pickupCity">City</Label>
|
|
598
|
+
<Input
|
|
599
|
+
id="pickupCity"
|
|
600
|
+
value={formData.pickupAddress.city}
|
|
601
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
602
|
+
updateAddress('pickupAddress', 'city', e.target.value)
|
|
603
|
+
}
|
|
604
|
+
placeholder="San Francisco"
|
|
605
|
+
/>
|
|
606
|
+
</div>
|
|
607
|
+
<div className="space-y-2">
|
|
608
|
+
<Label htmlFor="pickupState">State</Label>
|
|
609
|
+
<Input
|
|
610
|
+
id="pickupState"
|
|
611
|
+
value={formData.pickupAddress.state}
|
|
612
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
613
|
+
updateAddress('pickupAddress', 'state', e.target.value)
|
|
614
|
+
}
|
|
615
|
+
placeholder="CA"
|
|
616
|
+
/>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
620
|
+
<div className="space-y-2">
|
|
621
|
+
<Label htmlFor="pickupPostal">Postal Code</Label>
|
|
659
622
|
<Input
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
623
|
+
id="pickupPostal"
|
|
624
|
+
value={formData.pickupAddress.postalCode}
|
|
625
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
626
|
+
updateAddress('pickupAddress', 'postalCode', e.target.value)
|
|
627
|
+
}
|
|
628
|
+
placeholder="94103"
|
|
663
629
|
/>
|
|
664
630
|
</div>
|
|
665
631
|
<div className="space-y-2">
|
|
666
|
-
<Label>
|
|
632
|
+
<Label htmlFor="pickupCountry">Country</Label>
|
|
667
633
|
<Input
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
634
|
+
id="pickupCountry"
|
|
635
|
+
value={formData.pickupAddress.country}
|
|
636
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
637
|
+
updateAddress('pickupAddress', 'country', e.target.value)
|
|
638
|
+
}
|
|
639
|
+
placeholder="USA"
|
|
671
640
|
/>
|
|
672
641
|
</div>
|
|
642
|
+
</div>
|
|
643
|
+
</fieldset>
|
|
644
|
+
</CardContent>
|
|
645
|
+
</Card>
|
|
646
|
+
|
|
647
|
+
<div className="hidden items-center justify-center lg:flex" aria-hidden="true">
|
|
648
|
+
<ArrowRight size={18} className="text-text-muted" />
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
<Card className="border-border-seam shadow-none">
|
|
652
|
+
<CardHeader>
|
|
653
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
654
|
+
<MapPin size={17} className="text-text-secondary" aria-hidden="true" />
|
|
655
|
+
Delivery address <span className="text-status-error-text">*</span>
|
|
656
|
+
</CardTitle>
|
|
657
|
+
<CardDescription>Where the shipment is delivered.</CardDescription>
|
|
658
|
+
</CardHeader>
|
|
659
|
+
<CardContent className="space-y-4">
|
|
660
|
+
<fieldset className="space-y-4">
|
|
661
|
+
<legend className="sr-only">Delivery address details</legend>
|
|
662
|
+
<div className="space-y-2">
|
|
663
|
+
<Label htmlFor="deliveryStreet">
|
|
664
|
+
Street address <span className="text-status-error-text">*</span>
|
|
665
|
+
</Label>
|
|
666
|
+
<Input
|
|
667
|
+
id="deliveryStreet"
|
|
668
|
+
value={formData.deliveryAddress.street}
|
|
669
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
670
|
+
updateAddress('deliveryAddress', 'street', e.target.value)
|
|
671
|
+
}
|
|
672
|
+
placeholder="456 Oak Ave"
|
|
673
|
+
aria-required="true"
|
|
674
|
+
/>
|
|
675
|
+
</div>
|
|
676
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
673
677
|
<div className="space-y-2">
|
|
674
|
-
<Label>
|
|
678
|
+
<Label htmlFor="deliveryCity">City</Label>
|
|
675
679
|
<Input
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
updateItem(index, 'quantity', parseInt(e.target.value) || 1)
|
|
680
|
+
id="deliveryCity"
|
|
681
|
+
value={formData.deliveryAddress.city}
|
|
682
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
683
|
+
updateAddress('deliveryAddress', 'city', e.target.value)
|
|
681
684
|
}
|
|
685
|
+
placeholder="Los Angeles"
|
|
682
686
|
/>
|
|
683
687
|
</div>
|
|
684
688
|
<div className="space-y-2">
|
|
685
|
-
<Label>
|
|
689
|
+
<Label htmlFor="deliveryState">State</Label>
|
|
686
690
|
<Input
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
onChange={(e) =>
|
|
692
|
-
updateItem(index, 'weight', parseFloat(e.target.value) || 0)
|
|
691
|
+
id="deliveryState"
|
|
692
|
+
value={formData.deliveryAddress.state}
|
|
693
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
694
|
+
updateAddress('deliveryAddress', 'state', e.target.value)
|
|
693
695
|
}
|
|
696
|
+
placeholder="CA"
|
|
694
697
|
/>
|
|
695
698
|
</div>
|
|
699
|
+
</div>
|
|
700
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
696
701
|
<div className="space-y-2">
|
|
697
|
-
<Label>
|
|
702
|
+
<Label htmlFor="deliveryPostal">Postal code</Label>
|
|
698
703
|
<Input
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
onChange={(e) =>
|
|
704
|
-
updateItem(index, 'value', parseFloat(e.target.value) || 0)
|
|
704
|
+
id="deliveryPostal"
|
|
705
|
+
value={formData.deliveryAddress.postalCode}
|
|
706
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
707
|
+
updateAddress('deliveryAddress', 'postalCode', e.target.value)
|
|
705
708
|
}
|
|
709
|
+
placeholder="90210"
|
|
706
710
|
/>
|
|
707
711
|
</div>
|
|
708
|
-
<div className="
|
|
709
|
-
<
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
712
|
+
<div className="space-y-2">
|
|
713
|
+
<Label htmlFor="deliveryCountry">Country</Label>
|
|
714
|
+
<Input
|
|
715
|
+
id="deliveryCountry"
|
|
716
|
+
value={formData.deliveryAddress.country}
|
|
717
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
718
|
+
updateAddress('deliveryAddress', 'country', e.target.value)
|
|
719
|
+
}
|
|
720
|
+
placeholder="USA"
|
|
721
|
+
/>
|
|
718
722
|
</div>
|
|
719
723
|
</div>
|
|
720
|
-
</
|
|
721
|
-
|
|
724
|
+
</fieldset>
|
|
725
|
+
</CardContent>
|
|
726
|
+
</Card>
|
|
727
|
+
</div>
|
|
728
|
+
</FormSection>
|
|
729
|
+
</div>
|
|
730
|
+
|
|
731
|
+
<div id="items-section">
|
|
732
|
+
<FormSection
|
|
733
|
+
title={
|
|
734
|
+
<span className="flex items-center gap-2">
|
|
735
|
+
<Package size={18} aria-hidden="true" />
|
|
736
|
+
Shipment contents <Badge variant="secondary">{formData.items.length}</Badge>
|
|
737
|
+
</span>
|
|
738
|
+
}
|
|
739
|
+
description="Add each item being shipped. Use the catalog to prefill details or enter a one-off item manually."
|
|
740
|
+
actions={
|
|
741
|
+
<Button type="button" variant="outline" size="sm" onClick={addItem}>
|
|
742
|
+
<Plus size={16} />
|
|
743
|
+
Add item
|
|
744
|
+
</Button>
|
|
745
|
+
}
|
|
746
|
+
>
|
|
747
|
+
{formData.items.length === 0 ? (
|
|
748
|
+
<EmptyState
|
|
749
|
+
illustration="empty-files"
|
|
750
|
+
title="No items added yet"
|
|
751
|
+
description="Add the products this order will ship — each with a name, SKU, quantity and value."
|
|
752
|
+
action={
|
|
753
|
+
<Button type="button" variant="outline" onClick={addItem}>
|
|
754
|
+
<Plus size={16} className="mr-1.5" />
|
|
755
|
+
Add your first item
|
|
756
|
+
</Button>
|
|
757
|
+
}
|
|
758
|
+
/>
|
|
759
|
+
) : (
|
|
760
|
+
<div className="space-y-4">
|
|
761
|
+
<div className="rounded-card border border-border-subtle bg-bg-sunken/40 p-4">
|
|
762
|
+
<Label htmlFor="item-product-search">Search product catalog</Label>
|
|
763
|
+
<Input
|
|
764
|
+
id="item-product-search"
|
|
765
|
+
value={productSearch}
|
|
766
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
767
|
+
setProductSearch(e.target.value)
|
|
768
|
+
}
|
|
769
|
+
placeholder="Search by name or SKU"
|
|
770
|
+
className="mt-2"
|
|
771
|
+
/>
|
|
772
|
+
<p className="mt-2 text-xs text-text-secondary">
|
|
773
|
+
This filters the catalog picker for every item below. You can still edit any
|
|
774
|
+
autofilled value or enter item details manually.
|
|
775
|
+
</p>
|
|
776
|
+
</div>
|
|
777
|
+
<ol className="space-y-4" aria-label="Shipment items">
|
|
778
|
+
{formData.items.map((item, index) => (
|
|
779
|
+
<li key={item.id}>
|
|
780
|
+
<OrderItemRow
|
|
781
|
+
item={item}
|
|
782
|
+
index={index}
|
|
783
|
+
products={products}
|
|
784
|
+
productsLoading={productsLoading}
|
|
785
|
+
onUpdate={updateItem}
|
|
786
|
+
onRemove={removeItem}
|
|
787
|
+
/>
|
|
788
|
+
</li>
|
|
789
|
+
))}
|
|
790
|
+
</ol>
|
|
722
791
|
</div>
|
|
723
792
|
)}
|
|
724
|
-
</
|
|
725
|
-
</
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
</
|
|
750
|
-
<div className="md:col-span-2 space-y-2">
|
|
751
|
-
<Label htmlFor="specialInstructions">Special Instructions</Label>
|
|
752
|
-
<Textarea
|
|
753
|
-
id="specialInstructions"
|
|
754
|
-
value={formData.specialInstructions}
|
|
755
|
-
onChange={(e) => updateField('specialInstructions', e.target.value)}
|
|
756
|
-
placeholder="Any special handling or delivery instructions..."
|
|
757
|
-
rows={4}
|
|
758
|
-
/>
|
|
759
|
-
</div>
|
|
793
|
+
</FormSection>
|
|
794
|
+
</div>
|
|
795
|
+
|
|
796
|
+
<FormSection
|
|
797
|
+
title={
|
|
798
|
+
<span className="flex items-center gap-2">
|
|
799
|
+
<CalendarClock size={18} aria-hidden="true" />
|
|
800
|
+
Timing & handling
|
|
801
|
+
</span>
|
|
802
|
+
}
|
|
803
|
+
description="Optional delivery timing and instructions for the carrier."
|
|
804
|
+
>
|
|
805
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
806
|
+
<div className="space-y-2">
|
|
807
|
+
<Label htmlFor="scheduledDelivery">Scheduled delivery</Label>
|
|
808
|
+
<Input
|
|
809
|
+
id="scheduledDelivery"
|
|
810
|
+
type="datetime-local"
|
|
811
|
+
value={formData.scheduledDelivery}
|
|
812
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
813
|
+
updateField('scheduledDelivery', e.target.value)
|
|
814
|
+
}
|
|
815
|
+
/>
|
|
816
|
+
<p className="text-xs text-text-secondary">
|
|
817
|
+
Leave blank to dispatch as soon as a driver is available.
|
|
818
|
+
</p>
|
|
760
819
|
</div>
|
|
761
|
-
|
|
762
|
-
|
|
820
|
+
<div className="space-y-2 lg:col-span-2">
|
|
821
|
+
<Label htmlFor="specialInstructions">Special instructions</Label>
|
|
822
|
+
<Textarea
|
|
823
|
+
id="specialInstructions"
|
|
824
|
+
value={formData.specialInstructions}
|
|
825
|
+
onChange={(e: ChangeEvent<HTMLTextAreaElement>) =>
|
|
826
|
+
updateField('specialInstructions', e.target.value)
|
|
827
|
+
}
|
|
828
|
+
placeholder="Any special handling or delivery instructions..."
|
|
829
|
+
rows={4}
|
|
830
|
+
/>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
</FormSection>
|
|
763
834
|
</form>
|
|
764
835
|
</PageLayout>
|
|
765
836
|
);
|