@burdenoff/microfe-movethewheels 2026.925.1 → 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 +31 -31
- package/dist/constants/index.js +19 -5
- package/dist/hooks/useCustomers.js +0 -3
- package/dist/hooks/useFleet.js +197 -175
- package/dist/hooks/useInventory.js +144 -145
- package/dist/hooks/useRoutes.js +5 -14
- package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
- package/dist/movethewheels/constants/index.d.ts +2 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +53 -1
- package/dist/movethewheels/hooks/useInventory.d.ts +10 -0
- package/dist/movethewheels/types/index.d.ts +29 -108
- package/dist/node_modules/lucide-react/dist/esm/icons/sticky-note.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/user-x.js +30 -0
- package/dist/pages/CustomerDetailsPage.js +666 -344
- package/dist/pages/CustomersPage.js +71 -80
- package/dist/pages/DriverDetailsPage.js +702 -421
- package/dist/pages/DriversPage.js +106 -117
- package/dist/pages/FleetPage.js +5 -13
- package/dist/pages/InventoryPage.js +4 -4
- package/dist/pages/OrderDetailsPage.js +279 -274
- package/dist/pages/RouteDetailsPage.js +300 -263
- package/dist/pages/RoutesPage.js +130 -67
- package/dist/pages/VehicleDetailsPage.js +562 -287
- 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 +16 -2
- package/src/movethewheels/constants/index.ts +21 -0
- package/src/movethewheels/hooks/useCustomers.ts +0 -3
- package/src/movethewheels/hooks/useFleet.ts +236 -62
- 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/DriverDetailsPage.tsx +516 -156
- package/src/movethewheels/pages/DriversPage.tsx +28 -11
- package/src/movethewheels/pages/FleetPage.tsx +9 -11
- package/src/movethewheels/pages/InventoryPage.tsx +14 -5
- package/src/movethewheels/pages/OrderDetailsPage.tsx +12 -7
- package/src/movethewheels/pages/RouteDetailsPage.tsx +89 -41
- package/src/movethewheels/pages/RoutesPage.tsx +66 -2
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +282 -20
- 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 +30 -108
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Save,
|
|
18
18
|
Package,
|
|
19
19
|
X,
|
|
20
|
+
Trash2,
|
|
20
21
|
} from 'lucide-react';
|
|
21
22
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
22
23
|
import { TypeBadge, StatusBadge } from '../components/StatusBadge';
|
|
@@ -26,7 +27,6 @@ import {
|
|
|
26
27
|
CardHeader,
|
|
27
28
|
CardTitle,
|
|
28
29
|
Button,
|
|
29
|
-
Badge,
|
|
30
30
|
Input,
|
|
31
31
|
Label,
|
|
32
32
|
Select,
|
|
@@ -35,7 +35,17 @@ import {
|
|
|
35
35
|
SelectTrigger,
|
|
36
36
|
SelectValue,
|
|
37
37
|
} from '../components/ui';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
EmphasisPanel,
|
|
40
|
+
NextSteps,
|
|
41
|
+
PagePurpose,
|
|
42
|
+
ResponsiveDialog,
|
|
43
|
+
ResponsiveDialogContent,
|
|
44
|
+
ResponsiveDialogHeader,
|
|
45
|
+
ResponsiveDialogFooter,
|
|
46
|
+
ResponsiveDialogTitle,
|
|
47
|
+
ResponsiveDialogDescription,
|
|
48
|
+
} from '@burdenoff/fe-libs/ui';
|
|
39
49
|
import { useCustomer, useCustomers } from '../hooks/useCustomers';
|
|
40
50
|
import { useOrders } from '../hooks/useOrders';
|
|
41
51
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
@@ -44,34 +54,44 @@ import { joinPath, buildOrderCustomerPath, getSafeCustomerReturnPath } from '../
|
|
|
44
54
|
import { FormError } from '../components/FormError';
|
|
45
55
|
import { Notice, useNotice } from '../components/Notice';
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
interface CustomerFormState {
|
|
58
|
+
name: string;
|
|
59
|
+
type: 'b2b' | 'b2c';
|
|
60
|
+
email: string;
|
|
61
|
+
phone: string;
|
|
62
|
+
company: string;
|
|
63
|
+
street: string;
|
|
64
|
+
city: string;
|
|
65
|
+
state: string;
|
|
66
|
+
postalCode: string;
|
|
67
|
+
country: string;
|
|
68
|
+
businessLicense: string;
|
|
69
|
+
taxId: string;
|
|
70
|
+
rating: string;
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
const emptyCustomerForm: CustomerFormState = {
|
|
74
|
+
name: '',
|
|
75
|
+
type: 'b2c',
|
|
76
|
+
email: '',
|
|
77
|
+
phone: '',
|
|
78
|
+
company: '',
|
|
79
|
+
street: '',
|
|
80
|
+
city: '',
|
|
81
|
+
state: '',
|
|
82
|
+
postalCode: '',
|
|
83
|
+
country: '',
|
|
84
|
+
businessLicense: '',
|
|
85
|
+
taxId: '',
|
|
86
|
+
rating: '',
|
|
87
|
+
};
|
|
88
|
+
|
|
63
89
|
function NewCustomerForm() {
|
|
64
90
|
const { basePath, navigate } = useMoveTheWheels();
|
|
65
91
|
const [searchParams] = useSearchParams();
|
|
66
92
|
const returnTo = getSafeCustomerReturnPath(searchParams.get('returnTo'));
|
|
67
93
|
const { createCustomer } = useCustomers();
|
|
68
|
-
const [form, setForm] = useState(
|
|
69
|
-
name: '',
|
|
70
|
-
type: 'b2c' as 'b2b' | 'b2c',
|
|
71
|
-
email: '',
|
|
72
|
-
phone: '',
|
|
73
|
-
company: '',
|
|
74
|
-
});
|
|
94
|
+
const [form, setForm] = useState<CustomerFormState>(emptyCustomerForm);
|
|
75
95
|
const [submitting, setSubmitting] = useState(false);
|
|
76
96
|
const [error, setError] = useState<string | null>(null);
|
|
77
97
|
|
|
@@ -92,6 +112,19 @@ function NewCustomerForm() {
|
|
|
92
112
|
email: form.email.trim() || undefined,
|
|
93
113
|
phone: form.phone.trim() || undefined,
|
|
94
114
|
company: form.company.trim() || undefined,
|
|
115
|
+
address:
|
|
116
|
+
form.street || form.city || form.state || form.postalCode || form.country
|
|
117
|
+
? {
|
|
118
|
+
street: form.street.trim(),
|
|
119
|
+
city: form.city.trim(),
|
|
120
|
+
state: form.state.trim(),
|
|
121
|
+
postalCode: form.postalCode.trim(),
|
|
122
|
+
country: form.country.trim(),
|
|
123
|
+
}
|
|
124
|
+
: undefined,
|
|
125
|
+
businessLicense: form.businessLicense.trim() || undefined,
|
|
126
|
+
taxId: form.taxId.trim() || undefined,
|
|
127
|
+
rating: form.rating ? Number(form.rating) : undefined,
|
|
95
128
|
});
|
|
96
129
|
const destination = returnTo
|
|
97
130
|
? buildOrderCustomerPath(returnTo, created.id)
|
|
@@ -194,37 +227,153 @@ function NewCustomerForm() {
|
|
|
194
227
|
</div>
|
|
195
228
|
</div>
|
|
196
229
|
</EmphasisPanel>
|
|
230
|
+
|
|
231
|
+
<Card>
|
|
232
|
+
<CardHeader>
|
|
233
|
+
<CardTitle className="flex items-center gap-2">
|
|
234
|
+
<MapPin size={18} /> Address
|
|
235
|
+
</CardTitle>
|
|
236
|
+
</CardHeader>
|
|
237
|
+
<CardContent className="space-y-4">
|
|
238
|
+
<div className="space-y-2">
|
|
239
|
+
<Label htmlFor="street">Street</Label>
|
|
240
|
+
<Input
|
|
241
|
+
id="street"
|
|
242
|
+
value={form.street}
|
|
243
|
+
onChange={(e) => setForm((prev) => ({ ...prev, street: e.target.value }))}
|
|
244
|
+
placeholder="123 Main St"
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
248
|
+
<div className="space-y-2">
|
|
249
|
+
<Label htmlFor="city">City</Label>
|
|
250
|
+
<Input
|
|
251
|
+
id="city"
|
|
252
|
+
value={form.city}
|
|
253
|
+
onChange={(e) => setForm((prev) => ({ ...prev, city: e.target.value }))}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
<div className="space-y-2">
|
|
257
|
+
<Label htmlFor="state">State</Label>
|
|
258
|
+
<Input
|
|
259
|
+
id="state"
|
|
260
|
+
value={form.state}
|
|
261
|
+
onChange={(e) => setForm((prev) => ({ ...prev, state: e.target.value }))}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
<div className="space-y-2">
|
|
265
|
+
<Label htmlFor="postalCode">Postal Code</Label>
|
|
266
|
+
<Input
|
|
267
|
+
id="postalCode"
|
|
268
|
+
value={form.postalCode}
|
|
269
|
+
onChange={(e) => setForm((prev) => ({ ...prev, postalCode: e.target.value }))}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
<div className="space-y-2">
|
|
273
|
+
<Label htmlFor="country">Country</Label>
|
|
274
|
+
<Input
|
|
275
|
+
id="country"
|
|
276
|
+
value={form.country}
|
|
277
|
+
onChange={(e) => setForm((prev) => ({ ...prev, country: e.target.value }))}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</CardContent>
|
|
282
|
+
</Card>
|
|
283
|
+
|
|
284
|
+
<Card>
|
|
285
|
+
<CardHeader>
|
|
286
|
+
<CardTitle className="flex items-center gap-2">
|
|
287
|
+
<FileText size={18} /> Business Details
|
|
288
|
+
</CardTitle>
|
|
289
|
+
</CardHeader>
|
|
290
|
+
<CardContent>
|
|
291
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
292
|
+
<div className="space-y-2">
|
|
293
|
+
<Label htmlFor="businessLicense">Business License</Label>
|
|
294
|
+
<Input
|
|
295
|
+
id="businessLicense"
|
|
296
|
+
value={form.businessLicense}
|
|
297
|
+
onChange={(e) =>
|
|
298
|
+
setForm((prev) => ({ ...prev, businessLicense: e.target.value }))
|
|
299
|
+
}
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="space-y-2">
|
|
303
|
+
<Label htmlFor="taxId">Tax ID</Label>
|
|
304
|
+
<Input
|
|
305
|
+
id="taxId"
|
|
306
|
+
value={form.taxId}
|
|
307
|
+
onChange={(e) => setForm((prev) => ({ ...prev, taxId: e.target.value }))}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
<div className="space-y-2">
|
|
311
|
+
<Label htmlFor="rating">Rating (0–5)</Label>
|
|
312
|
+
<Input
|
|
313
|
+
id="rating"
|
|
314
|
+
type="number"
|
|
315
|
+
min={0}
|
|
316
|
+
max={5}
|
|
317
|
+
step={0.1}
|
|
318
|
+
value={form.rating}
|
|
319
|
+
onChange={(e) => setForm((prev) => ({ ...prev, rating: e.target.value }))}
|
|
320
|
+
/>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</CardContent>
|
|
324
|
+
</Card>
|
|
197
325
|
</form>
|
|
198
326
|
</PageLayout>
|
|
199
327
|
);
|
|
200
328
|
}
|
|
201
329
|
|
|
330
|
+
function customerToForm(customer: {
|
|
331
|
+
name: string;
|
|
332
|
+
email: string;
|
|
333
|
+
phone: string;
|
|
334
|
+
company?: string;
|
|
335
|
+
type: 'b2b' | 'b2c';
|
|
336
|
+
address: { street: string; city: string; state: string; postalCode: string; country: string };
|
|
337
|
+
businessLicense?: string;
|
|
338
|
+
taxId?: string;
|
|
339
|
+
rating: number;
|
|
340
|
+
}): CustomerFormState {
|
|
341
|
+
return {
|
|
342
|
+
name: customer.name,
|
|
343
|
+
email: customer.email,
|
|
344
|
+
phone: customer.phone,
|
|
345
|
+
company: customer.company ?? '',
|
|
346
|
+
type: customer.type,
|
|
347
|
+
street: customer.address.street,
|
|
348
|
+
city: customer.address.city,
|
|
349
|
+
state: customer.address.state,
|
|
350
|
+
postalCode: customer.address.postalCode,
|
|
351
|
+
country: customer.address.country,
|
|
352
|
+
businessLicense: customer.businessLicense ?? '',
|
|
353
|
+
taxId: customer.taxId ?? '',
|
|
354
|
+
rating: customer.rating ? String(customer.rating) : '',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
202
358
|
export default function CustomerDetailsPage() {
|
|
203
359
|
const { customerId } = useParams<{ customerId: string }>();
|
|
204
360
|
const { basePath, navigate } = useMoveTheWheels();
|
|
205
361
|
const { customer, isLoading, error, refetch } = useCustomer(customerId);
|
|
206
|
-
const { updateCustomer } = useCustomers();
|
|
362
|
+
const { updateCustomer, deleteCustomer } = useCustomers();
|
|
207
363
|
const { orders, isLoading: ordersLoading } = useOrders({ filters: { customerId } });
|
|
208
364
|
const { message: notice, notify } = useNotice();
|
|
209
365
|
|
|
210
366
|
const [editing, setEditing] = useState(false);
|
|
211
367
|
const [saving, setSaving] = useState(false);
|
|
212
368
|
const [editError, setEditError] = useState<string | null>(null);
|
|
213
|
-
const [form, setForm] = useState(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
company: '',
|
|
218
|
-
});
|
|
369
|
+
const [form, setForm] = useState<CustomerFormState>(emptyCustomerForm);
|
|
370
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
371
|
+
const [deleting, setDeleting] = useState(false);
|
|
372
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
219
373
|
|
|
220
374
|
useEffect(() => {
|
|
221
375
|
if (customer) {
|
|
222
|
-
setForm(
|
|
223
|
-
name: customer.name,
|
|
224
|
-
email: customer.email,
|
|
225
|
-
phone: customer.phone,
|
|
226
|
-
company: customer.company ?? '',
|
|
227
|
-
});
|
|
376
|
+
setForm(customerToForm(customer));
|
|
228
377
|
}
|
|
229
378
|
}, [customer]);
|
|
230
379
|
|
|
@@ -240,12 +389,7 @@ export default function CustomerDetailsPage() {
|
|
|
240
389
|
setEditError(null);
|
|
241
390
|
setEditing(false);
|
|
242
391
|
if (customer) {
|
|
243
|
-
setForm(
|
|
244
|
-
name: customer.name,
|
|
245
|
-
email: customer.email,
|
|
246
|
-
phone: customer.phone,
|
|
247
|
-
company: customer.company ?? '',
|
|
248
|
-
});
|
|
392
|
+
setForm(customerToForm(customer));
|
|
249
393
|
}
|
|
250
394
|
};
|
|
251
395
|
|
|
@@ -263,6 +407,17 @@ export default function CustomerDetailsPage() {
|
|
|
263
407
|
email: form.email.trim(),
|
|
264
408
|
phone: form.phone.trim(),
|
|
265
409
|
company: form.company.trim() || undefined,
|
|
410
|
+
type: form.type,
|
|
411
|
+
address: {
|
|
412
|
+
street: form.street.trim(),
|
|
413
|
+
city: form.city.trim(),
|
|
414
|
+
state: form.state.trim(),
|
|
415
|
+
postalCode: form.postalCode.trim(),
|
|
416
|
+
country: form.country.trim(),
|
|
417
|
+
},
|
|
418
|
+
businessLicense: form.businessLicense.trim() || undefined,
|
|
419
|
+
taxId: form.taxId.trim() || undefined,
|
|
420
|
+
rating: form.rating ? Number(form.rating) : undefined,
|
|
266
421
|
});
|
|
267
422
|
notify('Customer updated.');
|
|
268
423
|
setEditing(false);
|
|
@@ -274,6 +429,20 @@ export default function CustomerDetailsPage() {
|
|
|
274
429
|
}
|
|
275
430
|
};
|
|
276
431
|
|
|
432
|
+
const handleDelete = async () => {
|
|
433
|
+
if (!customerId) return;
|
|
434
|
+
setDeleting(true);
|
|
435
|
+
setDeleteError(null);
|
|
436
|
+
try {
|
|
437
|
+
await deleteCustomer(customerId);
|
|
438
|
+
setDeleteOpen(false);
|
|
439
|
+
goBack();
|
|
440
|
+
} catch (err) {
|
|
441
|
+
setDeleteError(err instanceof Error ? err.message : 'Failed to delete customer.');
|
|
442
|
+
setDeleting(false);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
277
446
|
// Handle new customer form
|
|
278
447
|
if (!customerId) {
|
|
279
448
|
return <NewCustomerForm />;
|
|
@@ -311,13 +480,12 @@ export default function CustomerDetailsPage() {
|
|
|
311
480
|
headerContent={
|
|
312
481
|
<div className="flex items-center gap-4">
|
|
313
482
|
<TypeBadge type={customer.type} />
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
</div>
|
|
483
|
+
{customer.rating > 0 && (
|
|
484
|
+
<div className="flex items-center gap-1">
|
|
485
|
+
<Star size={16} className="text-status-warning-text fill-current" />
|
|
486
|
+
<span className="font-medium">{customer.rating.toFixed(1)}</span>
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
321
489
|
</div>
|
|
322
490
|
}
|
|
323
491
|
>
|
|
@@ -398,6 +566,95 @@ export default function CustomerDetailsPage() {
|
|
|
398
566
|
onChange={(e) => setForm((prev) => ({ ...prev, phone: e.target.value }))}
|
|
399
567
|
/>
|
|
400
568
|
</div>
|
|
569
|
+
<div className="space-y-2">
|
|
570
|
+
<Label htmlFor="edit-type">Customer Type</Label>
|
|
571
|
+
<Select
|
|
572
|
+
value={form.type}
|
|
573
|
+
onValueChange={(value) =>
|
|
574
|
+
setForm((prev) => ({ ...prev, type: value as 'b2b' | 'b2c' }))
|
|
575
|
+
}
|
|
576
|
+
>
|
|
577
|
+
<SelectTrigger id="edit-type">
|
|
578
|
+
<SelectValue />
|
|
579
|
+
</SelectTrigger>
|
|
580
|
+
<SelectContent>
|
|
581
|
+
<SelectItem value="b2c">B2C (Individual)</SelectItem>
|
|
582
|
+
<SelectItem value="b2b">B2B (Business)</SelectItem>
|
|
583
|
+
</SelectContent>
|
|
584
|
+
</Select>
|
|
585
|
+
</div>
|
|
586
|
+
<div className="space-y-2">
|
|
587
|
+
<Label htmlFor="edit-rating">Rating (0–5)</Label>
|
|
588
|
+
<Input
|
|
589
|
+
id="edit-rating"
|
|
590
|
+
type="number"
|
|
591
|
+
min={0}
|
|
592
|
+
max={5}
|
|
593
|
+
step={0.1}
|
|
594
|
+
value={form.rating}
|
|
595
|
+
onChange={(e) => setForm((prev) => ({ ...prev, rating: e.target.value }))}
|
|
596
|
+
/>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
600
|
+
<div className="space-y-2">
|
|
601
|
+
<Label htmlFor="edit-street">Street</Label>
|
|
602
|
+
<Input
|
|
603
|
+
id="edit-street"
|
|
604
|
+
value={form.street}
|
|
605
|
+
onChange={(e) => setForm((prev) => ({ ...prev, street: e.target.value }))}
|
|
606
|
+
/>
|
|
607
|
+
</div>
|
|
608
|
+
<div className="space-y-2">
|
|
609
|
+
<Label htmlFor="edit-city">City</Label>
|
|
610
|
+
<Input
|
|
611
|
+
id="edit-city"
|
|
612
|
+
value={form.city}
|
|
613
|
+
onChange={(e) => setForm((prev) => ({ ...prev, city: e.target.value }))}
|
|
614
|
+
/>
|
|
615
|
+
</div>
|
|
616
|
+
<div className="space-y-2">
|
|
617
|
+
<Label htmlFor="edit-state">State</Label>
|
|
618
|
+
<Input
|
|
619
|
+
id="edit-state"
|
|
620
|
+
value={form.state}
|
|
621
|
+
onChange={(e) => setForm((prev) => ({ ...prev, state: e.target.value }))}
|
|
622
|
+
/>
|
|
623
|
+
</div>
|
|
624
|
+
<div className="space-y-2">
|
|
625
|
+
<Label htmlFor="edit-postalCode">Postal Code</Label>
|
|
626
|
+
<Input
|
|
627
|
+
id="edit-postalCode"
|
|
628
|
+
value={form.postalCode}
|
|
629
|
+
onChange={(e) => setForm((prev) => ({ ...prev, postalCode: e.target.value }))}
|
|
630
|
+
/>
|
|
631
|
+
</div>
|
|
632
|
+
<div className="space-y-2">
|
|
633
|
+
<Label htmlFor="edit-country">Country</Label>
|
|
634
|
+
<Input
|
|
635
|
+
id="edit-country"
|
|
636
|
+
value={form.country}
|
|
637
|
+
onChange={(e) => setForm((prev) => ({ ...prev, country: e.target.value }))}
|
|
638
|
+
/>
|
|
639
|
+
</div>
|
|
640
|
+
<div className="space-y-2">
|
|
641
|
+
<Label htmlFor="edit-businessLicense">Business License</Label>
|
|
642
|
+
<Input
|
|
643
|
+
id="edit-businessLicense"
|
|
644
|
+
value={form.businessLicense}
|
|
645
|
+
onChange={(e) =>
|
|
646
|
+
setForm((prev) => ({ ...prev, businessLicense: e.target.value }))
|
|
647
|
+
}
|
|
648
|
+
/>
|
|
649
|
+
</div>
|
|
650
|
+
<div className="space-y-2">
|
|
651
|
+
<Label htmlFor="edit-taxId">Tax ID</Label>
|
|
652
|
+
<Input
|
|
653
|
+
id="edit-taxId"
|
|
654
|
+
value={form.taxId}
|
|
655
|
+
onChange={(e) => setForm((prev) => ({ ...prev, taxId: e.target.value }))}
|
|
656
|
+
/>
|
|
657
|
+
</div>
|
|
401
658
|
</div>
|
|
402
659
|
</div>
|
|
403
660
|
) : (
|
|
@@ -428,23 +685,6 @@ export default function CustomerDetailsPage() {
|
|
|
428
685
|
</div>
|
|
429
686
|
</div>
|
|
430
687
|
)}
|
|
431
|
-
{(() => {
|
|
432
|
-
const website = safeHttpUrl(customer.website);
|
|
433
|
-
if (!website) return null;
|
|
434
|
-
return (
|
|
435
|
-
<div>
|
|
436
|
-
<p className="text-sm text-text-secondary">Website</p>
|
|
437
|
-
<a
|
|
438
|
-
href={website}
|
|
439
|
-
target="_blank"
|
|
440
|
-
rel="noopener noreferrer"
|
|
441
|
-
className="text-primary hover:underline"
|
|
442
|
-
>
|
|
443
|
-
{website}
|
|
444
|
-
</a>
|
|
445
|
-
</div>
|
|
446
|
-
);
|
|
447
|
-
})()}
|
|
448
688
|
</div>
|
|
449
689
|
</div>
|
|
450
690
|
)}
|
|
@@ -457,23 +697,10 @@ export default function CustomerDetailsPage() {
|
|
|
457
697
|
</CardTitle>
|
|
458
698
|
</CardHeader>
|
|
459
699
|
<CardContent>
|
|
460
|
-
<p>{formatAddress(customer.address)}</p>
|
|
700
|
+
<p>{formatAddress(customer.address) || '—'}</p>
|
|
461
701
|
</CardContent>
|
|
462
702
|
</Card>
|
|
463
703
|
|
|
464
|
-
{customer.notes && (
|
|
465
|
-
<Card>
|
|
466
|
-
<CardHeader>
|
|
467
|
-
<CardTitle className="flex items-center gap-2">
|
|
468
|
-
<FileText size={18} /> Notes
|
|
469
|
-
</CardTitle>
|
|
470
|
-
</CardHeader>
|
|
471
|
-
<CardContent>
|
|
472
|
-
<p>{customer.notes}</p>
|
|
473
|
-
</CardContent>
|
|
474
|
-
</Card>
|
|
475
|
-
)}
|
|
476
|
-
|
|
477
704
|
{/* Orders for this customer */}
|
|
478
705
|
<Card>
|
|
479
706
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
@@ -529,35 +756,27 @@ export default function CustomerDetailsPage() {
|
|
|
529
756
|
</div>
|
|
530
757
|
|
|
531
758
|
<div className="space-y-6">
|
|
532
|
-
|
|
533
|
-
<
|
|
534
|
-
<
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
<
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
<
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
<div>
|
|
554
|
-
<p className="text-sm text-text-secondary">Marketing Opt-in</p>
|
|
555
|
-
<Badge variant={customer.marketingOptIn ? 'default' : 'secondary'}>
|
|
556
|
-
{customer.marketingOptIn ? 'Yes' : 'No'}
|
|
557
|
-
</Badge>
|
|
558
|
-
</div>
|
|
559
|
-
</CardContent>
|
|
560
|
-
</Card>
|
|
759
|
+
{(customer.businessLicense || customer.taxId) && (
|
|
760
|
+
<Card>
|
|
761
|
+
<CardHeader>
|
|
762
|
+
<CardTitle>Business Details</CardTitle>
|
|
763
|
+
</CardHeader>
|
|
764
|
+
<CardContent className="space-y-3">
|
|
765
|
+
{customer.businessLicense && (
|
|
766
|
+
<div>
|
|
767
|
+
<p className="text-sm text-text-secondary">Business License</p>
|
|
768
|
+
<p className="font-mono">{customer.businessLicense}</p>
|
|
769
|
+
</div>
|
|
770
|
+
)}
|
|
771
|
+
{customer.taxId && (
|
|
772
|
+
<div>
|
|
773
|
+
<p className="text-sm text-text-secondary">Tax ID</p>
|
|
774
|
+
<p className="font-mono">{customer.taxId}</p>
|
|
775
|
+
</div>
|
|
776
|
+
)}
|
|
777
|
+
</CardContent>
|
|
778
|
+
</Card>
|
|
779
|
+
)}
|
|
561
780
|
|
|
562
781
|
<Card>
|
|
563
782
|
<CardHeader>
|
|
@@ -574,8 +793,56 @@ export default function CustomerDetailsPage() {
|
|
|
574
793
|
</div>
|
|
575
794
|
</CardContent>
|
|
576
795
|
</Card>
|
|
796
|
+
|
|
797
|
+
<Card className="border-status-error-border">
|
|
798
|
+
<CardHeader>
|
|
799
|
+
<CardTitle className="flex items-center gap-2 text-status-error-text">
|
|
800
|
+
<Trash2 size={18} /> Delete Customer
|
|
801
|
+
</CardTitle>
|
|
802
|
+
</CardHeader>
|
|
803
|
+
<CardContent>
|
|
804
|
+
<p className="mb-3 text-sm text-text-secondary">
|
|
805
|
+
Removes this customer from your account. Their past orders stay on record. This
|
|
806
|
+
cannot be undone from here.
|
|
807
|
+
</p>
|
|
808
|
+
<Button
|
|
809
|
+
type="button"
|
|
810
|
+
variant="outline"
|
|
811
|
+
className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
|
|
812
|
+
onClick={() => setDeleteOpen(true)}
|
|
813
|
+
>
|
|
814
|
+
<Trash2 size={16} /> Delete this customer
|
|
815
|
+
</Button>
|
|
816
|
+
</CardContent>
|
|
817
|
+
</Card>
|
|
577
818
|
</div>
|
|
578
819
|
</div>
|
|
820
|
+
|
|
821
|
+
<ResponsiveDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
822
|
+
<ResponsiveDialogContent>
|
|
823
|
+
<ResponsiveDialogHeader>
|
|
824
|
+
<ResponsiveDialogTitle>Delete this customer?</ResponsiveDialogTitle>
|
|
825
|
+
<ResponsiveDialogDescription>
|
|
826
|
+
This removes {customer.name} from your account. It cannot be undone from here.
|
|
827
|
+
</ResponsiveDialogDescription>
|
|
828
|
+
</ResponsiveDialogHeader>
|
|
829
|
+
<div className="px-1">
|
|
830
|
+
<FormError message={deleteError} />
|
|
831
|
+
</div>
|
|
832
|
+
<ResponsiveDialogFooter>
|
|
833
|
+
<Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
|
|
834
|
+
Cancel
|
|
835
|
+
</Button>
|
|
836
|
+
<Button
|
|
837
|
+
className="bg-status-error-text text-text-inverse hover:opacity-90"
|
|
838
|
+
onClick={handleDelete}
|
|
839
|
+
disabled={deleting}
|
|
840
|
+
>
|
|
841
|
+
{deleting ? 'Deleting...' : 'Delete customer'}
|
|
842
|
+
</Button>
|
|
843
|
+
</ResponsiveDialogFooter>
|
|
844
|
+
</ResponsiveDialogContent>
|
|
845
|
+
</ResponsiveDialog>
|
|
579
846
|
</PageLayout>
|
|
580
847
|
);
|
|
581
848
|
}
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useState } from 'react';
|
|
6
|
-
import { Users, Plus, RefreshCw, Download, Star,
|
|
6
|
+
import { Users, Plus, RefreshCw, Download, Star, Building2 } 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
10
|
import { TypeBadge } from '../components/StatusBadge';
|
|
11
|
-
import {
|
|
11
|
+
import { Avatar, AvatarFallback, Button } from '../components/ui';
|
|
12
12
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
13
13
|
import { useCustomers, useCustomerStats } from '../hooks/useCustomers';
|
|
14
14
|
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
@@ -77,15 +77,6 @@ export default function CustomersPage() {
|
|
|
77
77
|
</div>
|
|
78
78
|
),
|
|
79
79
|
},
|
|
80
|
-
{
|
|
81
|
-
id: 'status',
|
|
82
|
-
header: 'Status',
|
|
83
|
-
cell: (row) => (
|
|
84
|
-
<Badge variant={row.isActive ? 'default' : 'secondary'}>
|
|
85
|
-
{row.isActive ? 'Active' : 'Inactive'}
|
|
86
|
-
</Badge>
|
|
87
|
-
),
|
|
88
|
-
},
|
|
89
80
|
{
|
|
90
81
|
id: 'created',
|
|
91
82
|
header: 'Created',
|
|
@@ -135,11 +126,11 @@ export default function CustomersPage() {
|
|
|
135
126
|
accent="brand"
|
|
136
127
|
/>
|
|
137
128
|
<MetricCard
|
|
138
|
-
label="
|
|
139
|
-
value={stats.
|
|
140
|
-
icon={<
|
|
129
|
+
label="Avg. rating"
|
|
130
|
+
value={stats.avgRating > 0 ? stats.avgRating.toFixed(1) : '—'}
|
|
131
|
+
icon={<Star size={20} />}
|
|
141
132
|
accent="success"
|
|
142
|
-
subtitle="
|
|
133
|
+
subtitle="Across all customers"
|
|
143
134
|
/>
|
|
144
135
|
<MetricCard
|
|
145
136
|
label="Business accounts"
|