@burdenoff/microfe-movethewheels 2026.703.1 → 2026.703.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/{AIAssistantPage-C3ovZ0Fh.js → AIAssistantPage-C3cPsWt-.js} +1 -1
- package/dist/{AnalyticsPage-O8SpY-V4.js → AnalyticsPage-Dz6g2MvK.js} +6 -6
- package/dist/{CreateOrderPage-CB1_-_mN.js → CreateOrderPage-C5iwnjp4.js} +29 -58
- package/dist/CustomerDetailsPage-B6nwFyLO.js +406 -0
- package/dist/{CustomersPage-DRaLuHMC.js → CustomersPage-BKxMS1AV.js} +5 -5
- package/dist/{DashboardPage-DPjhNfKB.js → DashboardPage-BVjQi0EB.js} +11 -11
- package/dist/DriverDetailsPage-CN3WQ0B9.js +507 -0
- package/dist/{DriversPage-C2Bk0w4Q.js → DriversPage-CH9rjQC-.js} +6 -6
- package/dist/{FinancePage-lmp0Sdsw.js → FinancePage-DEDykSOP.js} +5 -5
- package/dist/{FleetPage-DZn5pIqd.js → FleetPage-DqkFi3s3.js} +5 -5
- package/dist/FormError-BlOjnXOq.js +15 -0
- package/dist/{ImportExportPage-BIUJZc7O.js → ImportExportPage-DKJRCezl.js} +1 -1
- package/dist/{InventoryPage-Cxh1zYJd.js → InventoryPage-e0ze63iX.js} +6 -6
- package/dist/{LiveTrackingPage-BY1yAkIt.js → LiveTrackingPage-DCX-K-LR.js} +5 -5
- package/dist/{MarketplacePage-RMuL17EA.js → MarketplacePage-CIQ97_zp.js} +1 -1
- package/dist/{OrderDetailsPage-C2Mf4KSy.js → OrderDetailsPage-DYiZ-9UK.js} +5 -5
- package/dist/{OrdersListPage-BrPBtnhp.js → OrdersListPage-Cn90P1Xx.js} +4 -4
- package/dist/ProductDetailsPage-C9uOiYLp.js +350 -0
- package/dist/{ProductsPage-I5DilIoY.js → ProductsPage-QJfHHpqB.js} +4 -4
- package/dist/{ReportsPage-BDXNmUng.js → ReportsPage-Zp53q3_8.js} +2 -2
- package/dist/RouteDetailsPage-DUStXSf9.js +531 -0
- package/dist/{RoutesPage-lqX9ylHP.js → RoutesPage-DMfoicM1.js} +3 -3
- package/dist/{SettingsPage-CQVIOaNs.js → SettingsPage-Bn-v0mbQ.js} +1 -1
- package/dist/{TrackingPage-BXrjz7V8.js → TrackingPage-W-jtdM3Y.js} +4 -4
- package/dist/VehicleDetailsPage-Bceo5oz3.js +410 -0
- package/dist/{VehiclesPage-6W39XT5W.js → VehiclesPage-ApKThYeG.js} +5 -5
- package/dist/WarehouseDetailsPage-L_6sWigM.js +493 -0
- package/dist/{WarehousesPage-CHvBqNaQ.js → WarehousesPage-DCbXXXWE.js} +3 -3
- package/dist/index.js +154 -138
- package/dist/trash-2-BKhar6Hs.js +32 -0
- package/dist/{useFleet-DiAC-IiH.js → useFleet-DKqXZ16m.js} +77 -56
- package/dist/{useInventory-DKoks37x.js → useInventory-DkyiVY_A.js} +73 -53
- package/dist/{useTracking-GJify4HN.js → useTracking-B-va1kO3.js} +2 -2
- package/package.json +1 -1
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +4 -0
- package/src/movethewheels/components/FormError.tsx +25 -0
- package/src/movethewheels/hooks/useFleet.ts +107 -0
- package/src/movethewheels/hooks/useInventory.ts +107 -0
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +153 -20
- package/src/movethewheels/pages/CustomersPage.tsx +1 -1
- package/src/movethewheels/pages/DriverDetailsPage.tsx +159 -2
- package/src/movethewheels/pages/DriversPage.tsx +6 -2
- package/src/movethewheels/pages/InventoryPage.tsx +1 -1
- package/src/movethewheels/pages/OrdersListPage.tsx +1 -1
- package/src/movethewheels/pages/ProductDetailsPage.tsx +146 -3
- package/src/movethewheels/pages/ProductsPage.tsx +6 -2
- package/src/movethewheels/pages/RouteDetailsPage.tsx +285 -25
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +158 -2
- package/src/movethewheels/pages/VehiclesPage.tsx +6 -2
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +224 -2
- package/src/movethewheels/pages/WarehousesPage.tsx +5 -1
- package/dist/CustomerDetailsPage-BaeKEI1D.js +0 -263
- package/dist/DriverDetailsPage-CSSQTOzn.js +0 -328
- package/dist/ProductDetailsPage-CezzzZcj.js +0 -193
- package/dist/RouteDetailsPage-BcomL20M.js +0 -263
- package/dist/VehicleDetailsPage-DZjLcJgr.js +0 -218
- package/dist/WarehouseDetailsPage-C10pgRGW.js +0 -236
- /package/dist/{MetricCard-7aYgzZHK.js → MetricCard-BY0QrMD4.js} +0 -0
- /package/dist/{StatusBadge-NpTeHVlw.js → StatusBadge-BZurYHMy.js} +0 -0
- /package/dist/{formatters-DhKZ-eA1.js → formatters-CF2S2l1m.js} +0 -0
- /package/dist/{trending-up-B7W-GL6P.js → trending-up-Dohgd4t_.js} +0 -0
- /package/dist/{triangle-alert-BonHFFNz.js → triangle-alert-i-XQL9Rd.js} +0 -0
- /package/dist/{truck-DrlfSpRC.js → truck-C0v_hbLR.js} +0 -0
- /package/dist/{useAnalytics-CgdzUfY_.js → useAnalytics-DS8n0xgg.js} +0 -0
- /package/dist/{useCustomers-QrlozybF.js → useCustomers-BmTNGxiI.js} +0 -0
- /package/dist/{useImportExport-BLMnAKCC.js → useImportExport-DHr-SWSw.js} +0 -0
- /package/dist/{useOrders-D3UZVy8Y.js → useOrders-Cfk1GLiq.js} +0 -0
- /package/dist/{useReports-Puq4DCH9.js → useReports-CTqZr-AB.js} +0 -0
- /package/dist/{useRoutes-Do57LvRb.js → useRoutes-BQWO607N.js} +0 -0
- /package/dist/{user-CjSoudeu.js → user-jyFU3lXk.js} +0 -0
- /package/dist/{users-BAM4gHTn.js → users-DZ-ilS6B.js} +0 -0
- /package/dist/{warehouse-C7MSr47B.js → warehouse-6m5kXMLu.js} +0 -0
|
@@ -2,15 +2,157 @@
|
|
|
2
2
|
* Product Details Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react';
|
|
5
6
|
import { useParams } from 'react-router-dom';
|
|
6
|
-
import { Package, ArrowLeft, Edit, Box, DollarSign, Tag } from 'lucide-react';
|
|
7
|
+
import { Package, ArrowLeft, Edit, Box, DollarSign, Tag, Save } from 'lucide-react';
|
|
7
8
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Card,
|
|
11
|
+
CardContent,
|
|
12
|
+
CardHeader,
|
|
13
|
+
CardTitle,
|
|
14
|
+
Button,
|
|
15
|
+
Badge,
|
|
16
|
+
Input,
|
|
17
|
+
Label,
|
|
18
|
+
} from '../components/ui';
|
|
9
19
|
import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
10
|
-
import { useProduct } from '../hooks/useInventory';
|
|
20
|
+
import { useProduct, useInventory } from '../hooks/useInventory';
|
|
11
21
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
12
22
|
import { formatCurrency } from '../utils/formatters';
|
|
13
23
|
import { joinPath } from '../utils/navigation';
|
|
24
|
+
import { FormError } from '../components/FormError';
|
|
25
|
+
|
|
26
|
+
function NewProductForm() {
|
|
27
|
+
const { basePath, navigate } = useMoveTheWheels();
|
|
28
|
+
const { createProduct } = useInventory();
|
|
29
|
+
const [form, setForm] = useState({
|
|
30
|
+
name: '',
|
|
31
|
+
sku: '',
|
|
32
|
+
category: '',
|
|
33
|
+
brand: '',
|
|
34
|
+
weight: '',
|
|
35
|
+
});
|
|
36
|
+
const [submitting, setSubmitting] = useState(false);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const goBack = () => navigate?.(joinPath(basePath, 'inventory/products'));
|
|
40
|
+
|
|
41
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
if (!form.name.trim() || !form.sku.trim()) {
|
|
44
|
+
setError('Product name and SKU are required.');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setSubmitting(true);
|
|
48
|
+
setError(null);
|
|
49
|
+
try {
|
|
50
|
+
const weight = form.weight.trim() ? Number(form.weight) : undefined;
|
|
51
|
+
const created = await createProduct({
|
|
52
|
+
name: form.name.trim(),
|
|
53
|
+
sku: form.sku.trim(),
|
|
54
|
+
category: form.category.trim() || undefined,
|
|
55
|
+
brand: form.brand.trim() || undefined,
|
|
56
|
+
weight: weight !== undefined && !Number.isNaN(weight) ? weight : undefined,
|
|
57
|
+
});
|
|
58
|
+
navigate?.(joinPath(basePath, `inventory/products/${created.id}`));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('Failed to create product:', err);
|
|
61
|
+
setError(err instanceof Error ? err.message : 'Failed to create product. Please try again.');
|
|
62
|
+
setSubmitting(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<PageLayout
|
|
68
|
+
title="Add Product"
|
|
69
|
+
subtitle="Add an item to your catalog"
|
|
70
|
+
icon={<Package size={24} />}
|
|
71
|
+
headerActions={
|
|
72
|
+
<div className="flex gap-2">
|
|
73
|
+
<Button variant="outline" onClick={goBack} disabled={submitting}>
|
|
74
|
+
<ArrowLeft size={16} /> Cancel
|
|
75
|
+
</Button>
|
|
76
|
+
<Button onClick={handleSubmit} disabled={submitting}>
|
|
77
|
+
<Save size={16} /> {submitting ? 'Saving...' : 'Add Product'}
|
|
78
|
+
</Button>
|
|
79
|
+
</div>
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
<PagePurpose icon={<Package size={18} />}>
|
|
83
|
+
Add a product so it can be added to orders and stocked in warehouses. A name and SKU are all
|
|
84
|
+
it takes to start — category, brand and weight help with picking, pricing and routing.
|
|
85
|
+
</PagePurpose>
|
|
86
|
+
|
|
87
|
+
<form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
|
|
88
|
+
<FormError message={error} />
|
|
89
|
+
<EmphasisPanel>
|
|
90
|
+
<h2 className="text-base font-semibold text-text-primary mb-4">Product Details</h2>
|
|
91
|
+
<div className="space-y-4">
|
|
92
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
93
|
+
<div className="space-y-2">
|
|
94
|
+
<Label htmlFor="name">
|
|
95
|
+
Name <span className="text-status-error-text">*</span>
|
|
96
|
+
</Label>
|
|
97
|
+
<Input
|
|
98
|
+
id="name"
|
|
99
|
+
value={form.name}
|
|
100
|
+
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
|
101
|
+
placeholder="Wireless Headphones"
|
|
102
|
+
required
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="space-y-2">
|
|
106
|
+
<Label htmlFor="sku">
|
|
107
|
+
SKU <span className="text-status-error-text">*</span>
|
|
108
|
+
</Label>
|
|
109
|
+
<Input
|
|
110
|
+
id="sku"
|
|
111
|
+
value={form.sku}
|
|
112
|
+
onChange={(e) => setForm((prev) => ({ ...prev, sku: e.target.value }))}
|
|
113
|
+
placeholder="SKU-001"
|
|
114
|
+
required
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
<Label htmlFor="category">Category</Label>
|
|
121
|
+
<Input
|
|
122
|
+
id="category"
|
|
123
|
+
value={form.category}
|
|
124
|
+
onChange={(e) => setForm((prev) => ({ ...prev, category: e.target.value }))}
|
|
125
|
+
placeholder="Electronics"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="space-y-2">
|
|
129
|
+
<Label htmlFor="brand">Brand</Label>
|
|
130
|
+
<Input
|
|
131
|
+
id="brand"
|
|
132
|
+
value={form.brand}
|
|
133
|
+
onChange={(e) => setForm((prev) => ({ ...prev, brand: e.target.value }))}
|
|
134
|
+
placeholder="Acme"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="space-y-2">
|
|
138
|
+
<Label htmlFor="weight">Weight (kg)</Label>
|
|
139
|
+
<Input
|
|
140
|
+
id="weight"
|
|
141
|
+
type="number"
|
|
142
|
+
min="0"
|
|
143
|
+
step="0.01"
|
|
144
|
+
value={form.weight}
|
|
145
|
+
onChange={(e) => setForm((prev) => ({ ...prev, weight: e.target.value }))}
|
|
146
|
+
placeholder="0.5"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</EmphasisPanel>
|
|
152
|
+
</form>
|
|
153
|
+
</PageLayout>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
14
156
|
|
|
15
157
|
export default function ProductDetailsPage() {
|
|
16
158
|
const { productId } = useParams<{ productId: string }>();
|
|
@@ -20,6 +162,7 @@ export default function ProductDetailsPage() {
|
|
|
20
162
|
const goBack = () => navigate?.(joinPath(basePath, 'inventory/products'));
|
|
21
163
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
22
164
|
|
|
165
|
+
if (!productId) return <NewProductForm />;
|
|
23
166
|
if (isLoading) return <LoadingState message="Loading product details..." />;
|
|
24
167
|
if (error || !product) return <ErrorState title="Product not found" onRetry={refetch} />;
|
|
25
168
|
|
|
@@ -98,10 +98,14 @@ export default function ProductsPage() {
|
|
|
98
98
|
icon={<Package size={24} />}
|
|
99
99
|
headerActions={
|
|
100
100
|
<CTAOverflowMenu
|
|
101
|
-
primary={{
|
|
101
|
+
primary={{
|
|
102
|
+
label: 'Add Product',
|
|
103
|
+
icon: <Plus size={16} />,
|
|
104
|
+
onSelect: () => goTo('inventory/products/new'),
|
|
105
|
+
}}
|
|
102
106
|
actions={[
|
|
103
107
|
{ label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
|
|
104
|
-
{ label: 'Export', icon: <Download size={16} />, onSelect: () =>
|
|
108
|
+
{ label: 'Export', icon: <Download size={16} />, onSelect: () => goTo('import-export') },
|
|
105
109
|
]}
|
|
106
110
|
/>
|
|
107
111
|
}
|
|
@@ -2,17 +2,298 @@
|
|
|
2
2
|
* Route Details Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react';
|
|
5
6
|
import { useParams } from 'react-router-dom';
|
|
6
|
-
import {
|
|
7
|
-
|
|
7
|
+
import {
|
|
8
|
+
Navigation,
|
|
9
|
+
ArrowLeft,
|
|
10
|
+
Edit,
|
|
11
|
+
MapPin,
|
|
12
|
+
Clock,
|
|
13
|
+
Zap,
|
|
14
|
+
CheckCircle,
|
|
15
|
+
Plus,
|
|
16
|
+
Trash2,
|
|
17
|
+
Save,
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
8
20
|
import { StatusBadge } from '../components/StatusBadge';
|
|
9
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
Card,
|
|
23
|
+
CardContent,
|
|
24
|
+
CardHeader,
|
|
25
|
+
CardTitle,
|
|
26
|
+
Button,
|
|
27
|
+
Badge,
|
|
28
|
+
Input,
|
|
29
|
+
Label,
|
|
30
|
+
Select,
|
|
31
|
+
SelectContent,
|
|
32
|
+
SelectItem,
|
|
33
|
+
SelectTrigger,
|
|
34
|
+
SelectValue,
|
|
35
|
+
} from '../components/ui';
|
|
10
36
|
import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
11
37
|
import { useRoute, useRoutes } from '../hooks/useRoutes';
|
|
38
|
+
import { useFleet } from '../hooks/useFleet';
|
|
12
39
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
13
40
|
import { formatDate, formatAddress } from '../utils/formatters';
|
|
14
41
|
import { joinPath } from '../utils/navigation';
|
|
15
42
|
import { ROUTE_STATUS_COLORS } from '../constants';
|
|
43
|
+
import { FormError } from '../components/FormError';
|
|
44
|
+
import type { Route as RouteType, RouteStop } from '../types';
|
|
45
|
+
|
|
46
|
+
const ROUTE_STATUS_OPTIONS: { value: RouteType['status']; label: string }[] = [
|
|
47
|
+
{ value: 'planned', label: 'Planned' },
|
|
48
|
+
{ value: 'in_progress', label: 'In Progress' },
|
|
49
|
+
{ value: 'completed', label: 'Completed' },
|
|
50
|
+
{ value: 'cancelled', label: 'Cancelled' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function NewRouteForm() {
|
|
54
|
+
const { basePath, navigate } = useMoveTheWheels();
|
|
55
|
+
const { createRoute } = useRoutes();
|
|
56
|
+
const { vehicles, drivers } = useFleet();
|
|
57
|
+
const [form, setForm] = useState<{
|
|
58
|
+
name: string;
|
|
59
|
+
status: RouteType['status'];
|
|
60
|
+
vehicleId: string;
|
|
61
|
+
driverId: string;
|
|
62
|
+
stops: { address: string; type: 'pickup' | 'delivery' }[];
|
|
63
|
+
}>({
|
|
64
|
+
name: '',
|
|
65
|
+
status: 'planned',
|
|
66
|
+
vehicleId: '',
|
|
67
|
+
driverId: '',
|
|
68
|
+
stops: [{ address: '', type: 'delivery' }],
|
|
69
|
+
});
|
|
70
|
+
const [submitting, setSubmitting] = useState(false);
|
|
71
|
+
const [error, setError] = useState<string | null>(null);
|
|
72
|
+
|
|
73
|
+
const goBack = () => navigate?.(joinPath(basePath, 'routes'));
|
|
74
|
+
|
|
75
|
+
const addStop = () =>
|
|
76
|
+
setForm((prev) => ({
|
|
77
|
+
...prev,
|
|
78
|
+
stops: [...prev.stops, { address: '', type: 'delivery' }],
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
const removeStop = (index: number) =>
|
|
82
|
+
setForm((prev) => ({
|
|
83
|
+
...prev,
|
|
84
|
+
stops: prev.stops.filter((_, i) => i !== index),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const updateStop = (index: number, field: 'address' | 'type', value: string) =>
|
|
88
|
+
setForm((prev) => ({
|
|
89
|
+
...prev,
|
|
90
|
+
stops: prev.stops.map((stop, i) => (i === index ? { ...stop, [field]: value } : stop)),
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
if (!form.name.trim()) {
|
|
96
|
+
setError('Route name is required.');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const filledStops = form.stops.filter((stop) => stop.address.trim());
|
|
100
|
+
if (filledStops.length === 0) {
|
|
101
|
+
setError('Add at least one stop with an address.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
setSubmitting(true);
|
|
105
|
+
setError(null);
|
|
106
|
+
try {
|
|
107
|
+
const stops: RouteStop[] = filledStops.map((stop, index) => ({
|
|
108
|
+
id: `stop-${index + 1}`,
|
|
109
|
+
orderId: '',
|
|
110
|
+
sequence: index + 1,
|
|
111
|
+
address: {
|
|
112
|
+
street: stop.address.trim(),
|
|
113
|
+
city: '',
|
|
114
|
+
state: '',
|
|
115
|
+
postalCode: '',
|
|
116
|
+
country: '',
|
|
117
|
+
},
|
|
118
|
+
type: stop.type,
|
|
119
|
+
scheduledTime: new Date(),
|
|
120
|
+
duration: 0,
|
|
121
|
+
status: 'pending',
|
|
122
|
+
}));
|
|
123
|
+
const created = await createRoute({
|
|
124
|
+
name: form.name.trim(),
|
|
125
|
+
status: form.status,
|
|
126
|
+
vehicleId: form.vehicleId || undefined,
|
|
127
|
+
driverId: form.driverId || undefined,
|
|
128
|
+
stops,
|
|
129
|
+
});
|
|
130
|
+
navigate?.(joinPath(basePath, `routes/${created.id}`));
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('Failed to create route:', err);
|
|
133
|
+
setError(err instanceof Error ? err.message : 'Failed to create route. Please try again.');
|
|
134
|
+
setSubmitting(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<PageLayout
|
|
140
|
+
title="Create Route"
|
|
141
|
+
subtitle="Plan a new delivery route"
|
|
142
|
+
icon={<Navigation size={24} />}
|
|
143
|
+
headerActions={
|
|
144
|
+
<div className="flex gap-2">
|
|
145
|
+
<Button variant="outline" onClick={goBack} disabled={submitting}>
|
|
146
|
+
<ArrowLeft size={16} /> Cancel
|
|
147
|
+
</Button>
|
|
148
|
+
<Button onClick={handleSubmit} disabled={submitting}>
|
|
149
|
+
<Save size={16} /> {submitting ? 'Saving...' : 'Create Route'}
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
}
|
|
153
|
+
>
|
|
154
|
+
<PagePurpose icon={<Navigation size={18} />}>
|
|
155
|
+
Plan a new delivery route — name it, list the stops to visit, and optionally assign a vehicle
|
|
156
|
+
and driver. Once created you can optimize it to sequence the stops into the shortest run.
|
|
157
|
+
</PagePurpose>
|
|
158
|
+
|
|
159
|
+
<form onSubmit={handleSubmit} className="max-w-3xl space-y-6">
|
|
160
|
+
<FormError message={error} />
|
|
161
|
+
|
|
162
|
+
<EmphasisPanel>
|
|
163
|
+
<h2 className="text-base font-semibold text-text-primary mb-4">Route Details</h2>
|
|
164
|
+
<div className="space-y-4">
|
|
165
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
166
|
+
<div className="space-y-2">
|
|
167
|
+
<Label htmlFor="name">
|
|
168
|
+
Route Name <span className="text-status-error-text">*</span>
|
|
169
|
+
</Label>
|
|
170
|
+
<Input
|
|
171
|
+
id="name"
|
|
172
|
+
value={form.name}
|
|
173
|
+
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
|
174
|
+
placeholder="Downtown morning run"
|
|
175
|
+
required
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
<div className="space-y-2">
|
|
179
|
+
<Label htmlFor="status">Status</Label>
|
|
180
|
+
<Select
|
|
181
|
+
value={form.status}
|
|
182
|
+
onValueChange={(value) =>
|
|
183
|
+
setForm((prev) => ({ ...prev, status: value as RouteType['status'] }))
|
|
184
|
+
}
|
|
185
|
+
>
|
|
186
|
+
<SelectTrigger id="status">
|
|
187
|
+
<SelectValue />
|
|
188
|
+
</SelectTrigger>
|
|
189
|
+
<SelectContent>
|
|
190
|
+
{ROUTE_STATUS_OPTIONS.map((option) => (
|
|
191
|
+
<SelectItem key={option.value} value={option.value}>
|
|
192
|
+
{option.label}
|
|
193
|
+
</SelectItem>
|
|
194
|
+
))}
|
|
195
|
+
</SelectContent>
|
|
196
|
+
</Select>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
200
|
+
<div className="space-y-2">
|
|
201
|
+
<Label htmlFor="vehicle">Vehicle (optional)</Label>
|
|
202
|
+
<Select
|
|
203
|
+
value={form.vehicleId}
|
|
204
|
+
onValueChange={(value) => setForm((prev) => ({ ...prev, vehicleId: value }))}
|
|
205
|
+
>
|
|
206
|
+
<SelectTrigger id="vehicle">
|
|
207
|
+
<SelectValue placeholder="Assign later" />
|
|
208
|
+
</SelectTrigger>
|
|
209
|
+
<SelectContent>
|
|
210
|
+
{vehicles.map((vehicle) => (
|
|
211
|
+
<SelectItem key={vehicle.id} value={vehicle.id}>
|
|
212
|
+
{vehicle.vehicleNumber} ({vehicle.type})
|
|
213
|
+
</SelectItem>
|
|
214
|
+
))}
|
|
215
|
+
</SelectContent>
|
|
216
|
+
</Select>
|
|
217
|
+
</div>
|
|
218
|
+
<div className="space-y-2">
|
|
219
|
+
<Label htmlFor="driver">Driver (optional)</Label>
|
|
220
|
+
<Select
|
|
221
|
+
value={form.driverId}
|
|
222
|
+
onValueChange={(value) => setForm((prev) => ({ ...prev, driverId: value }))}
|
|
223
|
+
>
|
|
224
|
+
<SelectTrigger id="driver">
|
|
225
|
+
<SelectValue placeholder="Assign later" />
|
|
226
|
+
</SelectTrigger>
|
|
227
|
+
<SelectContent>
|
|
228
|
+
{drivers.map((driver) => (
|
|
229
|
+
<SelectItem key={driver.id} value={driver.id}>
|
|
230
|
+
{driver.fullName} ({driver.employeeId})
|
|
231
|
+
</SelectItem>
|
|
232
|
+
))}
|
|
233
|
+
</SelectContent>
|
|
234
|
+
</Select>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</EmphasisPanel>
|
|
239
|
+
|
|
240
|
+
<Card>
|
|
241
|
+
<CardHeader className="flex flex-row items-center justify-between">
|
|
242
|
+
<CardTitle>
|
|
243
|
+
Stops <span className="text-status-error-text">*</span>
|
|
244
|
+
</CardTitle>
|
|
245
|
+
<Button type="button" variant="outline" size="sm" onClick={addStop}>
|
|
246
|
+
<Plus size={16} /> Add Stop
|
|
247
|
+
</Button>
|
|
248
|
+
</CardHeader>
|
|
249
|
+
<CardContent className="space-y-4">
|
|
250
|
+
{form.stops.map((stop, index) => (
|
|
251
|
+
<div key={index} className="grid grid-cols-1 md:grid-cols-12 gap-3 items-end">
|
|
252
|
+
<div className="md:col-span-7 space-y-2">
|
|
253
|
+
<Label htmlFor={`stop-address-${index}`}>Stop {index + 1} address</Label>
|
|
254
|
+
<Input
|
|
255
|
+
id={`stop-address-${index}`}
|
|
256
|
+
value={stop.address}
|
|
257
|
+
onChange={(e) => updateStop(index, 'address', e.target.value)}
|
|
258
|
+
placeholder="123 Main St, San Francisco, CA"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
<div className="md:col-span-4 space-y-2">
|
|
262
|
+
<Label htmlFor={`stop-type-${index}`}>Type</Label>
|
|
263
|
+
<Select
|
|
264
|
+
value={stop.type}
|
|
265
|
+
onValueChange={(value) => updateStop(index, 'type', value)}
|
|
266
|
+
>
|
|
267
|
+
<SelectTrigger id={`stop-type-${index}`}>
|
|
268
|
+
<SelectValue />
|
|
269
|
+
</SelectTrigger>
|
|
270
|
+
<SelectContent>
|
|
271
|
+
<SelectItem value="pickup">Pickup</SelectItem>
|
|
272
|
+
<SelectItem value="delivery">Delivery</SelectItem>
|
|
273
|
+
</SelectContent>
|
|
274
|
+
</Select>
|
|
275
|
+
</div>
|
|
276
|
+
<div className="md:col-span-1 flex justify-end">
|
|
277
|
+
<Button
|
|
278
|
+
type="button"
|
|
279
|
+
variant="ghost"
|
|
280
|
+
size="icon"
|
|
281
|
+
className="text-status-error-text"
|
|
282
|
+
onClick={() => removeStop(index)}
|
|
283
|
+
disabled={form.stops.length === 1}
|
|
284
|
+
aria-label={`Remove stop ${index + 1}`}
|
|
285
|
+
>
|
|
286
|
+
<Trash2 size={16} />
|
|
287
|
+
</Button>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
))}
|
|
291
|
+
</CardContent>
|
|
292
|
+
</Card>
|
|
293
|
+
</form>
|
|
294
|
+
</PageLayout>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
16
297
|
|
|
17
298
|
export default function RouteDetailsPage() {
|
|
18
299
|
const { routeId } = useParams<{ routeId: string }>();
|
|
@@ -25,28 +306,7 @@ export default function RouteDetailsPage() {
|
|
|
25
306
|
|
|
26
307
|
// Handle new route form
|
|
27
308
|
if (!routeId) {
|
|
28
|
-
return
|
|
29
|
-
<PageLayout
|
|
30
|
-
title="Create Route"
|
|
31
|
-
subtitle="Plan a new delivery route"
|
|
32
|
-
icon={<Navigation size={24} />}
|
|
33
|
-
headerActions={
|
|
34
|
-
<Button variant="outline" onClick={goBack}>
|
|
35
|
-
<ArrowLeft size={16} /> Back
|
|
36
|
-
</Button>
|
|
37
|
-
}
|
|
38
|
-
>
|
|
39
|
-
<PagePurpose icon={<Navigation size={18} />}>
|
|
40
|
-
Plan a new delivery route — pick the stops to visit, and the optimizer will sequence them
|
|
41
|
-
into the shortest driving plan so one driver can cover them in a single run.
|
|
42
|
-
</PagePurpose>
|
|
43
|
-
<EmptyState
|
|
44
|
-
illustration="onboarding"
|
|
45
|
-
title="Build your route"
|
|
46
|
-
description="The route planner lets you add stops, set the running order, and optimize for distance and time."
|
|
47
|
-
/>
|
|
48
|
-
</PageLayout>
|
|
49
|
-
);
|
|
309
|
+
return <NewRouteForm />;
|
|
50
310
|
}
|
|
51
311
|
|
|
52
312
|
if (isLoading) return <LoadingState message="Loading route details..." />;
|