@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.
Files changed (47) hide show
  1. package/dist/components/OrderItemRow.js +195 -109
  2. package/dist/components/StatusBadge.js +31 -31
  3. package/dist/constants/index.js +11 -3
  4. package/dist/generated/wspace-operations.d.ts +7 -0
  5. package/dist/hooks/useFleet.js +85 -76
  6. package/dist/hooks/useOrders.js +8 -4
  7. package/dist/hooks/useTracking.js +127 -66
  8. package/dist/movethewheels/components/OrderItemRow.d.ts +1 -2
  9. package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
  10. package/dist/movethewheels/constants/index.d.ts +1 -0
  11. package/dist/movethewheels/hooks/useFleet.d.ts +41 -1
  12. package/dist/movethewheels/hooks/useOrders.d.ts +1 -0
  13. package/dist/movethewheels/hooks/useTracking.d.ts +2 -2
  14. package/dist/movethewheels/types/index.d.ts +17 -39
  15. package/dist/node_modules/lucide-react/dist/esm/icons/sticky-note.js +11 -0
  16. package/dist/node_modules/lucide-react/dist/esm/icons/user-x.js +30 -0
  17. package/dist/pages/CreateOrderPage.js +597 -420
  18. package/dist/pages/DriverDetailsPage.js +702 -421
  19. package/dist/pages/DriversPage.js +106 -117
  20. package/dist/pages/FleetPage.js +4 -10
  21. package/dist/pages/OrderDetailsPage.js +290 -262
  22. package/dist/pages/OrdersListPage.js +4 -0
  23. package/dist/pages/RouteDetailsPage.js +264 -252
  24. package/dist/pages/TrackingPage.js +205 -195
  25. package/dist/pages/VehicleDetailsPage.js +403 -243
  26. package/dist/utils/mapMarkers.js +2 -1
  27. package/package.json +1 -1
  28. package/src/generated/wspace-operations.ts +10 -3
  29. package/src/movethewheels/components/OrderItemRow.tsx +162 -99
  30. package/src/movethewheels/components/StatusBadge.tsx +11 -2
  31. package/src/movethewheels/constants/index.ts +13 -0
  32. package/src/movethewheels/hooks/useFleet.ts +163 -44
  33. package/src/movethewheels/hooks/useOrders.ts +17 -3
  34. package/src/movethewheels/hooks/useTracking.ts +208 -27
  35. package/src/movethewheels/pages/CreateOrderPage.tsx +372 -263
  36. package/src/movethewheels/pages/DriverDetailsPage.tsx +516 -156
  37. package/src/movethewheels/pages/DriversPage.tsx +28 -11
  38. package/src/movethewheels/pages/FleetPage.tsx +7 -10
  39. package/src/movethewheels/pages/OrderDetailsPage.tsx +67 -30
  40. package/src/movethewheels/pages/OrdersListPage.tsx +1 -0
  41. package/src/movethewheels/pages/RouteDetailsPage.tsx +15 -7
  42. package/src/movethewheels/pages/TrackingPage.tsx +44 -14
  43. package/src/movethewheels/pages/VehicleDetailsPage.tsx +137 -15
  44. package/src/movethewheels/types/index.ts +17 -38
  45. package/src/movethewheels/utils/mapMarkers.ts +1 -0
  46. package/src/operations/wspace/movethewheels/queries/dashboard.graphql +5 -0
  47. 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
- Route as RouteIcon,
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 { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
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
- goBack();
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 onClick={handleSubmit} disabled={isSubmitting || customersLoading}>
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 this delivery before it goes out — change the customer, addresses, items or handling notes, then save.'
383
- : 'Book a new delivery in a few steps: choose who it’s for, set pickup and drop-off addresses, list what’s being shipped, and add any handling notes. Once created it joins the orders pipeline ready to assign and track.'}
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
- <NextSteps
387
- storageKey="movethewheels-create-order"
413
+ <Stepper
388
414
  steps={[
389
415
  {
390
- id: 'customer',
391
- label: 'Pick the customer first',
392
- description: 'Their saved contact details auto-fill the order.',
393
- icon: <Users size={16} />,
394
- done: !!formData.customerId,
395
- onClick: () => document.getElementById('customer')?.focus(),
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
- id: 'addresses',
399
- label: 'Set pickup & delivery addresses',
400
- description: 'Where the shipment is collected and dropped off.',
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
- id: 'items',
407
- label: 'Add the items to ship',
408
- description: 'List each product with its quantity and value.',
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
- {/* Order Details — the page's single emphasis zone (the form's starting point) */}
420
- <EmphasisPanel>
421
- <h2 className="text-base font-semibold text-text-primary mb-4">Order Details</h2>
422
- <div className="space-y-4">
423
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
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 &amp; 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
- </div>
470
- </EmphasisPanel>
471
-
472
- {/* Saved route — optional shortcut; manual addresses remain authoritative. */}
473
- <Card>
474
- <CardHeader>
475
- <CardTitle className="flex items-center gap-2">
476
- <RouteIcon size={18} />
477
- Saved Route (Optional)
478
- </CardTitle>
479
- </CardHeader>
480
- <CardContent className="space-y-2">
481
- <EntityCombobox
482
- id="saved-route"
483
- value={formData.routeId}
484
- onChange={selectSavedRoute}
485
- options={routes}
486
- loading={routesLoading}
487
- getId={(route) => route.id}
488
- getLabel={(route) => `${route.name} · ${route.stops.length} stops`}
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 &amp; 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
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
611
- <div className="space-y-2">
612
- <Label htmlFor="deliveryPostal">Postal Code</Label>
613
- <Input
614
- id="deliveryPostal"
615
- value={formData.deliveryAddress.postalCode}
616
- onChange={(e) => updateAddress('deliveryAddress', 'postalCode', e.target.value)}
617
- placeholder="90210"
618
- />
619
- </div>
620
- <div className="space-y-2">
621
- <Label htmlFor="deliveryCountry">Country</Label>
622
- <Input
623
- id="deliveryCountry"
624
- value={formData.deliveryAddress.country}
625
- onChange={(e) => updateAddress('deliveryAddress', 'country', e.target.value)}
626
- placeholder="USA"
627
- />
628
- </div>
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
- </CardContent>
631
- </Card>
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
- {/* Order Items */}
635
- <Card>
636
- <CardHeader className="flex flex-row items-center justify-between">
637
- <CardTitle>Order Items</CardTitle>
638
- <Button type="button" variant="outline" size="sm" onClick={addItem}>
639
- <Plus size={16} />
640
- Add Item
641
- </Button>
642
- </CardHeader>
643
- <CardContent>
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="space-y-2">
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) => setProductSearch(e.target.value)}
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
- Narrows the "Catalog item" picker below for every row. Picking a catalog item
668
- fills in name, SKU, weight, and value — you can still edit them, or skip the
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
- {formData.items.map((item, index) => (
673
- <OrderItemRow
674
- key={item.id}
675
- item={item}
676
- index={index}
677
- isFirst={index === 0}
678
- products={products}
679
- productsLoading={productsLoading}
680
- onUpdate={updateItem}
681
- onRemove={removeItem}
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
- </CardContent>
687
- </Card>
688
-
689
- {/* Scheduling & handling notes */}
690
- <Card>
691
- <CardHeader>
692
- <CardTitle>Scheduling &amp; Handling</CardTitle>
693
- </CardHeader>
694
- <CardContent>
695
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
696
- <div className="space-y-2">
697
- <Label htmlFor="scheduledDelivery" className="flex items-center gap-1.5">
698
- <CalendarClock size={14} className="text-text-secondary" />
699
- Scheduled Delivery
700
- </Label>
701
- <Input
702
- id="scheduledDelivery"
703
- type="datetime-local"
704
- value={formData.scheduledDelivery}
705
- onChange={(e) => updateField('scheduledDelivery', e.target.value)}
706
- />
707
- <p className="text-xs text-text-secondary">
708
- The delivery window promised to the customer. Leave blank to dispatch as soon as a
709
- driver is free.
710
- </p>
711
- </div>
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 &amp; 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
- </CardContent>
724
- </Card>
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
  );