@burdenoff/microfe-movethewheels 2026.626.2 → 2026.703.2
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-AvsMbunM.js → AIAssistantPage-C3cPsWt-.js} +100 -73
- package/dist/AnalyticsPage-Dz6g2MvK.js +215 -0
- package/dist/{CreateOrderPage-CjlnEtk3.js → CreateOrderPage-C5iwnjp4.js} +36 -67
- package/dist/CustomerDetailsPage-B6nwFyLO.js +406 -0
- package/dist/{CustomersPage-CGGfD4ki.js → CustomersPage-2Y05LVyh.js} +26 -28
- package/dist/{DashboardPage-D2HRqwg9.js → DashboardPage-BVjQi0EB.js} +85 -83
- package/dist/{DataTable-CD9ZhpLB.js → DataTable-DmciIKKM.js} +3 -3
- package/dist/DriverDetailsPage-CN3WQ0B9.js +507 -0
- package/dist/{DriversPage-DKiynISH.js → DriversPage-B0Zpx8OQ.js} +25 -27
- package/dist/{FinancePage-Vg8WPs38.js → FinancePage-DEDykSOP.js} +9 -9
- package/dist/FleetPage-DqkFi3s3.js +365 -0
- package/dist/FormError-BlOjnXOq.js +15 -0
- package/dist/ImportExportPage-DKJRCezl.js +290 -0
- package/dist/InventoryPage-DwfgCUQq.js +253 -0
- package/dist/{LiveTrackingPage-DQ1q4Id3.js → LiveTrackingPage-DCX-K-LR.js} +60 -56
- package/dist/MarketplacePage-CIQ97_zp.js +247 -0
- package/dist/{MetricCard-B7yMyvMX.js → MetricCard-BY0QrMD4.js} +1 -1
- package/dist/{OrderDetailsPage-CFASgsYX.js → OrderDetailsPage-DYiZ-9UK.js} +83 -85
- package/dist/{OrdersListPage-BHeT8S5o.js → OrdersListPage-BjDlCKJd.js} +31 -33
- package/dist/{PageLayout-D3N8MCTm.js → PageLayout-D4LDHJJq.js} +483 -422
- package/dist/ProductDetailsPage-C9uOiYLp.js +350 -0
- package/dist/{ProductsPage--xEpbQ8A.js → ProductsPage-C1G9h7Xd.js} +22 -24
- package/dist/ReportsPage-Zp53q3_8.js +284 -0
- package/dist/RouteDetailsPage-DUStXSf9.js +531 -0
- package/dist/RoutesPage-DMfoicM1.js +144 -0
- package/dist/SettingsPage-Bn-v0mbQ.js +421 -0
- package/dist/StatusBadge-BZurYHMy.js +152 -0
- package/dist/{TrackingPage-CcRc_dva.js → TrackingPage-W-jtdM3Y.js} +59 -61
- package/dist/VehicleDetailsPage-Bceo5oz3.js +410 -0
- package/dist/{VehiclesPage-UlxPWP9K.js → VehiclesPage-VBtOep91.js} +20 -22
- package/dist/WarehouseDetailsPage-L_6sWigM.js +493 -0
- package/dist/{WarehousesPage-CuTO-OPp.js → WarehousesPage-DCbXXXWE.js} +16 -18
- package/dist/{arrow-left-CBpZKkpq.js → arrow-left-n48Y-Wis.js} +1 -1
- package/dist/{box-CAaiElsb.js → box-D9Pal4HM.js} +1 -1
- package/dist/{chart-column-D1Ym15-S.js → chart-column-BFIJysDR.js} +1 -1
- package/dist/{chevron-right-BIFGi0UA.js → chevron-right-EMIIyyJ1.js} +1 -1
- package/dist/{circle-alert-DjmNo5_L.js → circle-alert-iY24lSlp.js} +1 -1
- package/dist/{circle-check-big-CjuPVo3_.js → circle-check-big-w0TrLKMF.js} +1 -1
- package/dist/{clock-BjkdnddD.js → clock-DJZtdl8u.js} +1 -1
- package/dist/{dollar-sign-IB4O0wuQ.js → dollar-sign-CgKQRz_e.js} +1 -1
- package/dist/{download-Dpa9vqoG.js → download-DRXxoiQR.js} +1 -1
- package/dist/{file-text-C8Uioy11.js → file-text-DWWY6_f5.js} +1 -1
- package/dist/{filter-Da5OZaHI.js → filter-DloLC2eE.js} +1 -1
- package/dist/index.js +186 -162
- package/dist/{map-_Per4sT9.js → map-CkJaULb2.js} +1 -1
- package/dist/{map-pin-BSaf5IDL.js → map-pin-BGiCvyCs.js} +1 -1
- package/dist/{navigation-DaTbrhw2.js → navigation-D36prnAn.js} +1 -1
- package/dist/{package-BE7U0wQE.js → package-BChUzpJS.js} +1 -1
- package/dist/{phone-DV4StZiA.js → phone-Ac9s3mo_.js} +1 -1
- package/dist/{plus-Dl1rLeJR.js → plus-Z76q-FZm.js} +1 -1
- package/dist/{refresh-cw-CWfERAit.js → refresh-cw-DKt6f5T6.js} +1 -1
- package/dist/{save-XH8vydOn.js → save-CojywNhG.js} +1 -1
- package/dist/{search-OxoIiL_C.js → search-CDGefqb3.js} +1 -1
- package/dist/{square-pen-hLIbFQFV.js → square-pen-CopE9GmB.js} +1 -1
- package/dist/{star-YsVV3MNQ.js → star-nHlLjDKR.js} +1 -1
- package/dist/{store-RCBl7HB-.js → store-BR9Voi5E.js} +1 -1
- package/dist/trash-2-BKhar6Hs.js +32 -0
- package/dist/{trending-up-C-85AwGT.js → trending-up-Dohgd4t_.js} +1 -1
- package/dist/{triangle-alert-BOhfGgmh.js → triangle-alert-i-XQL9Rd.js} +1 -1
- package/dist/{truck-CtDjNFbi.js → truck-C0v_hbLR.js} +1 -1
- package/dist/{useAnalytics-ph7eTIK6.js → useAnalytics-DS8n0xgg.js} +40 -40
- package/dist/useAssistant-D4v3oxD3.js +84 -0
- package/dist/{useCustomers-bS3a4ytk.js → useCustomers-BmTNGxiI.js} +24 -24
- package/dist/{useFleet-BdETplNE.js → useFleet-DKqXZ16m.js} +133 -109
- package/dist/useImportExport-DHr-SWSw.js +55 -0
- package/dist/{useInventory-Dwn18FPz.js → useInventory-DkyiVY_A.js} +95 -75
- package/dist/{useOrders-D_3_hGMp.js → useOrders-Cfk1GLiq.js} +42 -42
- package/dist/useReports-CTqZr-AB.js +78 -0
- package/dist/{useRoutes-v4aBaS-E.js → useRoutes-BQWO607N.js} +31 -31
- package/dist/useSettings-CkDH_-gM.js +81 -0
- package/dist/{useTracking-De2KIUNu.js → useTracking-B-va1kO3.js} +22 -22
- package/dist/{user-Dz5TJZFs.js → user-jyFU3lXk.js} +1 -1
- package/dist/{users-g2N0eL-Z.js → users-DZ-ilS6B.js} +1 -1
- package/dist/{warehouse-D3WWDeTX.js → warehouse-6m5kXMLu.js} +1 -1
- package/dist/{wrench-CmxY1n4I.js → wrench-CANXWxF-.js} +1 -1
- package/package.json +1 -1
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +31 -3
- package/src/movethewheels/components/FormError.tsx +25 -0
- package/src/movethewheels/components/StatusBadge.tsx +18 -37
- package/src/movethewheels/constants/index.ts +67 -26
- package/src/movethewheels/hooks/index.ts +12 -0
- package/src/movethewheels/hooks/useAnalytics.ts +20 -6
- package/src/movethewheels/hooks/useAssistant.ts +74 -0
- package/src/movethewheels/hooks/useFleet.ts +170 -59
- package/src/movethewheels/hooks/useImportExport.ts +101 -0
- package/src/movethewheels/hooks/useInventory.ts +107 -0
- package/src/movethewheels/hooks/useReports.ts +158 -0
- package/src/movethewheels/hooks/useSettings.ts +120 -0
- package/src/movethewheels/pages/AIAssistantPage.tsx +53 -15
- package/src/movethewheels/pages/AnalyticsPage.tsx +67 -64
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +154 -21
- package/src/movethewheels/pages/CustomersPage.tsx +1 -1
- package/src/movethewheels/pages/DashboardPage.tsx +42 -22
- package/src/movethewheels/pages/DriverDetailsPage.tsx +160 -3
- package/src/movethewheels/pages/DriversPage.tsx +6 -2
- package/src/movethewheels/pages/FleetPage.tsx +96 -38
- package/src/movethewheels/pages/ImportExportPage.tsx +174 -68
- package/src/movethewheels/pages/InventoryPage.tsx +66 -28
- package/src/movethewheels/pages/LiveTrackingPage.tsx +12 -3
- package/src/movethewheels/pages/MarketplacePage.tsx +87 -45
- package/src/movethewheels/pages/OrderDetailsPage.tsx +2 -2
- package/src/movethewheels/pages/ProductDetailsPage.tsx +146 -3
- package/src/movethewheels/pages/ProductsPage.tsx +5 -1
- package/src/movethewheels/pages/ReportsPage.tsx +194 -90
- package/src/movethewheels/pages/RouteDetailsPage.tsx +290 -36
- package/src/movethewheels/pages/RoutesPage.tsx +4 -9
- package/src/movethewheels/pages/SettingsPage.tsx +234 -56
- package/src/movethewheels/pages/TrackingPage.tsx +6 -6
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +158 -2
- package/src/movethewheels/pages/VehiclesPage.tsx +5 -1
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +224 -2
- package/src/movethewheels/pages/WarehousesPage.tsx +5 -1
- package/src/movethewheels/providers/MoveTheWheelsProvider.tsx +7 -6
- package/src/movethewheels/store/movethewheelsStore.ts +1 -1
- package/dist/AnalyticsPage-CCLfHgQZ.js +0 -212
- package/dist/CustomerDetailsPage-I6cdDHUi.js +0 -265
- package/dist/DriverDetailsPage-rg_pGNuP.js +0 -330
- package/dist/FleetPage-DddXt8r8.js +0 -332
- package/dist/ImportExportPage-BW6qO3kc.js +0 -248
- package/dist/InventoryPage-rMGuaJN-.js +0 -222
- package/dist/MarketplacePage-DKrXAv62.js +0 -219
- package/dist/ProductDetailsPage-CFGcbaHo.js +0 -195
- package/dist/ReportsPage-Bb7_90rK.js +0 -235
- package/dist/RouteDetailsPage-CVtDVb1N.js +0 -282
- package/dist/RoutesPage-s57PtgcQ.js +0 -163
- package/dist/SettingsPage-BYNCBOpN.js +0 -298
- package/dist/StatusBadge-CijYQXtb.js +0 -209
- package/dist/VehicleDetailsPage-3YDMPsPs.js +0 -220
- package/dist/WarehouseDetailsPage-Db7KeN-d.js +0 -238
- package/dist/graphqlClient-CdJyR_ed.js +0 -55
- package/dist/navigation-BgnOfsVd.js +0 -6
- package/dist/route-Dp9JiN_E.js +0 -51
- package/dist/settings-3Rw5NwFF.js +0 -28
- package/src/movethewheels/constants/mockData.ts +0 -1342
- /package/dist/{formatters-_vJlC-47.js → formatters-CF2S2l1m.js} +0 -0
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import { BarChart3, TrendingUp, Package, Truck, DollarSign, Users } from 'lucide-react';
|
|
6
6
|
import { PageLayout } from '../components/PageLayout';
|
|
7
|
-
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
8
|
-
import { Card, CardContent, CardHeader, CardTitle, Progress } from '../components/ui';
|
|
7
|
+
import { MetricCard, MetricGrid, MetricCardSkeleton } from '../components/MetricCard';
|
|
8
|
+
import { Card, CardContent, CardHeader, CardTitle, Progress, Button } from '../components/ui';
|
|
9
9
|
import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
10
10
|
import { useAnalytics, useOrderAnalytics, useFleetAnalytics } from '../hooks/useAnalytics';
|
|
11
11
|
import { formatCurrency } from '../utils/formatters';
|
|
12
12
|
|
|
13
13
|
export default function AnalyticsPage() {
|
|
14
|
-
const { analytics } = useAnalytics();
|
|
14
|
+
const { analytics, isLoading, error, refetch } = useAnalytics();
|
|
15
15
|
const { orderData } = useOrderAnalytics();
|
|
16
16
|
const { fleetData } = useFleetAnalytics();
|
|
17
17
|
|
|
@@ -27,43 +27,55 @@ export default function AnalyticsPage() {
|
|
|
27
27
|
to invest — whether that's more vehicles, better routes, or new customers.
|
|
28
28
|
</PagePurpose>
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
30
|
+
{error && !isLoading ? (
|
|
31
|
+
<Card className="border-status-error-border">
|
|
32
|
+
<CardContent className="p-6 text-center space-y-3">
|
|
33
|
+
<p className="text-status-error-text font-medium">Couldn't load analytics</p>
|
|
34
|
+
<p className="text-sm text-text-secondary">{error.message}</p>
|
|
35
|
+
<Button variant="outline" onClick={refetch}>
|
|
36
|
+
Retry
|
|
37
|
+
</Button>
|
|
38
|
+
</CardContent>
|
|
39
|
+
</Card>
|
|
40
|
+
) : isLoading ? (
|
|
41
|
+
<MetricGrid columns={5}>
|
|
42
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
43
|
+
<MetricCardSkeleton key={i} />
|
|
44
|
+
))}
|
|
45
|
+
</MetricGrid>
|
|
46
|
+
) : (
|
|
47
|
+
<MetricGrid columns={5}>
|
|
48
|
+
<MetricCard
|
|
49
|
+
label="Total Orders"
|
|
50
|
+
value={analytics.overview.totalOrders.toLocaleString()}
|
|
51
|
+
icon={<Package size={20} />}
|
|
52
|
+
/>
|
|
53
|
+
<MetricCard
|
|
54
|
+
treatment="glass"
|
|
55
|
+
glow
|
|
56
|
+
label="Revenue"
|
|
57
|
+
value={formatCurrency(analytics.overview.totalRevenue)}
|
|
58
|
+
icon={<DollarSign size={20} />}
|
|
59
|
+
/>
|
|
60
|
+
<MetricCard
|
|
61
|
+
label="Active Vehicles"
|
|
62
|
+
value={analytics.overview.activeVehicles}
|
|
63
|
+
icon={<Truck size={20} />}
|
|
64
|
+
/>
|
|
65
|
+
<MetricCard
|
|
66
|
+
label="On-Time Rate"
|
|
67
|
+
value={`${analytics.overview.onTimeDeliveryRate.toFixed(1)}%`}
|
|
68
|
+
icon={<TrendingUp size={20} />}
|
|
69
|
+
changeType={analytics.overview.onTimeDeliveryRate >= 90 ? 'positive' : 'neutral'}
|
|
70
|
+
/>
|
|
71
|
+
<MetricCard
|
|
72
|
+
label="Satisfaction"
|
|
73
|
+
value={analytics.overview.customerSatisfaction.toFixed(1)}
|
|
74
|
+
icon={<Users size={20} />}
|
|
75
|
+
subtitle="out of 5"
|
|
76
|
+
/>
|
|
77
|
+
</MetricGrid>
|
|
78
|
+
)}
|
|
67
79
|
|
|
68
80
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
69
81
|
<Card>
|
|
@@ -149,45 +161,36 @@ export default function AnalyticsPage() {
|
|
|
149
161
|
</div>
|
|
150
162
|
<Progress value={fleetData.vehicleUtilization} />
|
|
151
163
|
</div>
|
|
152
|
-
|
|
153
|
-
<div className="
|
|
154
|
-
<
|
|
155
|
-
|
|
164
|
+
{fleetData.vehiclesByType.length > 0 && (
|
|
165
|
+
<div className="pt-2">
|
|
166
|
+
<p className="text-sm text-text-secondary mb-2">Vehicles by Type</p>
|
|
167
|
+
{fleetData.vehiclesByType.map(({ type, count }) => (
|
|
168
|
+
<div key={type} className="flex justify-between text-sm py-1">
|
|
169
|
+
<span className="capitalize">{type}</span>
|
|
170
|
+
<span className="font-medium">{count}</span>
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
156
173
|
</div>
|
|
157
|
-
|
|
158
|
-
</div>
|
|
159
|
-
<div className="pt-2">
|
|
160
|
-
<p className="text-sm text-text-secondary mb-2">Vehicles by Type</p>
|
|
161
|
-
{fleetData.vehiclesByType.map(({ type, count }) => (
|
|
162
|
-
<div key={type} className="flex justify-between text-sm py-1">
|
|
163
|
-
<span className="capitalize">{type}</span>
|
|
164
|
-
<span className="font-medium">{count}</span>
|
|
165
|
-
</div>
|
|
166
|
-
))}
|
|
167
|
-
</div>
|
|
174
|
+
)}
|
|
168
175
|
</CardContent>
|
|
169
176
|
</Card>
|
|
170
177
|
|
|
171
178
|
{/* Performance Metrics — the page's single emphasis zone */}
|
|
172
179
|
<EmphasisPanel className="space-y-4">
|
|
173
180
|
<h2 className="text-base font-semibold text-text-primary">Performance Metrics</h2>
|
|
174
|
-
<div>
|
|
175
|
-
<p className="text-sm text-text-secondary">On-Time Pickup</p>
|
|
176
|
-
<p className="text-2xl font-bold">{analytics.performance.onTimePickup}%</p>
|
|
177
|
-
</div>
|
|
178
181
|
<div>
|
|
179
182
|
<p className="text-sm text-text-secondary">On-Time Delivery</p>
|
|
180
|
-
<p className="text-2xl font-bold">{analytics.performance.onTimeDelivery}%</p>
|
|
183
|
+
<p className="text-2xl font-bold">{analytics.performance.onTimeDelivery.toFixed(1)}%</p>
|
|
181
184
|
</div>
|
|
182
185
|
<div>
|
|
183
|
-
<p className="text-sm text-text-secondary">
|
|
184
|
-
<p className="text-2xl font-bold
|
|
185
|
-
{analytics.
|
|
186
|
+
<p className="text-sm text-text-secondary">Customer Satisfaction</p>
|
|
187
|
+
<p className="text-2xl font-bold">
|
|
188
|
+
{analytics.overview.customerSatisfaction.toFixed(1)}/5
|
|
186
189
|
</p>
|
|
187
190
|
</div>
|
|
188
191
|
<div>
|
|
189
192
|
<p className="text-sm text-text-secondary">Driver Ratings</p>
|
|
190
|
-
<p className="text-2xl font-bold">{analytics.performance.driverRatings}/5</p>
|
|
193
|
+
<p className="text-2xl font-bold">{analytics.performance.driverRatings.toFixed(1)}/5</p>
|
|
191
194
|
</div>
|
|
192
195
|
</EmphasisPanel>
|
|
193
196
|
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Customer Details Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react';
|
|
5
6
|
import { useParams } from 'react-router-dom';
|
|
6
7
|
import {
|
|
7
8
|
Users,
|
|
@@ -13,15 +14,164 @@ import {
|
|
|
13
14
|
Star,
|
|
14
15
|
Building,
|
|
15
16
|
FileText,
|
|
17
|
+
Save,
|
|
16
18
|
} from 'lucide-react';
|
|
17
19
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
18
20
|
import { TypeBadge } from '../components/StatusBadge';
|
|
19
|
-
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';
|
|
20
36
|
import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
21
|
-
import { useCustomer } from '../hooks/useCustomers';
|
|
37
|
+
import { useCustomer, useCustomers } from '../hooks/useCustomers';
|
|
22
38
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
23
39
|
import { formatAddress, formatDate } from '../utils/formatters';
|
|
24
40
|
import { joinPath } from '../utils/navigation';
|
|
41
|
+
import { FormError } from '../components/FormError';
|
|
42
|
+
|
|
43
|
+
function NewCustomerForm() {
|
|
44
|
+
const { basePath, navigate } = useMoveTheWheels();
|
|
45
|
+
const { createCustomer } = useCustomers();
|
|
46
|
+
const [form, setForm] = useState({
|
|
47
|
+
name: '',
|
|
48
|
+
type: 'b2c' as 'b2b' | 'b2c',
|
|
49
|
+
email: '',
|
|
50
|
+
phone: '',
|
|
51
|
+
company: '',
|
|
52
|
+
});
|
|
53
|
+
const [submitting, setSubmitting] = useState(false);
|
|
54
|
+
const [error, setError] = useState<string | null>(null);
|
|
55
|
+
|
|
56
|
+
const goBack = () => navigate?.(joinPath(basePath, 'customers'));
|
|
57
|
+
|
|
58
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
if (!form.name.trim()) {
|
|
61
|
+
setError('Customer name is required.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
setSubmitting(true);
|
|
65
|
+
setError(null);
|
|
66
|
+
try {
|
|
67
|
+
const created = await createCustomer({
|
|
68
|
+
name: form.name.trim(),
|
|
69
|
+
type: form.type,
|
|
70
|
+
email: form.email.trim() || undefined,
|
|
71
|
+
phone: form.phone.trim() || undefined,
|
|
72
|
+
company: form.company.trim() || undefined,
|
|
73
|
+
});
|
|
74
|
+
navigate?.(joinPath(basePath, `customers/${created.id}`));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('Failed to create customer:', err);
|
|
77
|
+
setError(err instanceof Error ? err.message : 'Failed to create customer. Please try again.');
|
|
78
|
+
setSubmitting(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<PageLayout
|
|
84
|
+
title="New Customer"
|
|
85
|
+
subtitle="Add a new customer to your account"
|
|
86
|
+
icon={<Users size={24} />}
|
|
87
|
+
headerActions={
|
|
88
|
+
<div className="flex gap-2">
|
|
89
|
+
<Button variant="outline" onClick={goBack} disabled={submitting}>
|
|
90
|
+
<ArrowLeft size={16} /> Cancel
|
|
91
|
+
</Button>
|
|
92
|
+
<Button onClick={handleSubmit} disabled={submitting}>
|
|
93
|
+
<Save size={16} /> {submitting ? 'Saving...' : 'Create Customer'}
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
<PagePurpose icon={<Users size={18} />}>
|
|
99
|
+
Add a customer once and their contact details auto-fill every future order you book for them.
|
|
100
|
+
Only a name is required — add email, phone and company to make deliveries easier to arrange.
|
|
101
|
+
</PagePurpose>
|
|
102
|
+
|
|
103
|
+
<form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
|
|
104
|
+
<FormError message={error} />
|
|
105
|
+
<EmphasisPanel>
|
|
106
|
+
<h2 className="text-base font-semibold text-text-primary mb-4">Customer Details</h2>
|
|
107
|
+
<div className="space-y-4">
|
|
108
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
109
|
+
<div className="space-y-2">
|
|
110
|
+
<Label htmlFor="name">
|
|
111
|
+
Name <span className="text-status-error-text">*</span>
|
|
112
|
+
</Label>
|
|
113
|
+
<Input
|
|
114
|
+
id="name"
|
|
115
|
+
value={form.name}
|
|
116
|
+
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
|
117
|
+
placeholder="Acme Corp or Jane Doe"
|
|
118
|
+
required
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="space-y-2">
|
|
122
|
+
<Label htmlFor="type">Customer Type</Label>
|
|
123
|
+
<Select
|
|
124
|
+
value={form.type}
|
|
125
|
+
onValueChange={(value) =>
|
|
126
|
+
setForm((prev) => ({ ...prev, type: value as 'b2b' | 'b2c' }))
|
|
127
|
+
}
|
|
128
|
+
>
|
|
129
|
+
<SelectTrigger id="type">
|
|
130
|
+
<SelectValue />
|
|
131
|
+
</SelectTrigger>
|
|
132
|
+
<SelectContent>
|
|
133
|
+
<SelectItem value="b2c">B2C (Individual)</SelectItem>
|
|
134
|
+
<SelectItem value="b2b">B2B (Business)</SelectItem>
|
|
135
|
+
</SelectContent>
|
|
136
|
+
</Select>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
140
|
+
<div className="space-y-2">
|
|
141
|
+
<Label htmlFor="email">Email</Label>
|
|
142
|
+
<Input
|
|
143
|
+
id="email"
|
|
144
|
+
type="email"
|
|
145
|
+
value={form.email}
|
|
146
|
+
onChange={(e) => setForm((prev) => ({ ...prev, email: e.target.value }))}
|
|
147
|
+
placeholder="customer@example.com"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
<Label htmlFor="phone">Phone</Label>
|
|
152
|
+
<Input
|
|
153
|
+
id="phone"
|
|
154
|
+
value={form.phone}
|
|
155
|
+
onChange={(e) => setForm((prev) => ({ ...prev, phone: e.target.value }))}
|
|
156
|
+
placeholder="+1-555-0100"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div className="space-y-2">
|
|
161
|
+
<Label htmlFor="company">Company</Label>
|
|
162
|
+
<Input
|
|
163
|
+
id="company"
|
|
164
|
+
value={form.company}
|
|
165
|
+
onChange={(e) => setForm((prev) => ({ ...prev, company: e.target.value }))}
|
|
166
|
+
placeholder="Company name (for B2B accounts)"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</EmphasisPanel>
|
|
171
|
+
</form>
|
|
172
|
+
</PageLayout>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
25
175
|
|
|
26
176
|
export default function CustomerDetailsPage() {
|
|
27
177
|
const { customerId } = useParams<{ customerId: string }>();
|
|
@@ -33,24 +183,7 @@ export default function CustomerDetailsPage() {
|
|
|
33
183
|
|
|
34
184
|
// Handle new customer form
|
|
35
185
|
if (!customerId) {
|
|
36
|
-
return
|
|
37
|
-
<PageLayout
|
|
38
|
-
title="New Customer"
|
|
39
|
-
subtitle="Add a new customer"
|
|
40
|
-
icon={<Users size={24} />}
|
|
41
|
-
headerActions={
|
|
42
|
-
<Button variant="outline" onClick={goBack}>
|
|
43
|
-
<ArrowLeft size={16} /> Back
|
|
44
|
-
</Button>
|
|
45
|
-
}
|
|
46
|
-
>
|
|
47
|
-
<Card>
|
|
48
|
-
<CardContent className="p-8 text-center">
|
|
49
|
-
<p className="text-text-secondary">Customer creation form would go here</p>
|
|
50
|
-
</CardContent>
|
|
51
|
-
</Card>
|
|
52
|
-
</PageLayout>
|
|
53
|
-
);
|
|
186
|
+
return <NewCustomerForm />;
|
|
54
187
|
}
|
|
55
188
|
|
|
56
189
|
if (isLoading) return <LoadingState message="Loading customer details..." />;
|
|
@@ -78,7 +211,7 @@ export default function CustomerDetailsPage() {
|
|
|
78
211
|
{customer.isActive ? 'Active' : 'Inactive'}
|
|
79
212
|
</Badge>
|
|
80
213
|
<div className="flex items-center gap-1">
|
|
81
|
-
<Star size={16} className="text-
|
|
214
|
+
<Star size={16} className="text-status-warning-text fill-current" />
|
|
82
215
|
<span className="font-medium">{customer.rating.toFixed(1)}</span>
|
|
83
216
|
</div>
|
|
84
217
|
</div>
|
|
@@ -59,7 +59,7 @@ export default function CustomersPage() {
|
|
|
59
59
|
header: 'Rating',
|
|
60
60
|
cell: (row) => (
|
|
61
61
|
<div className="flex items-center gap-1">
|
|
62
|
-
<Star size={14} className="text-
|
|
62
|
+
<Star size={14} className="text-status-warning-text fill-current" />
|
|
63
63
|
<span>{row.rating.toFixed(1)}</span>
|
|
64
64
|
</div>
|
|
65
65
|
),
|
|
@@ -56,6 +56,13 @@ export default function DashboardPage() {
|
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
const onRowKeyDown = (e: React.KeyboardEvent, action: () => void) => {
|
|
60
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
action();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
59
66
|
return (
|
|
60
67
|
<PageLayout
|
|
61
68
|
hero
|
|
@@ -93,9 +100,6 @@ export default function DashboardPage() {
|
|
|
93
100
|
label={t('movethewheels.dashboard.metrics.totalOrders', 'Total Orders')}
|
|
94
101
|
value={metrics.orders.total}
|
|
95
102
|
icon={<Package size={20} />}
|
|
96
|
-
change={`+${metrics.orders.today} ${t('movethewheels.dashboard.metrics.today', 'today')}`}
|
|
97
|
-
changeType="positive"
|
|
98
|
-
trend="up"
|
|
99
103
|
onClick={() => goTo('orders')}
|
|
100
104
|
/>
|
|
101
105
|
<MetricCard
|
|
@@ -118,9 +122,6 @@ export default function DashboardPage() {
|
|
|
118
122
|
label={t('movethewheels.dashboard.metrics.revenue', 'Revenue')}
|
|
119
123
|
value={formatCurrency(metrics.revenue.total)}
|
|
120
124
|
icon={<DollarSign size={20} />}
|
|
121
|
-
change="+12.5%"
|
|
122
|
-
changeType="positive"
|
|
123
|
-
trend="up"
|
|
124
125
|
onClick={() => goTo('analytics')}
|
|
125
126
|
/>
|
|
126
127
|
</MetricGrid>
|
|
@@ -167,8 +168,12 @@ export default function DashboardPage() {
|
|
|
167
168
|
{orders.slice(0, 5).map((order) => (
|
|
168
169
|
<div
|
|
169
170
|
key={order.id}
|
|
170
|
-
|
|
171
|
+
role="button"
|
|
172
|
+
tabIndex={0}
|
|
173
|
+
aria-label={`View order ${order.orderNumber}`}
|
|
174
|
+
className="p-4 hover:bg-bg-sunken/50 cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg"
|
|
171
175
|
onClick={() => goTo(`orders/${order.id}`)}
|
|
176
|
+
onKeyDown={(e) => onRowKeyDown(e, () => goTo(`orders/${order.id}`))}
|
|
172
177
|
>
|
|
173
178
|
<div className="flex items-center justify-between">
|
|
174
179
|
<div>
|
|
@@ -211,8 +216,15 @@ export default function DashboardPage() {
|
|
|
211
216
|
delivery.order ? (
|
|
212
217
|
<div
|
|
213
218
|
key={delivery.order.id}
|
|
214
|
-
|
|
219
|
+
role="button"
|
|
220
|
+
tabIndex={0}
|
|
221
|
+
aria-label={`Track order ${delivery.order.orderNumber}`}
|
|
222
|
+
className="p-4 hover:bg-bg-sunken/50 cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg"
|
|
215
223
|
onClick={() => goTo(`tracking?order=${delivery.order.id}`)}
|
|
224
|
+
onKeyDown={(e) =>
|
|
225
|
+
delivery.order &&
|
|
226
|
+
onRowKeyDown(e, () => goTo(`tracking?order=${delivery.order.id}`))
|
|
227
|
+
}
|
|
216
228
|
>
|
|
217
229
|
<div className="flex items-center justify-between">
|
|
218
230
|
<div>
|
|
@@ -257,18 +269,26 @@ export default function DashboardPage() {
|
|
|
257
269
|
</h2>
|
|
258
270
|
<div className="space-y-3">
|
|
259
271
|
{[
|
|
260
|
-
{
|
|
272
|
+
{
|
|
273
|
+
status: 'pending',
|
|
274
|
+
count: orderStats.byStatus.pending,
|
|
275
|
+
color: 'bg-status-warning-text',
|
|
276
|
+
},
|
|
261
277
|
{
|
|
262
278
|
status: 'in_transit',
|
|
263
279
|
count: orderStats.byStatus.in_transit,
|
|
264
|
-
color: 'bg-
|
|
280
|
+
color: 'bg-status-info-text',
|
|
265
281
|
},
|
|
266
282
|
{
|
|
267
283
|
status: 'delivered',
|
|
268
284
|
count: orderStats.byStatus.delivered,
|
|
269
|
-
color: 'bg-
|
|
285
|
+
color: 'bg-status-success-text',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
status: 'cancelled',
|
|
289
|
+
count: orderStats.byStatus.cancelled,
|
|
290
|
+
color: 'bg-status-error-text',
|
|
270
291
|
},
|
|
271
|
-
{ status: 'cancelled', count: orderStats.byStatus.cancelled, color: 'bg-red-500' },
|
|
272
292
|
].map(({ status, count, color }) => (
|
|
273
293
|
<div key={status} className="flex items-center justify-between">
|
|
274
294
|
<div className="flex items-center gap-2">
|
|
@@ -313,7 +333,7 @@ export default function DashboardPage() {
|
|
|
313
333
|
<p className="text-text-secondary">
|
|
314
334
|
{t('movethewheels.dashboard.fleetStatus.available', 'Available')}
|
|
315
335
|
</p>
|
|
316
|
-
<p className="text-lg font-semibold text-
|
|
336
|
+
<p className="text-lg font-semibold text-status-success-text">
|
|
317
337
|
{fleetStats.vehiclesByStatus.available}
|
|
318
338
|
</p>
|
|
319
339
|
</div>
|
|
@@ -321,7 +341,7 @@ export default function DashboardPage() {
|
|
|
321
341
|
<p className="text-text-secondary">
|
|
322
342
|
{t('movethewheels.dashboard.fleetStatus.inUse', 'In Use')}
|
|
323
343
|
</p>
|
|
324
|
-
<p className="text-lg font-semibold text-
|
|
344
|
+
<p className="text-lg font-semibold text-status-info-text">
|
|
325
345
|
{fleetStats.vehiclesByStatus.assigned}
|
|
326
346
|
</p>
|
|
327
347
|
</div>
|
|
@@ -329,7 +349,7 @@ export default function DashboardPage() {
|
|
|
329
349
|
<p className="text-text-secondary">
|
|
330
350
|
{t('movethewheels.dashboard.fleetStatus.maintenance', 'Maintenance')}
|
|
331
351
|
</p>
|
|
332
|
-
<p className="text-lg font-semibold text-
|
|
352
|
+
<p className="text-lg font-semibold text-status-warning-text">
|
|
333
353
|
{fleetStats.vehiclesByStatus.maintenance}
|
|
334
354
|
</p>
|
|
335
355
|
</div>
|
|
@@ -347,15 +367,15 @@ export default function DashboardPage() {
|
|
|
347
367
|
<Card>
|
|
348
368
|
<CardHeader>
|
|
349
369
|
<CardTitle className="text-base flex items-center gap-2">
|
|
350
|
-
<AlertTriangle size={16} className="text-
|
|
370
|
+
<AlertTriangle size={16} className="text-status-warning-text" />
|
|
351
371
|
{t('movethewheels.dashboard.alerts.title', 'Alerts')}
|
|
352
372
|
</CardTitle>
|
|
353
373
|
</CardHeader>
|
|
354
374
|
<CardContent>
|
|
355
375
|
<div className="space-y-3">
|
|
356
376
|
{orderStats.byStatus.pending > 5 && (
|
|
357
|
-
<div className="flex items-start gap-3 p-2 rounded bg-
|
|
358
|
-
<Clock size={16} className="text-
|
|
377
|
+
<div className="flex items-start gap-3 p-2 rounded bg-status-warning-bg-subtle">
|
|
378
|
+
<Clock size={16} className="text-status-warning-text mt-0.5" />
|
|
359
379
|
<div>
|
|
360
380
|
<p className="text-sm font-medium">
|
|
361
381
|
{t('movethewheels.dashboard.alerts.pendingOrders.title', 'Pending Orders')}
|
|
@@ -371,8 +391,8 @@ export default function DashboardPage() {
|
|
|
371
391
|
</div>
|
|
372
392
|
)}
|
|
373
393
|
{fleetStats.vehiclesByStatus.maintenance > 0 && (
|
|
374
|
-
<div className="flex items-start gap-3 p-2 rounded bg-
|
|
375
|
-
<Truck size={16} className="text-
|
|
394
|
+
<div className="flex items-start gap-3 p-2 rounded bg-status-warning-bg-subtle">
|
|
395
|
+
<Truck size={16} className="text-status-warning-text mt-0.5" />
|
|
376
396
|
<div>
|
|
377
397
|
<p className="text-sm font-medium">
|
|
378
398
|
{t(
|
|
@@ -391,8 +411,8 @@ export default function DashboardPage() {
|
|
|
391
411
|
</div>
|
|
392
412
|
)}
|
|
393
413
|
{fleetStats.driversByStatus.offline > 0 && (
|
|
394
|
-
<div className="flex items-start gap-3 p-2 rounded bg-
|
|
395
|
-
<Users size={16} className="text-
|
|
414
|
+
<div className="flex items-start gap-3 p-2 rounded bg-bg-sunken">
|
|
415
|
+
<Users size={16} className="text-text-secondary mt-0.5" />
|
|
396
416
|
<div>
|
|
397
417
|
<p className="text-sm font-medium">
|
|
398
418
|
{t('movethewheels.dashboard.alerts.offlineDrivers.title', 'Offline Drivers')}
|