@burdenoff/microfe-movethewheels 2026.805.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/components/CarbonFootprintPanel.js +108 -0
- package/dist/components/index.js +1 -0
- package/dist/generated/wspace-operations.d.ts +168 -912
- package/dist/generated/wspace-types.d.ts +78432 -35831
- package/dist/hooks/useAnalytics.js +45 -4
- 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 -32
- package/dist/movethewheels/components/CarbonFootprintPanel.d.ts +7 -0
- package/dist/movethewheels/components/index.d.ts +1 -0
- package/dist/movethewheels/hooks/index.d.ts +3 -3
- package/dist/movethewheels/hooks/useAnalytics.d.ts +22 -0
- 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/leaf.js +11 -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/AnalyticsPage.js +81 -79
- 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/components/CarbonFootprintPanel.tsx +133 -0
- package/src/movethewheels/components/UpcomingDemandPanel.tsx +20 -11
- package/src/movethewheels/components/index.ts +3 -0
- package/src/movethewheels/hooks/index.ts +11 -0
- package/src/movethewheels/hooks/useAnalytics.ts +86 -0
- package/src/movethewheels/hooks/useDemandForecast.ts +10 -4
- 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/AnalyticsPage.tsx +5 -5
- 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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carbon footprint panel (BOFF-4391).
|
|
3
|
+
*
|
|
4
|
+
* Surfaces workspace-level CO2e totals and per-vehicle intensity. Data comes
|
|
5
|
+
* from Query.emissionsSummary via useEmissionsSummary.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Leaf, Truck } from 'lucide-react';
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from './ui';
|
|
10
|
+
import { MetricCard, MetricGrid } from './MetricCard';
|
|
11
|
+
import { useEmissionsSummary } from '../hooks/useAnalytics';
|
|
12
|
+
|
|
13
|
+
function formatKg(value: number): string {
|
|
14
|
+
if (value >= 1000) {
|
|
15
|
+
return `${(value / 1000).toFixed(2)} t`;
|
|
16
|
+
}
|
|
17
|
+
return `${value.toFixed(1)} kg`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatKm(value: number): string {
|
|
21
|
+
if (value >= 1000) {
|
|
22
|
+
return `${(value / 1000).toFixed(1)}k km`;
|
|
23
|
+
}
|
|
24
|
+
return `${value.toFixed(1)} km`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function CarbonFootprintPanel() {
|
|
28
|
+
const { summary, isLoading, error } = useEmissionsSummary();
|
|
29
|
+
|
|
30
|
+
if (error) {
|
|
31
|
+
return (
|
|
32
|
+
<Card className="border-status-error-border">
|
|
33
|
+
<CardContent className="p-6 text-center">
|
|
34
|
+
<p className="text-sm text-status-error-text">Couldn't load emissions data</p>
|
|
35
|
+
</CardContent>
|
|
36
|
+
</Card>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isLoading) {
|
|
41
|
+
return (
|
|
42
|
+
<Card>
|
|
43
|
+
<CardHeader>
|
|
44
|
+
<CardTitle className="flex items-center gap-2">
|
|
45
|
+
<Leaf size={18} className="text-status-success-text" />
|
|
46
|
+
Carbon footprint
|
|
47
|
+
</CardTitle>
|
|
48
|
+
</CardHeader>
|
|
49
|
+
<CardContent>
|
|
50
|
+
<MetricGrid columns={3}>
|
|
51
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
52
|
+
<div key={i} className="h-24 rounded-xl bg-bg-sunken motion-safe:animate-pulse" />
|
|
53
|
+
))}
|
|
54
|
+
</MetricGrid>
|
|
55
|
+
</CardContent>
|
|
56
|
+
</Card>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hasData = summary.totalDistanceKm > 0;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle className="flex items-center gap-2">
|
|
66
|
+
<Leaf size={18} className="text-status-success-text" />
|
|
67
|
+
Carbon footprint
|
|
68
|
+
</CardTitle>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent className="space-y-6">
|
|
71
|
+
<MetricGrid columns={3}>
|
|
72
|
+
<MetricCard
|
|
73
|
+
label="Total CO2e"
|
|
74
|
+
value={formatKg(summary.totalCo2eKg)}
|
|
75
|
+
icon={<Leaf size={20} />}
|
|
76
|
+
accent="success"
|
|
77
|
+
/>
|
|
78
|
+
<MetricCard
|
|
79
|
+
label="Total distance"
|
|
80
|
+
value={formatKm(summary.totalDistanceKm)}
|
|
81
|
+
icon={<Truck size={20} />}
|
|
82
|
+
accent="blue"
|
|
83
|
+
/>
|
|
84
|
+
<MetricCard
|
|
85
|
+
label="Avg intensity"
|
|
86
|
+
value={`${(summary.averageCo2eKgPerKm * 1000).toFixed(1)} g/km`}
|
|
87
|
+
icon={<Leaf size={20} />}
|
|
88
|
+
accent="warning"
|
|
89
|
+
/>
|
|
90
|
+
</MetricGrid>
|
|
91
|
+
|
|
92
|
+
{hasData && summary.byVehicle.length > 0 && (
|
|
93
|
+
<div className="space-y-3">
|
|
94
|
+
<h4 className="text-sm font-medium text-text-secondary">By vehicle</h4>
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
{summary.byVehicle
|
|
97
|
+
.filter((v) => v.totalDistanceKm > 0 || v.co2eKg > 0)
|
|
98
|
+
.slice(0, 5)
|
|
99
|
+
.map((vehicle) => (
|
|
100
|
+
<div
|
|
101
|
+
key={vehicle.vehicleId ?? 'unassigned'}
|
|
102
|
+
className="flex items-center justify-between rounded-lg bg-bg-sunken px-3 py-2"
|
|
103
|
+
>
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<Truck size={16} className="text-text-secondary" />
|
|
106
|
+
<span className="text-sm font-medium text-text-primary">
|
|
107
|
+
{vehicle.vehicleNumber ?? 'Unassigned'}
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="text-right">
|
|
111
|
+
<p className="text-sm font-medium text-text-primary">
|
|
112
|
+
{formatKg(vehicle.co2eKg)}
|
|
113
|
+
</p>
|
|
114
|
+
<p className="text-xs text-text-secondary">
|
|
115
|
+
{formatKm(vehicle.totalDistanceKm)}
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{!hasData && (
|
|
125
|
+
<p className="text-sm text-text-secondary">
|
|
126
|
+
No route distance recorded yet. Assign vehicles and distances to routes to see emissions
|
|
127
|
+
estimates.
|
|
128
|
+
</p>
|
|
129
|
+
)}
|
|
130
|
+
</CardContent>
|
|
131
|
+
</Card>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -10,10 +10,7 @@ import { Calendar, TrendingUp, TrendingDown, AlertCircle } from 'lucide-react';
|
|
|
10
10
|
|
|
11
11
|
import { Card, CardContent, CardHeader, CardTitle, Skeleton } from './ui';
|
|
12
12
|
|
|
13
|
-
import type {
|
|
14
|
-
ForecastPoint,
|
|
15
|
-
ForecastVariance,
|
|
16
|
-
} from '../hooks/useDemandForecast';
|
|
13
|
+
import type { ForecastPoint, ForecastVariance } from '../hooks/useDemandForecast';
|
|
17
14
|
|
|
18
15
|
interface UpcomingDemandPanelProps {
|
|
19
16
|
laneForecast: ForecastPoint[];
|
|
@@ -42,7 +39,9 @@ const topByRevenue = (rows: ForecastPoint[], weekStart: string): ForecastPoint[]
|
|
|
42
39
|
};
|
|
43
40
|
|
|
44
41
|
const totalVolume = (rows: ForecastPoint[], weekStart: string): number =>
|
|
45
|
-
rows
|
|
42
|
+
rows
|
|
43
|
+
.filter((row) => row.periodStart === weekStart)
|
|
44
|
+
.reduce((sum, row) => sum + row.predictedVolume, 0);
|
|
46
45
|
|
|
47
46
|
const formatConfidence = (confidence: number): string => {
|
|
48
47
|
if (confidence <= 0) return 'Not enough history';
|
|
@@ -177,13 +176,18 @@ function ForecastList({ title, rows }: ForecastListProps) {
|
|
|
177
176
|
<p className="text-sm font-medium mb-3">{title}</p>
|
|
178
177
|
<ul className="space-y-2">
|
|
179
178
|
{rows.map((row) => (
|
|
180
|
-
<li
|
|
179
|
+
<li
|
|
180
|
+
key={`${row.scopeType}-${row.scopeKey}`}
|
|
181
|
+
className="flex items-center justify-between text-sm"
|
|
182
|
+
>
|
|
181
183
|
<span className="truncate font-medium">{row.scopeKey}</span>
|
|
182
184
|
<span className="ml-2 shrink-0 text-text-secondary">
|
|
183
185
|
{row.predictedVolume} jobs ·{' '}
|
|
184
|
-
{new Intl.NumberFormat(undefined, {
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
{new Intl.NumberFormat(undefined, {
|
|
187
|
+
style: 'currency',
|
|
188
|
+
currency: 'USD',
|
|
189
|
+
maximumFractionDigits: 0,
|
|
190
|
+
}).format(row.predictedRevenue)}
|
|
187
191
|
</span>
|
|
188
192
|
</li>
|
|
189
193
|
))}
|
|
@@ -207,9 +211,14 @@ function VarianceList({ rows }: VarianceListProps) {
|
|
|
207
211
|
{rows.map((row) => {
|
|
208
212
|
const isOvershoot = row.volumeVariance > 0;
|
|
209
213
|
const Icon = isOvershoot ? TrendingUp : TrendingDown;
|
|
210
|
-
const tone = isOvershoot
|
|
214
|
+
const tone = isOvershoot
|
|
215
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
216
|
+
: 'text-rose-600 dark:text-rose-400';
|
|
211
217
|
return (
|
|
212
|
-
<li
|
|
218
|
+
<li
|
|
219
|
+
key={`${row.scopeType}-${row.scopeKey}-${row.periodStart}`}
|
|
220
|
+
className="flex items-center justify-between text-sm"
|
|
221
|
+
>
|
|
213
222
|
<span className="flex items-center gap-2 truncate">
|
|
214
223
|
<Icon size={14} className={tone} aria-hidden="true" />
|
|
215
224
|
<span className="truncate font-medium">{row.scopeKey}</span>
|
|
@@ -12,9 +12,12 @@ export {
|
|
|
12
12
|
useFleet,
|
|
13
13
|
useVehicles,
|
|
14
14
|
useVehicle,
|
|
15
|
+
useVehicleDrivers,
|
|
15
16
|
useDrivers,
|
|
16
17
|
useDriver,
|
|
18
|
+
useDriverStatusCounts,
|
|
17
19
|
useFleetStats,
|
|
20
|
+
type DriverStatusCounts,
|
|
18
21
|
} from './useFleet';
|
|
19
22
|
|
|
20
23
|
// Inventory (Products & Warehouses)
|
|
@@ -22,9 +25,15 @@ export {
|
|
|
22
25
|
useInventory,
|
|
23
26
|
useProducts,
|
|
24
27
|
useProduct,
|
|
28
|
+
useProductStockLevels,
|
|
25
29
|
useWarehouses,
|
|
26
30
|
useWarehouse,
|
|
27
31
|
useInventoryStats,
|
|
32
|
+
useInventoryTransactions,
|
|
33
|
+
type InventoryStats,
|
|
34
|
+
type InventoryTransaction,
|
|
35
|
+
type InventoryTransactionType,
|
|
36
|
+
type ProductStockLevel,
|
|
28
37
|
} from './useInventory';
|
|
29
38
|
|
|
30
39
|
// Customers
|
|
@@ -43,6 +52,8 @@ export {
|
|
|
43
52
|
usePerformanceAnalytics,
|
|
44
53
|
useFinanceMetrics,
|
|
45
54
|
type FinanceMetrics,
|
|
55
|
+
useEmissionsSummary,
|
|
56
|
+
type EmissionsSummary,
|
|
46
57
|
} from './useAnalytics';
|
|
47
58
|
|
|
48
59
|
// Tracking
|
|
@@ -493,6 +493,22 @@ const FINANCE_METRICS_QUERY = `
|
|
|
493
493
|
}
|
|
494
494
|
`;
|
|
495
495
|
|
|
496
|
+
const EMISSIONS_SUMMARY_QUERY = `
|
|
497
|
+
query GetMovethewheelsEmissionsSummary {
|
|
498
|
+
emissionsSummary {
|
|
499
|
+
totalCo2eKg
|
|
500
|
+
totalDistanceKm
|
|
501
|
+
averageCo2eKgPerKm
|
|
502
|
+
byVehicle {
|
|
503
|
+
vehicleId
|
|
504
|
+
vehicleNumber
|
|
505
|
+
totalDistanceKm
|
|
506
|
+
co2eKg
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
`;
|
|
511
|
+
|
|
496
512
|
export interface FinanceMetrics {
|
|
497
513
|
totalRevenue: number;
|
|
498
514
|
deliveredRevenue: number;
|
|
@@ -593,3 +609,73 @@ export function usePerformanceAnalytics() {
|
|
|
593
609
|
|
|
594
610
|
return { performanceData, isLoading, error };
|
|
595
611
|
}
|
|
612
|
+
|
|
613
|
+
export interface EmissionsSummary {
|
|
614
|
+
totalCo2eKg: number;
|
|
615
|
+
totalDistanceKm: number;
|
|
616
|
+
averageCo2eKgPerKm: number;
|
|
617
|
+
byVehicle: Array<{
|
|
618
|
+
vehicleId: string | null;
|
|
619
|
+
vehicleNumber: string | null;
|
|
620
|
+
totalDistanceKm: number;
|
|
621
|
+
co2eKg: number;
|
|
622
|
+
}>;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Real emissions figures straight from Query.emissionsSummary (BOFF-4391).
|
|
627
|
+
* Returns totals, per-km intensity, and a per-vehicle breakdown sourced from
|
|
628
|
+
* cached route CO2e values and actual/estimated distances.
|
|
629
|
+
*/
|
|
630
|
+
export function useEmissionsSummary() {
|
|
631
|
+
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
632
|
+
useMoveTheWheels();
|
|
633
|
+
const [summary, setSummary] = useState<EmissionsSummary>({
|
|
634
|
+
totalCo2eKg: 0,
|
|
635
|
+
totalDistanceKm: 0,
|
|
636
|
+
averageCo2eKgPerKm: 0,
|
|
637
|
+
byVehicle: [],
|
|
638
|
+
});
|
|
639
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
640
|
+
const [error, setError] = useState<Error | null>(null);
|
|
641
|
+
|
|
642
|
+
const graphQLOptions = useMemo(
|
|
643
|
+
() => ({
|
|
644
|
+
apiGatewayUrl,
|
|
645
|
+
authToken,
|
|
646
|
+
workspaceToken,
|
|
647
|
+
workspaceId,
|
|
648
|
+
tenantId,
|
|
649
|
+
actorId: currentUser?.id,
|
|
650
|
+
}),
|
|
651
|
+
[apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
const load = useCallback(async () => {
|
|
655
|
+
setIsLoading(true);
|
|
656
|
+
setError(null);
|
|
657
|
+
try {
|
|
658
|
+
const data = await executeGraphQL<{
|
|
659
|
+
emissionsSummary: EmissionsSummary;
|
|
660
|
+
}>(graphQLOptions, EMISSIONS_SUMMARY_QUERY);
|
|
661
|
+
|
|
662
|
+
setSummary(data.emissionsSummary);
|
|
663
|
+
} catch (fetchError) {
|
|
664
|
+
setError(
|
|
665
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load emissions summary')
|
|
666
|
+
);
|
|
667
|
+
} finally {
|
|
668
|
+
setIsLoading(false);
|
|
669
|
+
}
|
|
670
|
+
}, [graphQLOptions]);
|
|
671
|
+
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
void load();
|
|
674
|
+
}, [load]);
|
|
675
|
+
|
|
676
|
+
const refetch = useCallback(() => {
|
|
677
|
+
void load();
|
|
678
|
+
}, [load]);
|
|
679
|
+
|
|
680
|
+
return { summary, isLoading, error, refetch };
|
|
681
|
+
}
|
|
@@ -134,9 +134,7 @@ export function useDemandForecast(weeks = 4, varianceLimit = 8): UseDemandForeca
|
|
|
134
134
|
setCustomerVariance(data.customerVariance ?? []);
|
|
135
135
|
} catch (fetchError) {
|
|
136
136
|
setError(
|
|
137
|
-
fetchError instanceof Error
|
|
138
|
-
? fetchError
|
|
139
|
-
: new Error('Failed to load demand forecast')
|
|
137
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load demand forecast')
|
|
140
138
|
);
|
|
141
139
|
} finally {
|
|
142
140
|
setIsLoading(false);
|
|
@@ -151,5 +149,13 @@ export function useDemandForecast(weeks = 4, varianceLimit = 8): UseDemandForeca
|
|
|
151
149
|
void load();
|
|
152
150
|
}, [load]);
|
|
153
151
|
|
|
154
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
laneForecast,
|
|
154
|
+
customerForecast,
|
|
155
|
+
laneVariance,
|
|
156
|
+
customerVariance,
|
|
157
|
+
isLoading,
|
|
158
|
+
error,
|
|
159
|
+
refetch,
|
|
160
|
+
};
|
|
155
161
|
}
|
|
@@ -46,6 +46,7 @@ interface ApiVehicle {
|
|
|
46
46
|
make?: string | null;
|
|
47
47
|
model?: string | null;
|
|
48
48
|
licensePlate?: string | null;
|
|
49
|
+
ownershipType?: string | null;
|
|
49
50
|
year?: number | null;
|
|
50
51
|
capacity?: Record<string, unknown> | null;
|
|
51
52
|
status: string;
|
|
@@ -83,6 +84,7 @@ const LIST_VEHICLES_QUERY = `
|
|
|
83
84
|
make
|
|
84
85
|
model
|
|
85
86
|
licensePlate
|
|
87
|
+
ownershipType
|
|
86
88
|
year
|
|
87
89
|
capacity
|
|
88
90
|
status
|
|
@@ -126,6 +128,7 @@ const GET_VEHICLE_QUERY = `
|
|
|
126
128
|
make
|
|
127
129
|
model
|
|
128
130
|
licensePlate
|
|
131
|
+
ownershipType
|
|
129
132
|
year
|
|
130
133
|
capacity
|
|
131
134
|
status
|
|
@@ -169,6 +172,7 @@ const UPDATE_VEHICLE_STATUS_MUTATION = `
|
|
|
169
172
|
make
|
|
170
173
|
model
|
|
171
174
|
licensePlate
|
|
175
|
+
ownershipType
|
|
172
176
|
year
|
|
173
177
|
capacity
|
|
174
178
|
status
|
|
@@ -234,6 +238,7 @@ const CREATE_VEHICLE_MUTATION = `
|
|
|
234
238
|
make
|
|
235
239
|
model
|
|
236
240
|
licensePlate
|
|
241
|
+
ownershipType
|
|
237
242
|
year
|
|
238
243
|
capacity
|
|
239
244
|
status
|
|
@@ -277,6 +282,7 @@ const UPDATE_VEHICLE_MUTATION = `
|
|
|
277
282
|
make
|
|
278
283
|
model
|
|
279
284
|
licensePlate
|
|
285
|
+
ownershipType
|
|
280
286
|
year
|
|
281
287
|
capacity
|
|
282
288
|
status
|
|
@@ -311,6 +317,21 @@ const UPDATE_DRIVER_MUTATION = `
|
|
|
311
317
|
}
|
|
312
318
|
`;
|
|
313
319
|
|
|
320
|
+
/**
|
|
321
|
+
* `fleetStats` only breaks drivers down into available / on-delivery, so an
|
|
322
|
+
* "on break" or "off duty" tile derived from it is always wrong (on-break
|
|
323
|
+
* drivers get lumped into the off-duty remainder). `driversCount` takes the
|
|
324
|
+
* same DriverFilterInput as the list, so each roster tile can be counted
|
|
325
|
+
* exactly, workspace-wide, instead of inferred from a subtraction.
|
|
326
|
+
*/
|
|
327
|
+
const DRIVER_STATUS_COUNTS_QUERY = `
|
|
328
|
+
query GetDriverStatusCounts {
|
|
329
|
+
onBreak: driversCount(filter: { status: ON_BREAK })
|
|
330
|
+
offDuty: driversCount(filter: { status: OFF_DUTY })
|
|
331
|
+
inactive: driversCount(filter: { status: INACTIVE })
|
|
332
|
+
}
|
|
333
|
+
`;
|
|
334
|
+
|
|
314
335
|
const FLEET_STATS_QUERY = `
|
|
315
336
|
query GetFleetStats {
|
|
316
337
|
fleetStats {
|
|
@@ -386,6 +407,7 @@ function mapVehicle(vehicle: ApiVehicle): Vehicle {
|
|
|
386
407
|
make: vehicle.make ?? '',
|
|
387
408
|
model: vehicle.model ?? '',
|
|
388
409
|
licensePlate: vehicle.licensePlate ?? '',
|
|
410
|
+
ownershipType: vehicle.ownershipType ?? undefined,
|
|
389
411
|
year: vehicle.year ?? new Date().getFullYear(),
|
|
390
412
|
capacity: {
|
|
391
413
|
weight: 0,
|
|
@@ -805,8 +827,16 @@ interface UseDriversResult {
|
|
|
805
827
|
assignVehicle: (driverId: string, vehicleId: string) => Promise<Driver>;
|
|
806
828
|
}
|
|
807
829
|
|
|
808
|
-
|
|
809
|
-
|
|
830
|
+
interface UseDriversOptions extends UseListOptions {
|
|
831
|
+
/**
|
|
832
|
+
* Narrows the roster server-side via `DriverFilterInput.status` so the pager
|
|
833
|
+
* still counts whole result sets rather than filtering one fetched page.
|
|
834
|
+
*/
|
|
835
|
+
status?: DriverStatus;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
|
|
839
|
+
const { limit = 200, offset = 0, status } = options;
|
|
810
840
|
const graphQLOptions = useGraphqlOptions();
|
|
811
841
|
const [drivers, setDrivers] = useState<Driver[]>([]);
|
|
812
842
|
const [hasMore, setHasMore] = useState(false);
|
|
@@ -821,6 +851,7 @@ export function useDrivers(options: UseListOptions = {}): UseDriversResult {
|
|
|
821
851
|
graphQLOptions,
|
|
822
852
|
LIST_DRIVERS_QUERY,
|
|
823
853
|
{
|
|
854
|
+
filter: status ? { status: toApiDriverStatus(status) } : undefined,
|
|
824
855
|
pagination: { limit, offset },
|
|
825
856
|
}
|
|
826
857
|
);
|
|
@@ -831,7 +862,7 @@ export function useDrivers(options: UseListOptions = {}): UseDriversResult {
|
|
|
831
862
|
} finally {
|
|
832
863
|
setIsLoading(false);
|
|
833
864
|
}
|
|
834
|
-
}, [graphQLOptions, limit, offset]);
|
|
865
|
+
}, [graphQLOptions, limit, offset, status]);
|
|
835
866
|
|
|
836
867
|
useEffect(() => {
|
|
837
868
|
void load();
|
|
@@ -928,6 +959,105 @@ export function useDriver(driverId: string | undefined) {
|
|
|
928
959
|
return { driver, isLoading, error, refetch };
|
|
929
960
|
}
|
|
930
961
|
|
|
962
|
+
export interface DriverStatusCounts {
|
|
963
|
+
onBreak: number;
|
|
964
|
+
offDuty: number;
|
|
965
|
+
inactive: number;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/** Exact per-status driver counts for the roster tiles (see DRIVER_STATUS_COUNTS_QUERY). */
|
|
969
|
+
export function useDriverStatusCounts() {
|
|
970
|
+
const graphQLOptions = useGraphqlOptions();
|
|
971
|
+
const [counts, setCounts] = useState<DriverStatusCounts>({
|
|
972
|
+
onBreak: 0,
|
|
973
|
+
offDuty: 0,
|
|
974
|
+
inactive: 0,
|
|
975
|
+
});
|
|
976
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
977
|
+
const [error, setError] = useState<Error | null>(null);
|
|
978
|
+
|
|
979
|
+
const load = useCallback(async () => {
|
|
980
|
+
setIsLoading(true);
|
|
981
|
+
setError(null);
|
|
982
|
+
try {
|
|
983
|
+
const data = await executeGraphQL<DriverStatusCounts>(
|
|
984
|
+
graphQLOptions,
|
|
985
|
+
DRIVER_STATUS_COUNTS_QUERY
|
|
986
|
+
);
|
|
987
|
+
setCounts({
|
|
988
|
+
onBreak: data.onBreak,
|
|
989
|
+
offDuty: data.offDuty,
|
|
990
|
+
inactive: data.inactive,
|
|
991
|
+
});
|
|
992
|
+
} catch (fetchError) {
|
|
993
|
+
setError(
|
|
994
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load driver status counts')
|
|
995
|
+
);
|
|
996
|
+
} finally {
|
|
997
|
+
setIsLoading(false);
|
|
998
|
+
}
|
|
999
|
+
}, [graphQLOptions]);
|
|
1000
|
+
|
|
1001
|
+
useEffect(() => {
|
|
1002
|
+
void load();
|
|
1003
|
+
}, [load]);
|
|
1004
|
+
|
|
1005
|
+
const refetch = useCallback(() => {
|
|
1006
|
+
void load();
|
|
1007
|
+
}, [load]);
|
|
1008
|
+
|
|
1009
|
+
return { counts, isLoading, error, refetch };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* The driver(s) currently assigned to a vehicle, resolved server-side through
|
|
1014
|
+
* `DriverFilterInput.vehicleId` (the same link `assignDriverVehicle` writes).
|
|
1015
|
+
* A vehicle normally has one driver, but the filter returns a list, so the
|
|
1016
|
+
* caller gets every assignment rather than a silently-picked first row.
|
|
1017
|
+
*/
|
|
1018
|
+
export function useVehicleDrivers(vehicleId: string | undefined) {
|
|
1019
|
+
const graphQLOptions = useGraphqlOptions();
|
|
1020
|
+
const [drivers, setDrivers] = useState<Driver[]>([]);
|
|
1021
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1022
|
+
const [error, setError] = useState<Error | null>(null);
|
|
1023
|
+
|
|
1024
|
+
const load = useCallback(async () => {
|
|
1025
|
+
if (!vehicleId) {
|
|
1026
|
+
setDrivers([]);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
setIsLoading(true);
|
|
1030
|
+
setError(null);
|
|
1031
|
+
try {
|
|
1032
|
+
const data = await executeGraphQL<{ drivers: ApiDriver[] }>(
|
|
1033
|
+
graphQLOptions,
|
|
1034
|
+
LIST_DRIVERS_QUERY,
|
|
1035
|
+
{
|
|
1036
|
+
filter: { vehicleId },
|
|
1037
|
+
pagination: { limit: 25, offset: 0 },
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
setDrivers(data.drivers.map(mapDriver));
|
|
1041
|
+
} catch (fetchError) {
|
|
1042
|
+
setError(
|
|
1043
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load assigned driver')
|
|
1044
|
+
);
|
|
1045
|
+
} finally {
|
|
1046
|
+
setIsLoading(false);
|
|
1047
|
+
}
|
|
1048
|
+
}, [graphQLOptions, vehicleId]);
|
|
1049
|
+
|
|
1050
|
+
useEffect(() => {
|
|
1051
|
+
void load();
|
|
1052
|
+
}, [load]);
|
|
1053
|
+
|
|
1054
|
+
const refetch = useCallback(() => {
|
|
1055
|
+
void load();
|
|
1056
|
+
}, [load]);
|
|
1057
|
+
|
|
1058
|
+
return { drivers, isLoading, error, refetch };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
931
1061
|
export function useFleetStats() {
|
|
932
1062
|
const graphQLOptions = useGraphqlOptions();
|
|
933
1063
|
const [stats, setStats] = useState({
|