@burdenoff/microfe-movethewheels 2026.930.3 → 2026.930.5

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) {
@@ -368,28 +407,10 @@ const TRANSFER_INVENTORY_STOCK_MUTATION = `
368
407
  mutation TransferInventoryStock($input: TransferInventoryStockInput!) {
369
408
  transferInventoryStock(input: $input) {
370
409
  source {
371
- id
372
- cargoItemId
373
- warehouseId
374
- onHand
375
- available
376
- allocated
377
- damaged
378
- reorderPoint
379
- createdAt
380
- updatedAt
410
+ ${INVENTORY_LINE_FIELDS}
381
411
  }
382
412
  destination {
383
- id
384
- cargoItemId
385
- warehouseId
386
- onHand
387
- available
388
- allocated
389
- damaged
390
- reorderPoint
391
- createdAt
392
- updatedAt
413
+ ${INVENTORY_LINE_FIELDS}
393
414
  }
394
415
  }
395
416
  }
@@ -1012,6 +1033,7 @@ export function useWarehouses(options: UseWarehousesOptions = {}) {
1012
1033
  const [totalWarehouses, setTotalWarehouses] = useState(0);
1013
1034
  const [hasMore, setHasMore] = useState(false);
1014
1035
  const [isLoading, setIsLoading] = useState(false);
1036
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
1015
1037
  const [error, setError] = useState<Error | null>(null);
1016
1038
 
1017
1039
  const load = useCallback(async () => {
@@ -1035,6 +1057,46 @@ export function useWarehouses(options: UseWarehousesOptions = {}) {
1035
1057
  }
1036
1058
  }, [graphQLOptions, limit, offset, search, status]);
1037
1059
 
1060
+ /** Appends the next server page (pickers); list pages keep using `offset`. */
1061
+ const loadMore = useCallback(async () => {
1062
+ if (!hasMore || isLoading || isLoadingMore) return;
1063
+ const nextOffset = offset + warehouses.length;
1064
+ setIsLoadingMore(true);
1065
+ setError(null);
1066
+ try {
1067
+ const data = await executeGraphQL<{
1068
+ warehouses: ApiWarehouse[];
1069
+ warehousesCount: number;
1070
+ }>(graphQLOptions, LIST_WAREHOUSES_QUERY, {
1071
+ filter: { search: search || undefined, status: status || undefined },
1072
+ pagination: { limit, offset: nextOffset },
1073
+ });
1074
+ const mapped = data.warehouses.map(mapWarehouse);
1075
+ setTotalWarehouses(data.warehousesCount);
1076
+ setHasMore(nextOffset + mapped.length < data.warehousesCount);
1077
+ setWarehouses((previous) => {
1078
+ const existingIds = new Set(previous.map((warehouse) => warehouse.id));
1079
+ return [...previous, ...mapped.filter((warehouse) => !existingIds.has(warehouse.id))];
1080
+ });
1081
+ } catch (fetchError) {
1082
+ setError(
1083
+ fetchError instanceof Error ? fetchError : new Error('Failed to load more warehouses')
1084
+ );
1085
+ } finally {
1086
+ setIsLoadingMore(false);
1087
+ }
1088
+ }, [
1089
+ graphQLOptions,
1090
+ hasMore,
1091
+ isLoading,
1092
+ isLoadingMore,
1093
+ limit,
1094
+ offset,
1095
+ search,
1096
+ status,
1097
+ warehouses.length,
1098
+ ]);
1099
+
1038
1100
  useEffect(() => {
1039
1101
  void load();
1040
1102
  }, [load]);
@@ -1044,8 +1106,10 @@ export function useWarehouses(options: UseWarehousesOptions = {}) {
1044
1106
  totalWarehouses,
1045
1107
  hasMore,
1046
1108
  isLoading,
1109
+ isLoadingMore,
1047
1110
  error,
1048
1111
  refetch: () => void load(),
1112
+ loadMore,
1049
1113
  };
1050
1114
  }
1051
1115
 
@@ -1161,45 +1225,49 @@ export function useInventoryStats() {
1161
1225
  const [isLoading, setIsLoading] = useState(false);
1162
1226
  const [error, setError] = useState<Error | null>(null);
1163
1227
 
1228
+ const load = useCallback(async () => {
1229
+ setIsLoading(true);
1230
+ setError(null);
1231
+ try {
1232
+ const data = await executeGraphQL<{
1233
+ inventoryStats: {
1234
+ totalOnHand: number;
1235
+ totalAvailable: number;
1236
+ totalAllocated: number;
1237
+ totalDamaged: number;
1238
+ lowStockCount: number;
1239
+ };
1240
+ cargoItemsCount: number;
1241
+ warehousesCount: number;
1242
+ }>(graphQLOptions, INVENTORY_STATS_QUERY);
1243
+
1244
+ setStats({
1245
+ totalOnHand: data.inventoryStats.totalOnHand,
1246
+ totalAvailable: data.inventoryStats.totalAvailable,
1247
+ totalAllocated: data.inventoryStats.totalAllocated,
1248
+ totalDamaged: data.inventoryStats.totalDamaged,
1249
+ lowStockCount: data.inventoryStats.lowStockCount,
1250
+ totalProducts: data.cargoItemsCount,
1251
+ totalWarehouses: data.warehousesCount,
1252
+ });
1253
+ } catch (fetchError) {
1254
+ setError(
1255
+ fetchError instanceof Error ? fetchError : new Error('Failed to load inventory stats')
1256
+ );
1257
+ } finally {
1258
+ setIsLoading(false);
1259
+ }
1260
+ }, [graphQLOptions]);
1261
+
1164
1262
  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
- };
1263
+ void load();
1264
+ }, [load]);
1198
1265
 
1266
+ const refetch = useCallback(() => {
1199
1267
  void load();
1200
- }, [graphQLOptions]);
1268
+ }, [load]);
1201
1269
 
1202
- return { stats, isLoading, error };
1270
+ return { stats, isLoading, error, refetch };
1203
1271
  }
1204
1272
 
1205
1273
  /**
@@ -1445,3 +1513,176 @@ export function useInventoryTransactions(options: UseInventoryTransactionsOption
1445
1513
 
1446
1514
  return { transactions, isLoading, error, refetch };
1447
1515
  }
1516
+
1517
+ export interface ReceiveStockInput {
1518
+ productId: string;
1519
+ warehouseId: string;
1520
+ /** Whole units received; must be > 0. */
1521
+ quantity: number;
1522
+ /** Only applied when the receipt creates a new stock line. */
1523
+ reorderPoint?: number | null;
1524
+ }
1525
+
1526
+ export interface TransferStockInput {
1527
+ sourceInventoryId: string;
1528
+ destinationWarehouseId: string;
1529
+ /** Whole units to move out of the source line's available stock; must be > 0. */
1530
+ quantity: number;
1531
+ }
1532
+
1533
+ function assertWholeUnits(quantity: number, label: string): void {
1534
+ if (!Number.isInteger(quantity) || quantity <= 0) {
1535
+ throw new Error(`${label} must be a positive whole number.`);
1536
+ }
1537
+ }
1538
+
1539
+ /**
1540
+ * Stock movements that do not depend on the list currently on screen: every
1541
+ * write re-reads the line first, so a stale table row can never overwrite
1542
+ * someone else's change (the service also rejects a mid-flight change).
1543
+ */
1544
+ export function useStockActions() {
1545
+ const graphQLOptions = useGraphqlOptions();
1546
+
1547
+ const findLine = useCallback(
1548
+ async (filter: {
1549
+ inventoryItemId?: string;
1550
+ cargoItemId?: string;
1551
+ warehouseId?: string;
1552
+ }): Promise<Inventory | null> => {
1553
+ const data = await executeGraphQL<{ inventoryItems: ApiInventoryItem[] }>(
1554
+ graphQLOptions,
1555
+ FIND_STOCK_LINE_QUERY,
1556
+ { filter, pagination: { limit: 1 } }
1557
+ );
1558
+ const line = data.inventoryItems[0];
1559
+ return line ? mapInventory(line) : null;
1560
+ },
1561
+ [graphQLOptions]
1562
+ );
1563
+
1564
+ /** Applies a signed change to on-hand and available; allocated/damaged are untouched. */
1565
+ const applyChange = useCallback(
1566
+ async (line: Inventory, change: number): Promise<Inventory> => {
1567
+ const onHand = line.quantity + change;
1568
+ const available = line.availableQuantity + change;
1569
+ if (available < 0) {
1570
+ throw new Error(
1571
+ `Only ${line.availableQuantity.toLocaleString()} unit(s) are available to remove.`
1572
+ );
1573
+ }
1574
+ if (onHand < 0) {
1575
+ throw new Error('On-hand stock cannot go below zero.');
1576
+ }
1577
+ const data = await executeGraphQL<{ adjustInventoryStock: ApiInventoryItem }>(
1578
+ graphQLOptions,
1579
+ ADJUST_INVENTORY_MUTATION,
1580
+ {
1581
+ input: {
1582
+ id: line.id,
1583
+ onHand,
1584
+ available,
1585
+ allocated: line.reservedQuantity,
1586
+ damaged: line.damagedQuantity,
1587
+ },
1588
+ }
1589
+ );
1590
+ return mapInventory(data.adjustInventoryStock);
1591
+ },
1592
+ [graphQLOptions]
1593
+ );
1594
+
1595
+ const adjustStock = useCallback(
1596
+ async (inventoryId: string, change: number): Promise<Inventory> => {
1597
+ if (!Number.isInteger(change) || change === 0) {
1598
+ throw new Error('Enter a whole-number change other than zero.');
1599
+ }
1600
+ const line = await findLine({ inventoryItemId: inventoryId });
1601
+ if (!line) throw new Error('This stock line no longer exists.');
1602
+ return applyChange(line, change);
1603
+ },
1604
+ [applyChange, findLine]
1605
+ );
1606
+
1607
+ const receiveStock = useCallback(
1608
+ async (input: ReceiveStockInput): Promise<Inventory> => {
1609
+ assertWholeUnits(input.quantity, 'Quantity received');
1610
+ const existing = await findLine({
1611
+ cargoItemId: input.productId,
1612
+ warehouseId: input.warehouseId,
1613
+ });
1614
+ if (existing) return applyChange(existing, input.quantity);
1615
+
1616
+ // New line: the service records the opening stock as a RECEIPT ledger entry.
1617
+ const data = await executeGraphQL<{ createInventoryItem: ApiInventoryItem }>(
1618
+ graphQLOptions,
1619
+ CREATE_INVENTORY_ITEM_MUTATION,
1620
+ {
1621
+ input: {
1622
+ cargoItemId: input.productId,
1623
+ warehouseId: input.warehouseId,
1624
+ onHand: input.quantity,
1625
+ available: input.quantity,
1626
+ ...(input.reorderPoint !== null && input.reorderPoint !== undefined
1627
+ ? { reorderPoint: input.reorderPoint }
1628
+ : {}),
1629
+ },
1630
+ }
1631
+ );
1632
+ return mapInventory(data.createInventoryItem);
1633
+ },
1634
+ [applyChange, findLine, graphQLOptions]
1635
+ );
1636
+
1637
+ const transferStock = useCallback(
1638
+ async (input: TransferStockInput): Promise<{ source: Inventory; destination: Inventory }> => {
1639
+ assertWholeUnits(input.quantity, 'Transfer quantity');
1640
+ const source = await findLine({ inventoryItemId: input.sourceInventoryId });
1641
+ if (!source) throw new Error('The source stock line no longer exists.');
1642
+ if (source.warehouseId === input.destinationWarehouseId) {
1643
+ throw new Error('Source and destination warehouses must be different.');
1644
+ }
1645
+ if (input.quantity > source.availableQuantity) {
1646
+ throw new Error(
1647
+ `Only ${source.availableQuantity.toLocaleString()} unit(s) are available to transfer.`
1648
+ );
1649
+ }
1650
+
1651
+ let destination = await findLine({
1652
+ cargoItemId: source.productId,
1653
+ warehouseId: input.destinationWarehouseId,
1654
+ });
1655
+ if (!destination) {
1656
+ // An empty line records no movement; the transfer below writes both ledger entries.
1657
+ const created = await executeGraphQL<{ createInventoryItem: ApiInventoryItem }>(
1658
+ graphQLOptions,
1659
+ CREATE_INVENTORY_ITEM_MUTATION,
1660
+ {
1661
+ input: {
1662
+ cargoItemId: source.productId,
1663
+ warehouseId: input.destinationWarehouseId,
1664
+ },
1665
+ }
1666
+ );
1667
+ destination = mapInventory(created.createInventoryItem);
1668
+ }
1669
+
1670
+ const data = await executeGraphQL<{
1671
+ transferInventoryStock: { source: ApiInventoryItem; destination: ApiInventoryItem };
1672
+ }>(graphQLOptions, TRANSFER_INVENTORY_STOCK_MUTATION, {
1673
+ input: {
1674
+ sourceInventoryItemId: source.id,
1675
+ destinationInventoryItemId: destination.id,
1676
+ quantity: input.quantity,
1677
+ },
1678
+ });
1679
+ return {
1680
+ source: mapInventory(data.transferInventoryStock.source),
1681
+ destination: mapInventory(data.transferInventoryStock.destination),
1682
+ };
1683
+ },
1684
+ [findLine, graphQLOptions]
1685
+ );
1686
+
1687
+ return { receiveStock, adjustStock, transferStock };
1688
+ }
@@ -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;
@@ -84,6 +94,21 @@ const TRANSACTION_LABELS: Record<InventoryTransactionType, string> = {
84
94
  ADJUSTMENT: 'Adjustment',
85
95
  };
86
96
 
97
+ /**
98
+ * Ledger quantity with its sign. RECEIPT/RETURN/ISSUE have a fixed direction, so
99
+ * the magnitude is shown with that sign. TRANSFER and ADJUSTMENT are neutral —
100
+ * the sign stored on the row is the only thing that says whether stock came in
101
+ * or went out (a transfer writes one row of each), so it must never be dropped.
102
+ */
103
+ function formatLedgerQuantity(direction: 'in' | 'out' | 'neutral', quantity: number): string {
104
+ const magnitude = Math.abs(quantity).toLocaleString();
105
+ if (direction === 'in') return `+${magnitude}`;
106
+ if (direction === 'out') return `−${magnitude}`;
107
+ if (quantity > 0) return `+${magnitude}`;
108
+ if (quantity < 0) return `−${magnitude}`;
109
+ return magnitude;
110
+ }
111
+
87
112
  export default function InventoryPage() {
88
113
  const { t: translate } = useI18n();
89
114
  const t = (key: string, fallback: string): string => {
@@ -110,12 +135,35 @@ export default function InventoryPage() {
110
135
  limit: pageSize,
111
136
  offset: pageIndex * pageSize,
112
137
  });
113
- const { stats } = useInventoryStats();
138
+ const { stats, refetch: refetchStats } = useInventoryStats();
114
139
  const {
115
140
  transactions,
116
141
  isLoading: transactionsLoading,
117
142
  error: transactionsError,
143
+ refetch: refetchTransactions,
118
144
  } = useInventoryTransactions({ limit: 8 });
145
+ const { message: notice, notify } = useNotice(6000);
146
+ const [receiveOpen, setReceiveOpen] = useState(false);
147
+ const [adjustTarget, setAdjustTarget] = useState<StockLineTarget | null>(null);
148
+ const [transferTarget, setTransferTarget] = useState<StockLineTarget | null>(null);
149
+
150
+ const toTarget = (row: StockRow): StockLineTarget => ({
151
+ id: row.id,
152
+ productName: row.productName,
153
+ sku: row.sku,
154
+ warehouseId: row.warehouseId,
155
+ warehouseName: row.warehouseName,
156
+ onHand: row.onHand,
157
+ available: row.available,
158
+ });
159
+
160
+ /** Reload everything a stock movement changes: the table, the cards and the ledger. */
161
+ const handleStockChanged = (message: string) => {
162
+ notify(message);
163
+ refetch();
164
+ refetchStats();
165
+ refetchTransactions();
166
+ };
119
167
 
120
168
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
121
169
  const onRowKeyDown = (e: React.KeyboardEvent, action: () => void) => {
@@ -135,6 +183,7 @@ export default function InventoryPage() {
135
183
  productId: item.productId,
136
184
  productName: item.productName ?? unknownProductLabel,
137
185
  sku: item.productSku ?? '—',
186
+ warehouseId: item.warehouseId,
138
187
  warehouseName: item.warehouseName ?? '—',
139
188
  onHand: item.quantity,
140
189
  allocated: item.reservedQuantity,
@@ -220,6 +269,36 @@ export default function InventoryPage() {
220
269
  />
221
270
  ),
222
271
  },
272
+ {
273
+ id: 'actions',
274
+ header: t('movethewheels.inventory.stock.actions', 'Actions'),
275
+ cell: (row) => (
276
+ // Row click navigates to the product, so keep these from bubbling to it.
277
+ <div
278
+ className="flex flex-wrap gap-2"
279
+ onClick={(e) => e.stopPropagation()}
280
+ onKeyDown={(e) => e.stopPropagation()}
281
+ >
282
+ <Button
283
+ type="button"
284
+ variant="outline"
285
+ size="sm"
286
+ onClick={() => setAdjustTarget(toTarget(row))}
287
+ >
288
+ <SlidersHorizontal size={14} /> {t('movethewheels.inventory.stock.adjust', 'Adjust')}
289
+ </Button>
290
+ <Button
291
+ type="button"
292
+ variant="outline"
293
+ size="sm"
294
+ onClick={() => setTransferTarget(toTarget(row))}
295
+ disabled={row.available <= 0}
296
+ >
297
+ <ArrowLeftRight size={14} /> {t('movethewheels.inventory.stock.transfer', 'Transfer')}
298
+ </Button>
299
+ </div>
300
+ ),
301
+ },
223
302
  ];
224
303
 
225
304
  return (
@@ -235,6 +314,11 @@ export default function InventoryPage() {
235
314
  onSelect: () => goTo('inventory/products/new'),
236
315
  }}
237
316
  actions={[
317
+ {
318
+ label: t('movethewheels.inventory.actions.receiveStock', 'Receive stock'),
319
+ icon: <PackagePlus size={16} />,
320
+ onSelect: () => setReceiveOpen(true),
321
+ },
238
322
  {
239
323
  label: t('movethewheels.inventory.actions.products', 'Products'),
240
324
  onSelect: () => goTo('inventory/products'),
@@ -254,6 +338,12 @@ export default function InventoryPage() {
254
338
  )}
255
339
  </PagePurpose>
256
340
 
341
+ {notice && (
342
+ <div className="mb-2">
343
+ <Notice message={notice} />
344
+ </div>
345
+ )}
346
+
257
347
  {error && !isLoading ? (
258
348
  <Card className="border-status-error-border">
259
349
  <CardContent className="p-6 text-center space-y-3">
@@ -462,8 +552,7 @@ export default function InventoryPage() {
462
552
  : 'font-medium text-text-primary'
463
553
  }
464
554
  >
465
- {direction === 'in' ? '+' : direction === 'out' ? '−' : ''}
466
- {Math.abs(transaction.quantity).toLocaleString()}
555
+ {formatLedgerQuantity(direction, transaction.quantity)}
467
556
  </span>
468
557
  </li>
469
558
  );
@@ -582,6 +671,28 @@ export default function InventoryPage() {
582
671
  </Card>
583
672
  </PageSection>
584
673
  </div>
674
+
675
+ <ReceiveStockDialog
676
+ open={receiveOpen}
677
+ onOpenChange={setReceiveOpen}
678
+ onDone={handleStockChanged}
679
+ />
680
+ <AdjustStockDialog
681
+ line={adjustTarget}
682
+ open={adjustTarget !== null}
683
+ onOpenChange={(open) => {
684
+ if (!open) setAdjustTarget(null);
685
+ }}
686
+ onDone={handleStockChanged}
687
+ />
688
+ <TransferStockDialog
689
+ line={transferTarget}
690
+ open={transferTarget !== null}
691
+ onOpenChange={(open) => {
692
+ if (!open) setTransferTarget(null);
693
+ }}
694
+ onDone={handleStockChanged}
695
+ />
585
696
  </PageLayout>
586
697
  );
587
698
  }
@@ -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
  },