@burdenoff/microfe-movethewheels 2026.822.1 → 2026.827.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.
Files changed (53) hide show
  1. package/dist/generated/wspace-operations.d.ts +168 -912
  2. package/dist/generated/wspace-types.d.ts +78432 -35831
  3. package/dist/hooks/useFleet.js +142 -74
  4. package/dist/hooks/useInventory.js +163 -70
  5. package/dist/hooks/useOrders.js +2 -2
  6. package/dist/hooks/useTracking.js +5 -5
  7. package/dist/index.js +33 -33
  8. package/dist/movethewheels/hooks/index.d.ts +2 -2
  9. package/dist/movethewheels/hooks/useFleet.d.ts +32 -1
  10. package/dist/movethewheels/hooks/useInventory.d.ts +67 -12
  11. package/dist/movethewheels/hooks/useOrders.d.ts +25 -2
  12. package/dist/movethewheels/types/index.d.ts +2 -0
  13. package/dist/node_modules/lucide-react/dist/esm/icons/arrow-down-right.js +11 -0
  14. package/dist/node_modules/lucide-react/dist/esm/icons/arrow-left-right.js +22 -0
  15. package/dist/node_modules/lucide-react/dist/esm/icons/arrow-up-right.js +11 -0
  16. package/dist/node_modules/lucide-react/dist/esm/icons/calendar-clock.js +32 -0
  17. package/dist/node_modules/lucide-react/dist/esm/icons/history.js +18 -0
  18. package/dist/node_modules/lucide-react/dist/esm/icons/lock.js +16 -0
  19. package/dist/node_modules/lucide-react/dist/esm/icons/user-round.js +13 -0
  20. package/dist/pages/CreateOrderPage.js +246 -195
  21. package/dist/pages/DeliveryCapturePage.js +91 -78
  22. package/dist/pages/DriversPage.js +202 -108
  23. package/dist/pages/InventoryPage.js +356 -131
  24. package/dist/pages/ProductDetailsPage.js +212 -156
  25. package/dist/pages/VehicleDetailsPage.js +257 -193
  26. package/dist/utils/formatters.js +4 -1
  27. package/package.json +1 -1
  28. package/src/generated/wspace-operations.ts +134 -929
  29. package/src/generated/wspace-types.ts +91730 -40059
  30. package/src/movethewheels/hooks/index.ts +9 -0
  31. package/src/movethewheels/hooks/useFleet.ts +133 -3
  32. package/src/movethewheels/hooks/useInventory.ts +311 -53
  33. package/src/movethewheels/hooks/useOrders.ts +31 -7
  34. package/src/movethewheels/hooks/useTracking.ts +14 -14
  35. package/src/movethewheels/pages/CreateOrderPage.tsx +67 -10
  36. package/src/movethewheels/pages/DeliveryCapturePage.tsx +8 -0
  37. package/src/movethewheels/pages/DriversPage.tsx +181 -31
  38. package/src/movethewheels/pages/InventoryPage.tsx +299 -19
  39. package/src/movethewheels/pages/ProductDetailsPage.tsx +68 -3
  40. package/src/movethewheels/pages/VehicleDetailsPage.tsx +86 -27
  41. package/src/movethewheels/types/index.ts +2 -0
  42. package/src/operations/wspace/movethewheels/fragments/core.graphql +23 -53
  43. package/src/operations/wspace/movethewheels/fragments/core.withdrawn.graphql +59 -0
  44. package/src/operations/wspace/movethewheels/mutations/entities.graphql +5 -5
  45. package/src/operations/wspace/movethewheels/mutations/marketplace-reports.graphql +18 -78
  46. package/src/operations/wspace/movethewheels/mutations/marketplace-reports.withdrawn.graphql +93 -0
  47. package/src/operations/wspace/movethewheels/mutations/orders.graphql +7 -7
  48. package/src/operations/wspace/movethewheels/queries/dashboard.graphql +3 -3
  49. package/src/operations/wspace/movethewheels/queries/entities.graphql +3 -3
  50. package/src/operations/wspace/movethewheels/queries/logistics.graphql +3 -3
  51. package/src/operations/wspace/movethewheels/queries/marketplace-reports.withdrawn.graphql +58 -0
  52. package/src/operations/wspace/movethewheels/queries/orders.graphql +3 -3
  53. package/src/operations/wspace/movethewheels/queries/marketplace-reports.graphql +0 -40
@@ -2,6 +2,7 @@
2
2
  * Inventory Page - Overview of inventory management
3
3
  */
4
4
 
5
+ import { useMemo } from 'react';
5
6
  import {
6
7
  Package,
7
8
  Warehouse,
@@ -9,18 +10,79 @@ import {
9
10
  ChevronRight,
10
11
  Plus,
11
12
  Boxes,
12
- DollarSign,
13
+ CircleCheck,
14
+ Lock,
15
+ History,
16
+ ArrowDownRight,
17
+ ArrowUpRight,
18
+ ArrowLeftRight,
13
19
  } from 'lucide-react';
14
20
  import { PageLayout, PageSection } from '../components/PageLayout';
15
21
  import { MetricCard, MetricGrid, MetricCardSkeleton } from '../components/MetricCard';
16
- import { Card, CardContent, Button, Badge, Progress } from '../components/ui';
22
+ import { DataTable, type Column } from '../components/DataTable';
23
+ import { StatusBadge } from '../components/StatusBadge';
24
+ import {
25
+ Card,
26
+ CardContent,
27
+ CardHeader,
28
+ CardTitle,
29
+ Button,
30
+ Badge,
31
+ Progress,
32
+ } from '../components/ui';
17
33
  import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
18
- import { useInventory, useInventoryStats } from '../hooks/useInventory';
34
+ import {
35
+ useInventory,
36
+ useInventoryStats,
37
+ useInventoryTransactions,
38
+ type InventoryTransactionType,
39
+ } from '../hooks/useInventory';
19
40
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
20
41
  import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
21
- import { formatCurrency } from '../utils/formatters';
42
+ import { formatCurrency, formatDate } from '../utils/formatters';
22
43
  import { joinPath } from '../utils/navigation';
23
44
 
45
+ /** One stock line joined to its catalog item and warehouse, for the position table. */
46
+ interface StockRow {
47
+ id: string;
48
+ productId: string;
49
+ productName: string;
50
+ sku: string;
51
+ warehouseName: string;
52
+ onHand: number;
53
+ allocated: number;
54
+ available: number;
55
+ reorderLevel: number;
56
+ stockState: 'out_of_stock' | 'low' | 'in_stock';
57
+ }
58
+
59
+ const STOCK_STATE_COLORS = {
60
+ out_of_stock: { bg: 'bg-status-error-bg-subtle', text: 'text-status-error-text' },
61
+ low: { bg: 'bg-status-warning-bg-subtle', text: 'text-status-warning-text' },
62
+ in_stock: { bg: 'bg-status-success-bg-subtle', text: 'text-status-success-text' },
63
+ } as const;
64
+
65
+ /**
66
+ * Movement direction per `InventoryTransactionType`. RECEIPT/RETURN add stock,
67
+ * ISSUE removes it, TRANSFER and ADJUSTMENT are direction-neutral in the ledger
68
+ * (the signed quantity on the row carries the direction).
69
+ */
70
+ const TRANSACTION_DIRECTION: Record<InventoryTransactionType, 'in' | 'out' | 'neutral'> = {
71
+ RECEIPT: 'in',
72
+ RETURN: 'in',
73
+ ISSUE: 'out',
74
+ TRANSFER: 'neutral',
75
+ ADJUSTMENT: 'neutral',
76
+ };
77
+
78
+ const TRANSACTION_LABELS: Record<InventoryTransactionType, string> = {
79
+ RECEIPT: 'Inbound',
80
+ RETURN: 'Return',
81
+ ISSUE: 'Outbound',
82
+ TRANSFER: 'Transfer',
83
+ ADJUSTMENT: 'Adjustment',
84
+ };
85
+
24
86
  export default function InventoryPage() {
25
87
  const { t: translate } = useI18n();
26
88
  const t = (key: string, fallback: string): string => {
@@ -30,6 +92,11 @@ export default function InventoryPage() {
30
92
  const { basePath, navigate } = useMoveTheWheels();
31
93
  const { inventory, products, warehouses, isLoading, error, refetch } = useInventory();
32
94
  const { stats } = useInventoryStats();
95
+ const {
96
+ transactions,
97
+ isLoading: transactionsLoading,
98
+ error: transactionsError,
99
+ } = useInventoryTransactions({ limit: 8 });
33
100
 
34
101
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
35
102
  const onRowKeyDown = (e: React.KeyboardEvent, action: () => void) => {
@@ -40,6 +107,104 @@ export default function InventoryPage() {
40
107
  };
41
108
  const lowStockItems = inventory.filter((inv) => inv.availableQuantity <= inv.reorderLevel);
42
109
 
110
+ const unknownProductLabel = t(
111
+ 'movethewheels.inventory.lowStock.unknownProduct',
112
+ 'Unknown Product'
113
+ );
114
+
115
+ const stockRows = useMemo<StockRow[]>(() => {
116
+ const productById = new Map(products.map((product) => [product.id, product]));
117
+ const warehouseById = new Map(warehouses.map((warehouse) => [warehouse.id, warehouse]));
118
+ return inventory.map((item) => {
119
+ const product = productById.get(item.productId);
120
+ return {
121
+ id: item.id,
122
+ productId: item.productId,
123
+ productName: product?.name ?? unknownProductLabel,
124
+ sku: product?.sku ?? '—',
125
+ warehouseName: warehouseById.get(item.warehouseId)?.name ?? '—',
126
+ onHand: item.quantity,
127
+ allocated: item.reservedQuantity,
128
+ available: item.availableQuantity,
129
+ reorderLevel: item.reorderLevel,
130
+ stockState:
131
+ item.quantity === 0
132
+ ? 'out_of_stock'
133
+ : item.availableQuantity <= item.reorderLevel
134
+ ? 'low'
135
+ : 'in_stock',
136
+ };
137
+ });
138
+ }, [inventory, products, warehouses, unknownProductLabel]);
139
+
140
+ const stockColumns: Column<StockRow>[] = [
141
+ {
142
+ id: 'product',
143
+ header: t('movethewheels.inventory.stock.product', 'Product'),
144
+ accessorKey: 'productName',
145
+ sortable: true,
146
+ cell: (row) => <span className="font-medium">{row.productName}</span>,
147
+ },
148
+ {
149
+ id: 'sku',
150
+ header: t('movethewheels.inventory.stock.sku', 'SKU'),
151
+ accessorKey: 'sku',
152
+ sortable: true,
153
+ cell: (row) => <span className="text-sm text-text-secondary">{row.sku}</span>,
154
+ },
155
+ {
156
+ id: 'warehouse',
157
+ header: t('movethewheels.inventory.stock.warehouse', 'Warehouse'),
158
+ accessorKey: 'warehouseName',
159
+ sortable: true,
160
+ },
161
+ {
162
+ id: 'onHand',
163
+ header: t('movethewheels.inventory.stock.onHand', 'On hand'),
164
+ accessorKey: 'onHand',
165
+ sortable: true,
166
+ cell: (row) => <span className="font-medium">{row.onHand.toLocaleString()}</span>,
167
+ },
168
+ {
169
+ id: 'allocated',
170
+ header: t('movethewheels.inventory.stock.allocated', 'Allocated'),
171
+ accessorKey: 'allocated',
172
+ sortable: true,
173
+ cell: (row) => row.allocated.toLocaleString(),
174
+ },
175
+ {
176
+ id: 'available',
177
+ header: t('movethewheels.inventory.stock.available', 'Available'),
178
+ accessorKey: 'available',
179
+ sortable: true,
180
+ cell: (row) => <span className="font-medium">{row.available.toLocaleString()}</span>,
181
+ },
182
+ {
183
+ id: 'reorderPoint',
184
+ header: t('movethewheels.inventory.stock.reorderPoint', 'Reorder point'),
185
+ accessorKey: 'reorderLevel',
186
+ sortable: true,
187
+ cell: (row) => (row.reorderLevel > 0 ? row.reorderLevel.toLocaleString() : '—'),
188
+ },
189
+ {
190
+ id: 'state',
191
+ header: t('movethewheels.inventory.stock.status', 'Status'),
192
+ cell: (row) => (
193
+ <StatusBadge
194
+ status={
195
+ row.stockState === 'out_of_stock'
196
+ ? t('movethewheels.inventory.stock.outOfStock', 'Out of stock')
197
+ : row.stockState === 'low'
198
+ ? t('movethewheels.inventory.stock.low', 'Low')
199
+ : t('movethewheels.inventory.stock.inStock', 'In stock')
200
+ }
201
+ customColors={STOCK_STATE_COLORS[row.stockState]}
202
+ size="sm"
203
+ />
204
+ ),
205
+ },
206
+ ];
207
+
43
208
  return (
44
209
  <PageLayout
45
210
  title={t('movethewheels.inventory.title', 'Inventory')}
@@ -68,7 +233,7 @@ export default function InventoryPage() {
68
233
  <PagePurpose icon={<Package size={18} />}>
69
234
  {t(
70
235
  'movethewheels.inventory.purpose',
71
- 'A real-time view of what you have and where. Track stock value and quantities across warehouses, catch low-stock items before they run out, and drill into products or warehouses to manage what you can promise on an order.'
236
+ 'A real-time view of what you have and where. Track on-hand, allocated and free-to-promise quantities across warehouses, follow every stock movement, catch low-stock items before they run out, and drill into products or warehouses to manage what you can promise on an order.'
72
237
  )}
73
238
  </PagePurpose>
74
239
 
@@ -85,35 +250,54 @@ export default function InventoryPage() {
85
250
  </CardContent>
86
251
  </Card>
87
252
  ) : isLoading ? (
88
- <MetricGrid columns={4}>
89
- {Array.from({ length: 4 }).map((_, i) => (
253
+ <MetricGrid columns={6}>
254
+ {Array.from({ length: 6 }).map((_, i) => (
90
255
  <MetricCardSkeleton key={i} />
91
256
  ))}
92
257
  </MetricGrid>
93
258
  ) : (
94
- <MetricGrid columns={4}>
259
+ <MetricGrid columns={6}>
95
260
  <MetricCard
96
- label={t('movethewheels.inventory.metrics.totalProducts', 'Total Products')}
97
- value={stats.totalProducts}
98
- icon={<Package size={20} />}
261
+ label={t('movethewheels.inventory.metrics.totalOnHand', 'Total on-hand')}
262
+ value={stats.totalOnHand.toLocaleString()}
263
+ icon={<Boxes size={20} />}
99
264
  accent="brand"
100
- onClick={() => goTo('inventory/products')}
265
+ subtitle={t('movethewheels.inventory.metrics.onHandHint', 'Units in the network')}
101
266
  />
102
267
  <MetricCard
103
- label={t('movethewheels.inventory.metrics.totalItems', 'Total Items')}
104
- value={stats.totalItems.toLocaleString()}
105
- icon={<Boxes size={20} />}
268
+ label={t('movethewheels.inventory.metrics.available', 'Available')}
269
+ value={stats.totalAvailable.toLocaleString()}
270
+ icon={<CircleCheck size={20} />}
106
271
  accent="success"
272
+ subtitle={t('movethewheels.inventory.metrics.availableHint', 'Free to promise')}
107
273
  />
108
274
  <MetricCard
109
- label={t('movethewheels.inventory.metrics.inventoryValue', 'Inventory Value')}
110
- value={formatCurrency(stats.totalValue)}
111
- icon={<DollarSign size={20} />}
275
+ label={t('movethewheels.inventory.metrics.allocated', 'Allocated')}
276
+ value={stats.totalAllocated.toLocaleString()}
277
+ icon={<Lock size={20} />}
112
278
  accent="blue"
279
+ subtitle={t('movethewheels.inventory.metrics.allocatedHint', 'Committed to orders')}
280
+ />
281
+ <MetricCard
282
+ label={t('movethewheels.inventory.metrics.lowStock', 'Low-stock items')}
283
+ value={stats.lowStockCount.toLocaleString()}
284
+ icon={<AlertTriangle size={20} />}
285
+ accent="warning"
286
+ subtitle={t(
287
+ 'movethewheels.inventory.metrics.lowStockHint',
288
+ 'At or under reorder point'
289
+ )}
290
+ />
291
+ <MetricCard
292
+ label={t('movethewheels.inventory.metrics.totalProducts', 'Catalog items')}
293
+ value={stats.totalProducts.toLocaleString()}
294
+ icon={<Package size={20} />}
295
+ accent="neutral"
296
+ onClick={() => goTo('inventory/products')}
113
297
  />
114
298
  <MetricCard
115
299
  label={t('movethewheels.inventory.metrics.warehouses', 'Warehouses')}
116
- value={stats.totalWarehouses}
300
+ value={stats.totalWarehouses.toLocaleString()}
117
301
  icon={<Warehouse size={20} />}
118
302
  accent="neutral"
119
303
  onClick={() => goTo('inventory/warehouses')}
@@ -164,6 +348,102 @@ export default function InventoryPage() {
164
348
  </EmphasisPanel>
165
349
  )}
166
350
 
351
+ {/* Stock position + ledger. The table is the per-warehouse stock line;
352
+ the side panel is the real movement history from Query.inventoryTransactions. */}
353
+ <div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
354
+ <div className="xl:col-span-2">
355
+ <PageSection title={t('movethewheels.inventory.stock.title', 'Stock position')}>
356
+ <DataTable
357
+ columns={stockColumns}
358
+ data={stockRows}
359
+ isLoading={isLoading}
360
+ error={error}
361
+ onRetry={refetch}
362
+ searchable
363
+ searchPlaceholder={t(
364
+ 'movethewheels.inventory.stock.search',
365
+ 'Search product, SKU or warehouse...'
366
+ )}
367
+ searchFields={['productName', 'sku', 'warehouseName'] as (keyof StockRow)[]}
368
+ onRowClick={(row) => goTo(`inventory/products/${row.productId}`)}
369
+ getRowId={(row) => row.id}
370
+ emptyIllustration="empty-data"
371
+ emptyTitle={t('movethewheels.inventory.stock.emptyTitle', 'No stock recorded yet')}
372
+ emptyDescription={t(
373
+ 'movethewheels.inventory.stock.emptyDescription',
374
+ 'Stock lines appear here once a product is received into a warehouse.'
375
+ )}
376
+ />
377
+ </PageSection>
378
+ </div>
379
+
380
+ <Card className="h-fit">
381
+ <CardHeader>
382
+ <CardTitle className="flex items-center gap-2">
383
+ <History size={18} />
384
+ {t('movethewheels.inventory.transactions.title', 'Recent transactions')}
385
+ </CardTitle>
386
+ </CardHeader>
387
+ <CardContent className="p-0">
388
+ {transactionsError ? (
389
+ <p className="p-6 text-sm text-status-error-text">{transactionsError.message}</p>
390
+ ) : transactionsLoading ? (
391
+ <p className="p-6 text-sm text-text-secondary">
392
+ {t('movethewheels.inventory.transactions.loading', 'Loading movements…')}
393
+ </p>
394
+ ) : transactions.length === 0 ? (
395
+ <p className="p-6 text-sm text-text-secondary">
396
+ {t(
397
+ 'movethewheels.inventory.transactions.empty',
398
+ 'No stock movements recorded yet. Receipts, issues and adjustments show up here as they happen.'
399
+ )}
400
+ </p>
401
+ ) : (
402
+ <ul className="divide-y">
403
+ {transactions.map((transaction) => {
404
+ const direction = TRANSACTION_DIRECTION[transaction.type];
405
+ const productName =
406
+ products.find((product) => product.id === transaction.productId)?.name ??
407
+ transaction.transactionNumber;
408
+ return (
409
+ <li key={transaction.id} className="flex items-start justify-between gap-3 p-4">
410
+ <div className="min-w-0">
411
+ <p className="flex items-center gap-1.5 font-medium">
412
+ {direction === 'in' ? (
413
+ <ArrowDownRight size={14} className="text-status-success-text" />
414
+ ) : direction === 'out' ? (
415
+ <ArrowUpRight size={14} className="text-status-warning-text" />
416
+ ) : (
417
+ <ArrowLeftRight size={14} className="text-text-secondary" />
418
+ )}
419
+ {TRANSACTION_LABELS[transaction.type]}
420
+ </p>
421
+ <p className="truncate text-sm text-text-secondary">{productName}</p>
422
+ <p className="text-xs text-text-secondary">
423
+ {formatDate(transaction.createdAt, 'short')}
424
+ </p>
425
+ </div>
426
+ <span
427
+ className={
428
+ direction === 'in'
429
+ ? 'font-medium text-status-success-text'
430
+ : direction === 'out'
431
+ ? 'font-medium text-status-warning-text'
432
+ : 'font-medium text-text-primary'
433
+ }
434
+ >
435
+ {direction === 'in' ? '+' : direction === 'out' ? '−' : ''}
436
+ {Math.abs(transaction.quantity).toLocaleString()}
437
+ </span>
438
+ </li>
439
+ );
440
+ })}
441
+ </ul>
442
+ )}
443
+ </CardContent>
444
+ </Card>
445
+ </div>
446
+
167
447
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
168
448
  <PageSection
169
449
  title={t('movethewheels.inventory.warehouses.title', 'Warehouses')}
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { useEffect, useState } from 'react';
6
6
  import { useLocation, useParams } from 'react-router';
7
- import { Package, ArrowLeft, Edit, Box, DollarSign, Tag, Save } from 'lucide-react';
7
+ import { Package, ArrowLeft, Edit, Box, DollarSign, Tag, Save, Warehouse } from 'lucide-react';
8
8
  import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
9
9
  import {
10
10
  Card,
@@ -17,7 +17,7 @@ import {
17
17
  Label,
18
18
  } from '../components/ui';
19
19
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
20
- import { useProduct, useInventory } from '../hooks/useInventory';
20
+ import { useProduct, useInventory, useProductStockLevels } from '../hooks/useInventory';
21
21
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
22
22
  import { formatCurrency } from '../utils/formatters';
23
23
  import { joinPath } from '../utils/navigation';
@@ -191,6 +191,13 @@ export default function ProductDetailsPage() {
191
191
  const { basePath, navigate } = useMoveTheWheels();
192
192
  const { product, isLoading, error, refetch } = useProduct(productId);
193
193
  const isEditRoute = useLocation().pathname.endsWith('/edit');
194
+ // Where this item is actually stocked — inventoryItems(filter: { cargoItemId }).
195
+ const {
196
+ levels: stockLevels,
197
+ isLoading: stockLoading,
198
+ error: stockError,
199
+ } = useProductStockLevels(isEditRoute ? undefined : productId);
200
+ const totalAvailable = stockLevels.reduce((sum, level) => sum + level.available, 0);
194
201
 
195
202
  const goBack = () => navigate?.(joinPath(basePath, 'inventory/products'));
196
203
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
@@ -248,7 +255,7 @@ export default function ProductDetailsPage() {
248
255
  label: 'Edit product details',
249
256
  description: 'Update dimensions, value or handling flags.',
250
257
  icon: <Tag size={16} />,
251
- onClick: () => {},
258
+ onClick: () => goTo(`inventory/products/${productId}/edit`),
252
259
  },
253
260
  ]}
254
261
  />
@@ -335,6 +342,64 @@ export default function ProductDetailsPage() {
335
342
  <p className="text-sm text-text-secondary">Unit price</p>
336
343
  </EmphasisPanel>
337
344
 
345
+ {/* Stock levels — one row per warehouse holding this item. */}
346
+ <Card>
347
+ <CardHeader>
348
+ <CardTitle className="flex items-center gap-2">
349
+ <Warehouse size={18} /> Stock Levels
350
+ </CardTitle>
351
+ </CardHeader>
352
+ <CardContent className="space-y-3">
353
+ {stockError ? (
354
+ <p className="text-sm text-status-error-text">{stockError.message}</p>
355
+ ) : stockLoading ? (
356
+ <p className="text-sm text-text-secondary">Loading stock levels…</p>
357
+ ) : stockLevels.length === 0 ? (
358
+ <div className="space-y-3">
359
+ <p className="text-sm text-text-secondary">
360
+ This item isn&apos;t stocked in any warehouse yet, so nothing can be promised on
361
+ an order.
362
+ </p>
363
+ <Button variant="outline" size="sm" onClick={() => goTo('inventory/warehouses')}>
364
+ View warehouses
365
+ </Button>
366
+ </div>
367
+ ) : (
368
+ <>
369
+ {stockLevels.map((level) => (
370
+ <div
371
+ key={level.inventoryItemId}
372
+ className="flex items-baseline justify-between gap-3"
373
+ >
374
+ <div className="min-w-0">
375
+ <p className="truncate text-sm font-medium">{level.warehouseName}</p>
376
+ <p className="text-xs text-text-secondary">
377
+ {level.allocated.toLocaleString()} allocated
378
+ {level.reorderPoint !== undefined
379
+ ? ` · reorder at ${level.reorderPoint.toLocaleString()}`
380
+ : ''}
381
+ </p>
382
+ </div>
383
+ <span
384
+ className={
385
+ level.reorderPoint !== undefined && level.available <= level.reorderPoint
386
+ ? 'font-semibold text-status-warning-text'
387
+ : 'font-semibold'
388
+ }
389
+ >
390
+ {level.available.toLocaleString()}
391
+ </span>
392
+ </div>
393
+ ))}
394
+ <div className="flex items-baseline justify-between gap-3 border-t border-border-seam pt-3">
395
+ <p className="text-sm text-text-secondary">Available across the network</p>
396
+ <span className="font-bold">{totalAvailable.toLocaleString()}</span>
397
+ </div>
398
+ </>
399
+ )}
400
+ </CardContent>
401
+ </Card>
402
+
338
403
  <Card>
339
404
  <CardHeader>
340
405
  <CardTitle>Identifiers</CardTitle>