@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
|
@@ -210,7 +210,7 @@ export default function OrderDetailsPage() {
|
|
|
210
210
|
</p>
|
|
211
211
|
)}
|
|
212
212
|
{order.actualPickup && (
|
|
213
|
-
<p className="text-sm text-
|
|
213
|
+
<p className="text-sm text-status-success-text mt-1">
|
|
214
214
|
<CheckCircle size={14} className="inline mr-1" />
|
|
215
215
|
Picked up: {formatDate(order.actualPickup, 'full')}
|
|
216
216
|
</p>
|
|
@@ -227,7 +227,7 @@ export default function OrderDetailsPage() {
|
|
|
227
227
|
</p>
|
|
228
228
|
)}
|
|
229
229
|
{order.actualDelivery && (
|
|
230
|
-
<p className="text-sm text-
|
|
230
|
+
<p className="text-sm text-status-success-text mt-1">
|
|
231
231
|
<CheckCircle size={14} className="inline mr-1" />
|
|
232
232
|
Delivered: {formatDate(order.actualDelivery, 'full')}
|
|
233
233
|
</p>
|
|
@@ -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,7 +98,11 @@ 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
108
|
{ label: 'Export', icon: <Download size={16} />, onSelect: () => {} },
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reports Page
|
|
3
|
+
*
|
|
4
|
+
* Lists reports from the backend (movethewheelsReports) and lets operators
|
|
5
|
+
* create, schedule and generate them via the real mutations. Pre-launch
|
|
6
|
+
* workspaces start empty — no fabricated report history is shown.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
|
-
import {
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { FileBarChart, Download, Calendar, Filter, Clock, Plus } from 'lucide-react';
|
|
6
11
|
import { PageLayout } from '../components/PageLayout';
|
|
7
12
|
import {
|
|
8
13
|
Card,
|
|
@@ -16,53 +21,77 @@ import {
|
|
|
16
21
|
SelectItem,
|
|
17
22
|
SelectTrigger,
|
|
18
23
|
SelectValue,
|
|
24
|
+
Skeleton,
|
|
19
25
|
} from '../components/ui';
|
|
20
|
-
import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
26
|
+
import { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
27
|
+
import { useReports, type CreateReportInput } from '../hooks/useReports';
|
|
28
|
+
import { formatDate } from '../utils/formatters';
|
|
29
|
+
|
|
30
|
+
const REPORT_TYPES: { value: string; label: string }[] = [
|
|
31
|
+
{ value: 'operations', label: 'Operations Summary' },
|
|
32
|
+
{ value: 'finance', label: 'Financial Report' },
|
|
33
|
+
{ value: 'fleet', label: 'Fleet Utilization' },
|
|
34
|
+
{ value: 'customers', label: 'Customer Analytics' },
|
|
35
|
+
{ value: 'inventory', label: 'Inventory Status' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
interface Feedback {
|
|
39
|
+
tone: 'success' | 'error';
|
|
40
|
+
text: string;
|
|
41
|
+
}
|
|
21
42
|
|
|
22
43
|
export default function ReportsPage() {
|
|
23
|
-
const reports =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
description:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
const { reports, isLoading, error, refetch, createReport, generateReport } = useReports();
|
|
45
|
+
const [reportType, setReportType] = useState('operations');
|
|
46
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
47
|
+
const [creating, setCreating] = useState(false);
|
|
48
|
+
const [feedback, setFeedback] = useState<Feedback | null>(null);
|
|
49
|
+
|
|
50
|
+
const typeLabel = (value: string) =>
|
|
51
|
+
REPORT_TYPES.find((item) => item.value === value)?.label ?? value;
|
|
52
|
+
|
|
53
|
+
const handleCreate = async (schedule?: string) => {
|
|
54
|
+
setCreating(true);
|
|
55
|
+
setFeedback(null);
|
|
56
|
+
const input: CreateReportInput = {
|
|
57
|
+
name: `${typeLabel(reportType)}${schedule ? ' (Scheduled)' : ''}`,
|
|
58
|
+
type: reportType,
|
|
59
|
+
description: `${typeLabel(reportType)} generated from live operational data`,
|
|
60
|
+
...(schedule ? { schedule } : {}),
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
await createReport(input);
|
|
64
|
+
setFeedback({
|
|
65
|
+
tone: 'success',
|
|
66
|
+
text: schedule ? 'Scheduled report created.' : 'Report created.',
|
|
67
|
+
});
|
|
68
|
+
} catch (createError) {
|
|
69
|
+
setFeedback({
|
|
70
|
+
tone: 'error',
|
|
71
|
+
text: createError instanceof Error ? createError.message : 'Could not create report.',
|
|
72
|
+
});
|
|
73
|
+
} finally {
|
|
74
|
+
setCreating(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleGenerate = async (id: string) => {
|
|
79
|
+
setBusyId(id);
|
|
80
|
+
setFeedback(null);
|
|
81
|
+
try {
|
|
82
|
+
await generateReport(id);
|
|
83
|
+
setFeedback({ tone: 'success', text: 'Report generated.' });
|
|
84
|
+
} catch (genError) {
|
|
85
|
+
setFeedback({
|
|
86
|
+
tone: 'error',
|
|
87
|
+
text: genError instanceof Error ? genError.message : 'Could not generate report.',
|
|
88
|
+
});
|
|
89
|
+
} finally {
|
|
90
|
+
setBusyId(null);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
61
93
|
|
|
62
|
-
const scheduledReports =
|
|
63
|
-
{ name: 'Daily Summary', nextRun: 'Tomorrow 6:00 AM', recipients: 3 },
|
|
64
|
-
{ name: 'Weekly Review', nextRun: 'Monday 9:00 AM', recipients: 5 },
|
|
65
|
-
];
|
|
94
|
+
const scheduledReports = reports.filter((report) => report.schedule);
|
|
66
95
|
|
|
67
96
|
return (
|
|
68
97
|
<PageLayout
|
|
@@ -71,19 +100,20 @@ export default function ReportsPage() {
|
|
|
71
100
|
icon={<FileBarChart size={24} />}
|
|
72
101
|
headerActions={
|
|
73
102
|
<div className="flex gap-2">
|
|
74
|
-
<Select
|
|
75
|
-
<SelectTrigger className="w-[
|
|
103
|
+
<Select value={reportType} onValueChange={setReportType}>
|
|
104
|
+
<SelectTrigger className="w-[190px]">
|
|
76
105
|
<SelectValue />
|
|
77
106
|
</SelectTrigger>
|
|
78
107
|
<SelectContent>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
{REPORT_TYPES.map((item) => (
|
|
109
|
+
<SelectItem key={item.value} value={item.value}>
|
|
110
|
+
{item.label}
|
|
111
|
+
</SelectItem>
|
|
112
|
+
))}
|
|
83
113
|
</SelectContent>
|
|
84
114
|
</Select>
|
|
85
|
-
<Button>
|
|
86
|
-
<
|
|
115
|
+
<Button onClick={() => void handleCreate()} disabled={creating}>
|
|
116
|
+
<Plus size={16} /> Create Report
|
|
87
117
|
</Button>
|
|
88
118
|
</div>
|
|
89
119
|
}
|
|
@@ -94,37 +124,94 @@ export default function ReportsPage() {
|
|
|
94
124
|
so stakeholders always have the latest numbers without logging in.
|
|
95
125
|
</PagePurpose>
|
|
96
126
|
|
|
127
|
+
{feedback && (
|
|
128
|
+
<div
|
|
129
|
+
role="status"
|
|
130
|
+
className={`rounded-lg p-3 text-sm ${
|
|
131
|
+
feedback.tone === 'success'
|
|
132
|
+
? 'bg-status-success-bg-subtle text-status-success-text'
|
|
133
|
+
: 'bg-status-error-bg-subtle text-status-error-text'
|
|
134
|
+
}`}
|
|
135
|
+
>
|
|
136
|
+
{feedback.text}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
97
140
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
98
141
|
<div className="lg:col-span-2 space-y-4">
|
|
99
142
|
<h3 className="font-semibold">Available Reports</h3>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
|
|
144
|
+
{isLoading ? (
|
|
145
|
+
<div className="space-y-3">
|
|
146
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
147
|
+
<Skeleton key={i} className="h-20 w-full rounded-lg" />
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
) : error ? (
|
|
151
|
+
<Card className="border-status-error-border">
|
|
152
|
+
<CardContent className="p-6 text-center space-y-3">
|
|
153
|
+
<p className="text-status-error-text font-medium">Couldn't load reports</p>
|
|
154
|
+
<p className="text-sm text-text-secondary">{error.message}</p>
|
|
155
|
+
<Button variant="outline" onClick={refetch}>
|
|
156
|
+
Retry
|
|
157
|
+
</Button>
|
|
158
|
+
</CardContent>
|
|
159
|
+
</Card>
|
|
160
|
+
) : reports.length === 0 ? (
|
|
161
|
+
<Card>
|
|
162
|
+
<CardContent className="p-0">
|
|
163
|
+
<IllustratedEmptyState
|
|
164
|
+
illustration="empty-data"
|
|
165
|
+
title="No reports yet"
|
|
166
|
+
description="Create your first report to turn live operational data into a shareable summary."
|
|
167
|
+
action={
|
|
168
|
+
<Button onClick={() => void handleCreate()} disabled={creating}>
|
|
169
|
+
<Plus size={16} className="mr-1.5" /> Create Report
|
|
122
170
|
</Button>
|
|
123
|
-
|
|
124
|
-
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
125
173
|
</CardContent>
|
|
126
174
|
</Card>
|
|
127
|
-
)
|
|
175
|
+
) : (
|
|
176
|
+
reports.map((report) => (
|
|
177
|
+
<Card key={report.id}>
|
|
178
|
+
<CardContent className="p-4">
|
|
179
|
+
<div className="flex items-center justify-between">
|
|
180
|
+
<div className="flex items-center gap-3">
|
|
181
|
+
<div className="w-10 h-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center">
|
|
182
|
+
<FileBarChart size={20} className="text-primary" />
|
|
183
|
+
</div>
|
|
184
|
+
<div>
|
|
185
|
+
<p className="font-medium">{report.name}</p>
|
|
186
|
+
<p className="text-sm text-text-secondary">
|
|
187
|
+
{report.description ?? typeLabel(report.type)}
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="flex items-center gap-3">
|
|
192
|
+
<div className="text-right">
|
|
193
|
+
<Badge variant="outline">{report.schedule ?? 'On demand'}</Badge>
|
|
194
|
+
{report.lastGenerated && (
|
|
195
|
+
<p className="text-xs text-text-secondary mt-1">
|
|
196
|
+
Last: {formatDate(new Date(report.lastGenerated), 'short')}
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
<Button
|
|
201
|
+
variant="outline"
|
|
202
|
+
size="sm"
|
|
203
|
+
aria-label={`Generate ${report.name}`}
|
|
204
|
+
disabled={busyId === report.id}
|
|
205
|
+
onClick={() => void handleGenerate(report.id)}
|
|
206
|
+
>
|
|
207
|
+
<Download size={14} />
|
|
208
|
+
</Button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</CardContent>
|
|
212
|
+
</Card>
|
|
213
|
+
))
|
|
214
|
+
)}
|
|
128
215
|
</div>
|
|
129
216
|
|
|
130
217
|
<div className="space-y-6">
|
|
@@ -133,14 +220,24 @@ export default function ReportsPage() {
|
|
|
133
220
|
<h2 className="flex items-center gap-2 text-base font-semibold text-text-primary">
|
|
134
221
|
<Clock size={18} /> Scheduled Reports
|
|
135
222
|
</h2>
|
|
136
|
-
{scheduledReports.
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
223
|
+
{scheduledReports.length === 0 ? (
|
|
224
|
+
<p className="text-sm text-text-secondary">
|
|
225
|
+
No scheduled reports yet. Schedule one to have it generated automatically.
|
|
226
|
+
</p>
|
|
227
|
+
) : (
|
|
228
|
+
scheduledReports.map((report) => (
|
|
229
|
+
<div key={report.id} className="p-3 border border-border-seam rounded-lg">
|
|
230
|
+
<p className="font-medium">{report.name}</p>
|
|
231
|
+
<p className="text-sm text-text-secondary">Schedule: {report.schedule}</p>
|
|
232
|
+
</div>
|
|
233
|
+
))
|
|
234
|
+
)}
|
|
235
|
+
<Button
|
|
236
|
+
variant="outline"
|
|
237
|
+
className="w-full"
|
|
238
|
+
disabled={creating}
|
|
239
|
+
onClick={() => void handleCreate('daily')}
|
|
240
|
+
>
|
|
144
241
|
Schedule New Report
|
|
145
242
|
</Button>
|
|
146
243
|
</EmphasisPanel>
|
|
@@ -150,15 +247,22 @@ export default function ReportsPage() {
|
|
|
150
247
|
<CardTitle>Quick Actions</CardTitle>
|
|
151
248
|
</CardHeader>
|
|
152
249
|
<CardContent className="space-y-2">
|
|
153
|
-
<Button
|
|
250
|
+
<Button
|
|
251
|
+
variant="outline"
|
|
252
|
+
className="w-full justify-start"
|
|
253
|
+
disabled={creating}
|
|
254
|
+
onClick={() => void handleCreate('weekly')}
|
|
255
|
+
>
|
|
154
256
|
<Calendar size={16} /> Schedule Report
|
|
155
257
|
</Button>
|
|
156
|
-
<Button
|
|
258
|
+
<Button
|
|
259
|
+
variant="outline"
|
|
260
|
+
className="w-full justify-start"
|
|
261
|
+
disabled={creating}
|
|
262
|
+
onClick={() => void handleCreate()}
|
|
263
|
+
>
|
|
157
264
|
<Filter size={16} /> Custom Report
|
|
158
265
|
</Button>
|
|
159
|
-
<Button variant="outline" className="w-full justify-start">
|
|
160
|
-
<Download size={16} /> Export All
|
|
161
|
-
</Button>
|
|
162
266
|
</CardContent>
|
|
163
267
|
</Card>
|
|
164
268
|
</div>
|