Overview
The Dashboard API provides comprehensive analytics and key performance indicators for store owners. It includes sales metrics, order analytics, product performance data, and actionable insights to help store owners make informed business decisions. Location:convex/stores/dashboard.ts
Dashboard Overview
Get comprehensive dashboard metrics with key performance indicators.Copy
const overview = await convex.query(api.stores.dashboard.getDashboardOverview);
Copy
{
"todaysSales": 150.50,
"activeOrders": 3,
"ordersToday": 12,
"totalProducts": 25,
"averageOrderValue": 45.30
}
Top Selling Products
Retrieve top-selling products based on quantity sold and revenue generated.Maximum number of products to return (default: 10)
Copy
const topProducts = await convex.query(api.stores.dashboard.getTopSellingProducts, {
limit: 5
});
Copy
[
{
"productId": "p123456789",
"productName": "Pizza Margherita",
"totalQuantitySold": 25,
"totalRevenue": 375.00,
"averagePrice": 15.00,
"productImage": "https://storage.convex.dev/pizza-margherita.jpg"
},
{
"productId": "p987654321",
"productName": "Garlic Bread",
"totalQuantitySold": 18,
"totalRevenue": 108.00,
"averagePrice": 6.00,
"productImage": "https://storage.convex.dev/garlic-bread.jpg"
},
{
"productId": "p555666777",
"productName": "Caesar Salad",
"totalQuantitySold": 15,
"totalRevenue": 180.00,
"averagePrice": 12.00,
"productImage": "https://storage.convex.dev/caesar-salad.jpg"
}
]
Attention Items
Get items that need immediate store owner attention with priority levels.Copy
const alerts = await convex.query(api.stores.dashboard.getAttentionItems);
Copy
[
{
"type": "low_stock",
"title": "Low Stock Alert",
"description": "3 products running low on stock",
"priority": "medium",
"actionUrl": "/products",
"count": 3
},
{
"type": "pending_orders",
"title": "Pending Orders",
"description": "2 orders waiting for confirmation",
"priority": "high",
"actionUrl": "/orders?status=pending",
"count": 2
},
{
"type": "missing_bank_details",
"title": "Bank Details Required",
"description": "Add bank account information to receive payouts",
"priority": "high",
"actionUrl": "/settings/bank"
}
]
Recent Orders
Get recent orders for quick overview and monitoring.Maximum number of orders to return (default: 5)
Copy
const recentOrders = await convex.query(api.stores.dashboard.getRecentOrders, {
limit: 10
});
Copy
[
{
"_id": "o123456789",
"orderDate": 1640995200000,
"totalAmount": 45.50,
"status": "delivered",
"customerName": "John Doe",
"itemCount": 3
},
{
"_id": "o987654321",
"orderDate": 1640994000000,
"totalAmount": 28.75,
"status": "preparing",
"customerName": "Jane Smith",
"itemCount": 2
},
{
"_id": "o555666777",
"orderDate": 1640992800000,
"totalAmount": 62.25,
"status": "out_for_delivery",
"customerName": "Mike Johnson",
"itemCount": 4
}
]
Complete Dashboard Component
Here’s a comprehensive dashboard implementation:Copy
import { useQuery } from 'convex/react';
import { api } from './convex/_generated/api';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
function StoreDashboard() {
const overview = useQuery(api.stores.dashboard.getDashboardOverview);
const topProducts = useQuery(api.stores.dashboard.getTopSellingProducts, { limit: 5 });
const attentionItems = useQuery(api.stores.dashboard.getAttentionItems);
const recentOrders = useQuery(api.stores.dashboard.getRecentOrders, { limit: 8 });
if (!overview) {
return <div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2">Loading dashboard...</span>
</div>;
}
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8'];
return (
<div className="p-6 space-y-6">
{/* Header */}
<div>
<h1 className="text-3xl font-bold text-gray-900">Store Dashboard</h1>
<p className="text-gray-600">Welcome back! Here's what's happening with your store today.</p>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="p-2 bg-green-100 rounded-lg">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Today's Sales</p>
<p className="text-2xl font-bold text-gray-900">${overview.todaysSales.toFixed(2)}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Active Orders</p>
<p className="text-2xl font-bold text-gray-900">{overview.activeOrders}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="p-2 bg-purple-100 rounded-lg">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Orders Today</p>
<p className="text-2xl font-bold text-gray-900">{overview.ordersToday}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="p-2 bg-yellow-100 rounded-lg">
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Products</p>
<p className="text-2xl font-bold text-gray-900">{overview.totalProducts}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="p-2 bg-indigo-100 rounded-lg">
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Avg Order Value</p>
<p className="text-2xl font-bold text-gray-900">${overview.averageOrderValue.toFixed(2)}</p>
</div>
</div>
</div>
</div>
{/* Attention Items */}
{attentionItems && attentionItems.length > 0 && (
<div className="bg-white rounded-lg shadow">
<div className="p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
🚨 Needs Your Attention
</h2>
<div className="space-y-3">
{attentionItems.map((item, index) => (
<div key={index} className={`p-4 rounded-lg border-l-4 ${
item.priority === 'high' ? 'bg-red-50 border-red-400' :
item.priority === 'medium' ? 'bg-yellow-50 border-yellow-400' :
'bg-blue-50 border-blue-400'
}`}>
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium text-gray-900">{item.title}</h3>
<p className="text-sm text-gray-600">{item.description}</p>
</div>
<div className="flex items-center space-x-2">
{item.count && (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
item.priority === 'high' ? 'bg-red-100 text-red-800' :
item.priority === 'medium' ? 'bg-yellow-100 text-yellow-800' :
'bg-blue-100 text-blue-800'
}`}>
{item.count}
</span>
)}
{item.actionUrl && (
<button className="text-sm text-blue-600 hover:text-blue-800">
Take Action →
</button>
)}
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Products Chart */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Top Selling Products</h2>
{topProducts && topProducts.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={topProducts}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="productName"
angle={-45}
textAnchor="end"
height={80}
interval={0}
/>
<YAxis />
<Tooltip />
<Bar dataKey="totalQuantitySold" fill="#3B82F6" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-64 text-gray-500">
No sales data available yet
</div>
)}
</div>
{/* Recent Orders */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Recent Orders</h2>
{recentOrders && recentOrders.length > 0 ? (
<div className="space-y-3">
{recentOrders.map((order) => (
<div key={order._id} className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div>
<p className="font-medium text-gray-900">
{order.customerName || 'Guest'}
</p>
<p className="text-sm text-gray-600">
{order.itemCount} items • {new Date(order.orderDate).toLocaleDateString()}
</p>
</div>
<div className="text-right">
<p className="font-medium text-gray-900">
${order.totalAmount.toFixed(2)}
</p>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
order.status === 'delivered' ? 'bg-green-100 text-green-800' :
order.status === 'preparing' ? 'bg-yellow-100 text-yellow-800' :
order.status === 'out_for_delivery' ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-800'
}`}>
{order.status.replace('_', ' ')}
</span>
</div>
</div>
))}
</div>
) : (
<div className="flex items-center justify-center h-64 text-gray-500">
No recent orders
</div>
)}
</div>
</div>
{/* Revenue Chart */}
{topProducts && topProducts.length > 0 && (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Revenue by Product</h2>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={topProducts}
cx="50%"
cy="50%"
labelLine={false}
label={({ productName, totalRevenue }) => `${productName}: $${totalRevenue}`}
outerRadius={80}
fill="#8884d8"
dataKey="totalRevenue"
>
{topProducts.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
)}
</div>
);
}
export default StoreDashboard;
Dashboard Metrics
Sales Analytics
Track daily sales, revenue trends, and average order values
Order Management
Monitor active orders, order status, and fulfillment metrics
Product Performance
Identify top-selling products and inventory insights
Business Intelligence
Get actionable insights and attention items for business growth
Attention Item Types
The dashboard identifies various issues that need store owner attention:Store Verification
Store Verification
Type:
store_verificationStore application pending approval or additional documents required.Copy
{
"type": "store_verification",
"title": "Store Verification Pending",
"description": "Your store is under review",
"priority": "high"
}
Low Stock
Low Stock
Type:
low_stockProducts running low on inventory that need restocking.Copy
{
"type": "low_stock",
"title": "Low Stock Alert",
"description": "3 products running low on stock",
"priority": "medium",
"count": 3,
"actionUrl": "/products"
}
Missing Bank Details
Missing Bank Details
Type:
missing_bank_detailsBank account information required for payout processing.Copy
{
"type": "missing_bank_details",
"title": "Bank Details Required",
"description": "Add bank account to receive payouts",
"priority": "high",
"actionUrl": "/settings/bank"
}
Low Wallet Balance
Low Wallet Balance
Type:
low_wallet_balanceStore wallet balance is low, may affect operations.Copy
{
"type": "low_wallet_balance",
"title": "Low Wallet Balance",
"description": "Wallet balance below minimum threshold",
"priority": "medium",
"actionUrl": "/wallet"
}
Pending Orders
Pending Orders
Type:
pending_ordersOrders waiting for confirmation or processing.Copy
{
"type": "pending_orders",
"title": "Pending Orders",
"description": "2 orders waiting for confirmation",
"priority": "high",
"count": 2,
"actionUrl": "/orders?status=pending"
}
Real-time Dashboard Updates
Dashboard data updates automatically as business activities occur:Copy
// Dashboard automatically reflects:
// - New orders placed
// - Order status changes
// - Product sales updates
// - Inventory changes
// - Payment processing
// - Wallet balance changes
function DashboardWithRealTimeUpdates() {
const overview = useQuery(api.stores.dashboard.getDashboardOverview);
// Data refreshes automatically when:
// - Customer places new order → ordersToday increases
// - Order is delivered → activeOrders decreases, todaysSales increases
// - Product inventory changes → attention items may update
// - Bank details added → attention items update
return <StoreDashboard overview={overview} />;
}
Error Handling
Store not found
Store not found
Status Code:
404Copy
{
"error": "Store not found for authenticated user"
}
Insufficient permissions
Insufficient permissions
Status Code:
403Copy
{
"error": "Access denied. User is not the store owner."
}
Store not approved
Store not approved
Status Code:
400Copy
{
"error": "Dashboard not available for unapproved stores"
}
Dashboard metrics are calculated in real-time and update automatically as business activities occur. Historical data is maintained for trend analysis.
Use the attention items to proactively address business issues before they impact operations. High-priority items should be addressed immediately.
