create-bluecopa-react-app 1.0.0
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/README.md +162 -0
- package/bin/create-bluecopa-react-app.js +171 -0
- package/package.json +40 -0
- package/templates/basic/.editorconfig +12 -0
- package/templates/basic/.env.example +10 -0
- package/templates/basic/README.md +213 -0
- package/templates/basic/index.html +13 -0
- package/templates/basic/package-lock.json +6343 -0
- package/templates/basic/package.json +68 -0
- package/templates/basic/postcss.config.js +6 -0
- package/templates/basic/setup.sh +46 -0
- package/templates/basic/src/App.tsx +95 -0
- package/templates/basic/src/components/dashboard/AdvancedAnalytics.tsx +351 -0
- package/templates/basic/src/components/dashboard/MetricsOverview.tsx +150 -0
- package/templates/basic/src/components/dashboard/TransactionCharts.tsx +215 -0
- package/templates/basic/src/components/dashboard/TransactionTable.tsx +172 -0
- package/templates/basic/src/components/ui/button.tsx +64 -0
- package/templates/basic/src/components/ui/card.tsx +79 -0
- package/templates/basic/src/components/ui/tabs.tsx +53 -0
- package/templates/basic/src/index.css +59 -0
- package/templates/basic/src/lib/utils.ts +6 -0
- package/templates/basic/src/main.tsx +9 -0
- package/templates/basic/src/pages/Dashboard.tsx +135 -0
- package/templates/basic/src/types/index.ts +94 -0
- package/templates/basic/tailwind.config.js +77 -0
- package/templates/basic/tsconfig.json +31 -0
- package/templates/basic/tsconfig.node.json +10 -0
- package/templates/basic/vite.config.ts +13 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bluecopa-dashboard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "BlueCopa Dashboard Template with TanStack Query and shadcn/ui",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"type-check": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@bluecopa/react": "^0.1.3",
|
|
15
|
+
"@radix-ui/react-accordion": "^1.1.2",
|
|
16
|
+
"@radix-ui/react-alert-dialog": "^1.0.5",
|
|
17
|
+
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
|
18
|
+
"@radix-ui/react-avatar": "^1.0.4",
|
|
19
|
+
"@radix-ui/react-checkbox": "^1.0.4",
|
|
20
|
+
"@radix-ui/react-collapsible": "^1.0.3",
|
|
21
|
+
"@radix-ui/react-dialog": "^1.0.5",
|
|
22
|
+
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
|
23
|
+
"@radix-ui/react-hover-card": "^1.0.7",
|
|
24
|
+
"@radix-ui/react-label": "^2.0.2",
|
|
25
|
+
"@radix-ui/react-menubar": "^1.0.4",
|
|
26
|
+
"@radix-ui/react-navigation-menu": "^1.1.4",
|
|
27
|
+
"@radix-ui/react-popover": "^1.0.7",
|
|
28
|
+
"@radix-ui/react-progress": "^1.0.3",
|
|
29
|
+
"@radix-ui/react-radio-group": "^1.1.3",
|
|
30
|
+
"@radix-ui/react-scroll-area": "^1.0.5",
|
|
31
|
+
"@radix-ui/react-select": "^2.0.0",
|
|
32
|
+
"@radix-ui/react-separator": "^1.0.3",
|
|
33
|
+
"@radix-ui/react-slider": "^1.1.2",
|
|
34
|
+
"@radix-ui/react-slot": "^1.0.2",
|
|
35
|
+
"@radix-ui/react-switch": "^1.0.3",
|
|
36
|
+
"@radix-ui/react-tabs": "^1.0.4",
|
|
37
|
+
"@radix-ui/react-toast": "^1.1.5",
|
|
38
|
+
"@radix-ui/react-toggle": "^1.0.3",
|
|
39
|
+
"@radix-ui/react-toggle-group": "^1.0.4",
|
|
40
|
+
"@radix-ui/react-tooltip": "^1.0.7",
|
|
41
|
+
"axios": "^1.6.0",
|
|
42
|
+
"class-variance-authority": "^0.7.0",
|
|
43
|
+
"clsx": "^2.0.0",
|
|
44
|
+
"date-fns": "^2.30.0",
|
|
45
|
+
"lucide-react": "^0.292.0",
|
|
46
|
+
"react": "^18.2.0",
|
|
47
|
+
"react-dom": "^18.2.0",
|
|
48
|
+
"react-router-dom": "^6.20.0",
|
|
49
|
+
"recharts": "^2.8.0",
|
|
50
|
+
"tailwind-merge": "^2.0.0",
|
|
51
|
+
"tailwindcss-animate": "^1.0.7"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/react": "^18.2.37",
|
|
55
|
+
"@types/react-dom": "^18.2.15",
|
|
56
|
+
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
|
57
|
+
"@typescript-eslint/parser": "^6.10.0",
|
|
58
|
+
"@vitejs/plugin-react": "^4.1.1",
|
|
59
|
+
"autoprefixer": "^10.4.16",
|
|
60
|
+
"eslint": "^8.53.0",
|
|
61
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
62
|
+
"eslint-plugin-react-refresh": "^0.4.4",
|
|
63
|
+
"postcss": "^8.4.31",
|
|
64
|
+
"tailwindcss": "^3.3.5",
|
|
65
|
+
"typescript": "^5.2.2",
|
|
66
|
+
"vite": "^4.5.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# BlueCopa React Dashboard Template Setup Script
|
|
4
|
+
# This script sets up the BlueCopa React dashboard template
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
PROJECT_NAME=${1:-bluecopa-dashboard}
|
|
9
|
+
TEMPLATE_DIR="$(dirname "$0")"
|
|
10
|
+
|
|
11
|
+
echo "🚀 Setting up BlueCopa React Dashboard: $PROJECT_NAME"
|
|
12
|
+
|
|
13
|
+
# Create project directory
|
|
14
|
+
if [ -d "$PROJECT_NAME" ]; then
|
|
15
|
+
echo "❌ Directory $PROJECT_NAME already exists"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Copy template files
|
|
20
|
+
echo "📁 Creating project structure..."
|
|
21
|
+
cp -r "$TEMPLATE_DIR" "$PROJECT_NAME"
|
|
22
|
+
cd "$PROJECT_NAME"
|
|
23
|
+
|
|
24
|
+
# Remove setup script from the new project
|
|
25
|
+
rm -f setup.sh
|
|
26
|
+
|
|
27
|
+
# Create .env file from example
|
|
28
|
+
if [ -f ".env.example" ]; then
|
|
29
|
+
cp .env.example .env
|
|
30
|
+
echo "📝 Created .env file from template"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Initialize git repository
|
|
34
|
+
echo "🔧 Initializing git repository..."
|
|
35
|
+
git init
|
|
36
|
+
git add .
|
|
37
|
+
git commit -m "Initial commit: BlueCopa React Dashboard template"
|
|
38
|
+
|
|
39
|
+
echo "✅ Project setup complete!"
|
|
40
|
+
echo ""
|
|
41
|
+
echo "Next steps:"
|
|
42
|
+
echo "1. cd $PROJECT_NAME"
|
|
43
|
+
echo "2. npm install"
|
|
44
|
+
echo "3. npm run dev"
|
|
45
|
+
echo ""
|
|
46
|
+
echo "🎉 Happy coding with BlueCopa!"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { reactQuery, ReactQueryDevtools, copaSetConfig } from '@bluecopa/react';
|
|
2
|
+
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
|
3
|
+
import Dashboard from './pages/Dashboard';
|
|
4
|
+
import './index.css';
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
const {QueryClient, QueryClientProvider} = reactQuery
|
|
8
|
+
// Create a client
|
|
9
|
+
const queryClient = new QueryClient({
|
|
10
|
+
defaultOptions: {
|
|
11
|
+
queries: {
|
|
12
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
13
|
+
// cacheTime: 1000 * 60 * 10, // 10 minutes (removed, not a valid property)
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function App() {
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let config = {} as any;
|
|
22
|
+
try {
|
|
23
|
+
config = JSON.parse(
|
|
24
|
+
atob(
|
|
25
|
+
'eyJhY2Nlc3NUb2tlbiI6ImV5SnJhV1FpT2lJd1pHSXpNemRoWXkwek9EVm1MVFJpT0RndFltWm1NQzFrTURJek5HUTFNamMxWlRnaUxDSmhiR2NpT2lKU1V6STFOaUo5LmV5SnpkV0lpT2lJd1ZFOXNiVlZ4ZURCVVMzTmxWRzQ0VnpablZ5SXNJbk4xWW1wbFkzUmZkSGx3WlNJNklsVnpaWElpTENKcGMzTWlPaUpvZEhSd2N6b3ZMMkpzZFdWamIzQmhMbU52YlNJc0lteGhjM1JmYm1GdFpTSTZJblJsYzNScGJtZGtaWFpBWW14MVpXTnZjR0V1WTI5dElpd2lkRzlyWlc1ZmRIbHdaU0k2SWtGalkyVnpjMVJ2YTJWdUlpd2liM0puWDJsa0lqb2lNRlJQYkd4VE9HY3pNa04xYmpoaVdsaDNXR3NpTENKcFpIQmZZV05qWlhOelgzUnZhMlZ1SWpvaU1ERkxORU0wVXpSSFJEWkRVVGhPVFUxR1JsTkJXbFpDUVRnaUxDSmxlSEFpT2pFM05UY3dPVE16T0RJc0ltbGhkQ0k2TVRjMU56QTROakU0TWl3aVptbHljM1JmYm1GdFpTSTZJblJsYzNScGJtZGtaWFpBWW14MVpXTnZjR0V1WTI5dElpd2lhblJwSWpvaU1GVjJkMEZaVUhwTU1VTm5NVlpUUkRKWFNqY2lMQ0psYldGcGJDSTZJblJsYzNScGJtZGtaWFpBWW14MVpXTnZjR0V1WTI5dEluMC51V2p3cHBCeExCNm95Q1JrSVdacUhmNGllTmVGQlFKXzdkRVRpelNCT0lwZUw2N1BKVzE1SUJhZFlPaTBnUm1PS3F2ZTlqXy00a2pUaFhBZnJxSTZ3M01ZUXkxT2pfV09NbS1LdEVNZDFxWDRoQmVhcDZJdWdWOURXNkpuT01lZ19iZUZhU3pfLTNDWWd4dlgxc3hna3BQUm9hZkpvdjZhMXU5VmV2aDlibTdabjFTbzZFeGp6RDZDa2toX0JDbkVyM1hmcWd0TGdvOHRlXzVuVVlFa1ZRdk5TSW9SNFFNWEFCbk9mTDA0WEp2VHBXS05DY1ZGTUJVYkxsNnZHNUpxUEhmbjNzTU5YbTdMTVN3OFdTaWV6elFQa2NnS0hvNy1XRk5HSUZYdmgwRFhtUEwzeloxVkljVE9senAyZWI5NWFiMnBfdEN5NUt0QjhrbFMyUzVheThKLTExcDRfYzlPYkxFZGJoWmZJRWpYbmlNMUhvWmtHcDhTN1VadG5fUDBtcFBYQlRmQVNob29YbEEwaDZqcVg0am5XaktsaDBPY1lNTlRLbWFfYk84Y2lHb2xxdkU0enljaGdZTEs5a2FWN0Jrcm9wbXJpTGlEMnNULU0tMlY5X0ZRbDZRdFdnMnllRl9SSUdqOGl3b194MUFxWjlDWjFTamZVVmFoSDFVWXQ0NlhOWkYyX0pjdnhEZ19tcE9vRU9kbjRQSDJRWlVSZS1xX0ZUdXdmN284TEg2R3IwdmNOWVpVWXBuMTBWaFRPbFFRZWlRYlQycmZySDVhallmaTY1RVBUZFQ2b1RNdjFxalo4ZTRTRzV2SVFsWkRYVW5fUE1GNlgtTk04NDZBWkd3M092TWtGMlNvdFR0S0Fvb1MxX0dMRDExd3ltZWNaSk96c2FyVF96ZyIsInJlZnJlc2hUb2tlbiI6IkFRSUNBSGd5VlBZaGhhUkFnU29PUk5CbjBBR205WmF4WktUakxVNHBXY3NiMGVyU0NRSDJYYm12MHU2Qk5DMVlTUVA3Z0dnOEFBQUIvRENDQWZnR0NTcUdTSWIzRFFFSEJxQ0NBZWt3Z2dIbEFnRUFNSUlCM2dZSktvWklodmNOQVFjQk1CNEdDV0NHU0FGbEF3UUJMakFSQkF6bk9HSGhjOFdKYXpPdUFQc0NBUkNBZ2dHdlBsYnNieEYwVlNSMllVc1F0bWZ5OWdiVmZaS1NTMU04ZUJCaXdjRU9NdjZzS0lZQzFtZGdtM21rZkJWY3pMb0Z0WThYTjIrQlpoREdWN0dBSmkzeDJOZFJKVmpWMHN4MmN4RWlDNmFOZll1cWFHbnFFZW5oN3VycFhIS1dnVjRMcUxLQi9TVmtnV2pNWWxnYjVXY2FOM0ZucjN5d0VEV0psaDRXYjB4NmJjVFFyRTdnb3lpRm9jN3VTb0VkNzh0YkdsdW1GdXNNSXhMTzR2R2x4UG96UThxYS8wSVZ0dThrYUJVamNhbU9uVHQ5UHUvUHBwV0xHeEN1M24xUzVBa21VZmxzRkFvMkNOM3kwNklyb0oxYmdWTEpRdFhZN0dab0VYQjhwWUY1UWpJMUVCZ1BzQ0N1eTZ0VldrMnk3YzRIYzdlUHBiMm9KRVMycE53WEZqNUtZU1MzaC85cVVLM284Z3didHkzeEtac290dGVJc2N3S21hVGJBWitVN2srM2Nkb3dUUVZ1b0tKSk9HeXdEM1piYkNSVkhKYjQrQUZ4aWc0ZG5YUW1PTU5aNTZaeVhQQTFqSmNOc1Izc0pRWWNGWCtQY1lOcXdYRmdYTjl6czZleTkxRU9lcUV1WGNCR2xjMVhSRlBCaEE3bUw2NEQ0VjE5UXVJMm0ySkkybkNwMzh6bmtaMVpDZlhSQnVLdE5HWmpWU2wzMit4N3VjUDlsQ2QxSnlDNzY2YS96TVQ4YjJPeGtPbHdISnVoa2pBPSIsIm9yZ0lkIjoib3JnXzAxR0U2Rzk3RUVaNU5LQlhTV0VZRUdaSEVLIiwiZW1haWwiOiJ0ZXN0aW5nZGV2QGJsdWVjb3BhLmNvbSIsIndvcmtzcGFjZUlkIjoicHJvZCIsInVzZXJJZCI6IjBUT2xtVXF4MFRLc2VUbjhXNmdXIiwidXNlck5hbWUiOiJ0ZXN0aW5nZGV2QGJsdWVjb3BhLmNvbSB0ZXN0aW5nZGV2QGJsdWVjb3BhLmNvbSIsIndlYlVybCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9zZXR0aW5ncy9kZXZpY2VzIiwiZGF0YVBsYW5lIjoiaHR0cHM6Ly9hcGktZGV2ZWxvcC5ibHVlY29wYS5jb20vZGF0YS1wbGFuZS1hcGkiLCJkZngiOiJodHRwczovL2FwaS1kZXZlbG9wLmJsdWVjb3BhLmNvbS9kZngiLCJhdXRoeiI6Imh0dHBzOi8vZGV2YXBpLmJsdWVjb3BhLmNvbS9hdXRoei1hcGkiLCJjb25uZWN0b3JBdXRoeiI6Imh0dHBzOi8vZGV2YXBpLmJsdWVjb3BhLmNvbS9hdXRoei1hcGkifQ=='
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.log(error);
|
|
30
|
+
}
|
|
31
|
+
copaSetConfig({
|
|
32
|
+
apiBaseUrl: "http://localhost:3000/api/v1",
|
|
33
|
+
accessToken: config.accessToken,
|
|
34
|
+
workspaceId: config.workspaceId
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<QueryClientProvider client={queryClient}>
|
|
40
|
+
<Router>
|
|
41
|
+
<div className="min-h-screen bg-background">
|
|
42
|
+
<header className="border-b">
|
|
43
|
+
<div className="container mx-auto px-4 py-4">
|
|
44
|
+
<div className="flex items-center justify-between">
|
|
45
|
+
<div className="flex items-center space-x-4">
|
|
46
|
+
<h1 className="text-2xl font-bold text-primary">BlueCopa</h1>
|
|
47
|
+
<span className="text-sm text-muted-foreground">Analytics Dashboard</span>
|
|
48
|
+
</div>
|
|
49
|
+
<nav className="flex items-center space-x-6">
|
|
50
|
+
<a href="/" className="text-sm font-medium text-foreground hover:text-primary">
|
|
51
|
+
Dashboard
|
|
52
|
+
</a>
|
|
53
|
+
<a href="/analytics" className="text-sm font-medium text-muted-foreground hover:text-primary">
|
|
54
|
+
Analytics
|
|
55
|
+
</a>
|
|
56
|
+
<a href="/reports" className="text-sm font-medium text-muted-foreground hover:text-primary">
|
|
57
|
+
Reports
|
|
58
|
+
</a>
|
|
59
|
+
</nav>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
|
|
64
|
+
<main className="container mx-auto px-4 py-8">
|
|
65
|
+
<Routes>
|
|
66
|
+
<Route path="/" element={<Dashboard />} />
|
|
67
|
+
<Route path="/dashboard" element={<Dashboard />} />
|
|
68
|
+
</Routes>
|
|
69
|
+
</main>
|
|
70
|
+
|
|
71
|
+
<footer className="border-t mt-auto">
|
|
72
|
+
<div className="container mx-auto px-4 py-6">
|
|
73
|
+
<div className="flex items-center justify-between">
|
|
74
|
+
<p className="text-sm text-muted-foreground">
|
|
75
|
+
© 2025 BlueCopa. Built with React, TanStack Query, and shadcn/ui.
|
|
76
|
+
</p>
|
|
77
|
+
<div className="flex items-center space-x-4">
|
|
78
|
+
<a href="#" className="text-sm text-muted-foreground hover:text-primary">
|
|
79
|
+
Documentation
|
|
80
|
+
</a>
|
|
81
|
+
<a href="#" className="text-sm text-muted-foreground hover:text-primary">
|
|
82
|
+
Support
|
|
83
|
+
</a>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</footer>
|
|
88
|
+
</div>
|
|
89
|
+
</Router>
|
|
90
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
91
|
+
</QueryClientProvider>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default App;
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { useDatasetSample } from '@bluecopa/react';
|
|
3
|
+
import { ComposedChart, Line, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { RefreshCw, TrendingUp, Calendar, Filter } from 'lucide-react';
|
|
8
|
+
import { format, parseISO } from 'date-fns';
|
|
9
|
+
|
|
10
|
+
interface AdvancedAnalyticsData {
|
|
11
|
+
month: string;
|
|
12
|
+
revenue: number;
|
|
13
|
+
transactions: number;
|
|
14
|
+
averageOrderValue: number;
|
|
15
|
+
onlinePercentage: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AdvancedAnalytics: React.FC = () => {
|
|
19
|
+
const {
|
|
20
|
+
data: datasetData,
|
|
21
|
+
isLoading,
|
|
22
|
+
error,
|
|
23
|
+
refetch
|
|
24
|
+
} = useDatasetSample("0UvoE3CHwqYqzrbGdgs1_large_transaction_dataset_csv");
|
|
25
|
+
|
|
26
|
+
// Process data for advanced analytics
|
|
27
|
+
const analyticsData = useMemo<AdvancedAnalyticsData[]>(() => {
|
|
28
|
+
if (!datasetData?.data?.data) return [];
|
|
29
|
+
|
|
30
|
+
const transactions = datasetData.data.data;
|
|
31
|
+
|
|
32
|
+
// Group by month
|
|
33
|
+
const monthlyData = new Map<string, {
|
|
34
|
+
revenue: number;
|
|
35
|
+
transactions: number;
|
|
36
|
+
onlineTransactions: number;
|
|
37
|
+
}>();
|
|
38
|
+
|
|
39
|
+
transactions.forEach(transaction => {
|
|
40
|
+
const date = parseISO(transaction.transaction_day);
|
|
41
|
+
const monthKey = format(date, 'yyyy-MM');
|
|
42
|
+
|
|
43
|
+
if (monthlyData.has(monthKey)) {
|
|
44
|
+
const existing = monthlyData.get(monthKey)!;
|
|
45
|
+
existing.revenue += transaction.total_amount;
|
|
46
|
+
existing.transactions += 1;
|
|
47
|
+
if (transaction.is_online) {
|
|
48
|
+
existing.onlineTransactions += 1;
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
monthlyData.set(monthKey, {
|
|
52
|
+
revenue: transaction.total_amount,
|
|
53
|
+
transactions: 1,
|
|
54
|
+
onlineTransactions: transaction.is_online ? 1 : 0
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Convert to array and calculate metrics
|
|
60
|
+
return Array.from(monthlyData.entries())
|
|
61
|
+
.map(([month, data]) => ({
|
|
62
|
+
month: format(parseISO(`${month}-01`), 'MMM yyyy'),
|
|
63
|
+
revenue: Math.round(data.revenue),
|
|
64
|
+
transactions: data.transactions,
|
|
65
|
+
averageOrderValue: Math.round(data.revenue / data.transactions),
|
|
66
|
+
onlinePercentage: Math.round((data.onlineTransactions / data.transactions) * 100)
|
|
67
|
+
}))
|
|
68
|
+
.sort((a, b) => a.month.localeCompare(b.month));
|
|
69
|
+
}, [datasetData]);
|
|
70
|
+
|
|
71
|
+
// Calculate growth metrics
|
|
72
|
+
const growthMetrics = useMemo(() => {
|
|
73
|
+
if (analyticsData.length < 2) return null;
|
|
74
|
+
|
|
75
|
+
const current = analyticsData[analyticsData.length - 1];
|
|
76
|
+
const previous = analyticsData[analyticsData.length - 2];
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
revenueGrowth: ((current.revenue - previous.revenue) / previous.revenue) * 100,
|
|
80
|
+
transactionGrowth: ((current.transactions - previous.transactions) / previous.transactions) * 100,
|
|
81
|
+
aovGrowth: ((current.averageOrderValue - previous.averageOrderValue) / previous.averageOrderValue) * 100
|
|
82
|
+
};
|
|
83
|
+
}, [analyticsData]);
|
|
84
|
+
|
|
85
|
+
// Top performing categories
|
|
86
|
+
const categoryPerformance = useMemo(() => {
|
|
87
|
+
if (!datasetData?.data?.data) return [];
|
|
88
|
+
|
|
89
|
+
const categoryMap = new Map<string, { revenue: number; transactions: number }>();
|
|
90
|
+
|
|
91
|
+
datasetData.data.data.forEach(transaction => {
|
|
92
|
+
const category = transaction.category;
|
|
93
|
+
if (categoryMap.has(category)) {
|
|
94
|
+
const existing = categoryMap.get(category)!;
|
|
95
|
+
existing.revenue += transaction.total_amount;
|
|
96
|
+
existing.transactions += 1;
|
|
97
|
+
} else {
|
|
98
|
+
categoryMap.set(category, {
|
|
99
|
+
revenue: transaction.total_amount,
|
|
100
|
+
transactions: 1
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return Array.from(categoryMap.entries())
|
|
106
|
+
.map(([category, data]) => ({
|
|
107
|
+
category,
|
|
108
|
+
revenue: Math.round(data.revenue),
|
|
109
|
+
transactions: data.transactions,
|
|
110
|
+
averageOrderValue: Math.round(data.revenue / data.transactions)
|
|
111
|
+
}))
|
|
112
|
+
.sort((a, b) => b.revenue - a.revenue)
|
|
113
|
+
.slice(0, 5);
|
|
114
|
+
}, [datasetData]);
|
|
115
|
+
|
|
116
|
+
if (isLoading) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="space-y-6">
|
|
119
|
+
{[1, 2, 3].map((i) => (
|
|
120
|
+
<Card key={i}>
|
|
121
|
+
<CardHeader>
|
|
122
|
+
<div className="h-6 w-48 bg-muted animate-pulse rounded"></div>
|
|
123
|
+
</CardHeader>
|
|
124
|
+
<CardContent>
|
|
125
|
+
<div className="h-64 bg-muted animate-pulse rounded"></div>
|
|
126
|
+
</CardContent>
|
|
127
|
+
</Card>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (error) {
|
|
134
|
+
return (
|
|
135
|
+
<Card>
|
|
136
|
+
<CardHeader>
|
|
137
|
+
<CardTitle className="text-red-600">Error Loading Analytics</CardTitle>
|
|
138
|
+
</CardHeader>
|
|
139
|
+
<CardContent>
|
|
140
|
+
<p className="text-muted-foreground mb-4">
|
|
141
|
+
Failed to load analytics data. Please try refreshing.
|
|
142
|
+
</p>
|
|
143
|
+
<Button onClick={() => refetch()} className="flex items-center gap-2">
|
|
144
|
+
<RefreshCw className="h-4 w-4" />
|
|
145
|
+
Retry
|
|
146
|
+
</Button>
|
|
147
|
+
</CardContent>
|
|
148
|
+
</Card>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="space-y-6">
|
|
154
|
+
{/* Growth Metrics Header */}
|
|
155
|
+
{growthMetrics && (
|
|
156
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
157
|
+
<Card>
|
|
158
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
159
|
+
<CardTitle className="text-sm font-medium">Revenue Growth</CardTitle>
|
|
160
|
+
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
|
161
|
+
</CardHeader>
|
|
162
|
+
<CardContent>
|
|
163
|
+
<div className={`text-2xl font-bold ${growthMetrics.revenueGrowth >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
|
164
|
+
{growthMetrics.revenueGrowth >= 0 ? '+' : ''}{growthMetrics.revenueGrowth.toFixed(1)}%
|
|
165
|
+
</div>
|
|
166
|
+
<p className="text-xs text-muted-foreground">vs previous month</p>
|
|
167
|
+
</CardContent>
|
|
168
|
+
</Card>
|
|
169
|
+
|
|
170
|
+
<Card>
|
|
171
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
172
|
+
<CardTitle className="text-sm font-medium">Transaction Growth</CardTitle>
|
|
173
|
+
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
174
|
+
</CardHeader>
|
|
175
|
+
<CardContent>
|
|
176
|
+
<div className={`text-2xl font-bold ${growthMetrics.transactionGrowth >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
|
177
|
+
{growthMetrics.transactionGrowth >= 0 ? '+' : ''}{growthMetrics.transactionGrowth.toFixed(1)}%
|
|
178
|
+
</div>
|
|
179
|
+
<p className="text-xs text-muted-foreground">vs previous month</p>
|
|
180
|
+
</CardContent>
|
|
181
|
+
</Card>
|
|
182
|
+
|
|
183
|
+
<Card>
|
|
184
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
185
|
+
<CardTitle className="text-sm font-medium">AOV Growth</CardTitle>
|
|
186
|
+
<Filter className="h-4 w-4 text-muted-foreground" />
|
|
187
|
+
</CardHeader>
|
|
188
|
+
<CardContent>
|
|
189
|
+
<div className={`text-2xl font-bold ${growthMetrics.aovGrowth >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
|
190
|
+
{growthMetrics.aovGrowth >= 0 ? '+' : ''}{growthMetrics.aovGrowth.toFixed(1)}%
|
|
191
|
+
</div>
|
|
192
|
+
<p className="text-xs text-muted-foreground">vs previous month</p>
|
|
193
|
+
</CardContent>
|
|
194
|
+
</Card>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
{/* Advanced Charts */}
|
|
199
|
+
<Tabs defaultValue="trends" className="space-y-4">
|
|
200
|
+
<TabsList>
|
|
201
|
+
<TabsTrigger value="trends">Revenue Trends</TabsTrigger>
|
|
202
|
+
<TabsTrigger value="performance">Category Performance</TabsTrigger>
|
|
203
|
+
<TabsTrigger value="insights">Business Insights</TabsTrigger>
|
|
204
|
+
</TabsList>
|
|
205
|
+
|
|
206
|
+
<TabsContent value="trends" className="space-y-4">
|
|
207
|
+
<Card>
|
|
208
|
+
<CardHeader>
|
|
209
|
+
<CardTitle>Revenue & Transaction Trends</CardTitle>
|
|
210
|
+
<CardDescription>
|
|
211
|
+
Monthly revenue, transaction count, and average order value over time
|
|
212
|
+
</CardDescription>
|
|
213
|
+
</CardHeader>
|
|
214
|
+
<CardContent>
|
|
215
|
+
<ResponsiveContainer width="100%" height={400}>
|
|
216
|
+
<ComposedChart data={analyticsData}>
|
|
217
|
+
<CartesianGrid strokeDasharray="3 3" />
|
|
218
|
+
<XAxis dataKey="month" />
|
|
219
|
+
<YAxis yAxisId="left" />
|
|
220
|
+
<YAxis yAxisId="right" orientation="right" />
|
|
221
|
+
<Tooltip
|
|
222
|
+
formatter={(value: number, name: string) => {
|
|
223
|
+
if (name === 'revenue') return [`₹${value.toLocaleString()}`, 'Revenue'];
|
|
224
|
+
if (name === 'averageOrderValue') return [`₹${value}`, 'AOV'];
|
|
225
|
+
if (name === 'onlinePercentage') return [`${value}%`, 'Online %'];
|
|
226
|
+
return [value, name];
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
<Legend />
|
|
230
|
+
<Bar yAxisId="left" dataKey="revenue" fill="#8884d8" name="Revenue" />
|
|
231
|
+
<Line yAxisId="right" type="monotone" dataKey="averageOrderValue" stroke="#82ca9d" name="Average Order Value" />
|
|
232
|
+
<Line yAxisId="right" type="monotone" dataKey="onlinePercentage" stroke="#ffc658" name="Online %" />
|
|
233
|
+
</ComposedChart>
|
|
234
|
+
</ResponsiveContainer>
|
|
235
|
+
</CardContent>
|
|
236
|
+
</Card>
|
|
237
|
+
</TabsContent>
|
|
238
|
+
|
|
239
|
+
<TabsContent value="performance" className="space-y-4">
|
|
240
|
+
<Card>
|
|
241
|
+
<CardHeader>
|
|
242
|
+
<CardTitle>Top Performing Categories</CardTitle>
|
|
243
|
+
<CardDescription>
|
|
244
|
+
Revenue and transaction metrics by product category
|
|
245
|
+
</CardDescription>
|
|
246
|
+
</CardHeader>
|
|
247
|
+
<CardContent>
|
|
248
|
+
<div className="space-y-4">
|
|
249
|
+
{categoryPerformance.map((category, index) => (
|
|
250
|
+
<div key={category.category} className="flex items-center justify-between p-4 border rounded-lg">
|
|
251
|
+
<div className="flex items-center space-x-4">
|
|
252
|
+
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
|
253
|
+
<span className="text-sm font-bold text-primary">#{index + 1}</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div>
|
|
256
|
+
<h4 className="font-medium">{category.category}</h4>
|
|
257
|
+
<p className="text-sm text-muted-foreground">
|
|
258
|
+
{category.transactions} transactions
|
|
259
|
+
</p>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="text-right">
|
|
263
|
+
<div className="font-bold">₹{category.revenue.toLocaleString()}</div>
|
|
264
|
+
<div className="text-sm text-muted-foreground">
|
|
265
|
+
₹{category.averageOrderValue} AOV
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
</CardContent>
|
|
272
|
+
</Card>
|
|
273
|
+
</TabsContent>
|
|
274
|
+
|
|
275
|
+
<TabsContent value="insights" className="space-y-4">
|
|
276
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
277
|
+
<Card>
|
|
278
|
+
<CardHeader>
|
|
279
|
+
<CardTitle>Key Insights</CardTitle>
|
|
280
|
+
</CardHeader>
|
|
281
|
+
<CardContent className="space-y-4">
|
|
282
|
+
<div className="flex items-start space-x-3">
|
|
283
|
+
<div className="w-2 h-2 bg-green-500 rounded-full mt-2"></div>
|
|
284
|
+
<div>
|
|
285
|
+
<h4 className="font-medium">Strong Online Growth</h4>
|
|
286
|
+
<p className="text-sm text-muted-foreground">
|
|
287
|
+
Online transactions showing consistent upward trend across all categories
|
|
288
|
+
</p>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
<div className="flex items-start space-x-3">
|
|
292
|
+
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div>
|
|
293
|
+
<div>
|
|
294
|
+
<h4 className="font-medium">Electronics Leading</h4>
|
|
295
|
+
<p className="text-sm text-muted-foreground">
|
|
296
|
+
Electronics category driving highest revenue with premium pricing
|
|
297
|
+
</p>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
<div className="flex items-start space-x-3">
|
|
301
|
+
<div className="w-2 h-2 bg-yellow-500 rounded-full mt-2"></div>
|
|
302
|
+
<div>
|
|
303
|
+
<h4 className="font-medium">Seasonal Patterns</h4>
|
|
304
|
+
<p className="text-sm text-muted-foreground">
|
|
305
|
+
Clear seasonal trends visible in transaction volume and AOV
|
|
306
|
+
</p>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</CardContent>
|
|
310
|
+
</Card>
|
|
311
|
+
|
|
312
|
+
<Card>
|
|
313
|
+
<CardHeader>
|
|
314
|
+
<CardTitle>Recommendations</CardTitle>
|
|
315
|
+
</CardHeader>
|
|
316
|
+
<CardContent className="space-y-4">
|
|
317
|
+
<div className="flex items-start space-x-3">
|
|
318
|
+
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2"></div>
|
|
319
|
+
<div>
|
|
320
|
+
<h4 className="font-medium">Optimize Mobile Experience</h4>
|
|
321
|
+
<p className="text-sm text-muted-foreground">
|
|
322
|
+
Focus on mobile app optimization to capture growing online segment
|
|
323
|
+
</p>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
<div className="flex items-start space-x-3">
|
|
327
|
+
<div className="w-2 h-2 bg-orange-500 rounded-full mt-2"></div>
|
|
328
|
+
<div>
|
|
329
|
+
<h4 className="font-medium">Category Expansion</h4>
|
|
330
|
+
<p className="text-sm text-muted-foreground">
|
|
331
|
+
Consider expanding grocery and furniture offerings
|
|
332
|
+
</p>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
<div className="flex items-start space-x-3">
|
|
336
|
+
<div className="w-2 h-2 bg-red-500 rounded-full mt-2"></div>
|
|
337
|
+
<div>
|
|
338
|
+
<h4 className="font-medium">Payment Incentives</h4>
|
|
339
|
+
<p className="text-sm text-muted-foreground">
|
|
340
|
+
Promote UPI payments with targeted incentives
|
|
341
|
+
</p>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</CardContent>
|
|
345
|
+
</Card>
|
|
346
|
+
</div>
|
|
347
|
+
</TabsContent>
|
|
348
|
+
</Tabs>
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
};
|