@burdenoff/microfe-movethewheels 2026.924.2 → 2026.925.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/OrderItemRow.js +195 -109
- package/dist/components/StatusBadge.js +31 -31
- package/dist/constants/index.js +11 -3
- package/dist/generated/wspace-operations.d.ts +7 -0
- package/dist/hooks/useFleet.js +85 -76
- package/dist/hooks/useOrders.js +8 -4
- package/dist/hooks/useTracking.js +127 -66
- package/dist/movethewheels/components/OrderItemRow.d.ts +1 -2
- package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
- package/dist/movethewheels/constants/index.d.ts +1 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +41 -1
- 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 +17 -39
- package/dist/node_modules/lucide-react/dist/esm/icons/sticky-note.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/user-x.js +30 -0
- package/dist/pages/CreateOrderPage.js +597 -420
- package/dist/pages/DriverDetailsPage.js +702 -421
- package/dist/pages/DriversPage.js +106 -117
- package/dist/pages/FleetPage.js +4 -10
- package/dist/pages/OrderDetailsPage.js +290 -262
- package/dist/pages/OrdersListPage.js +4 -0
- package/dist/pages/RouteDetailsPage.js +264 -252
- package/dist/pages/TrackingPage.js +205 -195
- package/dist/pages/VehicleDetailsPage.js +403 -243
- 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 +162 -99
- package/src/movethewheels/components/StatusBadge.tsx +11 -2
- package/src/movethewheels/constants/index.ts +13 -0
- package/src/movethewheels/hooks/useFleet.ts +163 -44
- package/src/movethewheels/hooks/useOrders.ts +17 -3
- package/src/movethewheels/hooks/useTracking.ts +208 -27
- package/src/movethewheels/pages/CreateOrderPage.tsx +372 -263
- package/src/movethewheels/pages/DriverDetailsPage.tsx +516 -156
- package/src/movethewheels/pages/DriversPage.tsx +28 -11
- package/src/movethewheels/pages/FleetPage.tsx +7 -10
- package/src/movethewheels/pages/OrderDetailsPage.tsx +67 -30
- package/src/movethewheels/pages/OrdersListPage.tsx +1 -0
- package/src/movethewheels/pages/RouteDetailsPage.tsx +15 -7
- package/src/movethewheels/pages/TrackingPage.tsx +44 -14
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +137 -15
- package/src/movethewheels/types/index.ts +17 -38
- 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,7 +4,7 @@
|
|
|
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,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
Users,
|
|
15
15
|
MapPin,
|
|
16
16
|
CalendarClock,
|
|
17
|
-
|
|
17
|
+
ArrowRight,
|
|
18
18
|
} from 'lucide-react';
|
|
19
19
|
import { PageLayout, EmptyState } from '../components/PageLayout';
|
|
20
20
|
import { FormError } from '../components/FormError';
|
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
CardContent,
|
|
24
24
|
CardHeader,
|
|
25
25
|
CardTitle,
|
|
26
|
+
CardDescription,
|
|
27
|
+
Badge,
|
|
28
|
+
Separator,
|
|
26
29
|
Button,
|
|
27
30
|
Input,
|
|
28
31
|
Label,
|
|
@@ -33,7 +36,8 @@ import {
|
|
|
33
36
|
SelectTrigger,
|
|
34
37
|
SelectValue,
|
|
35
38
|
} from '../components/ui';
|
|
36
|
-
import {
|
|
39
|
+
import { FormSection } from '@burdenoff/fe-libs/form';
|
|
40
|
+
import { EmphasisPanel, PagePurpose, Stepper } from '@burdenoff/fe-libs/ui';
|
|
37
41
|
import { useOrder, useOrders } from '../hooks/useOrders';
|
|
38
42
|
import { useCustomer, useCustomers } from '../hooks/useCustomers';
|
|
39
43
|
import { useRoute, useRoutes } from '../hooks/useRoutes';
|
|
@@ -258,8 +262,12 @@ export default function CreateOrderPage() {
|
|
|
258
262
|
navigate(joinPath(basePath, `orders/${orderId}`));
|
|
259
263
|
}
|
|
260
264
|
} else {
|
|
261
|
-
await createOrder(input);
|
|
262
|
-
|
|
265
|
+
const createdOrder = await createOrder(input);
|
|
266
|
+
if (navigate) {
|
|
267
|
+
navigate(joinPath(basePath, `orders/${createdOrder.id}`));
|
|
268
|
+
} else {
|
|
269
|
+
goBack();
|
|
270
|
+
}
|
|
263
271
|
}
|
|
264
272
|
} catch (error) {
|
|
265
273
|
setFormError(
|
|
@@ -359,18 +367,37 @@ export default function CreateOrderPage() {
|
|
|
359
367
|
navigate(joinPath(basePath, buildCustomerCreatePath(orderPath)));
|
|
360
368
|
};
|
|
361
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
|
+
|
|
362
388
|
return (
|
|
363
389
|
<PageLayout
|
|
364
390
|
title={isEditing ? 'Edit Order' : 'Create Order'}
|
|
365
391
|
subtitle={isEditing ? `Editing ${order?.orderNumber}` : 'Create a new delivery order'}
|
|
366
392
|
icon={<Package size={24} />}
|
|
393
|
+
maxWidth="2xl"
|
|
367
394
|
headerActions={
|
|
368
|
-
<div className="flex gap-2">
|
|
369
|
-
<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}>
|
|
370
397
|
<ArrowLeft size={16} />
|
|
371
398
|
Cancel
|
|
372
399
|
</Button>
|
|
373
|
-
<Button
|
|
400
|
+
<Button type="submit" form="order-form" disabled={isSubmitting || customersLoading}>
|
|
374
401
|
<Save size={16} />
|
|
375
402
|
{isSubmitting ? 'Saving...' : isEditing ? 'Save Changes' : 'Create Order'}
|
|
376
403
|
</Button>
|
|
@@ -379,48 +406,63 @@ export default function CreateOrderPage() {
|
|
|
379
406
|
>
|
|
380
407
|
<PagePurpose icon={<Package size={18} />}>
|
|
381
408
|
{isEditing
|
|
382
|
-
? 'Update
|
|
383
|
-
: '
|
|
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.'}
|
|
384
411
|
</PagePurpose>
|
|
385
412
|
|
|
386
|
-
<
|
|
387
|
-
storageKey="movethewheels-create-order"
|
|
413
|
+
<Stepper
|
|
388
414
|
steps={[
|
|
389
415
|
{
|
|
390
|
-
|
|
391
|
-
label: '
|
|
392
|
-
description: '
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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?',
|
|
396
424
|
},
|
|
397
425
|
{
|
|
398
|
-
|
|
399
|
-
label: '
|
|
400
|
-
description: '
|
|
401
|
-
icon: <MapPin size={16} />,
|
|
402
|
-
done: !!formData.pickupAddress.street && !!formData.deliveryAddress.street,
|
|
403
|
-
onClick: () => document.getElementById('pickupStreet')?.focus(),
|
|
426
|
+
key: 'items',
|
|
427
|
+
label: 'Shipment contents',
|
|
428
|
+
description: 'What is being shipped?',
|
|
404
429
|
},
|
|
405
430
|
{
|
|
406
|
-
|
|
407
|
-
label: '
|
|
408
|
-
description: '
|
|
409
|
-
icon: <Package size={16} />,
|
|
410
|
-
done: formData.items.length > 0,
|
|
411
|
-
onClick: addItem,
|
|
431
|
+
key: 'preferences',
|
|
432
|
+
label: 'Timing & handling',
|
|
433
|
+
description: 'Add optional delivery instructions.',
|
|
412
434
|
},
|
|
413
435
|
]}
|
|
436
|
+
active={activeStep}
|
|
437
|
+
onStepClick={focusStep}
|
|
438
|
+
allowSkipAhead
|
|
414
439
|
/>
|
|
415
440
|
|
|
416
|
-
<form onSubmit={handleSubmit} className="space-y-6">
|
|
441
|
+
<form id="order-form" onSubmit={handleSubmit} className="space-y-6">
|
|
417
442
|
<FormError message={formError} />
|
|
418
443
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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">
|
|
424
466
|
<CustomerPicker
|
|
425
467
|
customers={customers}
|
|
426
468
|
value={formData.customerId}
|
|
@@ -452,7 +494,7 @@ export default function CreateOrderPage() {
|
|
|
452
494
|
<Label htmlFor="priority">Priority</Label>
|
|
453
495
|
<Select
|
|
454
496
|
value={formData.priority}
|
|
455
|
-
onValueChange={(value) => updateField('priority', value)}
|
|
497
|
+
onValueChange={(value: string) => updateField('priority', value)}
|
|
456
498
|
>
|
|
457
499
|
<SelectTrigger id="priority">
|
|
458
500
|
<SelectValue />
|
|
@@ -464,183 +506,244 @@ export default function CreateOrderPage() {
|
|
|
464
506
|
<SelectItem value="urgent">Urgent</SelectItem>
|
|
465
507
|
</SelectContent>
|
|
466
508
|
</Select>
|
|
509
|
+
<p className="text-xs text-text-secondary">Normal is selected by default.</p>
|
|
467
510
|
</div>
|
|
468
511
|
</div>
|
|
469
|
-
</
|
|
470
|
-
</
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
renderOption={(route) => (
|
|
490
|
-
<>
|
|
491
|
-
{route.name} · {route.stops.length} stops
|
|
492
|
-
</>
|
|
493
|
-
)}
|
|
494
|
-
selectedOption={selectedRoute}
|
|
495
|
-
selectedLoading={selectedRouteLoading}
|
|
496
|
-
placeholder="Choose a saved route"
|
|
497
|
-
searchPlaceholder="Search saved routes"
|
|
498
|
-
emptyText="No saved routes found."
|
|
499
|
-
noneLabel="No saved route"
|
|
500
|
-
error={routesError ? 'Saved routes are unavailable' : undefined}
|
|
501
|
-
onSearchChange={setRouteSearch}
|
|
502
|
-
hasMore={routesHaveMore}
|
|
503
|
-
totalCount={totalRoutes}
|
|
504
|
-
/>
|
|
505
|
-
<p className="text-xs text-text-secondary">
|
|
506
|
-
{routesLoading
|
|
507
|
-
? 'Loading planned routes…'
|
|
508
|
-
: routesError
|
|
509
|
-
? 'Saved routes are unavailable; enter pickup and delivery addresses manually.'
|
|
510
|
-
: 'A saved route fills any blank address fields. You can always edit both addresses below.'}
|
|
511
|
-
</p>
|
|
512
|
-
</CardContent>
|
|
513
|
-
</Card>
|
|
514
|
-
|
|
515
|
-
{/* Addresses */}
|
|
516
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
517
|
-
{/* Pickup Address */}
|
|
518
|
-
<Card>
|
|
519
|
-
<CardHeader>
|
|
520
|
-
<CardTitle>Pickup Address</CardTitle>
|
|
521
|
-
</CardHeader>
|
|
522
|
-
<CardContent className="space-y-4">
|
|
523
|
-
<div className="space-y-2">
|
|
524
|
-
<Label htmlFor="pickupStreet">Street Address</Label>
|
|
525
|
-
<Input
|
|
526
|
-
id="pickupStreet"
|
|
527
|
-
value={formData.pickupAddress.street}
|
|
528
|
-
onChange={(e) => updateAddress('pickupAddress', 'street', e.target.value)}
|
|
529
|
-
placeholder="123 Main St"
|
|
530
|
-
/>
|
|
531
|
-
</div>
|
|
532
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
533
|
-
<div className="space-y-2">
|
|
534
|
-
<Label htmlFor="pickupCity">City</Label>
|
|
535
|
-
<Input
|
|
536
|
-
id="pickupCity"
|
|
537
|
-
value={formData.pickupAddress.city}
|
|
538
|
-
onChange={(e) => updateAddress('pickupAddress', 'city', e.target.value)}
|
|
539
|
-
placeholder="San Francisco"
|
|
540
|
-
/>
|
|
541
|
-
</div>
|
|
542
|
-
<div className="space-y-2">
|
|
543
|
-
<Label htmlFor="pickupState">State</Label>
|
|
544
|
-
<Input
|
|
545
|
-
id="pickupState"
|
|
546
|
-
value={formData.pickupAddress.state}
|
|
547
|
-
onChange={(e) => updateAddress('pickupAddress', 'state', e.target.value)}
|
|
548
|
-
placeholder="CA"
|
|
549
|
-
/>
|
|
550
|
-
</div>
|
|
551
|
-
</div>
|
|
552
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
553
|
-
<div className="space-y-2">
|
|
554
|
-
<Label htmlFor="pickupPostal">Postal Code</Label>
|
|
555
|
-
<Input
|
|
556
|
-
id="pickupPostal"
|
|
557
|
-
value={formData.pickupAddress.postalCode}
|
|
558
|
-
onChange={(e) => updateAddress('pickupAddress', 'postalCode', e.target.value)}
|
|
559
|
-
placeholder="94103"
|
|
560
|
-
/>
|
|
561
|
-
</div>
|
|
562
|
-
<div className="space-y-2">
|
|
563
|
-
<Label htmlFor="pickupCountry">Country</Label>
|
|
564
|
-
<Input
|
|
565
|
-
id="pickupCountry"
|
|
566
|
-
value={formData.pickupAddress.country}
|
|
567
|
-
onChange={(e) => updateAddress('pickupAddress', 'country', e.target.value)}
|
|
568
|
-
placeholder="USA"
|
|
569
|
-
/>
|
|
570
|
-
</div>
|
|
571
|
-
</div>
|
|
572
|
-
</CardContent>
|
|
573
|
-
</Card>
|
|
574
|
-
|
|
575
|
-
{/* Delivery Address */}
|
|
576
|
-
<Card>
|
|
577
|
-
<CardHeader>
|
|
578
|
-
<CardTitle>Delivery Address</CardTitle>
|
|
579
|
-
</CardHeader>
|
|
580
|
-
<CardContent className="space-y-4">
|
|
581
|
-
<div className="space-y-2">
|
|
582
|
-
<Label htmlFor="deliveryStreet">Street Address</Label>
|
|
583
|
-
<Input
|
|
584
|
-
id="deliveryStreet"
|
|
585
|
-
value={formData.deliveryAddress.street}
|
|
586
|
-
onChange={(e) => updateAddress('deliveryAddress', 'street', e.target.value)}
|
|
587
|
-
placeholder="456 Oak Ave"
|
|
588
|
-
/>
|
|
589
|
-
</div>
|
|
590
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
591
|
-
<div className="space-y-2">
|
|
592
|
-
<Label htmlFor="deliveryCity">City</Label>
|
|
593
|
-
<Input
|
|
594
|
-
id="deliveryCity"
|
|
595
|
-
value={formData.deliveryAddress.city}
|
|
596
|
-
onChange={(e) => updateAddress('deliveryAddress', 'city', e.target.value)}
|
|
597
|
-
placeholder="Los Angeles"
|
|
598
|
-
/>
|
|
599
|
-
</div>
|
|
600
|
-
<div className="space-y-2">
|
|
601
|
-
<Label htmlFor="deliveryState">State</Label>
|
|
602
|
-
<Input
|
|
603
|
-
id="deliveryState"
|
|
604
|
-
value={formData.deliveryAddress.state}
|
|
605
|
-
onChange={(e) => updateAddress('deliveryAddress', 'state', e.target.value)}
|
|
606
|
-
placeholder="CA"
|
|
607
|
-
/>
|
|
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>
|
|
608
532
|
</div>
|
|
533
|
+
<Badge variant="secondary">Optional</Badge>
|
|
609
534
|
</div>
|
|
610
|
-
<
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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}
|
|
558
|
+
/>
|
|
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>
|
|
622
|
+
<Input
|
|
623
|
+
id="pickupPostal"
|
|
624
|
+
value={formData.pickupAddress.postalCode}
|
|
625
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
626
|
+
updateAddress('pickupAddress', 'postalCode', e.target.value)
|
|
627
|
+
}
|
|
628
|
+
placeholder="94103"
|
|
629
|
+
/>
|
|
630
|
+
</div>
|
|
631
|
+
<div className="space-y-2">
|
|
632
|
+
<Label htmlFor="pickupCountry">Country</Label>
|
|
633
|
+
<Input
|
|
634
|
+
id="pickupCountry"
|
|
635
|
+
value={formData.pickupAddress.country}
|
|
636
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
637
|
+
updateAddress('pickupAddress', 'country', e.target.value)
|
|
638
|
+
}
|
|
639
|
+
placeholder="USA"
|
|
640
|
+
/>
|
|
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" />
|
|
629
649
|
</div>
|
|
630
|
-
|
|
631
|
-
|
|
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">
|
|
677
|
+
<div className="space-y-2">
|
|
678
|
+
<Label htmlFor="deliveryCity">City</Label>
|
|
679
|
+
<Input
|
|
680
|
+
id="deliveryCity"
|
|
681
|
+
value={formData.deliveryAddress.city}
|
|
682
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
683
|
+
updateAddress('deliveryAddress', 'city', e.target.value)
|
|
684
|
+
}
|
|
685
|
+
placeholder="Los Angeles"
|
|
686
|
+
/>
|
|
687
|
+
</div>
|
|
688
|
+
<div className="space-y-2">
|
|
689
|
+
<Label htmlFor="deliveryState">State</Label>
|
|
690
|
+
<Input
|
|
691
|
+
id="deliveryState"
|
|
692
|
+
value={formData.deliveryAddress.state}
|
|
693
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
694
|
+
updateAddress('deliveryAddress', 'state', e.target.value)
|
|
695
|
+
}
|
|
696
|
+
placeholder="CA"
|
|
697
|
+
/>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
701
|
+
<div className="space-y-2">
|
|
702
|
+
<Label htmlFor="deliveryPostal">Postal code</Label>
|
|
703
|
+
<Input
|
|
704
|
+
id="deliveryPostal"
|
|
705
|
+
value={formData.deliveryAddress.postalCode}
|
|
706
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
707
|
+
updateAddress('deliveryAddress', 'postalCode', e.target.value)
|
|
708
|
+
}
|
|
709
|
+
placeholder="90210"
|
|
710
|
+
/>
|
|
711
|
+
</div>
|
|
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
|
+
/>
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
</fieldset>
|
|
725
|
+
</CardContent>
|
|
726
|
+
</Card>
|
|
727
|
+
</div>
|
|
728
|
+
</FormSection>
|
|
632
729
|
</div>
|
|
633
730
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
+
>
|
|
644
747
|
{formData.items.length === 0 ? (
|
|
645
748
|
<EmptyState
|
|
646
749
|
illustration="empty-files"
|
|
@@ -655,73 +758,79 @@ export default function CreateOrderPage() {
|
|
|
655
758
|
/>
|
|
656
759
|
) : (
|
|
657
760
|
<div className="space-y-4">
|
|
658
|
-
<div className="
|
|
761
|
+
<div className="rounded-card border border-border-subtle bg-bg-sunken/40 p-4">
|
|
659
762
|
<Label htmlFor="item-product-search">Search product catalog</Label>
|
|
660
763
|
<Input
|
|
661
764
|
id="item-product-search"
|
|
662
765
|
value={productSearch}
|
|
663
|
-
onChange={(e) =>
|
|
766
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
767
|
+
setProductSearch(e.target.value)
|
|
768
|
+
}
|
|
664
769
|
placeholder="Search by name or SKU"
|
|
770
|
+
className="mt-2"
|
|
665
771
|
/>
|
|
666
|
-
<p className="text-xs text-text-secondary">
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
picker and enter details manually.
|
|
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.
|
|
670
775
|
</p>
|
|
671
776
|
</div>
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
key={item.id}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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>
|
|
684
791
|
</div>
|
|
685
792
|
)}
|
|
686
|
-
</
|
|
687
|
-
</
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
</
|
|
712
|
-
<div className="md:col-span-2 space-y-2">
|
|
713
|
-
<Label htmlFor="specialInstructions">Special Instructions</Label>
|
|
714
|
-
<Textarea
|
|
715
|
-
id="specialInstructions"
|
|
716
|
-
value={formData.specialInstructions}
|
|
717
|
-
onChange={(e) => updateField('specialInstructions', e.target.value)}
|
|
718
|
-
placeholder="Any special handling or delivery instructions..."
|
|
719
|
-
rows={4}
|
|
720
|
-
/>
|
|
721
|
-
</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>
|
|
722
819
|
</div>
|
|
723
|
-
|
|
724
|
-
|
|
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>
|
|
725
834
|
</form>
|
|
726
835
|
</PageLayout>
|
|
727
836
|
);
|