@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.
@@ -180,7 +180,7 @@ function G({ editId: r }) {
180
180
  }),
181
181
  children: [/* @__PURE__ */ B(ye, {
182
182
  icon: /* @__PURE__ */ B(o, { size: 18 }),
183
- children: "Plan a new delivery route — name it, list the stops to visit, and optionally assign a vehicle and driver. Once created you can optimize it to sequence the stops into the shortest run."
183
+ children: "Plan a new delivery route — name it, list the stops to visit, and optionally assign a vehicle and driver. Once created you can optimize it to sequence the stops into the shortest run. Optimizing also marks the route In Progress."
184
184
  }), /* @__PURE__ */ V("form", {
185
185
  onSubmit: $,
186
186
  className: "max-w-3xl space-y-6",
@@ -459,7 +459,7 @@ function K() {
459
459
  children: [
460
460
  /* @__PURE__ */ B(d, { size: 16 }),
461
461
  " ",
462
- W ? "Optimizing…" : "Optimize"
462
+ W ? "Optimizing…" : "Optimize & start"
463
463
  ]
464
464
  }),
465
465
  /* @__PURE__ */ V(_, {
@@ -488,8 +488,8 @@ function K() {
488
488
  steps: [
489
489
  ...m.optimized ? [] : [{
490
490
  id: "optimize",
491
- label: "Optimize this route",
492
- description: "Reorder stops to cut distance and driving time.",
491
+ label: "Optimize & start this route",
492
+ description: "Reorder stops to cut distance and driving time, then mark the route In Progress.",
493
493
  icon: /* @__PURE__ */ B(d, { size: 16 }),
494
494
  onClick: Ee
495
495
  }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-movethewheels",
3
- "version": "2026.930.2",
3
+ "version": "2026.930.4",
4
4
  "description": "Burdenoff MoveTheWheels Microfrontend - Logistics & Supply Chain Management Platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,512 @@
1
+ /**
2
+ * Stock movement dialogs — receive, adjust and transfer inventory.
3
+ *
4
+ * Every product/warehouse choice is a searchable, server-paginated
5
+ * `EntityCombobox`, so an operator never has to leave the page to look a record
6
+ * up. Writes go through `useStockActions`, which re-reads the stock line first
7
+ * and lets the service record the matching ledger entries atomically.
8
+ */
9
+ import { useEffect, useState } from 'react';
10
+ import {
11
+ ResponsiveDialog,
12
+ ResponsiveDialogContent,
13
+ ResponsiveDialogDescription,
14
+ ResponsiveDialogFooter,
15
+ ResponsiveDialogHeader,
16
+ ResponsiveDialogTitle,
17
+ } from '@burdenoff/fe-libs/ui';
18
+ import { Button, Input, Label } from './ui';
19
+ import { EntityCombobox } from './EntityCombobox';
20
+ import { FormError } from './FormError';
21
+ import {
22
+ useProduct,
23
+ useProducts,
24
+ useStockActions,
25
+ useWarehouse,
26
+ useWarehouses,
27
+ } from '../hooks/useInventory';
28
+
29
+ /** The stock line an adjust/transfer acts on (a row of the stock position table). */
30
+ export interface StockLineTarget {
31
+ id: string;
32
+ productName: string;
33
+ sku: string;
34
+ warehouseId: string;
35
+ warehouseName: string;
36
+ onHand: number;
37
+ available: number;
38
+ }
39
+
40
+ interface DialogBaseProps {
41
+ open: boolean;
42
+ onOpenChange: (open: boolean) => void;
43
+ /** Called after a successful write with a short human-readable summary. */
44
+ onDone: (message: string) => void;
45
+ }
46
+
47
+ function parseWholeNumber(raw: string): number | null {
48
+ const trimmed = raw.trim();
49
+ if (!/^[+-]?\d+$/.test(trimmed)) return null;
50
+ const value = Number(trimmed);
51
+ return Number.isSafeInteger(value) ? value : null;
52
+ }
53
+
54
+ function errorMessage(error: unknown, fallback: string): string {
55
+ return error instanceof Error && error.message ? error.message : fallback;
56
+ }
57
+
58
+ function ProductPicker({
59
+ id,
60
+ value,
61
+ onChange,
62
+ error,
63
+ }: {
64
+ id: string;
65
+ value: string;
66
+ onChange: (id: string) => void;
67
+ error?: string;
68
+ }) {
69
+ const [search, setSearch] = useState('');
70
+ const { products, totalProducts, hasMore, isLoading, isLoadingMore, loadMore } = useProducts({
71
+ search,
72
+ limit: 20,
73
+ });
74
+ const { product: selected, isLoading: selectedLoading } = useProduct(value || undefined);
75
+
76
+ return (
77
+ <EntityCombobox
78
+ id={id}
79
+ value={value}
80
+ onChange={onChange}
81
+ options={products}
82
+ loading={isLoading}
83
+ required
84
+ getId={(product) => product.id}
85
+ getLabel={(product) => `${product.name} (${product.sku})`}
86
+ renderOption={(product) => (
87
+ <>
88
+ {product.name} <span className="text-text-tertiary">({product.sku})</span>
89
+ </>
90
+ )}
91
+ selectedOption={selected}
92
+ selectedLoading={selectedLoading}
93
+ placeholder="Search your product catalog"
94
+ searchPlaceholder="Search by name or SKU…"
95
+ emptyText="No products found."
96
+ onSearchChange={setSearch}
97
+ hasMore={hasMore}
98
+ totalCount={totalProducts}
99
+ onLoadMore={loadMore}
100
+ loadingMore={isLoadingMore}
101
+ error={error}
102
+ />
103
+ );
104
+ }
105
+
106
+ function WarehousePicker({
107
+ id,
108
+ value,
109
+ onChange,
110
+ excludeId,
111
+ error,
112
+ }: {
113
+ id: string;
114
+ value: string;
115
+ onChange: (id: string) => void;
116
+ /** Hidden from the options (e.g. the transfer source). */
117
+ excludeId?: string;
118
+ error?: string;
119
+ }) {
120
+ const [search, setSearch] = useState('');
121
+ const { warehouses, totalWarehouses, hasMore, isLoading, isLoadingMore, loadMore } =
122
+ useWarehouses({ search, limit: 20 });
123
+ const { warehouse: selected, isLoading: selectedLoading } = useWarehouse(value || undefined);
124
+ const options = excludeId
125
+ ? warehouses.filter((warehouse) => warehouse.id !== excludeId)
126
+ : warehouses;
127
+
128
+ return (
129
+ <EntityCombobox
130
+ id={id}
131
+ value={value}
132
+ onChange={onChange}
133
+ options={options}
134
+ loading={isLoading}
135
+ required
136
+ getId={(warehouse) => warehouse.id}
137
+ getLabel={(warehouse) => `${warehouse.name} (${warehouse.code})`}
138
+ renderOption={(warehouse) => (
139
+ <>
140
+ {warehouse.name} <span className="text-text-tertiary">({warehouse.code})</span>
141
+ </>
142
+ )}
143
+ selectedOption={selected}
144
+ selectedLoading={selectedLoading}
145
+ placeholder="Search warehouses"
146
+ searchPlaceholder="Search by name or code…"
147
+ emptyText="No warehouses found."
148
+ onSearchChange={setSearch}
149
+ hasMore={hasMore}
150
+ totalCount={totalWarehouses}
151
+ onLoadMore={loadMore}
152
+ loadingMore={isLoadingMore}
153
+ error={error}
154
+ />
155
+ );
156
+ }
157
+
158
+ function DialogActions({
159
+ submitting,
160
+ submitLabel,
161
+ submittingLabel,
162
+ onCancel,
163
+ onSubmit,
164
+ }: {
165
+ submitting: boolean;
166
+ submitLabel: string;
167
+ submittingLabel: string;
168
+ onCancel: () => void;
169
+ onSubmit: () => void;
170
+ }) {
171
+ return (
172
+ <ResponsiveDialogFooter>
173
+ <Button variant="outline" onClick={onCancel} disabled={submitting}>
174
+ Cancel
175
+ </Button>
176
+ <Button onClick={onSubmit} disabled={submitting}>
177
+ {submitting ? submittingLabel : submitLabel}
178
+ </Button>
179
+ </ResponsiveDialogFooter>
180
+ );
181
+ }
182
+
183
+ export function ReceiveStockDialog({ open, onOpenChange, onDone }: DialogBaseProps) {
184
+ const { receiveStock } = useStockActions();
185
+ const [productId, setProductId] = useState('');
186
+ const [warehouseId, setWarehouseId] = useState('');
187
+ const [quantity, setQuantity] = useState('');
188
+ const [reorderPoint, setReorderPoint] = useState('');
189
+ const [error, setError] = useState<string | null>(null);
190
+ const [submitting, setSubmitting] = useState(false);
191
+
192
+ useEffect(() => {
193
+ if (!open) return;
194
+ setProductId('');
195
+ setWarehouseId('');
196
+ setQuantity('');
197
+ setReorderPoint('');
198
+ setError(null);
199
+ setSubmitting(false);
200
+ }, [open]);
201
+
202
+ const handleSubmit = async () => {
203
+ const units = parseWholeNumber(quantity);
204
+ const reorder = reorderPoint.trim() === '' ? null : parseWholeNumber(reorderPoint);
205
+ if (!productId) return setError('Choose the product you received.');
206
+ if (!warehouseId) return setError('Choose the warehouse it was received into.');
207
+ if (units === null || units <= 0)
208
+ return setError('Quantity must be a whole number above zero.');
209
+ if (reorder !== null && reorder < 0) {
210
+ return setError('Reorder point must be zero or more.');
211
+ }
212
+ if (reorderPoint.trim() !== '' && reorder === null) {
213
+ return setError('Reorder point must be a whole number.');
214
+ }
215
+ setSubmitting(true);
216
+ setError(null);
217
+ try {
218
+ const line = await receiveStock({
219
+ productId,
220
+ warehouseId,
221
+ quantity: units,
222
+ reorderPoint: reorder,
223
+ });
224
+ onOpenChange(false);
225
+ onDone(
226
+ `Received ${units.toLocaleString()} unit(s) — ${line.productName ?? 'product'} now has ${line.quantity.toLocaleString()} on hand at ${line.warehouseName ?? 'the warehouse'}.`
227
+ );
228
+ } catch (err) {
229
+ setError(errorMessage(err, 'Failed to receive stock.'));
230
+ } finally {
231
+ setSubmitting(false);
232
+ }
233
+ };
234
+
235
+ return (
236
+ <ResponsiveDialog open={open} onOpenChange={onOpenChange}>
237
+ <ResponsiveDialogContent>
238
+ <ResponsiveDialogHeader>
239
+ <ResponsiveDialogTitle>Receive stock</ResponsiveDialogTitle>
240
+ <ResponsiveDialogDescription>
241
+ Add units of a product to a warehouse. If the warehouse already stocks it, the units are
242
+ added to that line; otherwise a new stock line is created.
243
+ </ResponsiveDialogDescription>
244
+ </ResponsiveDialogHeader>
245
+ <div className="space-y-4 px-1">
246
+ <div className="space-y-2">
247
+ <Label htmlFor="receive-product">Product *</Label>
248
+ <ProductPicker id="receive-product" value={productId} onChange={setProductId} />
249
+ </div>
250
+ <div className="space-y-2">
251
+ <Label htmlFor="receive-warehouse">Warehouse *</Label>
252
+ <WarehousePicker id="receive-warehouse" value={warehouseId} onChange={setWarehouseId} />
253
+ </div>
254
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
255
+ <div className="space-y-2">
256
+ <Label htmlFor="receive-quantity">Quantity received *</Label>
257
+ <Input
258
+ id="receive-quantity"
259
+ type="number"
260
+ inputMode="numeric"
261
+ min={1}
262
+ step={1}
263
+ value={quantity}
264
+ onChange={(e) => setQuantity(e.target.value)}
265
+ placeholder="e.g. 50"
266
+ />
267
+ </div>
268
+ <div className="space-y-2">
269
+ <Label htmlFor="receive-reorder">Reorder point (optional)</Label>
270
+ <Input
271
+ id="receive-reorder"
272
+ type="number"
273
+ inputMode="numeric"
274
+ min={0}
275
+ step={1}
276
+ value={reorderPoint}
277
+ onChange={(e) => setReorderPoint(e.target.value)}
278
+ placeholder="Low-stock threshold"
279
+ />
280
+ <p className="text-xs text-text-secondary">
281
+ Only used when this creates a new stock line.
282
+ </p>
283
+ </div>
284
+ </div>
285
+ <FormError message={error} />
286
+ </div>
287
+ <DialogActions
288
+ submitting={submitting}
289
+ submitLabel="Receive stock"
290
+ submittingLabel="Receiving…"
291
+ onCancel={() => onOpenChange(false)}
292
+ onSubmit={handleSubmit}
293
+ />
294
+ </ResponsiveDialogContent>
295
+ </ResponsiveDialog>
296
+ );
297
+ }
298
+
299
+ export function AdjustStockDialog({
300
+ line,
301
+ open,
302
+ onOpenChange,
303
+ onDone,
304
+ }: DialogBaseProps & { line: StockLineTarget | null }) {
305
+ const { adjustStock } = useStockActions();
306
+ const [change, setChange] = useState('');
307
+ const [error, setError] = useState<string | null>(null);
308
+ const [submitting, setSubmitting] = useState(false);
309
+
310
+ useEffect(() => {
311
+ if (!open) return;
312
+ setChange('');
313
+ setError(null);
314
+ setSubmitting(false);
315
+ }, [open, line?.id]);
316
+
317
+ const delta = parseWholeNumber(change);
318
+ const preview =
319
+ line && delta !== null && delta !== 0
320
+ ? { onHand: line.onHand + delta, available: line.available + delta }
321
+ : null;
322
+ const previewInvalid = preview !== null && (preview.onHand < 0 || preview.available < 0);
323
+
324
+ const handleSubmit = async () => {
325
+ if (!line) return;
326
+ if (delta === null || delta === 0) {
327
+ return setError('Enter a whole-number change other than zero, e.g. 10 or -3.');
328
+ }
329
+ if (previewInvalid) {
330
+ return setError(`Only ${line.available.toLocaleString()} unit(s) are available to remove.`);
331
+ }
332
+ setSubmitting(true);
333
+ setError(null);
334
+ try {
335
+ const updated = await adjustStock(line.id, delta);
336
+ onOpenChange(false);
337
+ onDone(
338
+ `Adjusted ${line.productName} at ${line.warehouseName} by ${delta > 0 ? '+' : ''}${delta.toLocaleString()} — now ${updated.quantity.toLocaleString()} on hand.`
339
+ );
340
+ } catch (err) {
341
+ setError(errorMessage(err, 'Failed to adjust stock.'));
342
+ } finally {
343
+ setSubmitting(false);
344
+ }
345
+ };
346
+
347
+ return (
348
+ <ResponsiveDialog open={open} onOpenChange={onOpenChange}>
349
+ <ResponsiveDialogContent>
350
+ <ResponsiveDialogHeader>
351
+ <ResponsiveDialogTitle>Adjust stock</ResponsiveDialogTitle>
352
+ <ResponsiveDialogDescription>
353
+ {line
354
+ ? `${line.productName} (${line.sku}) at ${line.warehouseName}. Use a positive number to add units and a negative one to remove them, e.g. after a count or write-off.`
355
+ : 'Choose a stock line to adjust.'}
356
+ </ResponsiveDialogDescription>
357
+ </ResponsiveDialogHeader>
358
+ {line && (
359
+ <div className="space-y-4 px-1">
360
+ <dl className="grid grid-cols-2 gap-3 text-sm">
361
+ <div>
362
+ <dt className="text-text-secondary">On hand</dt>
363
+ <dd className="font-medium">{line.onHand.toLocaleString()}</dd>
364
+ </div>
365
+ <div>
366
+ <dt className="text-text-secondary">Available</dt>
367
+ <dd className="font-medium">{line.available.toLocaleString()}</dd>
368
+ </div>
369
+ </dl>
370
+ <div className="space-y-2">
371
+ <Label htmlFor="adjust-change">Change (units) *</Label>
372
+ <Input
373
+ id="adjust-change"
374
+ type="number"
375
+ inputMode="numeric"
376
+ step={1}
377
+ value={change}
378
+ onChange={(e) => setChange(e.target.value)}
379
+ placeholder="e.g. 10 or -3"
380
+ />
381
+ {preview && (
382
+ <p
383
+ className={`text-xs ${previewInvalid ? 'text-status-error-text' : 'text-text-secondary'}`}
384
+ >
385
+ {previewInvalid
386
+ ? 'This would take stock below zero.'
387
+ : `After this: ${preview.onHand.toLocaleString()} on hand, ${preview.available.toLocaleString()} available.`}
388
+ </p>
389
+ )}
390
+ </div>
391
+ <FormError message={error} />
392
+ </div>
393
+ )}
394
+ <DialogActions
395
+ submitting={submitting}
396
+ submitLabel="Apply adjustment"
397
+ submittingLabel="Applying…"
398
+ onCancel={() => onOpenChange(false)}
399
+ onSubmit={handleSubmit}
400
+ />
401
+ </ResponsiveDialogContent>
402
+ </ResponsiveDialog>
403
+ );
404
+ }
405
+
406
+ export function TransferStockDialog({
407
+ line,
408
+ open,
409
+ onOpenChange,
410
+ onDone,
411
+ }: DialogBaseProps & { line: StockLineTarget | null }) {
412
+ const { transferStock } = useStockActions();
413
+ const [destinationId, setDestinationId] = useState('');
414
+ const [quantity, setQuantity] = useState('');
415
+ const [error, setError] = useState<string | null>(null);
416
+ const [submitting, setSubmitting] = useState(false);
417
+
418
+ useEffect(() => {
419
+ if (!open) return;
420
+ setDestinationId('');
421
+ setQuantity('');
422
+ setError(null);
423
+ setSubmitting(false);
424
+ }, [open, line?.id]);
425
+
426
+ const handleSubmit = async () => {
427
+ if (!line) return;
428
+ const units = parseWholeNumber(quantity);
429
+ if (!destinationId) return setError('Choose the destination warehouse.');
430
+ if (destinationId === line.warehouseId) {
431
+ return setError('Source and destination warehouses must be different.');
432
+ }
433
+ if (units === null || units <= 0)
434
+ return setError('Quantity must be a whole number above zero.');
435
+ if (units > line.available) {
436
+ return setError(`Only ${line.available.toLocaleString()} unit(s) are available to transfer.`);
437
+ }
438
+ setSubmitting(true);
439
+ setError(null);
440
+ try {
441
+ const result = await transferStock({
442
+ sourceInventoryId: line.id,
443
+ destinationWarehouseId: destinationId,
444
+ quantity: units,
445
+ });
446
+ onOpenChange(false);
447
+ onDone(
448
+ `Transferred ${units.toLocaleString()} unit(s) of ${line.productName} from ${line.warehouseName} to ${result.destination.warehouseName ?? 'the destination'}.`
449
+ );
450
+ } catch (err) {
451
+ setError(errorMessage(err, 'Failed to transfer stock.'));
452
+ } finally {
453
+ setSubmitting(false);
454
+ }
455
+ };
456
+
457
+ return (
458
+ <ResponsiveDialog open={open} onOpenChange={onOpenChange}>
459
+ <ResponsiveDialogContent>
460
+ <ResponsiveDialogHeader>
461
+ <ResponsiveDialogTitle>Transfer stock</ResponsiveDialogTitle>
462
+ <ResponsiveDialogDescription>
463
+ {line
464
+ ? `Move available ${line.productName} (${line.sku}) out of ${line.warehouseName}. Both warehouses are updated together.`
465
+ : 'Choose a stock line to transfer.'}
466
+ </ResponsiveDialogDescription>
467
+ </ResponsiveDialogHeader>
468
+ {line && (
469
+ <div className="space-y-4 px-1">
470
+ <p className="text-sm text-text-secondary">
471
+ Available to transfer:{' '}
472
+ <span className="font-medium text-text-primary">
473
+ {line.available.toLocaleString()}
474
+ </span>
475
+ </p>
476
+ <div className="space-y-2">
477
+ <Label htmlFor="transfer-destination">Destination warehouse *</Label>
478
+ <WarehousePicker
479
+ id="transfer-destination"
480
+ value={destinationId}
481
+ onChange={setDestinationId}
482
+ excludeId={line.warehouseId}
483
+ />
484
+ </div>
485
+ <div className="space-y-2">
486
+ <Label htmlFor="transfer-quantity">Quantity *</Label>
487
+ <Input
488
+ id="transfer-quantity"
489
+ type="number"
490
+ inputMode="numeric"
491
+ min={1}
492
+ max={line.available}
493
+ step={1}
494
+ value={quantity}
495
+ onChange={(e) => setQuantity(e.target.value)}
496
+ placeholder="Units to move"
497
+ />
498
+ </div>
499
+ <FormError message={error} />
500
+ </div>
501
+ )}
502
+ <DialogActions
503
+ submitting={submitting}
504
+ submitLabel="Transfer stock"
505
+ submittingLabel="Transferring…"
506
+ onCancel={() => onOpenChange(false)}
507
+ onSubmit={handleSubmit}
508
+ />
509
+ </ResponsiveDialogContent>
510
+ </ResponsiveDialog>
511
+ );
512
+ }
@@ -33,10 +33,13 @@ export {
33
33
  useWarehouse,
34
34
  useInventoryStats,
35
35
  useInventoryTransactions,
36
+ useStockActions,
36
37
  type InventoryStats,
37
38
  type InventoryTransaction,
38
39
  type InventoryTransactionType,
39
40
  type ProductStockLevel,
41
+ type ReceiveStockInput,
42
+ type TransferStockInput,
40
43
  } from './useInventory';
41
44
 
42
45
  // Customers
@@ -75,6 +75,7 @@ interface UseFleetResult {
75
75
  updateVehicle: (id: string, input: CreateVehicleInput) => Promise<Vehicle>;
76
76
  updateDriver: (id: string, input: UpdateDriverInput) => Promise<Driver>;
77
77
  deleteVehicle: (id: string) => Promise<void>;
78
+ deleteDriver: (id: string) => Promise<void>;
78
79
  }
79
80
 
80
81
  export interface ApiVehicle {
@@ -256,6 +257,12 @@ const GET_DRIVER_QUERY = `
256
257
  }
257
258
  `;
258
259
 
260
+ const DELETE_DRIVER_MUTATION = `
261
+ mutation DeleteDriver($id: ID!) {
262
+ deleteDriver(id: $id)
263
+ }
264
+ `;
265
+
259
266
  const DELETE_VEHICLE_MUTATION = `
260
267
  mutation DeleteVehicle($id: ID!) {
261
268
  deleteVehicle(id: $id)
@@ -891,6 +898,16 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
891
898
  [graphQLOptions]
892
899
  );
893
900
 
901
+ const deleteDriver = useCallback(
902
+ async (id: string): Promise<void> => {
903
+ await executeGraphQL<{ deleteDriver: boolean }>(graphQLOptions, DELETE_DRIVER_MUTATION, {
904
+ id,
905
+ });
906
+ setDrivers((previous) => previous.filter((item) => item.id !== id));
907
+ },
908
+ [graphQLOptions]
909
+ );
910
+
894
911
  return {
895
912
  vehicles,
896
913
  drivers,
@@ -904,6 +921,7 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
904
921
  updateVehicle,
905
922
  updateDriver,
906
923
  deleteVehicle,
924
+ deleteDriver,
907
925
  };
908
926
  }
909
927