Skip to main content

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.
const overview = await convex.query(api.stores.dashboard.getDashboardOverview);
{
  "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.
limit
number
Maximum number of products to return (default: 10)
const topProducts = await convex.query(api.stores.dashboard.getTopSellingProducts, { 
  limit: 5 
});
[
  {
    "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.
const alerts = await convex.query(api.stores.dashboard.getAttentionItems);
[
  {
    "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.
limit
number
Maximum number of orders to return (default: 5)
const recentOrders = await convex.query(api.stores.dashboard.getRecentOrders, { 
  limit: 10 
});
[
  {
    "_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:
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:
Type: store_verificationStore application pending approval or additional documents required.
{
  "type": "store_verification",
  "title": "Store Verification Pending",
  "description": "Your store is under review",
  "priority": "high"
}
Type: low_stockProducts running low on inventory that need restocking.
{
  "type": "low_stock", 
  "title": "Low Stock Alert",
  "description": "3 products running low on stock",
  "priority": "medium",
  "count": 3,
  "actionUrl": "/products"
}
Type: missing_bank_detailsBank account information required for payout processing.
{
  "type": "missing_bank_details",
  "title": "Bank Details Required", 
  "description": "Add bank account to receive payouts",
  "priority": "high",
  "actionUrl": "/settings/bank"
}
Type: low_wallet_balanceStore wallet balance is low, may affect operations.
{
  "type": "low_wallet_balance",
  "title": "Low Wallet Balance",
  "description": "Wallet balance below minimum threshold", 
  "priority": "medium",
  "actionUrl": "/wallet"
}
Type: pending_ordersOrders waiting for confirmation or processing.
{
  "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:
// 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

Status Code: 404
{
  "error": "Store not found for authenticated user"
}
Status Code: 403
{
  "error": "Access denied. User is not the store owner."
}
Status Code: 400
{
  "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.