feat: Complete analytics implementation with all 7 pages, ROI calculation, conversion rate formatting, and chart improvements
This commit is contained in:
@@ -15,50 +15,50 @@ export interface AnalyticsParams {
|
|||||||
export const AnalyticsApi = {
|
export const AnalyticsApi = {
|
||||||
/**
|
/**
|
||||||
* Dashboard Overview
|
* Dashboard Overview
|
||||||
* GET /woonoow/v1/analytics/overview
|
* GET /analytics/overview
|
||||||
*/
|
*/
|
||||||
overview: (params?: AnalyticsParams) =>
|
overview: (params?: AnalyticsParams) =>
|
||||||
api.get('/woonoow/v1/analytics/overview', params),
|
api.get('/analytics/overview', params),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revenue Analytics
|
* Revenue Analytics
|
||||||
* GET /woonoow/v1/analytics/revenue
|
* GET /analytics/revenue
|
||||||
*/
|
*/
|
||||||
revenue: (params?: AnalyticsParams) =>
|
revenue: (params?: AnalyticsParams) =>
|
||||||
api.get('/woonoow/v1/analytics/revenue', params),
|
api.get('/analytics/revenue', params),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orders Analytics
|
* Orders Analytics
|
||||||
* GET /woonoow/v1/analytics/orders
|
* GET /analytics/orders
|
||||||
*/
|
*/
|
||||||
orders: (params?: AnalyticsParams) =>
|
orders: (params?: AnalyticsParams) =>
|
||||||
api.get('/woonoow/v1/analytics/orders', params),
|
api.get('/analytics/orders', params),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Products Analytics
|
* Products Analytics
|
||||||
* GET /woonoow/v1/analytics/products
|
* GET /analytics/products
|
||||||
*/
|
*/
|
||||||
products: (params?: AnalyticsParams) =>
|
products: (params?: AnalyticsParams) =>
|
||||||
api.get('/woonoow/v1/analytics/products', params),
|
api.get('/analytics/products', params),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Customers Analytics
|
* Customers Analytics
|
||||||
* GET /woonoow/v1/analytics/customers
|
* GET /analytics/customers
|
||||||
*/
|
*/
|
||||||
customers: (params?: AnalyticsParams) =>
|
customers: (params?: AnalyticsParams) =>
|
||||||
api.get('/woonoow/v1/analytics/customers', params),
|
api.get('/analytics/customers', params),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coupons Analytics
|
* Coupons Analytics
|
||||||
* GET /woonoow/v1/analytics/coupons
|
* GET /analytics/coupons
|
||||||
*/
|
*/
|
||||||
coupons: (params?: AnalyticsParams) =>
|
coupons: (params?: AnalyticsParams) =>
|
||||||
api.get('/woonoow/v1/analytics/coupons', params),
|
api.get('/analytics/coupons', params),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Taxes Analytics
|
* Taxes Analytics
|
||||||
* GET /woonoow/v1/analytics/taxes
|
* GET /analytics/taxes
|
||||||
*/
|
*/
|
||||||
taxes: (params?: AnalyticsParams) =>
|
taxes: (params?: AnalyticsParams) =>
|
||||||
api.get('/woonoow/v1/analytics/taxes', params),
|
api.get('/analytics/taxes', params),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useMemo, useRef } from 'react';
|
import React, { useState, useMemo, useRef } from 'react';
|
||||||
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, Label, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
import { BarChart, Bar, LineChart, Line, AreaChart, Area, ComposedChart, PieChart, Pie, Cell, Label, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||||
import { ShoppingCart, TrendingUp, Package, XCircle, DollarSign, CheckCircle, Clock } from 'lucide-react';
|
import { ShoppingCart, TrendingUp, Package, XCircle, DollarSign, CheckCircle, Clock } from 'lucide-react';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
||||||
@@ -16,12 +16,14 @@ import { DUMMY_ORDERS_DATA, OrdersData } from './data/dummyOrders';
|
|||||||
export default function OrdersAnalytics() {
|
export default function OrdersAnalytics() {
|
||||||
const { period } = useDashboardPeriod();
|
const { period } = useDashboardPeriod();
|
||||||
const store = getStoreCurrency();
|
const store = getStoreCurrency();
|
||||||
const [activeStatus, setActiveStatus] = useState('all');
|
|
||||||
const [hoverIndex, setHoverIndex] = useState<number | undefined>(undefined);
|
const [hoverIndex, setHoverIndex] = useState<number | undefined>(undefined);
|
||||||
const chartRef = useRef<any>(null);
|
const chartRef = useRef<any>(null);
|
||||||
|
|
||||||
// Fetch real data or use dummy data based on toggle
|
// Fetch real data or use dummy data based on toggle
|
||||||
const { data, isLoading, error, refetch } = useOrdersAnalytics(DUMMY_ORDERS_DATA);
|
const { data, isLoading, error, refetch } = useOrdersAnalytics(DUMMY_ORDERS_DATA);
|
||||||
|
|
||||||
|
// Auto-select first status (completed) on load
|
||||||
|
const [activeStatus, setActiveStatus] = useState(data.by_status[0]?.status_label || 'All');
|
||||||
|
|
||||||
// Filter chart data by period
|
// Filter chart data by period
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
@@ -205,7 +207,13 @@ export default function OrdersAnalytics() {
|
|||||||
description={__('Daily order count and status breakdown')}
|
description={__('Daily order count and status breakdown')}
|
||||||
>
|
>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<LineChart data={chartData}>
|
<ComposedChart data={chartData}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="totalOrdersGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
|
||||||
|
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0.05}/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
@@ -235,19 +243,39 @@ export default function OrdersAnalytics() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
<Line
|
{/* Total Orders as Area (background) */}
|
||||||
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="orders"
|
dataKey="orders"
|
||||||
name={__('Total Orders')}
|
name={__('Total Orders')}
|
||||||
stroke="#3b82f6"
|
stroke="#3b82f6"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
fill="url(#totalOrdersGradient)"
|
||||||
/>
|
/>
|
||||||
|
{/* Individual statuses as Lines */}
|
||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="completed"
|
dataKey="completed"
|
||||||
name={__('Completed')}
|
name={__('Completed')}
|
||||||
stroke="#10b981"
|
stroke="#10b981"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
dot={{ r: 3 }}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="processing"
|
||||||
|
name={__('Processing')}
|
||||||
|
stroke="#60a5fa"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ r: 3 }}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="pending"
|
||||||
|
name={__('Pending')}
|
||||||
|
stroke="#f59e0b"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ r: 3 }}
|
||||||
/>
|
/>
|
||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
@@ -255,8 +283,9 @@ export default function OrdersAnalytics() {
|
|||||||
name={__('Cancelled')}
|
name={__('Cancelled')}
|
||||||
stroke="#ef4444"
|
stroke="#ef4444"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
dot={{ r: 3 }}
|
||||||
/>
|
/>
|
||||||
</LineChart>
|
</ComposedChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { AreaChart, Area, BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, Sector, Label, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
import { AreaChart, Area, BarChart, Bar, LineChart, Line, ComposedChart, PieChart, Pie, Cell, Sector, Label, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||||
import { TrendingUp, TrendingDown, ShoppingCart, DollarSign, Package, Users, AlertTriangle, ArrowUpRight } from 'lucide-react';
|
import { TrendingUp, TrendingDown, ShoppingCart, DollarSign, Package, Users, AlertTriangle, ArrowUpRight } from 'lucide-react';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
||||||
@@ -125,11 +125,17 @@ const DUMMY_DATA = {
|
|||||||
|
|
||||||
// Metric card component
|
// Metric card component
|
||||||
function MetricCard({ title, value, change, icon: Icon, format = 'number', period }: any) {
|
function MetricCard({ title, value, change, icon: Icon, format = 'number', period }: any) {
|
||||||
|
const safeValue = value ?? 0;
|
||||||
|
const formattedValue = format === 'money' ? formatMoney(safeValue) : format === 'percent' ? `${safeValue.toFixed(2)}%` : safeValue.toLocaleString();
|
||||||
|
|
||||||
|
// Don't show comparison for "All Time"
|
||||||
|
const showComparison = change !== undefined && period !== 'all';
|
||||||
const isPositive = change >= 0;
|
const isPositive = change >= 0;
|
||||||
const formattedValue = format === 'money' ? formatMoney(value) : format === 'percent' ? `${value}%` : value.toLocaleString();
|
|
||||||
|
|
||||||
// Period comparison text
|
// Period comparison text
|
||||||
const periodText = period === '7' ? __('vs previous 7 days') : period === '14' ? __('vs previous 14 days') : __('vs previous 30 days');
|
const periodText = period === '7' ? __('vs previous 7 days') :
|
||||||
|
period === '14' ? __('vs previous 14 days') :
|
||||||
|
period === '30' ? __('vs previous 30 days') : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border bg-card p-6">
|
<div className="rounded-lg border bg-card p-6">
|
||||||
@@ -139,10 +145,12 @@ function MetricCard({ title, value, change, icon: Icon, format = 'number', perio
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-2xl font-bold">{formattedValue}</div>
|
<div className="text-2xl font-bold">{formattedValue}</div>
|
||||||
<div className={`flex items-center text-xs ${isPositive ? 'text-green-600' : 'text-red-600'}`}>
|
{showComparison && (
|
||||||
{isPositive ? <TrendingUp className="w-3 h-3 mr-1" /> : <TrendingDown className="w-3 h-3 mr-1" />}
|
<div className={`flex items-center text-xs ${isPositive ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
{Math.abs(change).toFixed(1)}% {periodText}
|
{isPositive ? <TrendingUp className="w-3 h-3 mr-1" /> : <TrendingDown className="w-3 h-3 mr-1" />}
|
||||||
</div>
|
{Math.abs(change).toFixed(1)}% {periodText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -152,13 +160,32 @@ function MetricCard({ title, value, change, icon: Icon, format = 'number', perio
|
|||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { period } = useDashboardPeriod();
|
const { period } = useDashboardPeriod();
|
||||||
const store = getStoreCurrency();
|
const store = getStoreCurrency();
|
||||||
const [activeStatus, setActiveStatus] = useState('all');
|
|
||||||
const [hoverIndex, setHoverIndex] = useState<number | undefined>(undefined);
|
const [hoverIndex, setHoverIndex] = useState<number | undefined>(undefined);
|
||||||
const [chartMetric, setChartMetric] = useState<'both' | 'revenue' | 'orders'>('both');
|
const [chartMetric, setChartMetric] = useState<'both' | 'revenue' | 'orders'>('both');
|
||||||
const chartRef = useRef<any>(null);
|
const chartRef = useRef<any>(null);
|
||||||
|
|
||||||
// Fetch real data or use dummy data based on toggle
|
// Fetch real data or use dummy data based on toggle
|
||||||
const { data, isLoading, error, refetch } = useOverviewAnalytics(DUMMY_DATA);
|
const { data, isLoading, error, refetch } = useOverviewAnalytics(DUMMY_DATA);
|
||||||
|
|
||||||
|
// Sort order status by importance (completed first)
|
||||||
|
const sortedOrderStatus = useMemo(() => {
|
||||||
|
const statusOrder = ['completed', 'processing', 'pending', 'on-hold', 'cancelled', 'refunded', 'failed'];
|
||||||
|
const orderStatus = data.orderStatus || data.orderStatusDistribution || [];
|
||||||
|
if (!Array.isArray(orderStatus)) return [];
|
||||||
|
|
||||||
|
return [...orderStatus].sort((a, b) => {
|
||||||
|
const statusA = (a.status || a.name || '').toLowerCase();
|
||||||
|
const statusB = (b.status || b.name || '').toLowerCase();
|
||||||
|
const posA = statusOrder.indexOf(statusA);
|
||||||
|
const posB = statusOrder.indexOf(statusB);
|
||||||
|
return (posA === -1 ? 999 : posA) - (posB === -1 ? 999 : posB);
|
||||||
|
});
|
||||||
|
}, [data.orderStatus, data.orderStatusDistribution]);
|
||||||
|
|
||||||
|
// Auto-select first status (completed) on load
|
||||||
|
const [activeStatus, setActiveStatus] = useState(
|
||||||
|
sortedOrderStatus[0]?.status || sortedOrderStatus[0]?.name || 'All'
|
||||||
|
);
|
||||||
|
|
||||||
// Filter chart data based on period
|
// Filter chart data based on period
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
@@ -349,11 +376,11 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
{chartMetric === 'both' ? (
|
{chartMetric === 'both' ? (
|
||||||
<AreaChart data={chartData}>
|
<ComposedChart data={chartData}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
|
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.4} />
|
||||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0.05} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||||
@@ -372,7 +399,7 @@ export default function Dashboard() {
|
|||||||
<p className="text-sm font-bold">{label}</p>
|
<p className="text-sm font-bold">{label}</p>
|
||||||
{payload.map((entry: any, index: number) => (
|
{payload.map((entry: any, index: number) => (
|
||||||
<p key={index} style={{ color: entry.color }}>
|
<p key={index} style={{ color: entry.color }}>
|
||||||
<span className="font-bold">{entry.name}:</span> {entry.name === __('Revenue') ? formatMoney(Number(entry.value)) : entry.value.toLocaleString()}
|
<span className="font-bold">{entry.name}:</span> {entry.name === __('Revenue') ? formatMoney(Number(entry.value || 0)) : (entry.value ?? 0).toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -382,9 +409,9 @@ export default function Dashboard() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
<Area yAxisId="left" type="monotone" dataKey="revenue" stroke="#3b82f6" fillOpacity={1} fill="url(#colorRevenue)" name={__('Revenue')} />
|
<Area yAxisId="left" type="monotone" dataKey="revenue" stroke="#3b82f6" strokeWidth={2.5} fillOpacity={1} fill="url(#colorRevenue)" name={__('Revenue')} />
|
||||||
<Line yAxisId="right" type="monotone" dataKey="orders" stroke="#10b981" strokeWidth={2} name={__('Orders')} />
|
<Line yAxisId="right" type="monotone" dataKey="orders" stroke="#10b981" strokeWidth={2.5} dot={{ r: 4, fill: '#10b981' }} activeDot={{ r: 6 }} name={__('Orders')} />
|
||||||
</AreaChart>
|
</ComposedChart>
|
||||||
) : chartMetric === 'revenue' ? (
|
) : chartMetric === 'revenue' ? (
|
||||||
<AreaChart data={chartData}>
|
<AreaChart data={chartData}>
|
||||||
<defs>
|
<defs>
|
||||||
@@ -433,7 +460,7 @@ export default function Dashboard() {
|
|||||||
<p className="text-sm font-bold">{label}</p>
|
<p className="text-sm font-bold">{label}</p>
|
||||||
{payload.map((entry: any, index: number) => (
|
{payload.map((entry: any, index: number) => (
|
||||||
<p key={index} style={{ color: entry.color }}>
|
<p key={index} style={{ color: entry.color }}>
|
||||||
<span className="font-bold">{entry.name}:</span> {entry.value.toLocaleString()}
|
<span className="font-bold">{entry.name}:</span> {(entry.value ?? 0).toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -462,14 +489,17 @@ export default function Dashboard() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent align="end">
|
<SelectContent align="end">
|
||||||
{DUMMY_DATA.orderStatusDistribution.map((status) => (
|
{sortedOrderStatus.map((status, idx) => {
|
||||||
<SelectItem key={status.name} value={status.name}>
|
const statusValue = status.status || status.name || `status-${idx}`;
|
||||||
<span className="flex items-center gap-2 text-xs">
|
return (
|
||||||
<span className="flex h-3 w-3 shrink-0 rounded" style={{ backgroundColor: status.color }} />
|
<SelectItem key={statusValue} value={statusValue}>
|
||||||
{status.name}
|
<span className="flex items-center gap-2 text-xs">
|
||||||
</span>
|
<span className="flex h-3 w-3 shrink-0 rounded" style={{ backgroundColor: status.color }} />
|
||||||
</SelectItem>
|
{statusValue}
|
||||||
))}
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -480,9 +510,9 @@ export default function Dashboard() {
|
|||||||
onMouseLeave={handleChartMouseLeave}
|
onMouseLeave={handleChartMouseLeave}
|
||||||
>
|
>
|
||||||
<Pie
|
<Pie
|
||||||
data={DUMMY_DATA.orderStatusDistribution}
|
data={sortedOrderStatus}
|
||||||
dataKey="value"
|
dataKey={(entry) => entry.count || entry.value || 0}
|
||||||
nameKey="name"
|
nameKey={(entry) => entry.status || entry.name || 'Unknown'}
|
||||||
cx="50%"
|
cx="50%"
|
||||||
cy="50%"
|
cy="50%"
|
||||||
innerRadius={70}
|
innerRadius={70}
|
||||||
@@ -492,8 +522,10 @@ export default function Dashboard() {
|
|||||||
onMouseLeave={onPieLeave}
|
onMouseLeave={onPieLeave}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
>
|
>
|
||||||
{data.orderStatusDistribution.map((entry: any, index: number) => {
|
{sortedOrderStatus.map((entry: any, index: number) => {
|
||||||
const activePieIndex = data.orderStatusDistribution.findIndex((item: any) => item.name === activeStatus);
|
const activePieIndex = sortedOrderStatus.findIndex((item: any) =>
|
||||||
|
(item.status || item.name) === activeStatus
|
||||||
|
);
|
||||||
const isActive = index === (hoverIndex !== undefined ? hoverIndex : activePieIndex);
|
const isActive = index === (hoverIndex !== undefined ? hoverIndex : activePieIndex);
|
||||||
return (
|
return (
|
||||||
<Cell
|
<Cell
|
||||||
@@ -524,7 +556,7 @@ export default function Dashboard() {
|
|||||||
y={viewBox.cy}
|
y={viewBox.cy}
|
||||||
className="fill-foreground text-3xl font-bold"
|
className="fill-foreground text-3xl font-bold"
|
||||||
>
|
>
|
||||||
{selectedData?.value.toLocaleString()}
|
{(selectedData?.value ?? 0).toLocaleString()}
|
||||||
</tspan>
|
</tspan>
|
||||||
<tspan
|
<tspan
|
||||||
x={viewBox.cx}
|
x={viewBox.cx}
|
||||||
@@ -558,16 +590,16 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
<TabsContent value="products" className="mt-0">
|
<TabsContent value="products" className="mt-0">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{DUMMY_DATA.topProducts.map((product) => (
|
{(data.topProducts || DUMMY_DATA.topProducts).slice(0, 5).map((product: any, index: number) => (
|
||||||
<div key={product.id} className="flex items-center justify-between p-2 rounded hover:bg-muted/50">
|
<div key={product.product_id || product.id || index} className="flex items-center justify-between p-2 rounded hover:bg-muted/50">
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
<div className="text-2xl">{product.image}</div>
|
<div className="text-2xl">{product.image || '📦'}</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium text-sm truncate">{product.name}</div>
|
<div className="font-medium text-sm truncate">{product.name}</div>
|
||||||
<div className="text-xs text-muted-foreground">{product.quantity} {__('sold')}</div>
|
<div className="text-xs text-muted-foreground">{product.items_sold || product.sales} {__('sold')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-medium text-sm">{formatMoney(product.revenue)}</div>
|
<div className="text-sm font-medium">{formatMoney(product.revenue)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -575,13 +607,13 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
<TabsContent value="customers" className="mt-0">
|
<TabsContent value="customers" className="mt-0">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{DUMMY_DATA.topCustomers.map((customer) => (
|
{(data.topCustomers || DUMMY_DATA.topCustomers).slice(0, 5).map((customer: any, index: number) => (
|
||||||
<div key={customer.id} className="flex items-center justify-between p-2 rounded hover:bg-muted/50">
|
<div key={customer.customer_id || customer.id || index} className="flex items-center justify-between p-2 rounded hover:bg-muted/50">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium text-sm truncate">{customer.name}</div>
|
<div className="font-medium text-sm truncate">{customer.name}</div>
|
||||||
<div className="text-xs text-muted-foreground">{customer.orders} {__('orders')}</div>
|
<div className="text-xs text-muted-foreground">{customer.order_count || customer.orders} {__('orders')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-medium text-sm">{formatMoney(customer.totalSpent)}</div>
|
<div className="text-sm font-medium">{formatMoney(customer.total_spent || customer.total)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -29,4 +29,12 @@ class Permissions {
|
|||||||
$nonce = $_SERVER['HTTP_X_WP_NONCE'] ?? '';
|
$nonce = $_SERVER['HTTP_X_WP_NONCE'] ?? '';
|
||||||
return (bool) wp_verify_nonce($nonce, 'wp_rest');
|
return (bool) wp_verify_nonce($nonce, 'wp_rest');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has admin/manage_woocommerce permission
|
||||||
|
* Used for analytics and admin-only endpoints
|
||||||
|
*/
|
||||||
|
public static function check_admin_permission(): bool {
|
||||||
|
return current_user_can('manage_woocommerce') || current_user_can('manage_options');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
namespace WooNooW\Api;
|
||||||
|
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WooNooW\Api\CheckoutController;
|
||||||
|
use WooNooW\Api\OrdersController;
|
||||||
|
use WooNooW\Api\AnalyticsController;
|
||||||
|
|
||||||
|
class Routes {
|
||||||
|
public static function init() {
|
||||||
|
// Initialize controllers (register action hooks)
|
||||||
|
OrdersController::init();
|
||||||
|
|
||||||
|
add_action('rest_api_init', function () {
|
||||||
|
$namespace = 'woonoow/v1';
|
||||||
|
// Defer to controllers to register their endpoints
|
||||||
|
CheckoutController::register();
|
||||||
|
OrdersController::register();
|
||||||
|
AnalyticsController::register_routes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user