@burdenoff/microfe-movethewheels 2026.930.2 → 2026.930.4

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.
@@ -348,6 +348,45 @@ const UPDATE_WAREHOUSE_MUTATION = `
348
348
  }
349
349
  `;
350
350
 
351
+ const INVENTORY_LINE_FIELDS = `
352
+ id
353
+ cargoItemId
354
+ warehouseId
355
+ onHand
356
+ available
357
+ allocated
358
+ damaged
359
+ reorderPoint
360
+ createdAt
361
+ updatedAt
362
+ cargoItem {
363
+ id
364
+ name
365
+ sku
366
+ }
367
+ warehouse {
368
+ id
369
+ name
370
+ code
371
+ }
372
+ `;
373
+
374
+ const FIND_STOCK_LINE_QUERY = `
375
+ query FindStockLine($filter: InventoryFilterInput, $pagination: PaginationInput) {
376
+ inventoryItems(filter: $filter, pagination: $pagination) {
377
+ ${INVENTORY_LINE_FIELDS}
378
+ }
379
+ }
380
+ `;
381
+
382
+ const CREATE_INVENTORY_ITEM_MUTATION = `
383
+ mutation CreateInventoryItem($input: CreateInventoryItemInput!) {
384
+ createInventoryItem(input: $input) {
385
+ ${INVENTORY_LINE_FIELDS}
386
+ }
387
+ }
388
+ `;
389
+
351
390
  const ADJUST_INVENTORY_MUTATION = `
352
391
  mutation AdjustInventoryStock($input: AdjustInventoryStockInput!) {
353
392
  adjustInventoryStock(input: $input) {
@@ -1012,6 +1051,7 @@ export function useWarehouses(options: UseWarehousesOptions = {}) {
1012
1051
  const [totalWarehouses, setTotalWarehouses] = useState(0);
1013
1052
  const [hasMore, setHasMore] = useState(false);
1014
1053
  const [isLoading, setIsLoading] = useState(false);
1054
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
1015
1055
  const [error, setError] = useState<Error | null>(null);
1016
1056
 
1017
1057
  const load = useCallback(async () => {
@@ -1035,6 +1075,46 @@ export function useWarehouses(options: UseWarehousesOptions = {}) {
1035
1075
  }
1036
1076
  }, [graphQLOptions, limit, offset, search, status]);
1037
1077
 
1078
+ /** Appends the next server page (pickers); list pages keep using `offset`. */
1079
+ const loadMore = useCallback(async () => {
1080
+ if (!hasMore || isLoading || isLoadingMore) return;
1081
+ const nextOffset = offset + warehouses.length;
1082
+ setIsLoadingMore(true);
1083
+ setError(null);
1084
+ try {
1085
+ const data = await executeGraphQL<{
1086
+ warehouses: ApiWarehouse[];
1087
+ warehousesCount: number;
1088
+ }>(graphQLOptions, LIST_WAREHOUSES_QUERY, {
1089
+ filter: { search: search || undefined, status: status || undefined },
1090
+ pagination: { limit, offset: nextOffset },
1091
+ });
1092
+ const mapped = data.warehouses.map(mapWarehouse);
1093
+ setTotalWarehouses(data.warehousesCount);
1094
+ setHasMore(nextOffset + mapped.length < data.warehousesCount);
1095
+ setWarehouses((previous) => {
1096
+ const existingIds = new Set(previous.map((warehouse) => warehouse.id));
1097
+ return [...previous, ...mapped.filter((warehouse) => !existingIds.has(warehouse.id))];
1098
+ });
1099
+ } catch (fetchError) {
1100
+ setError(
1101
+ fetchError instanceof Error ? fetchError : new Error('Failed to load more warehouses')
1102
+ );
1103
+ } finally {
1104
+ setIsLoadingMore(false);
1105
+ }
1106
+ }, [
1107
+ graphQLOptions,
1108
+ hasMore,
1109
+ isLoading,
1110
+ isLoadingMore,
1111
+ limit,
1112
+ offset,
1113
+ search,
1114
+ status,
1115
+ warehouses.length,
1116
+ ]);
1117
+
1038
1118
  useEffect(() => {
1039
1119
  void load();
1040
1120
  }, [load]);
@@ -1044,8 +1124,10 @@ export function useWarehouses(options: UseWarehousesOptions = {}) {
1044
1124
  totalWarehouses,
1045
1125
  hasMore,
1046
1126
  isLoading,
1127
+ isLoadingMore,
1047
1128
  error,
1048
1129
  refetch: () => void load(),
1130
+ loadMore,
1049
1131
  };
1050
1132
  }
1051
1133
 
@@ -1161,45 +1243,49 @@ export function useInventoryStats() {
1161
1243
  const [isLoading, setIsLoading] = useState(false);
1162
1244
  const [error, setError] = useState<Error | null>(null);
1163
1245
 
1246
+ const load = useCallback(async () => {
1247
+ setIsLoading(true);
1248
+ setError(null);
1249
+ try {
1250
+ const data = await executeGraphQL<{
1251
+ inventoryStats: {
1252
+ totalOnHand: number;
1253
+ totalAvailable: number;
1254
+ totalAllocated: number;
1255
+ totalDamaged: number;
1256
+ lowStockCount: number;
1257
+ };
1258
+ cargoItemsCount: number;
1259
+ warehousesCount: number;
1260
+ }>(graphQLOptions, INVENTORY_STATS_QUERY);
1261
+
1262
+ setStats({
1263
+ totalOnHand: data.inventoryStats.totalOnHand,
1264
+ totalAvailable: data.inventoryStats.totalAvailable,
1265
+ totalAllocated: data.inventoryStats.totalAllocated,
1266
+ totalDamaged: data.inventoryStats.totalDamaged,
1267
+ lowStockCount: data.inventoryStats.lowStockCount,
1268
+ totalProducts: data.cargoItemsCount,
1269
+ totalWarehouses: data.warehousesCount,
1270
+ });
1271
+ } catch (fetchError) {
1272
+ setError(
1273
+ fetchError instanceof Error ? fetchError : new Error('Failed to load inventory stats')
1274
+ );
1275
+ } finally {
1276
+ setIsLoading(false);
1277
+ }
1278
+ }, [graphQLOptions]);
1279
+
1164
1280
  useEffect(() => {
1165
- const load = async () => {
1166
- setIsLoading(true);
1167
- setError(null);
1168
- try {
1169
- const data = await executeGraphQL<{
1170
- inventoryStats: {
1171
- totalOnHand: number;
1172
- totalAvailable: number;
1173
- totalAllocated: number;
1174
- totalDamaged: number;
1175
- lowStockCount: number;
1176
- };
1177
- cargoItemsCount: number;
1178
- warehousesCount: number;
1179
- }>(graphQLOptions, INVENTORY_STATS_QUERY);
1180
-
1181
- setStats({
1182
- totalOnHand: data.inventoryStats.totalOnHand,
1183
- totalAvailable: data.inventoryStats.totalAvailable,
1184
- totalAllocated: data.inventoryStats.totalAllocated,
1185
- totalDamaged: data.inventoryStats.totalDamaged,
1186
- lowStockCount: data.inventoryStats.lowStockCount,
1187
- totalProducts: data.cargoItemsCount,
1188
- totalWarehouses: data.warehousesCount,
1189
- });
1190
- } catch (fetchError) {
1191
- setError(
1192
- fetchError instanceof Error ? fetchError : new Error('Failed to load inventory stats')
1193
- );
1194
- } finally {
1195
- setIsLoading(false);
1196
- }
1197
- };
1281
+ void load();
1282
+ }, [load]);
1198
1283
 
1284
+ const refetch = useCallback(() => {
1199
1285
  void load();
1200
- }, [graphQLOptions]);
1286
+ }, [load]);
1201
1287
 
1202
- return { stats, isLoading, error };
1288
+ return { stats, isLoading, error, refetch };
1203
1289
  }
1204
1290
 
1205
1291
  /**
@@ -1445,3 +1531,176 @@ export function useInventoryTransactions(options: UseInventoryTransactionsOption
1445
1531
 
1446
1532
  return { transactions, isLoading, error, refetch };
1447
1533
  }
1534
+
1535
+ export interface ReceiveStockInput {
1536
+ productId: string;
1537
+ warehouseId: string;
1538
+ /** Whole units received; must be > 0. */
1539
+ quantity: number;
1540
+ /** Only applied when the receipt creates a new stock line. */
1541
+ reorderPoint?: number | null;
1542
+ }
1543
+
1544
+ export interface TransferStockInput {
1545
+ sourceInventoryId: string;
1546
+ destinationWarehouseId: string;
1547
+ /** Whole units to move out of the source line's available stock; must be > 0. */
1548
+ quantity: number;
1549
+ }
1550
+
1551
+ function assertWholeUnits(quantity: number, label: string): void {
1552
+ if (!Number.isInteger(quantity) || quantity <= 0) {
1553
+ throw new Error(`${label} must be a positive whole number.`);
1554
+ }
1555
+ }
1556
+
1557
+ /**
1558
+ * Stock movements that do not depend on the list currently on screen: every
1559
+ * write re-reads the line first, so a stale table row can never overwrite
1560
+ * someone else's change (the service also rejects a mid-flight change).
1561
+ */
1562
+ export function useStockActions() {
1563
+ const graphQLOptions = useGraphqlOptions();
1564
+
1565
+ const findLine = useCallback(
1566
+ async (filter: {
1567
+ inventoryItemId?: string;
1568
+ cargoItemId?: string;
1569
+ warehouseId?: string;
1570
+ }): Promise<Inventory | null> => {
1571
+ const data = await executeGraphQL<{ inventoryItems: ApiInventoryItem[] }>(
1572
+ graphQLOptions,
1573
+ FIND_STOCK_LINE_QUERY,
1574
+ { filter, pagination: { limit: 1 } }
1575
+ );
1576
+ const line = data.inventoryItems[0];
1577
+ return line ? mapInventory(line) : null;
1578
+ },
1579
+ [graphQLOptions]
1580
+ );
1581
+
1582
+ /** Applies a signed change to on-hand and available; allocated/damaged are untouched. */
1583
+ const applyChange = useCallback(
1584
+ async (line: Inventory, change: number): Promise<Inventory> => {
1585
+ const onHand = line.quantity + change;
1586
+ const available = line.availableQuantity + change;
1587
+ if (available < 0) {
1588
+ throw new Error(
1589
+ `Only ${line.availableQuantity.toLocaleString()} unit(s) are available to remove.`
1590
+ );
1591
+ }
1592
+ if (onHand < 0) {
1593
+ throw new Error('On-hand stock cannot go below zero.');
1594
+ }
1595
+ const data = await executeGraphQL<{ adjustInventoryStock: ApiInventoryItem }>(
1596
+ graphQLOptions,
1597
+ ADJUST_INVENTORY_MUTATION,
1598
+ {
1599
+ input: {
1600
+ id: line.id,
1601
+ onHand,
1602
+ available,
1603
+ allocated: line.reservedQuantity,
1604
+ damaged: line.damagedQuantity,
1605
+ },
1606
+ }
1607
+ );
1608
+ return mapInventory(data.adjustInventoryStock);
1609
+ },
1610
+ [graphQLOptions]
1611
+ );
1612
+
1613
+ const adjustStock = useCallback(
1614
+ async (inventoryId: string, change: number): Promise<Inventory> => {
1615
+ if (!Number.isInteger(change) || change === 0) {
1616
+ throw new Error('Enter a whole-number change other than zero.');
1617
+ }
1618
+ const line = await findLine({ inventoryItemId: inventoryId });
1619
+ if (!line) throw new Error('This stock line no longer exists.');
1620
+ return applyChange(line, change);
1621
+ },
1622
+ [applyChange, findLine]
1623
+ );
1624
+
1625
+ const receiveStock = useCallback(
1626
+ async (input: ReceiveStockInput): Promise<Inventory> => {
1627
+ assertWholeUnits(input.quantity, 'Quantity received');
1628
+ const existing = await findLine({
1629
+ cargoItemId: input.productId,
1630
+ warehouseId: input.warehouseId,
1631
+ });
1632
+ if (existing) return applyChange(existing, input.quantity);
1633
+
1634
+ // New line: the service records the opening stock as a RECEIPT ledger entry.
1635
+ const data = await executeGraphQL<{ createInventoryItem: ApiInventoryItem }>(
1636
+ graphQLOptions,
1637
+ CREATE_INVENTORY_ITEM_MUTATION,
1638
+ {
1639
+ input: {
1640
+ cargoItemId: input.productId,
1641
+ warehouseId: input.warehouseId,
1642
+ onHand: input.quantity,
1643
+ available: input.quantity,
1644
+ ...(input.reorderPoint !== null && input.reorderPoint !== undefined
1645
+ ? { reorderPoint: input.reorderPoint }
1646
+ : {}),
1647
+ },
1648
+ }
1649
+ );
1650
+ return mapInventory(data.createInventoryItem);
1651
+ },
1652
+ [applyChange, findLine, graphQLOptions]
1653
+ );
1654
+
1655
+ const transferStock = useCallback(
1656
+ async (input: TransferStockInput): Promise<{ source: Inventory; destination: Inventory }> => {
1657
+ assertWholeUnits(input.quantity, 'Transfer quantity');
1658
+ const source = await findLine({ inventoryItemId: input.sourceInventoryId });
1659
+ if (!source) throw new Error('The source stock line no longer exists.');
1660
+ if (source.warehouseId === input.destinationWarehouseId) {
1661
+ throw new Error('Source and destination warehouses must be different.');
1662
+ }
1663
+ if (input.quantity > source.availableQuantity) {
1664
+ throw new Error(
1665
+ `Only ${source.availableQuantity.toLocaleString()} unit(s) are available to transfer.`
1666
+ );
1667
+ }
1668
+
1669
+ let destination = await findLine({
1670
+ cargoItemId: source.productId,
1671
+ warehouseId: input.destinationWarehouseId,
1672
+ });
1673
+ if (!destination) {
1674
+ // An empty line records no movement; the transfer below writes both ledger entries.
1675
+ const created = await executeGraphQL<{ createInventoryItem: ApiInventoryItem }>(
1676
+ graphQLOptions,
1677
+ CREATE_INVENTORY_ITEM_MUTATION,
1678
+ {
1679
+ input: {
1680
+ cargoItemId: source.productId,
1681
+ warehouseId: input.destinationWarehouseId,
1682
+ },
1683
+ }
1684
+ );
1685
+ destination = mapInventory(created.createInventoryItem);
1686
+ }
1687
+
1688
+ const data = await executeGraphQL<{
1689
+ transferInventoryStock: { source: ApiInventoryItem; destination: ApiInventoryItem };
1690
+ }>(graphQLOptions, TRANSFER_INVENTORY_STOCK_MUTATION, {
1691
+ input: {
1692
+ sourceInventoryItemId: source.id,
1693
+ destinationInventoryItemId: destination.id,
1694
+ quantity: input.quantity,
1695
+ },
1696
+ });
1697
+ return {
1698
+ source: mapInventory(data.transferInventoryStock.source),
1699
+ destination: mapInventory(data.transferInventoryStock.destination),
1700
+ };
1701
+ },
1702
+ [findLine, graphQLOptions]
1703
+ );
1704
+
1705
+ return { receiveStock, adjustStock, transferStock };
1706
+ }
@@ -15,6 +15,7 @@ import {
15
15
  MapPin,
16
16
  Save,
17
17
  UserX,
18
+ Trash2,
18
19
  Truck,
19
20
  StickyNote,
20
21
  } from 'lucide-react';
@@ -120,7 +121,7 @@ const emptyForm: DriverFormState = {
120
121
 
121
122
  function NewDriverForm({ editId }: { editId?: string }) {
122
123
  const { basePath, navigate } = useMoveTheWheels();
123
- const { createDriver, updateDriver } = useFleet();
124
+ const { createDriver, updateDriver, deleteDriver } = useFleet();
124
125
  const { driver, refetch: refetchDriver } = useDriver(editId);
125
126
  const isEditing = !!editId;
126
127
  const [form, setForm] = useState<DriverFormState>(emptyForm);
@@ -130,6 +131,9 @@ function NewDriverForm({ editId }: { editId?: string }) {
130
131
  const [dismissReason, setDismissReason] = useState('');
131
132
  const [dismissing, setDismissing] = useState(false);
132
133
  const [dismissError, setDismissError] = useState<string | null>(null);
134
+ const [deleteOpen, setDeleteOpen] = useState(false);
135
+ const [deleting, setDeleting] = useState(false);
136
+ const [deleteError, setDeleteError] = useState<string | null>(null);
133
137
 
134
138
  const [vehicleSearch, setVehicleSearch] = useState('');
135
139
  const debouncedVehicleSearch = useDebouncedValue(vehicleSearch);
@@ -250,6 +254,21 @@ function NewDriverForm({ editId }: { editId?: string }) {
250
254
  }
251
255
  };
252
256
 
257
+ const handleDelete = async () => {
258
+ if (!editId) return;
259
+ setDeleting(true);
260
+ setDeleteError(null);
261
+ try {
262
+ await deleteDriver(editId);
263
+ setDeleteOpen(false);
264
+ navigate?.(joinPath(basePath, 'fleet/drivers'));
265
+ } catch (err) {
266
+ setDeleteError(err instanceof Error ? err.message : 'Failed to delete driver.');
267
+ } finally {
268
+ setDeleting(false);
269
+ }
270
+ };
271
+
253
272
  const alreadyDismissed = isEditing && Boolean(driver?.terminatedAt);
254
273
 
255
274
  return (
@@ -585,8 +604,60 @@ function NewDriverForm({ editId }: { editId?: string }) {
585
604
  </CardContent>
586
605
  </Card>
587
606
  )}
607
+
608
+ {isEditing && (
609
+ <Card className="border-status-error-border">
610
+ <CardHeader>
611
+ <CardTitle className="flex items-center gap-2 text-status-error-text">
612
+ <Trash2 size={18} /> Delete Driver
613
+ </CardTitle>
614
+ </CardHeader>
615
+ <CardContent>
616
+ <p className="mb-3 text-sm text-text-secondary">
617
+ Removes this driver record, for example one entered by mistake, and unlinks them
618
+ from any vehicle. To keep delivery history for someone who has left, dismiss them
619
+ instead. This cannot be undone from here.
620
+ </p>
621
+ <Button
622
+ type="button"
623
+ variant="outline"
624
+ className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
625
+ onClick={() => setDeleteOpen(true)}
626
+ >
627
+ <Trash2 size={16} /> Delete this driver
628
+ </Button>
629
+ </CardContent>
630
+ </Card>
631
+ )}
588
632
  </form>
589
633
 
634
+ <ResponsiveDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
635
+ <ResponsiveDialogContent>
636
+ <ResponsiveDialogHeader>
637
+ <ResponsiveDialogTitle>Delete this driver?</ResponsiveDialogTitle>
638
+ <ResponsiveDialogDescription>
639
+ This removes {`${form.firstName} ${form.lastName}`.trim() || 'this driver'} and
640
+ unlinks them from any vehicle. It cannot be undone from here.
641
+ </ResponsiveDialogDescription>
642
+ </ResponsiveDialogHeader>
643
+ <div className="px-1">
644
+ <FormError message={deleteError} />
645
+ </div>
646
+ <ResponsiveDialogFooter>
647
+ <Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
648
+ Cancel
649
+ </Button>
650
+ <Button
651
+ className="bg-status-error-text text-text-inverse hover:opacity-90"
652
+ onClick={handleDelete}
653
+ disabled={deleting}
654
+ >
655
+ {deleting ? 'Deleting...' : 'Delete driver'}
656
+ </Button>
657
+ </ResponsiveDialogFooter>
658
+ </ResponsiveDialogContent>
659
+ </ResponsiveDialog>
660
+
590
661
  <ResponsiveDialog open={dismissOpen} onOpenChange={setDismissOpen}>
591
662
  <ResponsiveDialogContent>
592
663
  <ResponsiveDialogHeader>
@@ -16,11 +16,20 @@ import {
16
16
  ArrowDownRight,
17
17
  ArrowUpRight,
18
18
  ArrowLeftRight,
19
+ PackagePlus,
20
+ SlidersHorizontal,
19
21
  } from 'lucide-react';
20
22
  import { PageLayout, PageSection } from '../components/PageLayout';
21
23
  import { MetricCard, MetricGrid, MetricCardSkeleton } from '../components/MetricCard';
22
24
  import { DataTable, type Column } from '../components/DataTable';
23
25
  import { StatusBadge } from '../components/StatusBadge';
26
+ import { Notice, useNotice } from '../components/Notice';
27
+ import {
28
+ AdjustStockDialog,
29
+ ReceiveStockDialog,
30
+ TransferStockDialog,
31
+ type StockLineTarget,
32
+ } from '../components/StockActionDialogs';
24
33
  import {
25
34
  Card,
26
35
  CardContent,
@@ -49,6 +58,7 @@ interface StockRow {
49
58
  productId: string;
50
59
  productName: string;
51
60
  sku: string;
61
+ warehouseId: string;
52
62
  warehouseName: string;
53
63
  onHand: number;
54
64
  allocated: number;
@@ -110,12 +120,35 @@ export default function InventoryPage() {
110
120
  limit: pageSize,
111
121
  offset: pageIndex * pageSize,
112
122
  });
113
- const { stats } = useInventoryStats();
123
+ const { stats, refetch: refetchStats } = useInventoryStats();
114
124
  const {
115
125
  transactions,
116
126
  isLoading: transactionsLoading,
117
127
  error: transactionsError,
128
+ refetch: refetchTransactions,
118
129
  } = useInventoryTransactions({ limit: 8 });
130
+ const { message: notice, notify } = useNotice(6000);
131
+ const [receiveOpen, setReceiveOpen] = useState(false);
132
+ const [adjustTarget, setAdjustTarget] = useState<StockLineTarget | null>(null);
133
+ const [transferTarget, setTransferTarget] = useState<StockLineTarget | null>(null);
134
+
135
+ const toTarget = (row: StockRow): StockLineTarget => ({
136
+ id: row.id,
137
+ productName: row.productName,
138
+ sku: row.sku,
139
+ warehouseId: row.warehouseId,
140
+ warehouseName: row.warehouseName,
141
+ onHand: row.onHand,
142
+ available: row.available,
143
+ });
144
+
145
+ /** Reload everything a stock movement changes: the table, the cards and the ledger. */
146
+ const handleStockChanged = (message: string) => {
147
+ notify(message);
148
+ refetch();
149
+ refetchStats();
150
+ refetchTransactions();
151
+ };
119
152
 
120
153
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
121
154
  const onRowKeyDown = (e: React.KeyboardEvent, action: () => void) => {
@@ -135,6 +168,7 @@ export default function InventoryPage() {
135
168
  productId: item.productId,
136
169
  productName: item.productName ?? unknownProductLabel,
137
170
  sku: item.productSku ?? '—',
171
+ warehouseId: item.warehouseId,
138
172
  warehouseName: item.warehouseName ?? '—',
139
173
  onHand: item.quantity,
140
174
  allocated: item.reservedQuantity,
@@ -220,6 +254,36 @@ export default function InventoryPage() {
220
254
  />
221
255
  ),
222
256
  },
257
+ {
258
+ id: 'actions',
259
+ header: t('movethewheels.inventory.stock.actions', 'Actions'),
260
+ cell: (row) => (
261
+ // Row click navigates to the product, so keep these from bubbling to it.
262
+ <div
263
+ className="flex flex-wrap gap-2"
264
+ onClick={(e) => e.stopPropagation()}
265
+ onKeyDown={(e) => e.stopPropagation()}
266
+ >
267
+ <Button
268
+ type="button"
269
+ variant="outline"
270
+ size="sm"
271
+ onClick={() => setAdjustTarget(toTarget(row))}
272
+ >
273
+ <SlidersHorizontal size={14} /> {t('movethewheels.inventory.stock.adjust', 'Adjust')}
274
+ </Button>
275
+ <Button
276
+ type="button"
277
+ variant="outline"
278
+ size="sm"
279
+ onClick={() => setTransferTarget(toTarget(row))}
280
+ disabled={row.available <= 0}
281
+ >
282
+ <ArrowLeftRight size={14} /> {t('movethewheels.inventory.stock.transfer', 'Transfer')}
283
+ </Button>
284
+ </div>
285
+ ),
286
+ },
223
287
  ];
224
288
 
225
289
  return (
@@ -235,6 +299,11 @@ export default function InventoryPage() {
235
299
  onSelect: () => goTo('inventory/products/new'),
236
300
  }}
237
301
  actions={[
302
+ {
303
+ label: t('movethewheels.inventory.actions.receiveStock', 'Receive stock'),
304
+ icon: <PackagePlus size={16} />,
305
+ onSelect: () => setReceiveOpen(true),
306
+ },
238
307
  {
239
308
  label: t('movethewheels.inventory.actions.products', 'Products'),
240
309
  onSelect: () => goTo('inventory/products'),
@@ -254,6 +323,12 @@ export default function InventoryPage() {
254
323
  )}
255
324
  </PagePurpose>
256
325
 
326
+ {notice && (
327
+ <div className="mb-2">
328
+ <Notice message={notice} />
329
+ </div>
330
+ )}
331
+
257
332
  {error && !isLoading ? (
258
333
  <Card className="border-status-error-border">
259
334
  <CardContent className="p-6 text-center space-y-3">
@@ -582,6 +657,28 @@ export default function InventoryPage() {
582
657
  </Card>
583
658
  </PageSection>
584
659
  </div>
660
+
661
+ <ReceiveStockDialog
662
+ open={receiveOpen}
663
+ onOpenChange={setReceiveOpen}
664
+ onDone={handleStockChanged}
665
+ />
666
+ <AdjustStockDialog
667
+ line={adjustTarget}
668
+ open={adjustTarget !== null}
669
+ onOpenChange={(open) => {
670
+ if (!open) setAdjustTarget(null);
671
+ }}
672
+ onDone={handleStockChanged}
673
+ />
674
+ <TransferStockDialog
675
+ line={transferTarget}
676
+ open={transferTarget !== null}
677
+ onOpenChange={(open) => {
678
+ if (!open) setTransferTarget(null);
679
+ }}
680
+ onDone={handleStockChanged}
681
+ />
585
682
  </PageLayout>
586
683
  );
587
684
  }
@@ -252,7 +252,7 @@ function NewRouteForm({ editId }: { editId?: string }) {
252
252
  <PagePurpose icon={<Navigation size={18} />}>
253
253
  Plan a new delivery route — name it, list the stops to visit, and optionally assign a
254
254
  vehicle and driver. Once created you can optimize it to sequence the stops into the shortest
255
- run.
255
+ run. Optimizing also marks the route In Progress.
256
256
  </PagePurpose>
257
257
 
258
258
  <form onSubmit={handleSubmit} className="max-w-3xl space-y-6">
@@ -535,7 +535,7 @@ export default function RouteDetailsPage() {
535
535
  </Button>
536
536
  {!route.optimized && (
537
537
  <Button variant="outline" onClick={handleOptimize} disabled={optimizing}>
538
- <Zap size={16} /> {optimizing ? 'Optimizing…' : 'Optimize'}
538
+ <Zap size={16} /> {optimizing ? 'Optimizing…' : 'Optimize & start'}
539
539
  </Button>
540
540
  )}
541
541
  <Button onClick={() => navigate?.(joinPath(basePath, `routes/${routeId}/edit`))}>
@@ -569,8 +569,9 @@ export default function RouteDetailsPage() {
569
569
  ? [
570
570
  {
571
571
  id: 'optimize',
572
- label: 'Optimize this route',
573
- description: 'Reorder stops to cut distance and driving time.',
572
+ label: 'Optimize & start this route',
573
+ description:
574
+ 'Reorder stops to cut distance and driving time, then mark the route In Progress.',
574
575
  icon: <Zap size={16} />,
575
576
  onClick: handleOptimize,
576
577
  },