feat: Fill missing dates in charts + no-data states
## Task 1: Fill Missing Dates in Chart Data ✅ **Issue:** Charts only show dates with actual data, causing: - Gaps in timeline - Tight/crowded lines on mobile - Inconsistent X-axis **Solution:** Backend now fills ALL dates in range with zeros **Files Updated:** - `includes/Api/AnalyticsController.php` - `calculate_revenue_metrics()` - Revenue chart - `calculate_orders_metrics()` - Orders chart - `calculate_coupons_metrics()` - Coupons chart **Implementation:** ```php // Create data map from query results $data_map = []; foreach ($chart_data_raw as $row) { $data_map[$row->date] = [...]; } // Fill ALL dates in range for ($i = $days - 1; $i >= 0; $i--) { $date = date('Y-m-d', strtotime("-{$i} days")); if (isset($data_map[$date])) { // Use real data } else { // Fill with zeros } } ``` **Result:** - ✅ Consistent X-axis with all dates - ✅ No gaps in timeline - ✅ Better mobile display (evenly spaced) --- ## Task 2: No-Data States for Charts ✅ **Issue:** Charts show broken/empty state when no data **Solution:** Show friendly message like Overview does **Files Updated:** - `admin-spa/src/routes/Dashboard/Revenue.tsx` - `admin-spa/src/routes/Dashboard/Orders.tsx` - `admin-spa/src/routes/Dashboard/Coupons.tsx` **Implementation:** ```tsx {chartData.length === 0 || chartData.every(d => d.value === 0) ? ( <div className="flex items-center justify-center h-[300px]"> <div className="text-center"> <Package className="w-12 h-12 text-muted-foreground mx-auto mb-3" /> <p className="text-muted-foreground font-medium"> No {type} data available </p> <p className="text-sm text-muted-foreground mt-1"> Data will appear once you have {action} </p> </div> </div> ) : ( <ResponsiveContainer>...</ResponsiveContainer> )} ``` **Result:** - ✅ Revenue: "No revenue data available" - ✅ Orders: "No orders data available" - ✅ Coupons: "No coupon usage data available" - ✅ Consistent with Overview page - ✅ User-friendly empty states --- ## Summary ✅ **Backend:** All dates filled in chart data ✅ **Frontend:** No-data states added to 3 charts ✅ **UX:** Consistent, professional empty states **Next:** VIP customer settings + mobile chart optimization
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||||
import { Tag, DollarSign, TrendingUp, ShoppingCart } from 'lucide-react';
|
import { Tag, DollarSign, TrendingUp, ShoppingCart, Package } from 'lucide-react';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
||||||
import { useDashboardPeriod } from '@/hooks/useDashboardPeriod';
|
import { useDashboardPeriod } from '@/hooks/useDashboardPeriod';
|
||||||
@@ -223,8 +223,19 @@ export default function CouponsReport() {
|
|||||||
title={__('Coupon Usage Over Time')}
|
title={__('Coupon Usage Over Time')}
|
||||||
description={__('Daily coupon usage and discount amount')}
|
description={__('Daily coupon usage and discount amount')}
|
||||||
>
|
>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
{chartData.length === 0 || chartData.every((d: any) => d.uses === 0) ? (
|
||||||
<LineChart data={chartData}>
|
<div className="flex items-center justify-center h-[300px]">
|
||||||
|
<div className="text-center">
|
||||||
|
<Package className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
|
||||||
|
<p className="text-muted-foreground font-medium">{__('No coupon usage data available')}</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{__('Coupon data will appear once customers use coupons')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<LineChart data={chartData}>
|
||||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
@@ -283,6 +294,7 @@ export default function CouponsReport() {
|
|||||||
/>
|
/>
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
<ChartCard
|
<ChartCard
|
||||||
|
|||||||
@@ -206,8 +206,19 @@ export default function OrdersAnalytics() {
|
|||||||
title={__('Orders Over Time')}
|
title={__('Orders Over Time')}
|
||||||
description={__('Daily order count and status breakdown')}
|
description={__('Daily order count and status breakdown')}
|
||||||
>
|
>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
{chartData.length === 0 || chartData.every((d: any) => d.orders === 0) ? (
|
||||||
<ComposedChart data={chartData}>
|
<div className="flex items-center justify-center h-[300px]">
|
||||||
|
<div className="text-center">
|
||||||
|
<Package className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
|
||||||
|
<p className="text-muted-foreground font-medium">{__('No orders data available')}</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{__('Order data will appear once you have orders')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<ComposedChart data={chartData}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="totalOrdersGradient" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="totalOrdersGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
|
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
|
||||||
@@ -287,6 +298,7 @@ export default function OrdersAnalytics() {
|
|||||||
/>
|
/>
|
||||||
</ComposedChart>
|
</ComposedChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{/* Two Column Layout */}
|
{/* Two Column Layout */}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||||
import { DollarSign, TrendingUp, TrendingDown, CreditCard, Truck, RefreshCw } from 'lucide-react';
|
import { DollarSign, TrendingUp, TrendingDown, CreditCard, Truck, RefreshCw, Package } from 'lucide-react';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
||||||
import { useDashboardPeriod } from '@/hooks/useDashboardPeriod';
|
import { useDashboardPeriod } from '@/hooks/useDashboardPeriod';
|
||||||
@@ -396,8 +396,19 @@ export default function RevenueAnalytics() {
|
|||||||
</Select>
|
</Select>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
{chartData.length === 0 || chartData.every((d: any) => d.gross === 0 && d.net === 0) ? (
|
||||||
<AreaChart data={chartData}>
|
<div className="flex items-center justify-center h-[300px]">
|
||||||
|
<div className="text-center">
|
||||||
|
<Package className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
|
||||||
|
<p className="text-muted-foreground font-medium">{__('No revenue data available')}</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{__('Revenue data will appear once you have completed orders')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<AreaChart data={chartData}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="colorGross" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="colorGross" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
|
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
|
||||||
@@ -467,6 +478,7 @@ export default function RevenueAnalytics() {
|
|||||||
/>
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{/* Revenue Breakdown Tables */}
|
{/* Revenue Breakdown Tables */}
|
||||||
|
|||||||
@@ -534,20 +534,39 @@ class AnalyticsController {
|
|||||||
$total_tax = 0;
|
$total_tax = 0;
|
||||||
$total_refunds = 0;
|
$total_refunds = 0;
|
||||||
$order_count = 0;
|
$order_count = 0;
|
||||||
$chart_data = [];
|
|
||||||
|
|
||||||
|
// Create a map of existing data by date
|
||||||
|
$data_map = [];
|
||||||
foreach ($chart_data_raw as $row) {
|
foreach ($chart_data_raw as $row) {
|
||||||
$gross = round(floatval($row->gross), 2);
|
$data_map[$row->date] = [
|
||||||
$net = round(floatval($row->net), 2);
|
'gross' => round(floatval($row->gross), 2),
|
||||||
$tax = round(floatval($row->tax), 2);
|
'net' => round(floatval($row->net), 2),
|
||||||
|
'tax' => round(floatval($row->tax), 2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in ALL dates in the range (including dates with no data)
|
||||||
|
$chart_data = [];
|
||||||
|
for ($i = $days - 1; $i >= 0; $i--) {
|
||||||
|
$date = date('Y-m-d', strtotime("-{$i} days"));
|
||||||
|
|
||||||
|
if (isset($data_map[$date])) {
|
||||||
|
$gross = $data_map[$date]['gross'];
|
||||||
|
$net = $data_map[$date]['net'];
|
||||||
|
$tax = $data_map[$date]['tax'];
|
||||||
|
} else {
|
||||||
|
// No data for this date, fill with zeros
|
||||||
|
$gross = 0.00;
|
||||||
|
$net = 0.00;
|
||||||
|
$tax = 0.00;
|
||||||
|
}
|
||||||
|
|
||||||
$total_gross += $gross;
|
$total_gross += $gross;
|
||||||
$total_net += $net;
|
$total_net += $net;
|
||||||
$total_tax += $tax;
|
$total_tax += $tax;
|
||||||
|
|
||||||
// Format for frontend
|
|
||||||
$chart_data[] = [
|
$chart_data[] = [
|
||||||
'date' => $row->date,
|
'date' => $date,
|
||||||
'gross' => $gross,
|
'gross' => $gross,
|
||||||
'net' => $net,
|
'net' => $net,
|
||||||
'tax' => $tax,
|
'tax' => $tax,
|
||||||
@@ -930,11 +949,10 @@ class AnalyticsController {
|
|||||||
LIMIT %d
|
LIMIT %d
|
||||||
", $days));
|
", $days));
|
||||||
|
|
||||||
// Format chart data
|
// Format chart data - Create a map of existing data by date
|
||||||
$formatted_chart = [];
|
$data_map = [];
|
||||||
foreach ($chart_data as $row) {
|
foreach ($chart_data as $row) {
|
||||||
$formatted_chart[] = [
|
$data_map[$row->date] = [
|
||||||
'date' => $row->date,
|
|
||||||
'orders' => intval($row->orders),
|
'orders' => intval($row->orders),
|
||||||
'completed' => intval($row->completed),
|
'completed' => intval($row->completed),
|
||||||
'processing' => intval($row->processing),
|
'processing' => intval($row->processing),
|
||||||
@@ -945,6 +963,28 @@ class AnalyticsController {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill in ALL dates in the range (including dates with no data)
|
||||||
|
$formatted_chart = [];
|
||||||
|
for ($i = $days - 1; $i >= 0; $i--) {
|
||||||
|
$date = date('Y-m-d', strtotime("-{$i} days"));
|
||||||
|
|
||||||
|
if (isset($data_map[$date])) {
|
||||||
|
$formatted_chart[] = array_merge(['date' => $date], $data_map[$date]);
|
||||||
|
} else {
|
||||||
|
// No data for this date, fill with zeros
|
||||||
|
$formatted_chart[] = [
|
||||||
|
'date' => $date,
|
||||||
|
'orders' => 0,
|
||||||
|
'completed' => 0,
|
||||||
|
'processing' => 0,
|
||||||
|
'pending' => 0,
|
||||||
|
'cancelled' => 0,
|
||||||
|
'refunded' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate average order value and rates
|
// Calculate average order value and rates
|
||||||
$avg_order_value = $total_orders > 0 ? round($total_revenue / $total_orders, 2) : 0;
|
$avg_order_value = $total_orders > 0 ? round($total_revenue / $total_orders, 2) : 0;
|
||||||
|
|
||||||
@@ -1189,21 +1229,39 @@ class AnalyticsController {
|
|||||||
ORDER BY date ASC
|
ORDER BY date ASC
|
||||||
", $days));
|
", $days));
|
||||||
|
|
||||||
$formatted_chart = [];
|
// Create a map of existing data by date
|
||||||
|
$data_map = [];
|
||||||
$revenue_with_coupons = 0;
|
$revenue_with_coupons = 0;
|
||||||
|
|
||||||
foreach ($usage_chart as $row) {
|
foreach ($usage_chart as $row) {
|
||||||
$revenue = round(floatval($row->revenue), 2);
|
$revenue = round(floatval($row->revenue), 2);
|
||||||
$revenue_with_coupons += $revenue;
|
$revenue_with_coupons += $revenue;
|
||||||
|
|
||||||
$formatted_chart[] = [
|
$data_map[$row->date] = [
|
||||||
'date' => $row->date,
|
|
||||||
'uses' => intval($row->uses),
|
'uses' => intval($row->uses),
|
||||||
'discount' => round(floatval($row->discount), 2),
|
'discount' => round(floatval($row->discount), 2),
|
||||||
'revenue' => $revenue,
|
'revenue' => $revenue,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill in ALL dates in the range (including dates with no data)
|
||||||
|
$formatted_chart = [];
|
||||||
|
for ($i = $days - 1; $i >= 0; $i--) {
|
||||||
|
$date = date('Y-m-d', strtotime("-{$i} days"));
|
||||||
|
|
||||||
|
if (isset($data_map[$date])) {
|
||||||
|
$formatted_chart[] = array_merge(['date' => $date], $data_map[$date]);
|
||||||
|
} else {
|
||||||
|
// No data for this date, fill with zeros
|
||||||
|
$formatted_chart[] = [
|
||||||
|
'date' => $date,
|
||||||
|
'uses' => 0,
|
||||||
|
'discount' => 0.00,
|
||||||
|
'revenue' => 0.00,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$avg_discount_per_order = $total_uses > 0 ? round($total_discount / $total_uses, 2) : 0;
|
$avg_discount_per_order = $total_uses > 0 ? round($total_discount / $total_uses, 2) : 0;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
Reference in New Issue
Block a user