@burdenoff/microfe-movethewheels 2026.822.1 → 2026.827.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/generated/wspace-operations.d.ts +168 -912
- package/dist/generated/wspace-types.d.ts +78432 -35831
- package/dist/hooks/useFleet.js +142 -74
- package/dist/hooks/useInventory.js +163 -70
- package/dist/hooks/useOrders.js +2 -2
- package/dist/hooks/useTracking.js +5 -5
- package/dist/index.js +33 -33
- package/dist/movethewheels/hooks/index.d.ts +2 -2
- package/dist/movethewheels/hooks/useFleet.d.ts +32 -1
- package/dist/movethewheels/hooks/useInventory.d.ts +67 -12
- package/dist/movethewheels/hooks/useOrders.d.ts +25 -2
- package/dist/movethewheels/types/index.d.ts +2 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/arrow-down-right.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/arrow-left-right.js +22 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/arrow-up-right.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/calendar-clock.js +32 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/history.js +18 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/lock.js +16 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/user-round.js +13 -0
- package/dist/pages/CreateOrderPage.js +246 -195
- package/dist/pages/DeliveryCapturePage.js +91 -78
- package/dist/pages/DriversPage.js +202 -108
- package/dist/pages/InventoryPage.js +356 -131
- package/dist/pages/ProductDetailsPage.js +212 -156
- package/dist/pages/VehicleDetailsPage.js +257 -193
- package/dist/utils/formatters.js +4 -1
- package/package.json +1 -1
- package/src/generated/wspace-operations.ts +134 -929
- package/src/generated/wspace-types.ts +91730 -40059
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useFleet.ts +133 -3
- package/src/movethewheels/hooks/useInventory.ts +311 -53
- package/src/movethewheels/hooks/useOrders.ts +31 -7
- package/src/movethewheels/hooks/useTracking.ts +14 -14
- package/src/movethewheels/pages/CreateOrderPage.tsx +67 -10
- package/src/movethewheels/pages/DeliveryCapturePage.tsx +8 -0
- package/src/movethewheels/pages/DriversPage.tsx +181 -31
- package/src/movethewheels/pages/InventoryPage.tsx +299 -19
- package/src/movethewheels/pages/ProductDetailsPage.tsx +68 -3
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +86 -27
- package/src/movethewheels/types/index.ts +2 -0
- package/src/operations/wspace/movethewheels/fragments/core.graphql +23 -53
- package/src/operations/wspace/movethewheels/fragments/core.withdrawn.graphql +59 -0
- package/src/operations/wspace/movethewheels/mutations/entities.graphql +5 -5
- package/src/operations/wspace/movethewheels/mutations/marketplace-reports.graphql +18 -78
- package/src/operations/wspace/movethewheels/mutations/marketplace-reports.withdrawn.graphql +93 -0
- package/src/operations/wspace/movethewheels/mutations/orders.graphql +7 -7
- package/src/operations/wspace/movethewheels/queries/dashboard.graphql +3 -3
- package/src/operations/wspace/movethewheels/queries/entities.graphql +3 -3
- package/src/operations/wspace/movethewheels/queries/logistics.graphql +3 -3
- package/src/operations/wspace/movethewheels/queries/marketplace-reports.withdrawn.graphql +58 -0
- package/src/operations/wspace/movethewheels/queries/orders.graphql +3 -3
- package/src/operations/wspace/movethewheels/queries/marketplace-reports.graphql +0 -40
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { useEffect, useState } from 'react';
|
|
8
8
|
import { useParams } from 'react-router';
|
|
9
|
-
import { Package, ArrowLeft, Save, Plus, Trash2, Users, MapPin } from 'lucide-react';
|
|
9
|
+
import { Package, ArrowLeft, Save, Plus, Trash2, Users, MapPin, CalendarClock } from 'lucide-react';
|
|
10
10
|
import { PageLayout, EmptyState } from '../components/PageLayout';
|
|
11
11
|
import { FormError } from '../components/FormError';
|
|
12
12
|
import {
|
|
@@ -31,6 +31,22 @@ import { useCustomers } from '../hooks/useCustomers';
|
|
|
31
31
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
32
32
|
import { joinPath } from '../utils/navigation';
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* `<input type="datetime-local">` needs a `YYYY-MM-DDTHH:mm` string in LOCAL
|
|
36
|
+
* time — `Date.toISOString()` would shift the value by the UTC offset and show
|
|
37
|
+
* the operator a different slot than the one stored. Returns '' for no date.
|
|
38
|
+
*/
|
|
39
|
+
function toDateTimeLocalValue(date: Date | undefined): string {
|
|
40
|
+
if (!date || Number.isNaN(date.getTime())) {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
const pad = (value: number) => String(value).padStart(2, '0');
|
|
44
|
+
return (
|
|
45
|
+
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
|
|
46
|
+
`T${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
export default function CreateOrderPage() {
|
|
35
51
|
const { orderId } = useParams<{ orderId: string }>();
|
|
36
52
|
const { basePath, navigate } = useMoveTheWheels();
|
|
@@ -58,6 +74,7 @@ export default function CreateOrderPage() {
|
|
|
58
74
|
country: order?.deliveryAddress.country || 'USA',
|
|
59
75
|
},
|
|
60
76
|
items: order?.items || [],
|
|
77
|
+
scheduledDelivery: toDateTimeLocalValue(order?.scheduledDelivery),
|
|
61
78
|
specialInstructions: order?.specialInstructions || '',
|
|
62
79
|
});
|
|
63
80
|
|
|
@@ -89,6 +106,7 @@ export default function CreateOrderPage() {
|
|
|
89
106
|
country: order.deliveryAddress.country || 'USA',
|
|
90
107
|
},
|
|
91
108
|
items: order.items || [],
|
|
109
|
+
scheduledDelivery: toDateTimeLocalValue(order.scheduledDelivery),
|
|
92
110
|
specialInstructions: order.specialInstructions || '',
|
|
93
111
|
});
|
|
94
112
|
}, [order]);
|
|
@@ -113,6 +131,9 @@ export default function CreateOrderPage() {
|
|
|
113
131
|
if (formData.items.length === 0) {
|
|
114
132
|
return 'Add at least one item to the order.';
|
|
115
133
|
}
|
|
134
|
+
if (formData.scheduledDelivery && Number.isNaN(Date.parse(formData.scheduledDelivery))) {
|
|
135
|
+
return 'Enter a valid scheduled delivery date and time.';
|
|
136
|
+
}
|
|
116
137
|
return null;
|
|
117
138
|
};
|
|
118
139
|
|
|
@@ -146,6 +167,8 @@ export default function CreateOrderPage() {
|
|
|
146
167
|
pickupAddress: formData.pickupAddress,
|
|
147
168
|
deliveryAddress: formData.deliveryAddress,
|
|
148
169
|
items: formData.items,
|
|
170
|
+
// `null` clears an existing slot on edit; the service field is nullable.
|
|
171
|
+
scheduledDelivery: formData.scheduledDelivery ? new Date(formData.scheduledDelivery) : null,
|
|
149
172
|
specialInstructions: formData.specialInstructions,
|
|
150
173
|
};
|
|
151
174
|
|
|
@@ -466,7 +489,7 @@ export default function CreateOrderPage() {
|
|
|
466
489
|
{formData.items.map((item, index) => (
|
|
467
490
|
<div key={item.id}>
|
|
468
491
|
{index > 0 && <Separator className="my-4" />}
|
|
469
|
-
<div className="grid grid-cols-1 md:grid-cols-
|
|
492
|
+
<div className="grid grid-cols-1 md:grid-cols-7 gap-4">
|
|
470
493
|
<div className="md:col-span-2 space-y-2">
|
|
471
494
|
<Label>Item Name</Label>
|
|
472
495
|
<Input
|
|
@@ -494,6 +517,18 @@ export default function CreateOrderPage() {
|
|
|
494
517
|
}
|
|
495
518
|
/>
|
|
496
519
|
</div>
|
|
520
|
+
<div className="space-y-2">
|
|
521
|
+
<Label>Weight (kg)</Label>
|
|
522
|
+
<Input
|
|
523
|
+
type="number"
|
|
524
|
+
min="0"
|
|
525
|
+
step="0.1"
|
|
526
|
+
value={item.weight}
|
|
527
|
+
onChange={(e) =>
|
|
528
|
+
updateItem(index, 'weight', parseFloat(e.target.value) || 0)
|
|
529
|
+
}
|
|
530
|
+
/>
|
|
531
|
+
</div>
|
|
497
532
|
<div className="space-y-2">
|
|
498
533
|
<Label>Value ($)</Label>
|
|
499
534
|
<Input
|
|
@@ -525,18 +560,40 @@ export default function CreateOrderPage() {
|
|
|
525
560
|
</CardContent>
|
|
526
561
|
</Card>
|
|
527
562
|
|
|
528
|
-
{/*
|
|
563
|
+
{/* Scheduling & handling notes */}
|
|
529
564
|
<Card>
|
|
530
565
|
<CardHeader>
|
|
531
|
-
<CardTitle>
|
|
566
|
+
<CardTitle>Scheduling & Handling</CardTitle>
|
|
532
567
|
</CardHeader>
|
|
533
568
|
<CardContent>
|
|
534
|
-
<
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
569
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
570
|
+
<div className="space-y-2">
|
|
571
|
+
<Label htmlFor="scheduledDelivery" className="flex items-center gap-1.5">
|
|
572
|
+
<CalendarClock size={14} className="text-text-secondary" />
|
|
573
|
+
Scheduled Delivery
|
|
574
|
+
</Label>
|
|
575
|
+
<Input
|
|
576
|
+
id="scheduledDelivery"
|
|
577
|
+
type="datetime-local"
|
|
578
|
+
value={formData.scheduledDelivery}
|
|
579
|
+
onChange={(e) => updateField('scheduledDelivery', e.target.value)}
|
|
580
|
+
/>
|
|
581
|
+
<p className="text-xs text-text-secondary">
|
|
582
|
+
The delivery window promised to the customer. Leave blank to dispatch as soon as a
|
|
583
|
+
driver is free.
|
|
584
|
+
</p>
|
|
585
|
+
</div>
|
|
586
|
+
<div className="md:col-span-2 space-y-2">
|
|
587
|
+
<Label htmlFor="specialInstructions">Special Instructions</Label>
|
|
588
|
+
<Textarea
|
|
589
|
+
id="specialInstructions"
|
|
590
|
+
value={formData.specialInstructions}
|
|
591
|
+
onChange={(e) => updateField('specialInstructions', e.target.value)}
|
|
592
|
+
placeholder="Any special handling or delivery instructions..."
|
|
593
|
+
rows={4}
|
|
594
|
+
/>
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
540
597
|
</CardContent>
|
|
541
598
|
</Card>
|
|
542
599
|
</form>
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
Textarea,
|
|
39
39
|
} from '../components/ui';
|
|
40
40
|
import { SignaturePad, type SignaturePadHandle } from '../components/SignaturePad';
|
|
41
|
+
import { StatusBadge, PriorityBadge } from '../components/StatusBadge';
|
|
42
|
+
import { mapOrderPriority, mapOrderStatus } from '../hooks/useOrders';
|
|
41
43
|
import { FormError } from '../components/FormError';
|
|
42
44
|
import { Notice, useNotice } from '../components/Notice';
|
|
43
45
|
import {
|
|
@@ -284,6 +286,12 @@ export default function DeliveryCapturePage() {
|
|
|
284
286
|
<p className="font-medium flex items-center gap-2">
|
|
285
287
|
<Package size={16} /> {job.deliveryJobNumber}
|
|
286
288
|
</p>
|
|
289
|
+
{/* Stop state + urgency, straight off the job record — a driver
|
|
290
|
+
needs to know which stop is already out for delivery. */}
|
|
291
|
+
<div className="flex flex-wrap items-center gap-1.5 py-1">
|
|
292
|
+
<StatusBadge status={mapOrderStatus(job.status)} type="order" size="sm" />
|
|
293
|
+
<PriorityBadge priority={mapOrderPriority(job.priority)} size="sm" />
|
|
294
|
+
</div>
|
|
287
295
|
{job.deliveryAddress && (
|
|
288
296
|
<p className="text-sm text-text-secondary truncate">
|
|
289
297
|
{formatAddress(job.deliveryAddress)}
|
|
@@ -2,32 +2,92 @@
|
|
|
2
2
|
* Drivers Page - List of all drivers
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useState } from 'react';
|
|
6
|
-
import {
|
|
5
|
+
import { useMemo, useState } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
Users,
|
|
8
|
+
Plus,
|
|
9
|
+
RefreshCw,
|
|
10
|
+
Download,
|
|
11
|
+
Star,
|
|
12
|
+
UserCheck,
|
|
13
|
+
Coffee,
|
|
14
|
+
UserMinus,
|
|
15
|
+
Filter,
|
|
16
|
+
Truck,
|
|
17
|
+
} from 'lucide-react';
|
|
7
18
|
import { PageLayout } from '../components/PageLayout';
|
|
8
19
|
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
9
20
|
import { DataTable, type Column } from '../components/DataTable';
|
|
10
21
|
import { StatusBadge } from '../components/StatusBadge';
|
|
11
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
Avatar,
|
|
24
|
+
AvatarFallback,
|
|
25
|
+
Button,
|
|
26
|
+
Card,
|
|
27
|
+
CardContent,
|
|
28
|
+
Select,
|
|
29
|
+
SelectContent,
|
|
30
|
+
SelectItem,
|
|
31
|
+
SelectTrigger,
|
|
32
|
+
SelectValue,
|
|
33
|
+
} from '../components/ui';
|
|
12
34
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
13
|
-
import { useDrivers, useFleetStats } from '../hooks/useFleet';
|
|
35
|
+
import { useDrivers, useDriverStatusCounts, useFleetStats, useVehicles } from '../hooks/useFleet';
|
|
14
36
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
15
37
|
import { formatDate } from '../utils/formatters';
|
|
16
38
|
import { joinPath } from '../utils/navigation';
|
|
17
|
-
import type { Driver } from '../types';
|
|
39
|
+
import type { Driver, DriverStatus } from '../types';
|
|
40
|
+
|
|
41
|
+
const STATUS_OPTIONS: Array<{ value: DriverStatus | 'all'; label: string }> = [
|
|
42
|
+
{ value: 'all', label: 'All statuses' },
|
|
43
|
+
{ value: 'available', label: 'Available' },
|
|
44
|
+
{ value: 'busy', label: 'On delivery' },
|
|
45
|
+
{ value: 'on_break', label: 'On break' },
|
|
46
|
+
{ value: 'offline', label: 'Off duty' },
|
|
47
|
+
];
|
|
18
48
|
|
|
19
49
|
export default function DriversPage() {
|
|
20
50
|
const { basePath, navigate } = useMoveTheWheels();
|
|
21
51
|
const [page, setPage] = useState(0);
|
|
22
52
|
const [pageSize, setPageSize] = useState(25);
|
|
53
|
+
const [statusFilter, setStatusFilter] = useState<DriverStatus | 'all'>('all');
|
|
54
|
+
const [licenseClassFilter, setLicenseClassFilter] = useState<string>('all');
|
|
23
55
|
const { drivers, isLoading, error, refetch } = useDrivers({
|
|
24
56
|
limit: pageSize,
|
|
25
57
|
offset: page * pageSize,
|
|
58
|
+
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
26
59
|
});
|
|
27
60
|
// Workforce totals + tiles from the live aggregate, not the current page.
|
|
28
61
|
const { stats } = useFleetStats();
|
|
62
|
+
// Exact ON_BREAK / OFF_DUTY / INACTIVE counts — fleetStats cannot split these.
|
|
63
|
+
const { counts } = useDriverStatusCounts();
|
|
64
|
+
// Drivers carry only `vehicleId`; the roster shows the fleet number, so the
|
|
65
|
+
// vehicle list is joined client-side into an id -> number lookup.
|
|
66
|
+
const { vehicles } = useVehicles({ limit: 500 });
|
|
67
|
+
const vehicleNumberById = useMemo(
|
|
68
|
+
() => new Map(vehicles.map((vehicle) => [vehicle.id, vehicle.vehicleNumber])),
|
|
69
|
+
[vehicles]
|
|
70
|
+
);
|
|
29
71
|
const totalDrivers = stats.totalDrivers;
|
|
30
72
|
|
|
73
|
+
// `DriverFilterInput` has no licenseClass, so this narrowing is applied to the
|
|
74
|
+
// fetched page only — never presented as a workspace-wide count.
|
|
75
|
+
const licenseClasses = useMemo(
|
|
76
|
+
() =>
|
|
77
|
+
Array.from(new Set(drivers.map((driver) => driver.licenseType).filter(Boolean))).sort(
|
|
78
|
+
(a, b) => a.localeCompare(b)
|
|
79
|
+
),
|
|
80
|
+
[drivers]
|
|
81
|
+
);
|
|
82
|
+
const visibleDrivers = useMemo(
|
|
83
|
+
() =>
|
|
84
|
+
licenseClassFilter === 'all'
|
|
85
|
+
? drivers
|
|
86
|
+
: drivers.filter((driver) => driver.licenseType === licenseClassFilter),
|
|
87
|
+
[drivers, licenseClassFilter]
|
|
88
|
+
);
|
|
89
|
+
const filtersActive = statusFilter !== 'all' || licenseClassFilter !== 'all';
|
|
90
|
+
|
|
31
91
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
32
92
|
|
|
33
93
|
const columns: Column<Driver>[] = [
|
|
@@ -58,24 +118,6 @@ export default function DriversPage() {
|
|
|
58
118
|
sortable: true,
|
|
59
119
|
cell: (row) => <StatusBadge status={row.status} type="driver" />,
|
|
60
120
|
},
|
|
61
|
-
{
|
|
62
|
-
id: 'rating',
|
|
63
|
-
header: 'Rating',
|
|
64
|
-
cell: (row) => (
|
|
65
|
-
<div className="flex items-center gap-1">
|
|
66
|
-
<Star size={14} className="text-status-warning-text fill-current" />
|
|
67
|
-
<span className="font-medium">{row.ratings.overall.toFixed(1)}</span>
|
|
68
|
-
<span className="text-text-secondary text-sm">({row.ratings.totalRatings})</span>
|
|
69
|
-
</div>
|
|
70
|
-
),
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
id: 'deliveries',
|
|
74
|
-
header: 'Deliveries',
|
|
75
|
-
accessorKey: 'completedOrders',
|
|
76
|
-
sortable: true,
|
|
77
|
-
cell: (row) => <span className="font-medium">{row.completedOrders.toLocaleString()}</span>,
|
|
78
|
-
},
|
|
79
121
|
{
|
|
80
122
|
id: 'license',
|
|
81
123
|
header: 'License',
|
|
@@ -86,6 +128,52 @@ export default function DriversPage() {
|
|
|
86
128
|
</div>
|
|
87
129
|
),
|
|
88
130
|
},
|
|
131
|
+
{
|
|
132
|
+
id: 'phone',
|
|
133
|
+
header: 'Phone',
|
|
134
|
+
accessorKey: 'phone',
|
|
135
|
+
cell: (row) =>
|
|
136
|
+
row.phone ? (
|
|
137
|
+
<a
|
|
138
|
+
href={`tel:${row.phone}`}
|
|
139
|
+
className="text-sm hover:underline"
|
|
140
|
+
onClick={(e) => e.stopPropagation()}
|
|
141
|
+
>
|
|
142
|
+
{row.phone}
|
|
143
|
+
</a>
|
|
144
|
+
) : (
|
|
145
|
+
<span className="text-sm text-text-secondary">—</span>
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'rating',
|
|
150
|
+
header: 'Rating',
|
|
151
|
+
cell: (row) =>
|
|
152
|
+
row.ratings.overall > 0 ? (
|
|
153
|
+
<div className="flex items-center gap-1">
|
|
154
|
+
<Star size={14} className="text-status-warning-text fill-current" />
|
|
155
|
+
<span className="font-medium">{row.ratings.overall.toFixed(1)}</span>
|
|
156
|
+
</div>
|
|
157
|
+
) : (
|
|
158
|
+
<span className="text-sm text-text-secondary">Not rated</span>
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'vehicle',
|
|
163
|
+
header: 'Assigned vehicle',
|
|
164
|
+
cell: (row) => {
|
|
165
|
+
if (!row.assignedVehicle) {
|
|
166
|
+
return <span className="text-sm text-text-secondary">Unassigned</span>;
|
|
167
|
+
}
|
|
168
|
+
const vehicleNumber = vehicleNumberById.get(row.assignedVehicle);
|
|
169
|
+
return (
|
|
170
|
+
<span className="inline-flex items-center gap-1.5 text-sm font-medium">
|
|
171
|
+
<Truck size={14} className="text-text-secondary" />
|
|
172
|
+
{vehicleNumber ?? 'Assigned'}
|
|
173
|
+
</span>
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
},
|
|
89
177
|
{
|
|
90
178
|
id: 'joined',
|
|
91
179
|
header: 'Join Date',
|
|
@@ -119,8 +207,8 @@ export default function DriversPage() {
|
|
|
119
207
|
}
|
|
120
208
|
>
|
|
121
209
|
<PagePurpose icon={<Users size={18} />}>
|
|
122
|
-
Your delivery workforce. Track each driver's availability,
|
|
123
|
-
|
|
210
|
+
Your delivery workforce. Track each driver's availability, rating, licence status,
|
|
211
|
+
contact number and the vehicle they are on — open a driver to review their record before
|
|
124
212
|
assigning them to an order or route.
|
|
125
213
|
</PagePurpose>
|
|
126
214
|
|
|
@@ -137,15 +225,17 @@ export default function DriversPage() {
|
|
|
137
225
|
/>
|
|
138
226
|
<MetricCard
|
|
139
227
|
label="On break"
|
|
140
|
-
value={
|
|
228
|
+
value={counts.onBreak}
|
|
141
229
|
icon={<Coffee size={20} />}
|
|
142
230
|
accent="warning"
|
|
231
|
+
subtitle="Back on shift shortly"
|
|
143
232
|
/>
|
|
144
233
|
<MetricCard
|
|
145
234
|
label="Off duty"
|
|
146
|
-
value={
|
|
235
|
+
value={counts.offDuty}
|
|
147
236
|
icon={<UserMinus size={20} />}
|
|
148
237
|
accent="neutral"
|
|
238
|
+
subtitle={counts.inactive > 0 ? `${counts.inactive} inactive` : undefined}
|
|
149
239
|
/>
|
|
150
240
|
<MetricCard
|
|
151
241
|
label="Avg rating"
|
|
@@ -157,9 +247,65 @@ export default function DriversPage() {
|
|
|
157
247
|
</MetricGrid>
|
|
158
248
|
</div>
|
|
159
249
|
|
|
250
|
+
{/* Filters */}
|
|
251
|
+
<Card>
|
|
252
|
+
<CardContent className="p-4">
|
|
253
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
254
|
+
<div className="flex items-center gap-2">
|
|
255
|
+
<Filter size={16} className="text-text-secondary" />
|
|
256
|
+
<span className="text-sm font-medium">Filters:</span>
|
|
257
|
+
</div>
|
|
258
|
+
<Select
|
|
259
|
+
value={statusFilter}
|
|
260
|
+
onValueChange={(value) => {
|
|
261
|
+
setStatusFilter(value as DriverStatus | 'all');
|
|
262
|
+
setPage(0);
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
<SelectTrigger className="w-[180px]">
|
|
266
|
+
<SelectValue placeholder="Status" />
|
|
267
|
+
</SelectTrigger>
|
|
268
|
+
<SelectContent>
|
|
269
|
+
{STATUS_OPTIONS.map((option) => (
|
|
270
|
+
<SelectItem key={option.value} value={option.value}>
|
|
271
|
+
{option.label}
|
|
272
|
+
</SelectItem>
|
|
273
|
+
))}
|
|
274
|
+
</SelectContent>
|
|
275
|
+
</Select>
|
|
276
|
+
<Select value={licenseClassFilter} onValueChange={setLicenseClassFilter}>
|
|
277
|
+
<SelectTrigger className="w-[180px]">
|
|
278
|
+
<SelectValue placeholder="License class" />
|
|
279
|
+
</SelectTrigger>
|
|
280
|
+
<SelectContent>
|
|
281
|
+
<SelectItem value="all">All licence classes</SelectItem>
|
|
282
|
+
{licenseClasses.map((licenseClass) => (
|
|
283
|
+
<SelectItem key={licenseClass} value={licenseClass}>
|
|
284
|
+
{licenseClass}
|
|
285
|
+
</SelectItem>
|
|
286
|
+
))}
|
|
287
|
+
</SelectContent>
|
|
288
|
+
</Select>
|
|
289
|
+
{filtersActive && (
|
|
290
|
+
<Button
|
|
291
|
+
variant="ghost"
|
|
292
|
+
size="sm"
|
|
293
|
+
onClick={() => {
|
|
294
|
+
setStatusFilter('all');
|
|
295
|
+
setLicenseClassFilter('all');
|
|
296
|
+
setPage(0);
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
Clear filters
|
|
300
|
+
</Button>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
</CardContent>
|
|
304
|
+
</Card>
|
|
305
|
+
|
|
160
306
|
<DataTable
|
|
161
307
|
columns={columns}
|
|
162
|
-
data={
|
|
308
|
+
data={visibleDrivers}
|
|
163
309
|
isLoading={isLoading}
|
|
164
310
|
error={error}
|
|
165
311
|
onRetry={refetch}
|
|
@@ -175,10 +321,14 @@ export default function DriversPage() {
|
|
|
175
321
|
setPageSize(size);
|
|
176
322
|
setPage(0);
|
|
177
323
|
}}
|
|
178
|
-
totalItems={totalDrivers}
|
|
324
|
+
totalItems={filtersActive ? undefined : totalDrivers}
|
|
179
325
|
emptyIllustration="empty-people"
|
|
180
|
-
emptyTitle=
|
|
181
|
-
emptyDescription=
|
|
326
|
+
emptyTitle={filtersActive ? 'No drivers match these filters' : 'No drivers yet'}
|
|
327
|
+
emptyDescription={
|
|
328
|
+
filtersActive
|
|
329
|
+
? 'Clear the status or licence-class filter to see the rest of the roster.'
|
|
330
|
+
: 'Add drivers to start assigning deliveries and routes.'
|
|
331
|
+
}
|
|
182
332
|
/>
|
|
183
333
|
</PageLayout>
|
|
184
334
|
);
|