@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.
@@ -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
- Route as RouteIcon,
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 { 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';
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
- goBack();
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 onClick={handleSubmit} disabled={isSubmitting || customersLoading}>
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 this delivery before it goes out — change the customer, addresses, items or handling notes, then save.'
378
- : '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.'}
379
411
  </PagePurpose>
380
412
 
381
- <NextSteps
382
- storageKey="movethewheels-create-order"
413
+ <Stepper
383
414
  steps={[
384
415
  {
385
- id: 'customer',
386
- label: 'Pick the customer first',
387
- description: 'Their saved contact details auto-fill the order.',
388
- icon: <Users size={16} />,
389
- done: !!formData.customerId,
390
- 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?',
391
424
  },
392
425
  {
393
- id: 'addresses',
394
- label: 'Set pickup & delivery addresses',
395
- description: 'Where the shipment is collected and dropped off.',
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
- id: 'items',
402
- label: 'Add the items to ship',
403
- description: 'List each product with its quantity and value.',
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
- {/* Order Details — the page's single emphasis zone (the form's starting point) */}
415
- <EmphasisPanel>
416
- <h2 className="text-base font-semibold text-text-primary mb-4">Order Details</h2>
417
- <div className="space-y-4">
418
- <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">
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
- </div>
465
- </EmphasisPanel>
466
-
467
- {/* Saved route — optional shortcut; manual addresses remain authoritative. */}
468
- <Card>
469
- <CardHeader>
470
- <CardTitle className="flex items-center gap-2">
471
- <RouteIcon size={18} />
472
- Saved Route (Optional)
473
- </CardTitle>
474
- </CardHeader>
475
- <CardContent className="space-y-2">
476
- <EntityCombobox
477
- id="saved-route"
478
- value={formData.routeId}
479
- onChange={selectSavedRoute}
480
- options={routes}
481
- loading={routesLoading}
482
- getId={(route) => route.id}
483
- getLabel={(route) => `${route.name} · ${route.stops.length} stops`}
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 &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>
565
532
  </div>
533
+ <Badge variant="secondary">Optional</Badge>
566
534
  </div>
567
- </CardContent>
568
- </Card>
569
-
570
- {/* Delivery Address */}
571
- <Card>
572
- <CardHeader>
573
- <CardTitle>Delivery Address</CardTitle>
574
- </CardHeader>
575
- <CardContent className="space-y-4">
576
- <div className="space-y-2">
577
- <Label htmlFor="deliveryStreet">Street Address</Label>
578
- <Input
579
- id="deliveryStreet"
580
- value={formData.deliveryAddress.street}
581
- onChange={(e) => updateAddress('deliveryAddress', 'street', e.target.value)}
582
- placeholder="456 Oak Ave"
583
- />
584
- </div>
585
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
586
- <div className="space-y-2">
587
- <Label htmlFor="deliveryCity">City</Label>
588
- <Input
589
- id="deliveryCity"
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
- <div className="space-y-4">
653
- {formData.items.map((item, index) => (
654
- <div key={item.id}>
655
- {index > 0 && <Separator className="my-4" />}
656
- <div className="grid grid-cols-1 md:grid-cols-7 gap-4">
657
- <div className="md:col-span-2 space-y-2">
658
- <Label>Item Name</Label>
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
- value={item.name}
661
- onChange={(e) => updateItem(index, 'name', e.target.value)}
662
- placeholder="Product name"
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>SKU</Label>
632
+ <Label htmlFor="pickupCountry">Country</Label>
667
633
  <Input
668
- value={item.sku}
669
- onChange={(e) => updateItem(index, 'sku', e.target.value)}
670
- placeholder="SKU-001"
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>Quantity</Label>
678
+ <Label htmlFor="deliveryCity">City</Label>
675
679
  <Input
676
- type="number"
677
- min="1"
678
- value={item.quantity}
679
- onChange={(e) =>
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>Weight (kg)</Label>
689
+ <Label htmlFor="deliveryState">State</Label>
686
690
  <Input
687
- type="number"
688
- min="0"
689
- step="0.1"
690
- value={item.weight}
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>Value ($)</Label>
702
+ <Label htmlFor="deliveryPostal">Postal code</Label>
698
703
  <Input
699
- type="number"
700
- min="0"
701
- step="0.01"
702
- value={item.value}
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="flex items-end">
709
- <Button
710
- type="button"
711
- variant="ghost"
712
- size="icon"
713
- className="text-status-error-text"
714
- onClick={() => removeItem(index)}
715
- >
716
- <Trash2 size={16} />
717
- </Button>
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
- </div>
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
- </CardContent>
725
- </Card>
726
-
727
- {/* Scheduling & handling notes */}
728
- <Card>
729
- <CardHeader>
730
- <CardTitle>Scheduling &amp; Handling</CardTitle>
731
- </CardHeader>
732
- <CardContent>
733
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
734
- <div className="space-y-2">
735
- <Label htmlFor="scheduledDelivery" className="flex items-center gap-1.5">
736
- <CalendarClock size={14} className="text-text-secondary" />
737
- Scheduled Delivery
738
- </Label>
739
- <Input
740
- id="scheduledDelivery"
741
- type="datetime-local"
742
- value={formData.scheduledDelivery}
743
- onChange={(e) => updateField('scheduledDelivery', e.target.value)}
744
- />
745
- <p className="text-xs text-text-secondary">
746
- The delivery window promised to the customer. Leave blank to dispatch as soon as a
747
- driver is free.
748
- </p>
749
- </div>
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 &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>
760
819
  </div>
761
- </CardContent>
762
- </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>
763
834
  </form>
764
835
  </PageLayout>
765
836
  );