@burdenoff/microfe-movethewheels 2026.925.2 → 2026.925.3
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.
- package/dist/components/StatusBadge.js +30 -30
- package/dist/constants/index.js +12 -5
- package/dist/hooks/useCustomers.js +0 -3
- package/dist/hooks/useFleet.js +144 -131
- package/dist/hooks/useInventory.js +144 -145
- package/dist/hooks/useRoutes.js +5 -14
- package/dist/movethewheels/constants/index.d.ts +1 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +13 -1
- package/dist/movethewheels/hooks/useInventory.d.ts +10 -0
- package/dist/movethewheels/types/index.d.ts +13 -70
- package/dist/pages/CustomerDetailsPage.js +666 -344
- package/dist/pages/CustomersPage.js +71 -80
- package/dist/pages/FleetPage.js +1 -3
- package/dist/pages/InventoryPage.js +4 -4
- package/dist/pages/RouteDetailsPage.js +273 -248
- package/dist/pages/RoutesPage.js +130 -67
- package/dist/pages/VehicleDetailsPage.js +506 -391
- package/dist/pages/VehiclesPage.js +119 -70
- package/dist/pages/WarehouseDetailsPage.js +410 -370
- package/dist/pages/WarehousesPage.js +131 -82
- package/package.json +1 -1
- package/src/movethewheels/components/StatusBadge.tsx +8 -3
- package/src/movethewheels/constants/index.ts +9 -0
- package/src/movethewheels/hooks/useCustomers.ts +0 -3
- package/src/movethewheels/hooks/useFleet.ts +74 -19
- package/src/movethewheels/hooks/useInventory.ts +76 -32
- package/src/movethewheels/hooks/useRoutes.ts +17 -12
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +376 -109
- package/src/movethewheels/pages/CustomersPage.tsx +6 -15
- package/src/movethewheels/pages/FleetPage.tsx +2 -1
- package/src/movethewheels/pages/InventoryPage.tsx +14 -5
- package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -34
- package/src/movethewheels/pages/RoutesPage.tsx +66 -2
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +151 -11
- package/src/movethewheels/pages/VehiclesPage.tsx +68 -3
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +213 -135
- package/src/movethewheels/pages/WarehousesPage.tsx +77 -16
- package/src/movethewheels/types/index.ts +14 -70
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { useEffect, useState } from 'react';
|
|
6
6
|
import { useLocation, useParams } from 'react-router';
|
|
7
|
-
import { Warehouse, ArrowLeft, Edit, MapPin, Clock,
|
|
7
|
+
import { Warehouse, ArrowLeft, Edit, MapPin, Clock, Box, Save, Trash2 } from 'lucide-react';
|
|
8
8
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
9
9
|
import {
|
|
10
10
|
Card,
|
|
@@ -22,7 +22,17 @@ import {
|
|
|
22
22
|
SelectTrigger,
|
|
23
23
|
SelectValue,
|
|
24
24
|
} from '../components/ui';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
EmphasisPanel,
|
|
27
|
+
NextSteps,
|
|
28
|
+
PagePurpose,
|
|
29
|
+
ResponsiveDialog,
|
|
30
|
+
ResponsiveDialogContent,
|
|
31
|
+
ResponsiveDialogHeader,
|
|
32
|
+
ResponsiveDialogFooter,
|
|
33
|
+
ResponsiveDialogTitle,
|
|
34
|
+
ResponsiveDialogDescription,
|
|
35
|
+
} from '@burdenoff/fe-libs/ui';
|
|
26
36
|
import { useWarehouse, useInventory } from '../hooks/useInventory';
|
|
27
37
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
28
38
|
import { formatAddress } from '../utils/formatters';
|
|
@@ -37,24 +47,36 @@ const WAREHOUSE_TYPE_OPTIONS = [
|
|
|
37
47
|
{ value: 'RETAIL', label: 'Retail' },
|
|
38
48
|
];
|
|
39
49
|
|
|
50
|
+
const WAREHOUSE_STATUS_OPTIONS = [
|
|
51
|
+
{ value: 'ACTIVE', label: 'Active' },
|
|
52
|
+
{ value: 'INACTIVE', label: 'Inactive' },
|
|
53
|
+
];
|
|
54
|
+
|
|
40
55
|
function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
41
56
|
const { basePath, navigate } = useMoveTheWheels();
|
|
42
|
-
const { createWarehouse, updateWarehouse } = useInventory();
|
|
57
|
+
const { createWarehouse, updateWarehouse, deleteWarehouse } = useInventory();
|
|
43
58
|
const { warehouse } = useWarehouse(editId);
|
|
44
59
|
const isEditing = !!editId;
|
|
45
60
|
const [form, setForm] = useState({
|
|
46
61
|
code: '',
|
|
47
62
|
name: '',
|
|
48
63
|
type: 'DISTRIBUTION',
|
|
64
|
+
status: 'ACTIVE',
|
|
49
65
|
street: '',
|
|
50
66
|
city: '',
|
|
51
67
|
state: '',
|
|
52
68
|
postalCode: '',
|
|
53
69
|
country: 'USA',
|
|
54
70
|
totalCapacity: '',
|
|
71
|
+
usedCapacity: '',
|
|
72
|
+
contactEmail: '',
|
|
73
|
+
contactPhone: '',
|
|
55
74
|
});
|
|
56
75
|
const [submitting, setSubmitting] = useState(false);
|
|
57
76
|
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
78
|
+
const [deleting, setDeleting] = useState(false);
|
|
79
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
58
80
|
|
|
59
81
|
useEffect(() => {
|
|
60
82
|
if (!editId || !warehouse) return;
|
|
@@ -62,12 +84,16 @@ function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
|
62
84
|
code: warehouse.code || '',
|
|
63
85
|
name: warehouse.name || '',
|
|
64
86
|
type: warehouse.type ? warehouse.type.toUpperCase() : 'DISTRIBUTION',
|
|
87
|
+
status: warehouse.status || 'ACTIVE',
|
|
65
88
|
street: warehouse.address.street || '',
|
|
66
89
|
city: warehouse.address.city || '',
|
|
67
90
|
state: warehouse.address.state || '',
|
|
68
91
|
postalCode: warehouse.address.postalCode || '',
|
|
69
92
|
country: warehouse.address.country || 'USA',
|
|
70
93
|
totalCapacity: warehouse.capacity.totalSpace ? String(warehouse.capacity.totalSpace) : '',
|
|
94
|
+
usedCapacity: warehouse.capacity.usedSpace ? String(warehouse.capacity.usedSpace) : '',
|
|
95
|
+
contactEmail: warehouse.contactInfo?.email || '',
|
|
96
|
+
contactPhone: warehouse.contactInfo?.phone || '',
|
|
71
97
|
});
|
|
72
98
|
}, [editId, warehouse]);
|
|
73
99
|
|
|
@@ -90,10 +116,12 @@ function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
|
90
116
|
setError(null);
|
|
91
117
|
try {
|
|
92
118
|
const totalCapacity = form.totalCapacity.trim() ? Number(form.totalCapacity) : undefined;
|
|
119
|
+
const usedCapacity = form.usedCapacity.trim() ? Number(form.usedCapacity) : undefined;
|
|
93
120
|
const warehouseInput = {
|
|
94
121
|
code: form.code.trim(),
|
|
95
122
|
name: form.name.trim(),
|
|
96
123
|
type: form.type,
|
|
124
|
+
status: form.status,
|
|
97
125
|
address: {
|
|
98
126
|
street: form.street.trim(),
|
|
99
127
|
city: form.city.trim(),
|
|
@@ -101,8 +129,17 @@ function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
|
101
129
|
postalCode: form.postalCode.trim(),
|
|
102
130
|
country: form.country.trim(),
|
|
103
131
|
},
|
|
132
|
+
contactInfo:
|
|
133
|
+
form.contactEmail.trim() || form.contactPhone.trim()
|
|
134
|
+
? {
|
|
135
|
+
email: form.contactEmail.trim() || undefined,
|
|
136
|
+
phone: form.contactPhone.trim() || undefined,
|
|
137
|
+
}
|
|
138
|
+
: undefined,
|
|
104
139
|
totalCapacity:
|
|
105
140
|
totalCapacity !== undefined && !Number.isNaN(totalCapacity) ? totalCapacity : undefined,
|
|
141
|
+
usedCapacity:
|
|
142
|
+
usedCapacity !== undefined && !Number.isNaN(usedCapacity) ? usedCapacity : undefined,
|
|
106
143
|
};
|
|
107
144
|
if (editId) {
|
|
108
145
|
await updateWarehouse(editId, warehouseInput);
|
|
@@ -120,6 +157,20 @@ function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
|
120
157
|
}
|
|
121
158
|
};
|
|
122
159
|
|
|
160
|
+
const handleDelete = async () => {
|
|
161
|
+
if (!editId) return;
|
|
162
|
+
setDeleting(true);
|
|
163
|
+
setDeleteError(null);
|
|
164
|
+
try {
|
|
165
|
+
await deleteWarehouse(editId);
|
|
166
|
+
setDeleteOpen(false);
|
|
167
|
+
goBack();
|
|
168
|
+
} catch (err) {
|
|
169
|
+
setDeleteError(err instanceof Error ? err.message : 'Failed to delete warehouse.');
|
|
170
|
+
setDeleting(false);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
123
174
|
return (
|
|
124
175
|
<PageLayout
|
|
125
176
|
title={isEditing ? 'Edit Warehouse' : 'Add Warehouse'}
|
|
@@ -196,16 +247,65 @@ function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
|
196
247
|
</Select>
|
|
197
248
|
</div>
|
|
198
249
|
</div>
|
|
199
|
-
<div className="
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
250
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
251
|
+
<div className="space-y-2">
|
|
252
|
+
<Label htmlFor="status">Status</Label>
|
|
253
|
+
<Select value={form.status} onValueChange={(value) => update('status', value)}>
|
|
254
|
+
<SelectTrigger id="status">
|
|
255
|
+
<SelectValue />
|
|
256
|
+
</SelectTrigger>
|
|
257
|
+
<SelectContent>
|
|
258
|
+
{WAREHOUSE_STATUS_OPTIONS.map((option) => (
|
|
259
|
+
<SelectItem key={option.value} value={option.value}>
|
|
260
|
+
{option.label}
|
|
261
|
+
</SelectItem>
|
|
262
|
+
))}
|
|
263
|
+
</SelectContent>
|
|
264
|
+
</Select>
|
|
265
|
+
</div>
|
|
266
|
+
<div className="space-y-2">
|
|
267
|
+
<Label htmlFor="totalCapacity">Total Capacity (sq ft)</Label>
|
|
268
|
+
<Input
|
|
269
|
+
id="totalCapacity"
|
|
270
|
+
type="number"
|
|
271
|
+
min="0"
|
|
272
|
+
value={form.totalCapacity}
|
|
273
|
+
onChange={(e) => update('totalCapacity', e.target.value)}
|
|
274
|
+
placeholder="50000"
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
<div className="space-y-2">
|
|
278
|
+
<Label htmlFor="usedCapacity">Used Capacity (sq ft)</Label>
|
|
279
|
+
<Input
|
|
280
|
+
id="usedCapacity"
|
|
281
|
+
type="number"
|
|
282
|
+
min="0"
|
|
283
|
+
value={form.usedCapacity}
|
|
284
|
+
onChange={(e) => update('usedCapacity', e.target.value)}
|
|
285
|
+
placeholder="0"
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
290
|
+
<div className="space-y-2">
|
|
291
|
+
<Label htmlFor="contactEmail">Contact Email</Label>
|
|
292
|
+
<Input
|
|
293
|
+
id="contactEmail"
|
|
294
|
+
type="email"
|
|
295
|
+
value={form.contactEmail}
|
|
296
|
+
onChange={(e) => update('contactEmail', e.target.value)}
|
|
297
|
+
placeholder="dc-sf@example.com"
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
<div className="space-y-2">
|
|
301
|
+
<Label htmlFor="contactPhone">Contact Phone</Label>
|
|
302
|
+
<Input
|
|
303
|
+
id="contactPhone"
|
|
304
|
+
value={form.contactPhone}
|
|
305
|
+
onChange={(e) => update('contactPhone', e.target.value)}
|
|
306
|
+
placeholder="+1-555-0100"
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
209
309
|
</div>
|
|
210
310
|
</div>
|
|
211
311
|
</EmphasisPanel>
|
|
@@ -268,7 +368,56 @@ function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
|
268
368
|
</div>
|
|
269
369
|
</CardContent>
|
|
270
370
|
</Card>
|
|
371
|
+
|
|
372
|
+
{isEditing && (
|
|
373
|
+
<Card className="border-status-error-border">
|
|
374
|
+
<CardHeader>
|
|
375
|
+
<CardTitle className="flex items-center gap-2 text-status-error-text">
|
|
376
|
+
<Trash2 size={18} /> Delete Warehouse
|
|
377
|
+
</CardTitle>
|
|
378
|
+
</CardHeader>
|
|
379
|
+
<CardContent>
|
|
380
|
+
<p className="mb-3 text-sm text-text-secondary">
|
|
381
|
+
Removes this warehouse from your network. This cannot be undone from here.
|
|
382
|
+
</p>
|
|
383
|
+
<Button
|
|
384
|
+
type="button"
|
|
385
|
+
variant="outline"
|
|
386
|
+
className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
|
|
387
|
+
onClick={() => setDeleteOpen(true)}
|
|
388
|
+
>
|
|
389
|
+
<Trash2 size={16} /> Delete this warehouse
|
|
390
|
+
</Button>
|
|
391
|
+
</CardContent>
|
|
392
|
+
</Card>
|
|
393
|
+
)}
|
|
271
394
|
</form>
|
|
395
|
+
|
|
396
|
+
<ResponsiveDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
397
|
+
<ResponsiveDialogContent>
|
|
398
|
+
<ResponsiveDialogHeader>
|
|
399
|
+
<ResponsiveDialogTitle>Delete this warehouse?</ResponsiveDialogTitle>
|
|
400
|
+
<ResponsiveDialogDescription>
|
|
401
|
+
This removes {form.name || 'this warehouse'} and cannot be undone from here.
|
|
402
|
+
</ResponsiveDialogDescription>
|
|
403
|
+
</ResponsiveDialogHeader>
|
|
404
|
+
<div className="px-1">
|
|
405
|
+
<FormError message={deleteError} />
|
|
406
|
+
</div>
|
|
407
|
+
<ResponsiveDialogFooter>
|
|
408
|
+
<Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
|
|
409
|
+
Cancel
|
|
410
|
+
</Button>
|
|
411
|
+
<Button
|
|
412
|
+
className="bg-status-error-text text-text-inverse hover:opacity-90"
|
|
413
|
+
onClick={handleDelete}
|
|
414
|
+
disabled={deleting}
|
|
415
|
+
>
|
|
416
|
+
{deleting ? 'Deleting...' : 'Delete warehouse'}
|
|
417
|
+
</Button>
|
|
418
|
+
</ResponsiveDialogFooter>
|
|
419
|
+
</ResponsiveDialogContent>
|
|
420
|
+
</ResponsiveDialog>
|
|
272
421
|
</PageLayout>
|
|
273
422
|
);
|
|
274
423
|
}
|
|
@@ -287,7 +436,7 @@ export default function WarehouseDetailsPage() {
|
|
|
287
436
|
if (isLoading) return <LoadingState message="Loading warehouse details..." />;
|
|
288
437
|
if (error || !warehouse) return <ErrorState title="Warehouse not found" onRetry={refetch} />;
|
|
289
438
|
|
|
290
|
-
const
|
|
439
|
+
const isActive = warehouse.status.toUpperCase() === 'ACTIVE';
|
|
291
440
|
|
|
292
441
|
return (
|
|
293
442
|
<PageLayout
|
|
@@ -310,8 +459,8 @@ export default function WarehouseDetailsPage() {
|
|
|
310
459
|
}
|
|
311
460
|
headerContent={
|
|
312
461
|
<div className="flex gap-2">
|
|
313
|
-
<Badge variant={
|
|
314
|
-
{
|
|
462
|
+
<Badge variant={isActive ? 'default' : 'secondary'}>
|
|
463
|
+
{isActive ? 'Active' : 'Inactive'}
|
|
315
464
|
</Badge>
|
|
316
465
|
<Badge variant="outline" className="capitalize">
|
|
317
466
|
{warehouse.type.replace('_', ' ')}
|
|
@@ -320,9 +469,9 @@ export default function WarehouseDetailsPage() {
|
|
|
320
469
|
}
|
|
321
470
|
>
|
|
322
471
|
<PagePurpose icon={<Warehouse size={18} />}>
|
|
323
|
-
A storage and fulfilment site at a glance — how full it is, its
|
|
324
|
-
|
|
325
|
-
|
|
472
|
+
A storage and fulfilment site at a glance — how full it is, its operating hours and how to
|
|
473
|
+
reach it. Use it to decide whether this warehouse has the space and hours to fulfil an
|
|
474
|
+
order.
|
|
326
475
|
</PagePurpose>
|
|
327
476
|
|
|
328
477
|
<NextSteps
|
|
@@ -352,65 +501,19 @@ export default function WarehouseDetailsPage() {
|
|
|
352
501
|
<h2 className="flex items-center gap-2 text-base font-semibold text-text-primary mb-4">
|
|
353
502
|
<Box size={18} /> Capacity
|
|
354
503
|
</h2>
|
|
355
|
-
<div
|
|
356
|
-
<div>
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
<span>{utilization.toFixed(0)}%</span>
|
|
360
|
-
</div>
|
|
361
|
-
<Progress value={utilization} />
|
|
362
|
-
<p className="text-sm text-text-secondary mt-1">
|
|
363
|
-
{warehouse.capacity.usedSpace.toLocaleString()} /{' '}
|
|
364
|
-
{warehouse.capacity.totalSpace.toLocaleString()} sq ft
|
|
365
|
-
</p>
|
|
366
|
-
</div>
|
|
367
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
368
|
-
<div>
|
|
369
|
-
<p className="text-sm text-text-secondary">Pallets Used</p>
|
|
370
|
-
<p className="text-2xl font-bold">
|
|
371
|
-
{warehouse.capacity.usedPallets}
|
|
372
|
-
<span className="text-sm font-normal text-text-secondary">
|
|
373
|
-
{' '}
|
|
374
|
-
/ {warehouse.capacity.totalPallets}
|
|
375
|
-
</span>
|
|
376
|
-
</p>
|
|
377
|
-
</div>
|
|
504
|
+
<div>
|
|
505
|
+
<div className="flex justify-between text-sm mb-2">
|
|
506
|
+
<span>Space Utilization</span>
|
|
507
|
+
<span>{warehouse.utilization.toFixed(0)}%</span>
|
|
378
508
|
</div>
|
|
509
|
+
<Progress value={warehouse.utilization} />
|
|
510
|
+
<p className="text-sm text-text-secondary mt-1">
|
|
511
|
+
{warehouse.capacity.usedSpace.toLocaleString()} /{' '}
|
|
512
|
+
{warehouse.capacity.totalSpace.toLocaleString()} sq ft
|
|
513
|
+
</p>
|
|
379
514
|
</div>
|
|
380
515
|
</EmphasisPanel>
|
|
381
516
|
|
|
382
|
-
<Card>
|
|
383
|
-
<CardHeader>
|
|
384
|
-
<CardTitle>Zones ({warehouse.zones.length})</CardTitle>
|
|
385
|
-
</CardHeader>
|
|
386
|
-
<CardContent>
|
|
387
|
-
<div className="space-y-3">
|
|
388
|
-
{warehouse.zones.map((zone) => (
|
|
389
|
-
<div
|
|
390
|
-
key={zone.id}
|
|
391
|
-
className="flex items-center justify-between p-3 border rounded-lg"
|
|
392
|
-
>
|
|
393
|
-
<div>
|
|
394
|
-
<p className="font-medium">{zone.name}</p>
|
|
395
|
-
<p className="text-sm text-text-secondary capitalize">
|
|
396
|
-
{zone.type} - {zone.location}
|
|
397
|
-
</p>
|
|
398
|
-
</div>
|
|
399
|
-
<div className="text-right">
|
|
400
|
-
<Progress
|
|
401
|
-
value={(zone.currentOccupancy / zone.capacity) * 100}
|
|
402
|
-
className="w-24 h-2"
|
|
403
|
-
/>
|
|
404
|
-
<p className="text-sm text-text-secondary">
|
|
405
|
-
{zone.currentOccupancy} / {zone.capacity}
|
|
406
|
-
</p>
|
|
407
|
-
</div>
|
|
408
|
-
</div>
|
|
409
|
-
))}
|
|
410
|
-
</div>
|
|
411
|
-
</CardContent>
|
|
412
|
-
</Card>
|
|
413
|
-
|
|
414
517
|
<Card>
|
|
415
518
|
<CardHeader>
|
|
416
519
|
<CardTitle className="flex items-center gap-2">
|
|
@@ -418,16 +521,20 @@ export default function WarehouseDetailsPage() {
|
|
|
418
521
|
</CardTitle>
|
|
419
522
|
</CardHeader>
|
|
420
523
|
<CardContent>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
524
|
+
{Object.keys(warehouse.operatingHours).length > 0 ? (
|
|
525
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
526
|
+
{Object.entries(warehouse.operatingHours).map(([day, hours]) => (
|
|
527
|
+
<div key={day}>
|
|
528
|
+
<p className="text-sm font-medium">{day}</p>
|
|
529
|
+
<p className="text-sm text-text-secondary">
|
|
530
|
+
{hours ? `${hours.open} - ${hours.close}` : 'Closed'}
|
|
531
|
+
</p>
|
|
532
|
+
</div>
|
|
533
|
+
))}
|
|
534
|
+
</div>
|
|
535
|
+
) : (
|
|
536
|
+
<p className="text-sm text-text-secondary">Not recorded</p>
|
|
537
|
+
)}
|
|
431
538
|
</CardContent>
|
|
432
539
|
</Card>
|
|
433
540
|
</div>
|
|
@@ -440,60 +547,31 @@ export default function WarehouseDetailsPage() {
|
|
|
440
547
|
</CardTitle>
|
|
441
548
|
</CardHeader>
|
|
442
549
|
<CardContent>
|
|
443
|
-
<p className="text-sm">{formatAddress(warehouse.address)}</p>
|
|
550
|
+
<p className="text-sm">{formatAddress(warehouse.address) || '—'}</p>
|
|
444
551
|
</CardContent>
|
|
445
552
|
</Card>
|
|
446
553
|
|
|
447
|
-
|
|
448
|
-
<
|
|
449
|
-
<
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
<
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
<p className="text-sm text-text-secondary">Orders Fulfilled</p>
|
|
469
|
-
<p className="text-2xl font-bold">
|
|
470
|
-
{warehouse.metrics.ordersFulfilled.toLocaleString()}
|
|
471
|
-
</p>
|
|
472
|
-
</div>
|
|
473
|
-
</CardContent>
|
|
474
|
-
</Card>
|
|
475
|
-
|
|
476
|
-
<Card>
|
|
477
|
-
<CardHeader>
|
|
478
|
-
<CardTitle className="flex items-center gap-2">
|
|
479
|
-
<Users size={18} /> Staff ({warehouse.staff.length})
|
|
480
|
-
</CardTitle>
|
|
481
|
-
</CardHeader>
|
|
482
|
-
<CardContent>
|
|
483
|
-
{warehouse.staff.length > 0 ? (
|
|
484
|
-
<div className="space-y-2">
|
|
485
|
-
{warehouse.staff.map((member, i) => (
|
|
486
|
-
<div key={i} className="flex items-center justify-between p-2 border rounded">
|
|
487
|
-
<span className="text-sm capitalize">{member.role}</span>
|
|
488
|
-
<Badge variant="outline">{member.shift}</Badge>
|
|
489
|
-
</div>
|
|
490
|
-
))}
|
|
491
|
-
</div>
|
|
492
|
-
) : (
|
|
493
|
-
<p className="text-text-secondary">No staff assigned</p>
|
|
494
|
-
)}
|
|
495
|
-
</CardContent>
|
|
496
|
-
</Card>
|
|
554
|
+
{(warehouse.contactInfo?.email || warehouse.contactInfo?.phone) && (
|
|
555
|
+
<Card>
|
|
556
|
+
<CardHeader>
|
|
557
|
+
<CardTitle>Contact</CardTitle>
|
|
558
|
+
</CardHeader>
|
|
559
|
+
<CardContent className="space-y-3">
|
|
560
|
+
{warehouse.contactInfo?.email && (
|
|
561
|
+
<div>
|
|
562
|
+
<p className="text-sm text-text-secondary">Email</p>
|
|
563
|
+
<p className="font-medium">{warehouse.contactInfo.email}</p>
|
|
564
|
+
</div>
|
|
565
|
+
)}
|
|
566
|
+
{warehouse.contactInfo?.phone && (
|
|
567
|
+
<div>
|
|
568
|
+
<p className="text-sm text-text-secondary">Phone</p>
|
|
569
|
+
<p className="font-medium">{warehouse.contactInfo.phone}</p>
|
|
570
|
+
</div>
|
|
571
|
+
)}
|
|
572
|
+
</CardContent>
|
|
573
|
+
</Card>
|
|
574
|
+
)}
|
|
497
575
|
</div>
|
|
498
576
|
</div>
|
|
499
577
|
</PageLayout>
|
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useState } from 'react';
|
|
6
|
-
import { Warehouse, Plus, RefreshCw, CircleCheck, Gauge } from 'lucide-react';
|
|
6
|
+
import { Warehouse, Plus, RefreshCw, CircleCheck, Gauge, Filter } from 'lucide-react';
|
|
7
7
|
import { PageLayout } from '../components/PageLayout';
|
|
8
8
|
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
9
9
|
import { DataTable, type Column } from '../components/DataTable';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Badge,
|
|
12
|
+
Progress,
|
|
13
|
+
Card,
|
|
14
|
+
CardContent,
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
Button,
|
|
21
|
+
} from '../components/ui';
|
|
11
22
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
12
23
|
import { useWarehouses, useWarehouseStats } from '../hooks/useInventory';
|
|
13
24
|
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
@@ -15,17 +26,26 @@ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
|
15
26
|
import { joinPath } from '../utils/navigation';
|
|
16
27
|
import type { Warehouse as WarehouseType } from '../types';
|
|
17
28
|
|
|
29
|
+
const STATUS_OPTIONS = [
|
|
30
|
+
{ value: 'all', label: 'All statuses' },
|
|
31
|
+
{ value: 'ACTIVE', label: 'Active' },
|
|
32
|
+
{ value: 'INACTIVE', label: 'Inactive' },
|
|
33
|
+
];
|
|
34
|
+
|
|
18
35
|
export default function WarehousesPage() {
|
|
19
36
|
const { basePath, navigate } = useMoveTheWheels();
|
|
20
37
|
const [page, setPage] = useState(0);
|
|
21
38
|
const [pageSize, setPageSize] = useState(25);
|
|
22
39
|
const [search, setSearch] = useState('');
|
|
40
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
23
41
|
const debouncedSearch = useDebouncedValue(search);
|
|
24
42
|
const { warehouses, totalWarehouses, hasMore, isLoading, error, refetch } = useWarehouses({
|
|
25
43
|
limit: pageSize,
|
|
26
44
|
offset: page * pageSize,
|
|
27
45
|
search: debouncedSearch,
|
|
46
|
+
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
28
47
|
});
|
|
48
|
+
const filtersActive = statusFilter !== 'all' || search.length > 0;
|
|
29
49
|
// Bounded aggregate for the tiles + subtitle — not the truncated page.
|
|
30
50
|
const { stats } = useWarehouseStats();
|
|
31
51
|
|
|
@@ -83,25 +103,23 @@ export default function WarehousesPage() {
|
|
|
83
103
|
id: 'capacity',
|
|
84
104
|
header: 'Capacity',
|
|
85
105
|
cell: (row) => (
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</p>
|
|
91
|
-
<p className="text-text-secondary">
|
|
92
|
-
{row.capacity.usedPallets} / {row.capacity.totalPallets} pallets
|
|
93
|
-
</p>
|
|
94
|
-
</div>
|
|
106
|
+
<p className="text-sm">
|
|
107
|
+
{row.capacity.usedSpace.toLocaleString()} / {row.capacity.totalSpace.toLocaleString()} sq
|
|
108
|
+
ft
|
|
109
|
+
</p>
|
|
95
110
|
),
|
|
96
111
|
},
|
|
97
112
|
{
|
|
98
113
|
id: 'status',
|
|
99
114
|
header: 'Status',
|
|
100
|
-
cell: (row) =>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
cell: (row) => {
|
|
116
|
+
const isActive = row.status.toUpperCase() === 'ACTIVE';
|
|
117
|
+
return (
|
|
118
|
+
<Badge variant={isActive ? 'default' : 'secondary'}>
|
|
119
|
+
{isActive ? 'Active' : 'Inactive'}
|
|
120
|
+
</Badge>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
105
123
|
},
|
|
106
124
|
];
|
|
107
125
|
|
|
@@ -152,6 +170,49 @@ export default function WarehousesPage() {
|
|
|
152
170
|
</MetricGrid>
|
|
153
171
|
</div>
|
|
154
172
|
|
|
173
|
+
{/* Filters */}
|
|
174
|
+
<Card>
|
|
175
|
+
<CardContent className="p-4">
|
|
176
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
177
|
+
<div className="flex items-center gap-2">
|
|
178
|
+
<Filter size={16} className="text-text-secondary" />
|
|
179
|
+
<span className="text-sm font-medium">Filters:</span>
|
|
180
|
+
</div>
|
|
181
|
+
<Select
|
|
182
|
+
value={statusFilter}
|
|
183
|
+
onValueChange={(value) => {
|
|
184
|
+
setStatusFilter(value);
|
|
185
|
+
setPage(0);
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<SelectTrigger className="w-[180px]">
|
|
189
|
+
<SelectValue placeholder="Status" />
|
|
190
|
+
</SelectTrigger>
|
|
191
|
+
<SelectContent>
|
|
192
|
+
{STATUS_OPTIONS.map((option) => (
|
|
193
|
+
<SelectItem key={option.value} value={option.value}>
|
|
194
|
+
{option.label}
|
|
195
|
+
</SelectItem>
|
|
196
|
+
))}
|
|
197
|
+
</SelectContent>
|
|
198
|
+
</Select>
|
|
199
|
+
{filtersActive && (
|
|
200
|
+
<Button
|
|
201
|
+
variant="ghost"
|
|
202
|
+
size="sm"
|
|
203
|
+
onClick={() => {
|
|
204
|
+
setStatusFilter('all');
|
|
205
|
+
setSearch('');
|
|
206
|
+
setPage(0);
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
Clear filters
|
|
210
|
+
</Button>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
</CardContent>
|
|
214
|
+
</Card>
|
|
215
|
+
|
|
155
216
|
<DataTable
|
|
156
217
|
columns={columns}
|
|
157
218
|
data={warehouses}
|