create-bluecopa-react-app 1.0.3 → 1.0.5
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 +76 -238
- package/package.json +1 -1
- package/templates/latest/{Agent.md → AGENT.md} +21 -27
- package/templates/latest/README.md +15 -38
- package/templates/latest/clean.sh +1 -0
- package/templates/latest/package-lock.json +8 -8
- package/templates/latest/package.json +1 -1
- package/templates/latest/setup.sh +5 -4
- package/templates/latest/src/App.tsx +11 -7
- package/templates/latest/src/components/charts/AreaChart.tsx +80 -0
- package/templates/latest/src/components/charts/DonutChart.tsx +73 -0
- package/templates/latest/src/components/charts/SparkAreaChart.tsx +52 -0
- package/templates/latest/src/components/layout/dashboard-header.tsx +1 -1
- package/templates/latest/src/components/layout/dashboard-layout.tsx +19 -11
- package/templates/latest/src/components/{page → layout}/navbar.tsx +2 -0
- package/templates/latest/src/components/layout/sidebar.tsx +2 -1
- package/templates/latest/src/components/page/dashboard/DashboardMetrics.tsx +97 -0
- package/templates/latest/src/components/page/dashboard/PaymentMethodsAnalysis.tsx +182 -0
- package/templates/latest/src/components/page/dashboard/RevenueAnalytics.tsx +505 -0
- package/templates/latest/src/components/page/dashboard/SalesAnalytics.tsx +313 -0
- package/templates/latest/src/components/page/dashboard/TransactionsTable.tsx +256 -0
- package/templates/latest/src/components/page/dashboard/dashboard-utils.ts +147 -0
- package/templates/latest/src/components/page/dashboard/dashboard.tsx +185 -0
- package/templates/latest/src/components/ui/bluecopa-logo.tsx +3 -0
- package/templates/latest/src/components/ui/label.tsx +0 -2
- package/templates/latest/src/components/ui/select.tsx +0 -2
- package/templates/latest/src/pages/Dashboard.tsx +1 -1
- package/templates/latest/src/single-spa.tsx +38 -28
- package/templates/latest/tailwind.config.js +1 -2
- package/templates/latest/tsconfig.app.json +6 -0
- package/templates/latest/tsconfig.json +6 -6
- package/templates/latest/tsconfig.node.json +5 -1
- package/templates/latest/vite.config.ts +1 -1
- package/templates/latest/src/components/page/dashboard.tsx +0 -1506
|
@@ -42,10 +42,11 @@ else
|
|
|
42
42
|
fi
|
|
43
43
|
|
|
44
44
|
echo "🎨 Setting up Tailwind CSS..."
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
if [ ! -f tailwind.config.js ] && [ ! -f tailwind.config.cjs ]; then
|
|
46
|
+
npx tailwindcss init -p
|
|
47
|
+
else
|
|
48
|
+
echo "✅ Tailwind already configured"
|
|
49
|
+
fi
|
|
49
50
|
|
|
50
51
|
echo "✅ Setup complete!"
|
|
51
52
|
echo ""
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { Routes, Route } from 'react-router-dom'
|
|
1
|
+
import { Routes, Route, Navigate } from 'react-router-dom'
|
|
2
|
+
import { Suspense, lazy } from 'react'
|
|
2
3
|
import QueryProvider from '@/providers/query-provider'
|
|
3
4
|
import Home from '@/pages/Home'
|
|
4
|
-
|
|
5
|
+
const Dashboard = lazy(() => import('@/pages/Dashboard'))
|
|
5
6
|
|
|
6
7
|
export default function App() {
|
|
7
8
|
return (
|
|
8
9
|
<QueryProvider>
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
<Suspense fallback={<div className="p-4 text-sm text-muted-foreground">Loading…</div>}>
|
|
11
|
+
<Routes>
|
|
12
|
+
<Route path="/" element={<Home />} />
|
|
13
|
+
<Route path="/dashboard" element={<Dashboard />} />
|
|
14
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
15
|
+
</Routes>
|
|
16
|
+
</Suspense>
|
|
13
17
|
</QueryProvider>
|
|
14
18
|
)
|
|
15
|
-
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ResponsiveContainer,
|
|
4
|
+
AreaChart as RechartsAreaChart,
|
|
5
|
+
Area,
|
|
6
|
+
XAxis,
|
|
7
|
+
YAxis,
|
|
8
|
+
CartesianGrid,
|
|
9
|
+
Tooltip,
|
|
10
|
+
Legend
|
|
11
|
+
} from 'recharts';
|
|
12
|
+
|
|
13
|
+
interface AreaChartProps {
|
|
14
|
+
data: any[];
|
|
15
|
+
index: string;
|
|
16
|
+
categories: string[];
|
|
17
|
+
colors?: string[];
|
|
18
|
+
className?: string;
|
|
19
|
+
valueFormatter?: (value: any) => any;
|
|
20
|
+
showLegend?: boolean;
|
|
21
|
+
showGridLines?: boolean;
|
|
22
|
+
showXAxis?: boolean;
|
|
23
|
+
showYAxis?: boolean;
|
|
24
|
+
yAxisWidth?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const AreaChart: React.FC<AreaChartProps> = ({
|
|
28
|
+
data,
|
|
29
|
+
index,
|
|
30
|
+
categories,
|
|
31
|
+
colors = ['#3B82F6'],
|
|
32
|
+
className = "h-80",
|
|
33
|
+
valueFormatter,
|
|
34
|
+
showLegend = true,
|
|
35
|
+
showGridLines = true,
|
|
36
|
+
showXAxis = true,
|
|
37
|
+
showYAxis = true,
|
|
38
|
+
yAxisWidth
|
|
39
|
+
}) => {
|
|
40
|
+
const colorMap: { [key: string]: string } = {
|
|
41
|
+
blue: '#3B82F6',
|
|
42
|
+
emerald: '#10B981',
|
|
43
|
+
violet: '#8B5CF6',
|
|
44
|
+
rose: '#F43F5E',
|
|
45
|
+
amber: '#F59E0B',
|
|
46
|
+
cyan: '#06B6D4',
|
|
47
|
+
slate: '#64748B',
|
|
48
|
+
indigo: '#6366F1'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getColor = (color: string, index: number) => {
|
|
52
|
+
return colorMap[color] || colors[index] || '#3B82F6';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={className}>
|
|
57
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
58
|
+
<RechartsAreaChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
59
|
+
{showGridLines && <CartesianGrid strokeDasharray="3 3" />}
|
|
60
|
+
{showXAxis && <XAxis dataKey={index} />}
|
|
61
|
+
{showYAxis && <YAxis width={yAxisWidth || 60} />}
|
|
62
|
+
<Tooltip
|
|
63
|
+
formatter={(value: any) => valueFormatter ? valueFormatter(value) : value}
|
|
64
|
+
/>
|
|
65
|
+
{showLegend && <Legend />}
|
|
66
|
+
{categories?.map((category: string, idx: number) => (
|
|
67
|
+
<Area
|
|
68
|
+
key={category}
|
|
69
|
+
type="monotone"
|
|
70
|
+
dataKey={category}
|
|
71
|
+
stroke={getColor(colors[idx], idx)}
|
|
72
|
+
fill={getColor(colors[idx], idx)}
|
|
73
|
+
fillOpacity={0.3}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
</RechartsAreaChart>
|
|
77
|
+
</ResponsiveContainer>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ResponsiveContainer,
|
|
4
|
+
PieChart,
|
|
5
|
+
Pie,
|
|
6
|
+
Cell,
|
|
7
|
+
Tooltip,
|
|
8
|
+
Legend
|
|
9
|
+
} from 'recharts';
|
|
10
|
+
|
|
11
|
+
interface DonutChartProps {
|
|
12
|
+
data: any[];
|
|
13
|
+
category: string;
|
|
14
|
+
index: string;
|
|
15
|
+
colors?: string[];
|
|
16
|
+
className?: string;
|
|
17
|
+
valueFormatter?: (value: any) => any;
|
|
18
|
+
variant?: "donut" | "pie";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DonutChart: React.FC<DonutChartProps> = ({
|
|
22
|
+
data,
|
|
23
|
+
category,
|
|
24
|
+
index,
|
|
25
|
+
colors = ['#3B82F6'],
|
|
26
|
+
className = "h-64",
|
|
27
|
+
valueFormatter,
|
|
28
|
+
variant = "donut"
|
|
29
|
+
}) => {
|
|
30
|
+
const colorMap: { [key: string]: string } = {
|
|
31
|
+
blue: '#3B82F6',
|
|
32
|
+
emerald: '#10B981',
|
|
33
|
+
violet: '#8B5CF6',
|
|
34
|
+
rose: '#F43F5E',
|
|
35
|
+
amber: '#F59E0B',
|
|
36
|
+
cyan: '#06B6D4',
|
|
37
|
+
slate: '#64748B',
|
|
38
|
+
indigo: '#6366F1'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getColor = (color: string, index: number) => {
|
|
42
|
+
return colorMap[color] || colors[index] || '#3B82F6';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const COLORS = colors.map((color: string, idx: number) => getColor(color, idx));
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={className}>
|
|
49
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
50
|
+
<PieChart>
|
|
51
|
+
<Pie
|
|
52
|
+
data={data}
|
|
53
|
+
cx="50%"
|
|
54
|
+
cy="50%"
|
|
55
|
+
innerRadius={variant === "donut" ? 60 : 0}
|
|
56
|
+
outerRadius={80}
|
|
57
|
+
paddingAngle={5}
|
|
58
|
+
dataKey={category}
|
|
59
|
+
nameKey={index}
|
|
60
|
+
>
|
|
61
|
+
{data.map((_entry: any, index: number) => (
|
|
62
|
+
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
63
|
+
))}
|
|
64
|
+
</Pie>
|
|
65
|
+
<Tooltip
|
|
66
|
+
formatter={(value: any) => valueFormatter ? valueFormatter(value) : value}
|
|
67
|
+
/>
|
|
68
|
+
<Legend />
|
|
69
|
+
</PieChart>
|
|
70
|
+
</ResponsiveContainer>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ResponsiveContainer,
|
|
4
|
+
AreaChart as RechartsAreaChart,
|
|
5
|
+
Area
|
|
6
|
+
} from 'recharts';
|
|
7
|
+
|
|
8
|
+
interface SparkAreaChartProps {
|
|
9
|
+
data: any[];
|
|
10
|
+
categories: string[];
|
|
11
|
+
colors?: string[];
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SparkAreaChart: React.FC<SparkAreaChartProps> = ({
|
|
16
|
+
data,
|
|
17
|
+
categories,
|
|
18
|
+
colors = ['#3B82F6'],
|
|
19
|
+
className = "h-10"
|
|
20
|
+
}) => {
|
|
21
|
+
const colorMap: { [key: string]: string } = {
|
|
22
|
+
blue: '#3B82F6',
|
|
23
|
+
emerald: '#10B981',
|
|
24
|
+
violet: '#8B5CF6',
|
|
25
|
+
rose: '#F43F5E',
|
|
26
|
+
amber: '#F59E0B',
|
|
27
|
+
cyan: '#06B6D4'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getColor = (color: string, index: number) => {
|
|
31
|
+
return colorMap[color] || colors[index] || '#3B82F6';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={className}>
|
|
36
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
37
|
+
<RechartsAreaChart data={data} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
|
38
|
+
{categories?.map((category: string, idx: number) => (
|
|
39
|
+
<Area
|
|
40
|
+
key={category}
|
|
41
|
+
type="monotone"
|
|
42
|
+
dataKey={category}
|
|
43
|
+
stroke={getColor(colors[idx], idx)}
|
|
44
|
+
fill={getColor(colors[idx], idx)}
|
|
45
|
+
fillOpacity={0.6}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</RechartsAreaChart>
|
|
49
|
+
</ResponsiveContainer>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -87,7 +87,7 @@ export default function DashboardHeader({ title, subtitle }: DashboardHeaderProp
|
|
|
87
87
|
</div>
|
|
88
88
|
|
|
89
89
|
{/* Notifications */}
|
|
90
|
-
<Button variant="ghost" size="sm" className="relative">
|
|
90
|
+
<Button variant="ghost" size="sm" className="relative" aria-label="Open notifications, 3 unread">
|
|
91
91
|
<Bell className="h-5 w-5" />
|
|
92
92
|
<span className="absolute -top-1 -right-1 h-4 w-4 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
|
|
93
93
|
3
|
|
@@ -1,29 +1,37 @@
|
|
|
1
|
-
import { ReactNode } from
|
|
2
|
-
import Sidebar from
|
|
3
|
-
import DashboardHeader from
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import Sidebar from "./sidebar";
|
|
3
|
+
import DashboardHeader from "./dashboard-header";
|
|
4
4
|
|
|
5
5
|
interface DashboardLayoutProps {
|
|
6
|
-
children: ReactNode
|
|
7
|
-
title?: string
|
|
8
|
-
subtitle?: string
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
title?: string;
|
|
8
|
+
subtitle?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export default function DashboardLayout({
|
|
11
|
+
export default function DashboardLayout({
|
|
12
|
+
children,
|
|
13
|
+
title,
|
|
14
|
+
subtitle,
|
|
15
|
+
}: DashboardLayoutProps) {
|
|
12
16
|
return (
|
|
13
17
|
<div className="flex h-screen bg-gray-50">
|
|
14
18
|
{/* Sidebar */}
|
|
15
19
|
<Sidebar />
|
|
16
|
-
|
|
20
|
+
|
|
17
21
|
{/* Main content area */}
|
|
18
22
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
19
23
|
{/* Header */}
|
|
20
24
|
<DashboardHeader title={title} subtitle={subtitle} />
|
|
21
|
-
|
|
25
|
+
|
|
22
26
|
{/* Main content */}
|
|
23
|
-
<main
|
|
27
|
+
<main
|
|
28
|
+
id="main-content"
|
|
29
|
+
role="main"
|
|
30
|
+
className="flex-1 overflow-y-auto p-6"
|
|
31
|
+
>
|
|
24
32
|
{children}
|
|
25
33
|
</main>
|
|
26
34
|
</div>
|
|
27
35
|
</div>
|
|
28
|
-
)
|
|
36
|
+
);
|
|
29
37
|
}
|
|
@@ -60,6 +60,8 @@ export default function Navbar() {
|
|
|
60
60
|
type="button"
|
|
61
61
|
className="inline-flex items-center justify-center p-2 rounded-md text-blue-200 hover:text-white hover:bg-blue-700"
|
|
62
62
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
63
|
+
aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
|
|
64
|
+
aria-expanded={mobileMenuOpen}
|
|
63
65
|
>
|
|
64
66
|
{mobileMenuOpen ? (
|
|
65
67
|
<X className="h-6 w-6" />
|
|
@@ -22,7 +22,7 @@ export default function Sidebar() {
|
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
24
|
{/* Navigation */}
|
|
25
|
-
<nav className="flex-1 space-y-1 px-3 py-4">
|
|
25
|
+
<nav className="flex-1 space-y-1 px-3 py-4" aria-label="Main navigation">
|
|
26
26
|
{navigation.map((item) => {
|
|
27
27
|
const isActive = location.pathname === item.href;
|
|
28
28
|
return (
|
|
@@ -35,6 +35,7 @@ export default function Sidebar() {
|
|
|
35
35
|
? "bg-blue-50 text-blue-700 border-r-2 border-blue-600"
|
|
36
36
|
: "text-gray-700 hover:bg-gray-50 hover:text-gray-900"
|
|
37
37
|
)}
|
|
38
|
+
aria-current={isActive ? "page" : undefined}
|
|
38
39
|
>
|
|
39
40
|
<item.icon
|
|
40
41
|
className={cn(
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Card, CardContent, CardTitle } from '@/components/ui/card';
|
|
3
|
+
import { ArrowUpRight, ArrowDownRight } from 'lucide-react';
|
|
4
|
+
import { SparkAreaChart } from '@/components/charts/SparkAreaChart';
|
|
5
|
+
|
|
6
|
+
interface Metric {
|
|
7
|
+
title: string;
|
|
8
|
+
value: string;
|
|
9
|
+
change: string;
|
|
10
|
+
trend: 'up' | 'down';
|
|
11
|
+
icon: string;
|
|
12
|
+
color: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DashboardMetricsProps {
|
|
16
|
+
ecommerceMetrics: Metric[];
|
|
17
|
+
monthlyRevenueData: any[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const iconMap: { [key: string]: React.ElementType } = {
|
|
21
|
+
DollarSign: ({ className }: { className?: string }) => (
|
|
22
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
23
|
+
<line x1="12" y1="1" x2="12" y2="23"></line>
|
|
24
|
+
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
|
25
|
+
</svg>
|
|
26
|
+
),
|
|
27
|
+
ShoppingCart: ({ className }: { className?: string }) => (
|
|
28
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
29
|
+
<circle cx="9" cy="21" r="1"></circle>
|
|
30
|
+
<circle cx="20" cy="21" r="1"></circle>
|
|
31
|
+
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
|
32
|
+
</svg>
|
|
33
|
+
),
|
|
34
|
+
TrendingUp: ({ className }: { className?: string }) => (
|
|
35
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
36
|
+
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
|
|
37
|
+
<polyline points="17 6 23 6 23 12"></polyline>
|
|
38
|
+
</svg>
|
|
39
|
+
),
|
|
40
|
+
Users: ({ className }: { className?: string }) => (
|
|
41
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
42
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
|
43
|
+
<circle cx="9" cy="7" r="4"></circle>
|
|
44
|
+
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
|
45
|
+
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
|
46
|
+
</svg>
|
|
47
|
+
)
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({
|
|
51
|
+
ecommerceMetrics,
|
|
52
|
+
monthlyRevenueData
|
|
53
|
+
}) => {
|
|
54
|
+
return (
|
|
55
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
56
|
+
{ecommerceMetrics.map((metric, index) => {
|
|
57
|
+
const IconComponent = iconMap[metric.icon];
|
|
58
|
+
return (
|
|
59
|
+
<Card key={index}>
|
|
60
|
+
<CardContent className="pt-6">
|
|
61
|
+
<div className="flex items-center justify-between space-y-0 pb-2">
|
|
62
|
+
<CardTitle className="text-sm font-medium text-gray-600">
|
|
63
|
+
{metric.title}
|
|
64
|
+
</CardTitle>
|
|
65
|
+
{IconComponent && <IconComponent className={`h-4 w-4 ${metric.color}`} />}
|
|
66
|
+
</div>
|
|
67
|
+
<div className="flex items-center space-x-2">
|
|
68
|
+
<div className="text-2xl font-bold">{metric.value}</div>
|
|
69
|
+
<div className={`flex items-center text-sm ${
|
|
70
|
+
metric.trend === 'up' ? 'text-green-600' : 'text-red-600'
|
|
71
|
+
}`}>
|
|
72
|
+
{metric.trend === 'up' ? (
|
|
73
|
+
<ArrowUpRight className="h-4 w-4" />
|
|
74
|
+
) : (
|
|
75
|
+
<ArrowDownRight className="h-4 w-4" />
|
|
76
|
+
)}
|
|
77
|
+
{metric.change}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
{/* Mini trend chart */}
|
|
81
|
+
{monthlyRevenueData.length > 0 && (
|
|
82
|
+
<div className="mt-4">
|
|
83
|
+
<SparkAreaChart
|
|
84
|
+
data={monthlyRevenueData.slice(-6)} // Last 6 months
|
|
85
|
+
categories={[index === 0 ? "revenue" : "orders"]}
|
|
86
|
+
colors={[index === 0 ? "emerald" : index === 1 ? "blue" : index === 2 ? "violet" : "amber"]}
|
|
87
|
+
className="h-10 w-full"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</CardContent>
|
|
92
|
+
</Card>
|
|
93
|
+
);
|
|
94
|
+
})}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { DollarSign } from 'lucide-react';
|
|
5
|
+
import {
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
PieChart,
|
|
8
|
+
Pie,
|
|
9
|
+
Cell,
|
|
10
|
+
Tooltip,
|
|
11
|
+
Legend
|
|
12
|
+
} from 'recharts';
|
|
13
|
+
import { DonutChart } from '@/components/charts/DonutChart';
|
|
14
|
+
|
|
15
|
+
interface PaymentMethodsAnalysisProps {
|
|
16
|
+
paymentMethodData: any[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const PaymentMethodsAnalysis: React.FC<PaymentMethodsAnalysisProps> = ({
|
|
20
|
+
paymentMethodData
|
|
21
|
+
}) => {
|
|
22
|
+
// Process data for charts
|
|
23
|
+
const donutChartData = paymentMethodData.map((item: any) => ({
|
|
24
|
+
method: item.method,
|
|
25
|
+
count: item.count
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const pieChartData = paymentMethodData.map((item: any) => ({
|
|
29
|
+
name: item.method,
|
|
30
|
+
value: item.total,
|
|
31
|
+
count: item.count,
|
|
32
|
+
percentage: Math.round((item.total / paymentMethodData.reduce((sum: number, m: any) => sum + m.total, 0)) * 100)
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
37
|
+
{/* Enhanced Payment Method Distribution */}
|
|
38
|
+
<Card>
|
|
39
|
+
<CardHeader className="pb-4">
|
|
40
|
+
<div className="flex items-center justify-between">
|
|
41
|
+
<div>
|
|
42
|
+
<CardTitle className="text-lg font-bold">Payment Methods</CardTitle>
|
|
43
|
+
<CardDescription className="text-sm text-gray-600">
|
|
44
|
+
Transaction distribution by payment type
|
|
45
|
+
</CardDescription>
|
|
46
|
+
</div>
|
|
47
|
+
<Badge variant="outline" className="text-blue-600 border-blue-200">
|
|
48
|
+
{paymentMethodData.length} Methods
|
|
49
|
+
</Badge>
|
|
50
|
+
</div>
|
|
51
|
+
</CardHeader>
|
|
52
|
+
<CardContent>
|
|
53
|
+
{paymentMethodData.length > 0 ? (
|
|
54
|
+
<div className="space-y-6">
|
|
55
|
+
{/* Enhanced Payment Method Donut Chart */}
|
|
56
|
+
<div className="relative bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-lg border border-blue-100">
|
|
57
|
+
<h3 className="text-sm font-semibold text-blue-800 mb-4 text-center">Payment Distribution</h3>
|
|
58
|
+
<div className="h-80">
|
|
59
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
60
|
+
<PieChart>
|
|
61
|
+
<defs>
|
|
62
|
+
<filter id="shadow" height="130%">
|
|
63
|
+
<feDropShadow dx="2" dy="2" stdDeviation="3" floodOpacity="0.2"/>
|
|
64
|
+
</filter>
|
|
65
|
+
</defs>
|
|
66
|
+
<Pie
|
|
67
|
+
data={pieChartData}
|
|
68
|
+
cx="50%"
|
|
69
|
+
cy="50%"
|
|
70
|
+
labelLine={false}
|
|
71
|
+
label={({ name, percentage }: any) => `${name}: ${percentage}%`}
|
|
72
|
+
outerRadius={100}
|
|
73
|
+
innerRadius={60}
|
|
74
|
+
paddingAngle={5}
|
|
75
|
+
dataKey="value"
|
|
76
|
+
filter="url(#shadow)"
|
|
77
|
+
>
|
|
78
|
+
{paymentMethodData.map((_entry: any, index: number) => {
|
|
79
|
+
const colors = ['#3B82F6', '#10B981', '#8B5CF6', '#F59E0B', '#F43F5E', '#06B6D4'];
|
|
80
|
+
return (
|
|
81
|
+
<Cell
|
|
82
|
+
key={`cell-${index}`}
|
|
83
|
+
fill={colors[index % colors.length]}
|
|
84
|
+
stroke="#fff"
|
|
85
|
+
strokeWidth={2}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</Pie>
|
|
90
|
+
<Tooltip
|
|
91
|
+
formatter={(value: any, name: string, props: any) => [
|
|
92
|
+
`$${value.toLocaleString()}`,
|
|
93
|
+
`${name} (${props.payload.count} transactions)`
|
|
94
|
+
]}
|
|
95
|
+
labelStyle={{ color: '#374151', fontWeight: 'bold' }}
|
|
96
|
+
contentStyle={{
|
|
97
|
+
backgroundColor: '#fff',
|
|
98
|
+
border: '1px solid #e5e7eb',
|
|
99
|
+
borderRadius: '8px',
|
|
100
|
+
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
<Legend
|
|
104
|
+
verticalAlign="bottom"
|
|
105
|
+
height={36}
|
|
106
|
+
iconType="circle"
|
|
107
|
+
wrapperStyle={{ fontSize: '12px', paddingTop: '20px' }}
|
|
108
|
+
/>
|
|
109
|
+
</PieChart>
|
|
110
|
+
</ResponsiveContainer>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Enhanced Payment Method Details */}
|
|
115
|
+
<div className="space-y-3">
|
|
116
|
+
<h3 className="text-sm font-semibold text-gray-700">Payment Method Breakdown</h3>
|
|
117
|
+
{paymentMethodData.map((method: any, index: number) => {
|
|
118
|
+
const colors = ['bg-blue-500', 'bg-emerald-500', 'bg-violet-500', 'bg-amber-500', 'bg-rose-500', 'bg-cyan-500'];
|
|
119
|
+
const textColors = ['text-blue-600', 'text-emerald-600', 'text-violet-600', 'text-amber-600', 'text-rose-600', 'text-cyan-600'];
|
|
120
|
+
const bgColors = ['bg-blue-50', 'bg-emerald-50', 'bg-violet-50', 'bg-amber-50', 'bg-rose-50', 'bg-cyan-50'];
|
|
121
|
+
const percentage = Math.round((method.total / paymentMethodData.reduce((sum: number, m: any) => sum + m.total, 0)) * 100);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div key={index} className={`flex items-center justify-between p-4 rounded-lg border hover:shadow-sm transition-all ${bgColors[index] || 'bg-gray-50'}`}>
|
|
125
|
+
<div className="flex items-center space-x-4">
|
|
126
|
+
<div className={`w-4 h-4 rounded-full ${colors[index] || 'bg-gray-400'}`} />
|
|
127
|
+
<div>
|
|
128
|
+
<div className="font-semibold text-gray-900">{method.method}</div>
|
|
129
|
+
<div className="text-sm text-gray-500">{method.count} transactions</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="text-right">
|
|
133
|
+
<div className={`text-lg font-bold ${textColors[index] || 'text-gray-600'}`}>
|
|
134
|
+
${method.total.toLocaleString()}
|
|
135
|
+
</div>
|
|
136
|
+
<div className="text-sm text-gray-500">{percentage}% of total</div>
|
|
137
|
+
<div className="w-20 bg-gray-200 rounded-full h-1.5 mt-1">
|
|
138
|
+
<div
|
|
139
|
+
className={`h-1.5 rounded-full ${colors[index] || 'bg-gray-400'}`}
|
|
140
|
+
style={{ width: `${percentage}%` }}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
})}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
) : (
|
|
150
|
+
<div className="text-center text-gray-500 py-12">
|
|
151
|
+
<DollarSign className="h-12 w-12 mx-auto text-gray-300 mb-4" />
|
|
152
|
+
<p className="text-lg font-medium">No payment data available</p>
|
|
153
|
+
<p className="text-sm">Process payments to see distribution</p>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</CardContent>
|
|
157
|
+
</Card>
|
|
158
|
+
|
|
159
|
+
{/* Payment Method Distribution */}
|
|
160
|
+
<Card>
|
|
161
|
+
<CardHeader>
|
|
162
|
+
<CardTitle>Payment Method Distribution</CardTitle>
|
|
163
|
+
<CardDescription>
|
|
164
|
+
Transaction count by payment method
|
|
165
|
+
</CardDescription>
|
|
166
|
+
</CardHeader>
|
|
167
|
+
<CardContent>
|
|
168
|
+
<DonutChart
|
|
169
|
+
className="h-64"
|
|
170
|
+
data={donutChartData}
|
|
171
|
+
category="count"
|
|
172
|
+
index="method"
|
|
173
|
+
valueFormatter={(number: number) =>
|
|
174
|
+
`${number} transactions`
|
|
175
|
+
}
|
|
176
|
+
colors={["emerald", "blue", "violet", "rose", "amber"]}
|
|
177
|
+
/>
|
|
178
|
+
</CardContent>
|
|
179
|
+
</Card>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
};
|