@burdenoff/microfe-movethewheels 2026.925.2 → 2026.929.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/StatusBadge.js +34 -31
- package/dist/constants/index.js +12 -5
- package/dist/hooks/useCustomers.js +0 -3
- package/dist/hooks/useFleet.js +148 -135
- package/dist/hooks/useInventory.js +146 -146
- package/dist/hooks/useRoutes.js +5 -14
- package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
- package/dist/movethewheels/constants/index.d.ts +1 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +17 -4
- package/dist/movethewheels/hooks/useInventory.d.ts +11 -0
- package/dist/movethewheels/types/index.d.ts +19 -75
- package/dist/pages/CustomerDetailsPage.js +666 -344
- package/dist/pages/CustomersPage.js +71 -80
- package/dist/pages/DriverDetailsPage.js +2 -2
- package/dist/pages/DriversPage.js +2 -2
- package/dist/pages/FleetPage.js +1 -3
- package/dist/pages/InventoryPage.js +5 -5
- package/dist/pages/RouteDetailsPage.js +273 -248
- package/dist/pages/RoutesPage.js +130 -67
- package/dist/pages/VehicleDetailsPage.js +527 -389
- package/dist/pages/VehiclesPage.js +120 -71
- package/dist/pages/WarehouseDetailsPage.js +411 -368
- package/dist/pages/WarehousesPage.js +132 -83
- package/package.json +1 -1
- package/src/movethewheels/components/StatusBadge.tsx +15 -7
- package/src/movethewheels/constants/index.ts +9 -0
- package/src/movethewheels/hooks/useCustomers.ts +0 -3
- package/src/movethewheels/hooks/useFleet.ts +89 -25
- package/src/movethewheels/hooks/useInventory.ts +77 -33
- 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/DriverDetailsPage.tsx +3 -1
- package/src/movethewheels/pages/DriversPage.tsx +5 -1
- package/src/movethewheels/pages/FleetPage.tsx +2 -1
- package/src/movethewheels/pages/InventoryPage.tsx +18 -6
- package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -34
- package/src/movethewheels/pages/RoutesPage.tsx +66 -2
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +185 -23
- package/src/movethewheels/pages/VehiclesPage.tsx +72 -5
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +217 -138
- package/src/movethewheels/pages/WarehousesPage.tsx +81 -17
- package/src/movethewheels/types/index.ts +28 -75
|
@@ -11,24 +11,44 @@ import {
|
|
|
11
11
|
CircleCheck,
|
|
12
12
|
Route as RouteIcon,
|
|
13
13
|
Wrench,
|
|
14
|
+
Filter,
|
|
14
15
|
} from 'lucide-react';
|
|
15
16
|
import { PageLayout } from '../components/PageLayout';
|
|
16
17
|
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
17
18
|
import { DataTable, type Column } from '../components/DataTable';
|
|
18
19
|
import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
Progress,
|
|
22
|
+
Card,
|
|
23
|
+
CardContent,
|
|
24
|
+
Select,
|
|
25
|
+
SelectContent,
|
|
26
|
+
SelectItem,
|
|
27
|
+
SelectTrigger,
|
|
28
|
+
SelectValue,
|
|
29
|
+
Button,
|
|
30
|
+
} from '../components/ui';
|
|
20
31
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
21
32
|
import { useVehicles, useFleetStats } from '../hooks/useFleet';
|
|
22
33
|
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
23
34
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
24
35
|
import { joinPath } from '../utils/navigation';
|
|
25
|
-
import
|
|
36
|
+
import { VEHICLE_STATUS_LABELS } from '../constants';
|
|
37
|
+
import type { Vehicle, VehicleStatus } from '../types';
|
|
38
|
+
|
|
39
|
+
const STATUS_OPTIONS: Array<{ value: VehicleStatus | 'all'; label: string }> = [
|
|
40
|
+
{ value: 'all', label: 'All statuses' },
|
|
41
|
+
...(Object.entries(VEHICLE_STATUS_LABELS) as Array<[VehicleStatus, string]>).map(
|
|
42
|
+
([value, label]) => ({ value, label })
|
|
43
|
+
),
|
|
44
|
+
];
|
|
26
45
|
|
|
27
46
|
export default function VehiclesPage() {
|
|
28
47
|
const { basePath, navigate } = useMoveTheWheels();
|
|
29
48
|
const [page, setPage] = useState(0);
|
|
30
49
|
const [pageSize, setPageSize] = useState(25);
|
|
31
50
|
const [search, setSearch] = useState('');
|
|
51
|
+
const [statusFilter, setStatusFilter] = useState<VehicleStatus | 'all'>('all');
|
|
32
52
|
const debouncedSearch = useDebouncedValue(search);
|
|
33
53
|
const {
|
|
34
54
|
vehicles,
|
|
@@ -41,7 +61,9 @@ export default function VehiclesPage() {
|
|
|
41
61
|
limit: pageSize,
|
|
42
62
|
offset: page * pageSize,
|
|
43
63
|
search: debouncedSearch || undefined,
|
|
64
|
+
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
44
65
|
});
|
|
66
|
+
const filtersActive = statusFilter !== 'all' || search.length > 0;
|
|
45
67
|
// Fleet totals + tiles from the live aggregate, not the current page.
|
|
46
68
|
const { stats } = useFleetStats();
|
|
47
69
|
const totalVehicles = stats.totalVehicles;
|
|
@@ -74,7 +96,7 @@ export default function VehiclesPage() {
|
|
|
74
96
|
<p className="font-medium">
|
|
75
97
|
{row.make} {row.model}
|
|
76
98
|
</p>
|
|
77
|
-
<p className="text-sm text-text-secondary">{row.year}</p>
|
|
99
|
+
<p className="text-sm text-text-secondary">{row.year ?? '—'}</p>
|
|
78
100
|
</div>
|
|
79
101
|
),
|
|
80
102
|
},
|
|
@@ -90,8 +112,10 @@ export default function VehiclesPage() {
|
|
|
90
112
|
header: 'Capacity',
|
|
91
113
|
cell: (row) => (
|
|
92
114
|
<div className="text-sm">
|
|
93
|
-
<p>{row.capacity.weight} kg</p>
|
|
94
|
-
<p className="text-text-secondary">
|
|
115
|
+
<p>{row.capacity.weight !== undefined ? `${row.capacity.weight} kg` : '—'}</p>
|
|
116
|
+
<p className="text-text-secondary">
|
|
117
|
+
{row.capacity.volume !== undefined ? `${row.capacity.volume} m³` : '—'}
|
|
118
|
+
</p>
|
|
95
119
|
</div>
|
|
96
120
|
),
|
|
97
121
|
},
|
|
@@ -178,6 +202,49 @@ export default function VehiclesPage() {
|
|
|
178
202
|
</MetricGrid>
|
|
179
203
|
</div>
|
|
180
204
|
|
|
205
|
+
{/* Filters */}
|
|
206
|
+
<Card>
|
|
207
|
+
<CardContent className="p-4">
|
|
208
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
209
|
+
<div className="flex items-center gap-2">
|
|
210
|
+
<Filter size={16} className="text-text-secondary" />
|
|
211
|
+
<span className="text-sm font-medium">Filters:</span>
|
|
212
|
+
</div>
|
|
213
|
+
<Select
|
|
214
|
+
value={statusFilter}
|
|
215
|
+
onValueChange={(value) => {
|
|
216
|
+
setStatusFilter(value as VehicleStatus | 'all');
|
|
217
|
+
setPage(0);
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<SelectTrigger className="w-[180px]">
|
|
221
|
+
<SelectValue placeholder="Status" />
|
|
222
|
+
</SelectTrigger>
|
|
223
|
+
<SelectContent>
|
|
224
|
+
{STATUS_OPTIONS.map((option) => (
|
|
225
|
+
<SelectItem key={option.value} value={option.value}>
|
|
226
|
+
{option.label}
|
|
227
|
+
</SelectItem>
|
|
228
|
+
))}
|
|
229
|
+
</SelectContent>
|
|
230
|
+
</Select>
|
|
231
|
+
{filtersActive && (
|
|
232
|
+
<Button
|
|
233
|
+
variant="ghost"
|
|
234
|
+
size="sm"
|
|
235
|
+
onClick={() => {
|
|
236
|
+
setStatusFilter('all');
|
|
237
|
+
setSearch('');
|
|
238
|
+
setPage(0);
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
Clear filters
|
|
242
|
+
</Button>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
</CardContent>
|
|
246
|
+
</Card>
|
|
247
|
+
|
|
181
248
|
<DataTable
|
|
182
249
|
columns={columns}
|
|
183
250
|
data={vehicles}
|
|
@@ -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
|
|
|
40
|
-
|
|
50
|
+
const WAREHOUSE_STATUS_OPTIONS = [
|
|
51
|
+
{ value: 'ACTIVE', label: 'Active' },
|
|
52
|
+
{ value: 'INACTIVE', label: 'Inactive' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function NewWarehouseForm({ editId, onSaved }: { editId?: string; onSaved?: () => Promise<void> }) {
|
|
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,11 +129,21 @@ 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);
|
|
146
|
+
await onSaved?.();
|
|
109
147
|
navigate?.(joinPath(basePath, `inventory/warehouses/${editId}`));
|
|
110
148
|
} else {
|
|
111
149
|
const created = await createWarehouse(warehouseInput);
|
|
@@ -120,6 +158,20 @@ function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
|
120
158
|
}
|
|
121
159
|
};
|
|
122
160
|
|
|
161
|
+
const handleDelete = async () => {
|
|
162
|
+
if (!editId) return;
|
|
163
|
+
setDeleting(true);
|
|
164
|
+
setDeleteError(null);
|
|
165
|
+
try {
|
|
166
|
+
await deleteWarehouse(editId);
|
|
167
|
+
setDeleteOpen(false);
|
|
168
|
+
goBack();
|
|
169
|
+
} catch (err) {
|
|
170
|
+
setDeleteError(err instanceof Error ? err.message : 'Failed to delete warehouse.');
|
|
171
|
+
setDeleting(false);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
123
175
|
return (
|
|
124
176
|
<PageLayout
|
|
125
177
|
title={isEditing ? 'Edit Warehouse' : 'Add Warehouse'}
|
|
@@ -196,16 +248,65 @@ function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
|
196
248
|
</Select>
|
|
197
249
|
</div>
|
|
198
250
|
</div>
|
|
199
|
-
<div className="
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
251
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
252
|
+
<div className="space-y-2">
|
|
253
|
+
<Label htmlFor="status">Status</Label>
|
|
254
|
+
<Select value={form.status} onValueChange={(value) => update('status', value)}>
|
|
255
|
+
<SelectTrigger id="status">
|
|
256
|
+
<SelectValue />
|
|
257
|
+
</SelectTrigger>
|
|
258
|
+
<SelectContent>
|
|
259
|
+
{WAREHOUSE_STATUS_OPTIONS.map((option) => (
|
|
260
|
+
<SelectItem key={option.value} value={option.value}>
|
|
261
|
+
{option.label}
|
|
262
|
+
</SelectItem>
|
|
263
|
+
))}
|
|
264
|
+
</SelectContent>
|
|
265
|
+
</Select>
|
|
266
|
+
</div>
|
|
267
|
+
<div className="space-y-2">
|
|
268
|
+
<Label htmlFor="totalCapacity">Total Capacity (sq ft)</Label>
|
|
269
|
+
<Input
|
|
270
|
+
id="totalCapacity"
|
|
271
|
+
type="number"
|
|
272
|
+
min="0"
|
|
273
|
+
value={form.totalCapacity}
|
|
274
|
+
onChange={(e) => update('totalCapacity', e.target.value)}
|
|
275
|
+
placeholder="50000"
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
<div className="space-y-2">
|
|
279
|
+
<Label htmlFor="usedCapacity">Used Capacity (sq ft)</Label>
|
|
280
|
+
<Input
|
|
281
|
+
id="usedCapacity"
|
|
282
|
+
type="number"
|
|
283
|
+
min="0"
|
|
284
|
+
value={form.usedCapacity}
|
|
285
|
+
onChange={(e) => update('usedCapacity', e.target.value)}
|
|
286
|
+
placeholder="0"
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
291
|
+
<div className="space-y-2">
|
|
292
|
+
<Label htmlFor="contactEmail">Contact Email</Label>
|
|
293
|
+
<Input
|
|
294
|
+
id="contactEmail"
|
|
295
|
+
type="email"
|
|
296
|
+
value={form.contactEmail}
|
|
297
|
+
onChange={(e) => update('contactEmail', e.target.value)}
|
|
298
|
+
placeholder="dc-sf@example.com"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="space-y-2">
|
|
302
|
+
<Label htmlFor="contactPhone">Contact Phone</Label>
|
|
303
|
+
<Input
|
|
304
|
+
id="contactPhone"
|
|
305
|
+
value={form.contactPhone}
|
|
306
|
+
onChange={(e) => update('contactPhone', e.target.value)}
|
|
307
|
+
placeholder="+1-555-0100"
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
209
310
|
</div>
|
|
210
311
|
</div>
|
|
211
312
|
</EmphasisPanel>
|
|
@@ -268,7 +369,56 @@ function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
|
268
369
|
</div>
|
|
269
370
|
</CardContent>
|
|
270
371
|
</Card>
|
|
372
|
+
|
|
373
|
+
{isEditing && (
|
|
374
|
+
<Card className="border-status-error-border">
|
|
375
|
+
<CardHeader>
|
|
376
|
+
<CardTitle className="flex items-center gap-2 text-status-error-text">
|
|
377
|
+
<Trash2 size={18} /> Delete Warehouse
|
|
378
|
+
</CardTitle>
|
|
379
|
+
</CardHeader>
|
|
380
|
+
<CardContent>
|
|
381
|
+
<p className="mb-3 text-sm text-text-secondary">
|
|
382
|
+
Removes this warehouse from your network. This cannot be undone from here.
|
|
383
|
+
</p>
|
|
384
|
+
<Button
|
|
385
|
+
type="button"
|
|
386
|
+
variant="outline"
|
|
387
|
+
className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
|
|
388
|
+
onClick={() => setDeleteOpen(true)}
|
|
389
|
+
>
|
|
390
|
+
<Trash2 size={16} /> Delete this warehouse
|
|
391
|
+
</Button>
|
|
392
|
+
</CardContent>
|
|
393
|
+
</Card>
|
|
394
|
+
)}
|
|
271
395
|
</form>
|
|
396
|
+
|
|
397
|
+
<ResponsiveDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
398
|
+
<ResponsiveDialogContent>
|
|
399
|
+
<ResponsiveDialogHeader>
|
|
400
|
+
<ResponsiveDialogTitle>Delete this warehouse?</ResponsiveDialogTitle>
|
|
401
|
+
<ResponsiveDialogDescription>
|
|
402
|
+
This removes {form.name || 'this warehouse'} and cannot be undone from here.
|
|
403
|
+
</ResponsiveDialogDescription>
|
|
404
|
+
</ResponsiveDialogHeader>
|
|
405
|
+
<div className="px-1">
|
|
406
|
+
<FormError message={deleteError} />
|
|
407
|
+
</div>
|
|
408
|
+
<ResponsiveDialogFooter>
|
|
409
|
+
<Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
|
|
410
|
+
Cancel
|
|
411
|
+
</Button>
|
|
412
|
+
<Button
|
|
413
|
+
className="bg-status-error-text text-text-inverse hover:opacity-90"
|
|
414
|
+
onClick={handleDelete}
|
|
415
|
+
disabled={deleting}
|
|
416
|
+
>
|
|
417
|
+
{deleting ? 'Deleting...' : 'Delete warehouse'}
|
|
418
|
+
</Button>
|
|
419
|
+
</ResponsiveDialogFooter>
|
|
420
|
+
</ResponsiveDialogContent>
|
|
421
|
+
</ResponsiveDialog>
|
|
272
422
|
</PageLayout>
|
|
273
423
|
);
|
|
274
424
|
}
|
|
@@ -277,17 +427,17 @@ export default function WarehouseDetailsPage() {
|
|
|
277
427
|
const { warehouseId } = useParams<{ warehouseId: string }>();
|
|
278
428
|
const { basePath, navigate } = useMoveTheWheels();
|
|
279
429
|
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
280
|
-
const { warehouse, isLoading, error, refetch } = useWarehouse(warehouseId);
|
|
430
|
+
const { warehouse, isLoading, error, refetch, reload } = useWarehouse(warehouseId);
|
|
281
431
|
|
|
282
432
|
const goBack = () => navigate?.(joinPath(basePath, 'inventory/warehouses'));
|
|
283
433
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
284
434
|
|
|
285
435
|
if (!warehouseId) return <NewWarehouseForm />;
|
|
286
|
-
if (isEditRoute) return <NewWarehouseForm editId={warehouseId} />;
|
|
436
|
+
if (isEditRoute) return <NewWarehouseForm editId={warehouseId} onSaved={reload} />;
|
|
287
437
|
if (isLoading) return <LoadingState message="Loading warehouse details..." />;
|
|
288
438
|
if (error || !warehouse) return <ErrorState title="Warehouse not found" onRetry={refetch} />;
|
|
289
439
|
|
|
290
|
-
const
|
|
440
|
+
const isActive = warehouse.status.toUpperCase() === 'ACTIVE';
|
|
291
441
|
|
|
292
442
|
return (
|
|
293
443
|
<PageLayout
|
|
@@ -310,8 +460,8 @@ export default function WarehouseDetailsPage() {
|
|
|
310
460
|
}
|
|
311
461
|
headerContent={
|
|
312
462
|
<div className="flex gap-2">
|
|
313
|
-
<Badge variant={
|
|
314
|
-
{
|
|
463
|
+
<Badge variant={isActive ? 'default' : 'secondary'}>
|
|
464
|
+
{isActive ? 'Active' : 'Inactive'}
|
|
315
465
|
</Badge>
|
|
316
466
|
<Badge variant="outline" className="capitalize">
|
|
317
467
|
{warehouse.type.replace('_', ' ')}
|
|
@@ -320,9 +470,9 @@ export default function WarehouseDetailsPage() {
|
|
|
320
470
|
}
|
|
321
471
|
>
|
|
322
472
|
<PagePurpose icon={<Warehouse size={18} />}>
|
|
323
|
-
A storage and fulfilment site at a glance — how full it is, its
|
|
324
|
-
|
|
325
|
-
|
|
473
|
+
A storage and fulfilment site at a glance — how full it is, its operating hours and how to
|
|
474
|
+
reach it. Use it to decide whether this warehouse has the space and hours to fulfil an
|
|
475
|
+
order.
|
|
326
476
|
</PagePurpose>
|
|
327
477
|
|
|
328
478
|
<NextSteps
|
|
@@ -352,65 +502,19 @@ export default function WarehouseDetailsPage() {
|
|
|
352
502
|
<h2 className="flex items-center gap-2 text-base font-semibold text-text-primary mb-4">
|
|
353
503
|
<Box size={18} /> Capacity
|
|
354
504
|
</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>
|
|
505
|
+
<div>
|
|
506
|
+
<div className="flex justify-between text-sm mb-2">
|
|
507
|
+
<span>Space Utilization</span>
|
|
508
|
+
<span>{warehouse.utilization.toFixed(0)}%</span>
|
|
378
509
|
</div>
|
|
510
|
+
<Progress value={warehouse.utilization} />
|
|
511
|
+
<p className="text-sm text-text-secondary mt-1">
|
|
512
|
+
{warehouse.capacity.usedSpace.toLocaleString()} /{' '}
|
|
513
|
+
{warehouse.capacity.totalSpace.toLocaleString()} sq ft
|
|
514
|
+
</p>
|
|
379
515
|
</div>
|
|
380
516
|
</EmphasisPanel>
|
|
381
517
|
|
|
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
518
|
<Card>
|
|
415
519
|
<CardHeader>
|
|
416
520
|
<CardTitle className="flex items-center gap-2">
|
|
@@ -418,16 +522,20 @@ export default function WarehouseDetailsPage() {
|
|
|
418
522
|
</CardTitle>
|
|
419
523
|
</CardHeader>
|
|
420
524
|
<CardContent>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
525
|
+
{Object.keys(warehouse.operatingHours).length > 0 ? (
|
|
526
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
527
|
+
{Object.entries(warehouse.operatingHours).map(([day, hours]) => (
|
|
528
|
+
<div key={day}>
|
|
529
|
+
<p className="text-sm font-medium">{day}</p>
|
|
530
|
+
<p className="text-sm text-text-secondary">
|
|
531
|
+
{hours ? `${hours.open} - ${hours.close}` : 'Closed'}
|
|
532
|
+
</p>
|
|
533
|
+
</div>
|
|
534
|
+
))}
|
|
535
|
+
</div>
|
|
536
|
+
) : (
|
|
537
|
+
<p className="text-sm text-text-secondary">Not recorded</p>
|
|
538
|
+
)}
|
|
431
539
|
</CardContent>
|
|
432
540
|
</Card>
|
|
433
541
|
</div>
|
|
@@ -440,60 +548,31 @@ export default function WarehouseDetailsPage() {
|
|
|
440
548
|
</CardTitle>
|
|
441
549
|
</CardHeader>
|
|
442
550
|
<CardContent>
|
|
443
|
-
<p className="text-sm">{formatAddress(warehouse.address)}</p>
|
|
551
|
+
<p className="text-sm">{formatAddress(warehouse.address) || '—'}</p>
|
|
444
552
|
</CardContent>
|
|
445
553
|
</Card>
|
|
446
554
|
|
|
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>
|
|
555
|
+
{(warehouse.contactInfo?.email || warehouse.contactInfo?.phone) && (
|
|
556
|
+
<Card>
|
|
557
|
+
<CardHeader>
|
|
558
|
+
<CardTitle>Contact</CardTitle>
|
|
559
|
+
</CardHeader>
|
|
560
|
+
<CardContent className="space-y-3">
|
|
561
|
+
{warehouse.contactInfo?.email && (
|
|
562
|
+
<div>
|
|
563
|
+
<p className="text-sm text-text-secondary">Email</p>
|
|
564
|
+
<p className="font-medium">{warehouse.contactInfo.email}</p>
|
|
565
|
+
</div>
|
|
566
|
+
)}
|
|
567
|
+
{warehouse.contactInfo?.phone && (
|
|
568
|
+
<div>
|
|
569
|
+
<p className="text-sm text-text-secondary">Phone</p>
|
|
570
|
+
<p className="font-medium">{warehouse.contactInfo.phone}</p>
|
|
571
|
+
</div>
|
|
572
|
+
)}
|
|
573
|
+
</CardContent>
|
|
574
|
+
</Card>
|
|
575
|
+
)}
|
|
497
576
|
</div>
|
|
498
577
|
</div>
|
|
499
578
|
</PageLayout>
|