Thought for 3 seconds### Tutorial: Building a Simple Product Sales Dashboard with Django and HTMX
In this tutorial, we’ll build a real-time product sales dashboard that displays key business metrics and updates automatically. We’ll keep it simple but effective, focusing on the most important sales data.
Project Overview
We’ll create a dashboard that shows:
- Total sales and revenue today
- Top selling products
- Sales trend over time (line chart)
- Sales by product category (pie chart)
The dashboard will update automatically every few seconds without page refreshes.
Step 1: Set Up Your Django Project
First, let’s create a new Django project:
# Create a virtual environment
python -m venv venv
# Activate the virtual environment
# On Windows:
venv\Scripts\activate
# On macOS/Linux:
source venv/bin/activate
# Install Django and django-htmx
pip install django django-htmx
# Create a new Django project
django-admin startproject sales_dashboard
# Navigate to the project directory
cd sales_dashboard
# Create a new app
python manage.py startapp dashboardStep 2: Configure Your Project
Update your settings.py file:
# sales_dashboard/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'dashboard', # Add your app
'django_htmx', # Add django-htmx
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_htmx.middleware.HtmxMiddleware', # Add HTMX middleware
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # Add templates directory
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]Step 3: Create Models
Let’s create models for our products and sales:
# dashboard/models.py
from django.db import models
from django.utils import timezone
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "Categories"
class Product(models.Model):
name = models.CharField(max_length=200)
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products')
price = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return self.name
class Sale(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='sales')
quantity = models.PositiveIntegerField(default=1)
timestamp = models.DateTimeField(default=timezone.now)
def __str__(self):
return f"{self.quantity} x {self.product.name} at {self.timestamp}"
@property
def total_price(self):
return self.product.price * self.quantityNow run migrations to create the database tables:
python manage.py makemigrations
python manage.py migrateStep 4: Create Admin Interface
Let’s set up the admin interface to easily add products and sales:
# dashboard/admin.py
from django.contrib import admin
from .models import Category, Product, Sale
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name',)
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'price')
list_filter = ('category',)
search_fields = ('name',)
@admin.register(Sale)
class SaleAdmin(admin.ModelAdmin):
list_display = ('product', 'quantity', 'timestamp', 'get_total_price')
list_filter = ('product__category', 'timestamp')
date_hierarchy = 'timestamp'
def get_total_price(self, obj):
return f"${obj.total_price}"
get_total_price.short_description = 'Total Price'Step 5: Create Demo Data Script
Let’s create a management command to generate demo data:
# dashboard/management/commands/generate_demo_data.py
from django.core.management.base import BaseCommand
from django.utils import timezone
from dashboard.models import Category, Product, Sale
import random
from datetime import timedelta
class Command(BaseCommand):
help = 'Generates demo data for the sales dashboard'
def handle(self, *args, **kwargs):
# Create categories
categories = [
'Electronics',
'Clothing',
'Home & Kitchen',
'Books',
'Toys',
]
for cat_name in categories:
Category.objects.get_or_create(name=cat_name)
self.stdout.write(self.style.SUCCESS(f'Created {len(categories)} categories'))
# Create products
products_data = [
# Electronics
{'name': 'Smartphone', 'category': 'Electronics', 'price': 699.99},
{'name': 'Laptop', 'category': 'Electronics', 'price': 1299.99},
{'name': 'Headphones', 'category': 'Electronics', 'price': 149.99},
{'name': 'Tablet', 'category': 'Electronics', 'price': 499.99},
# Clothing
{'name': 'T-Shirt', 'category': 'Clothing', 'price': 19.99},
{'name': 'Jeans', 'category': 'Clothing', 'price': 49.99},
{'name': 'Sneakers', 'category': 'Clothing', 'price': 79.99},
{'name': 'Jacket', 'category': 'Clothing', 'price': 89.99},
# Home & Kitchen
{'name': 'Coffee Maker', 'category': 'Home & Kitchen', 'price': 79.99},
{'name': 'Blender', 'category': 'Home & Kitchen', 'price': 49.99},
{'name': 'Toaster', 'category': 'Home & Kitchen', 'price': 29.99},
# Books
{'name': 'Fiction Book', 'category': 'Books', 'price': 14.99},
{'name': 'Cookbook', 'category': 'Books', 'price': 24.99},
{'name': 'Self-Help Book', 'category': 'Books', 'price': 19.99},
# Toys
{'name': 'Action Figure', 'category': 'Toys', 'price': 12.99},
{'name': 'Board Game', 'category': 'Toys', 'price': 34.99},
{'name': 'Puzzle', 'category': 'Toys', 'price': 19.99},
]
for product_data in products_data:
category = Category.objects.get(name=product_data['category'])
Product.objects.get_or_create(
name=product_data['name'],
defaults={
'category': category,
'price': product_data['price']
}
)
self.stdout.write(self.style.SUCCESS(f'Created {len(products_data)} products'))
# Generate sales for the past 7 days
products = Product.objects.all()
now = timezone.now()
# Delete existing sales
Sale.objects.all().delete()
# Generate sales for each day
for days_ago in range(7, -1, -1): # 7 days ago to today
date = now - timedelta(days=days_ago)
# More sales for recent days
num_sales = random.randint(10, 50) if days_ago < 3 else random.randint(5, 30)
for _ in range(num_sales):
product = random.choice(products)
quantity = random.randint(1, 5)
# Random time during the day
hour = random.randint(8, 20) # Between 8 AM and 8 PM
minute = random.randint(0, 59)
second = random.randint(0, 59)
sale_time = date.replace(hour=hour, minute=minute, second=second)
Sale.objects.create(
product=product,
quantity=quantity,
timestamp=sale_time
)
total_sales = Sale.objects.count()
self.stdout.write(self.style.SUCCESS(f'Generated {total_sales} sales'))Run the command to generate demo data:
python manage.py generate_demo_dataStep 6: Create Views
Now, let’s create views for our dashboard:
# dashboard/views.py
from django.shortcuts import render
from django.utils import timezone
from django.db.models import Sum, Count, F
from django.http import HttpResponse
from datetime import timedelta
import json
import random
from .models import Category, Product, Sale
def dashboard(request):
"""Main dashboard view"""
return render(request, 'dashboard/dashboard.html')
def total_sales_today(request):
"""Return the total number of sales and revenue today"""
today = timezone.now().date()
# Get sales for today
sales = Sale.objects.filter(timestamp__date=today)
total_count = sales.count()
# Calculate total revenue
total_revenue = sales.aggregate(
total=Sum(F('quantity') * F('product__price'))
)['total'] or 0
return render(request, 'dashboard/partials/total_sales.html', {
'total_count': total_count,
'total_revenue': total_revenue
})
def top_products(request):
"""Return the top selling products today"""
today = timezone.now().date()
# Get top 5 products by quantity sold today
top_products = Product.objects.filter(
sales__timestamp__date=today
).annotate(
total_sold=Sum('sales__quantity')
).order_by('-total_sold')[:5]
return render(request, 'dashboard/partials/top_products.html', {
'top_products': top_products
})
def sales_trend(request):
"""Return sales trend for the last 7 days"""
end_date = timezone.now().date()
start_date = end_date - timedelta(days=6) # Last 7 days
# Prepare data structure
dates = []
sales_counts = []
revenue_data = []
# Get data for each day
current_date = start_date
while current_date <= end_date:
dates.append(current_date.strftime('%Y-%m-%d'))
# Get sales for this day
day_sales = Sale.objects.filter(timestamp__date=current_date)
sales_counts.append(day_sales.count())
# Calculate revenue for this day
day_revenue = day_sales.aggregate(
total=Sum(F('quantity') * F('product__price'))
)['total'] or 0
revenue_data.append(float(day_revenue))
current_date += timedelta(days=1)
context = {
'dates': dates,
'sales_counts': sales_counts,
'revenue_data': revenue_data,
}
return render(request, 'dashboard/partials/sales_trend.html', context)
def sales_by_category(request):
"""Return sales grouped by product category"""
today = timezone.now().date()
# Get sales by category
categories = Category.objects.all()
labels = []
data = []
for category in categories:
labels.append(category.name)
# Calculate total sales for this category today
category_sales = Sale.objects.filter(
product__category=category,
timestamp__date=today
).aggregate(
total=Sum('quantity')
)['total'] or 0
data.append(category_sales)
context = {
'labels': labels,
'data': data,
}
return render(request, 'dashboard/partials/sales_by_category.html', context)
def add_random_sale(request):
"""Add a random sale (for demo purposes)"""
products = Product.objects.all()
if not products:
return HttpResponse("No products available")
product = random.choice(products)
quantity = random.randint(1, 3)
Sale.objects.create(
product=product,
quantity=quantity
)
return HttpResponse(f"Added sale: {quantity} x {product.name}")Step 7: Create URLs
Set up the URL patterns:
# dashboard/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.dashboard, name='dashboard'),
path('total-sales/', views.total_sales_today, name='total_sales'),
path('top-products/', views.top_products, name='top_products'),
path('sales-trend/', views.sales_trend, name='sales_trend'),
path('sales-by-category/', views.sales_by_category, name='sales_by_category'),
path('add-random-sale/', views.add_random_sale, name='add_random_sale'),
]# sales_dashboard/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('dashboard.urls')),
]Step 8: Create Templates
First, create the necessary directories:
mkdir -p templates/dashboard/partialsNow, let’s create our templates:
<!-- templates/dashboard/dashboard.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sales Dashboard</title>
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
.chart-container {
position: relative;
height: 300px;
width: 100%;
}
</style>
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<header class="mb-8">
<h1 class="text-3xl font-bold text-gray-800">Sales Dashboard</h1>
<p class="text-gray-600">Real-time sales data updates every 5 seconds</p>
</header>
<!-- Dashboard Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Total Sales Card -->
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">Today's Sales</h2>
<div id="total-sales"
hx-get="{% url 'total_sales' %}"
hx-trigger="load, every 5s">
Loading...
</div>
</div>
<!-- Add Random Sale (Demo) -->
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">Demo Controls</h2>
<button hx-post="{% url 'add_random_sale' %}"
hx-swap="none"
class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded">
Simulate New Sale
</button>
<p class="text-sm text-gray-500 mt-2">
Click to add a random sale (dashboard will update automatically)
</p>
</div>
<!-- Top Products -->
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">Top Selling Products Today</h2>
<div id="top-products"
hx-get="{% url 'top_products' %}"
hx-trigger="load, every 5s">
Loading...
</div>
</div>
<!-- Sales by Category Chart -->
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">Sales by Category</h2>
<div id="sales-by-category"
hx-get="{% url 'sales_by_category' %}"
hx-trigger="load, every 5s">
Loading...
</div>
</div>
<!-- Sales Trend Chart -->
<div class="bg-white rounded-lg shadow-md p-6 md:col-span-2">
<h2 class="text-xl font-semibold mb-4">Sales Trend (Last 7 Days)</h2>
<div id="sales-trend"
hx-get="{% url 'sales_trend' %}"
hx-trigger="load, every 5s">
Loading...
</div>
</div>
</div>
</div>
</body>
</html>Now, let’s create the partial templates that will be loaded via HTMX:
<!-- templates/dashboard/partials/total_sales.html -->
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-sm text-gray-500">Total Orders</div>
<div class="text-4xl font-bold text-blue-600">{{ total_count }}</div>
</div>
<div>
<div class="text-sm text-gray-500">Total Revenue</div>
<div class="text-4xl font-bold text-green-600">${{ total_revenue|floatformat:2 }}</div>
</div>
</div><!-- templates/dashboard/partials/top_products.html -->
{% if top_products %}
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Product</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Units Sold</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for product in top_products %}
<tr>
<td class="px-4 py-2 whitespace-nowrap text-sm font-medium text-gray-900">{{ product.name }}</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">{{ product.category.name }}</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">{{ product.total_sold }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-gray-500">No sales data available for today.</p>
{% endif %}<!-- templates/dashboard/partials/sales_trend.html -->
<div class="chart-container">
<canvas id="salesTrendChart"></canvas>
</div>
<script>
// Destroy existing chart if it exists
if (window.salesTrendChart) {
window.salesTrendChart.destroy();
}
// Create new chart
const ctx = document.getElementById('salesTrendChart').getContext('2d');
window.salesTrendChart = new Chart(ctx, {
type: 'line',
data: {
labels: {{ dates|safe }},
datasets: [
{
label: 'Orders',
data: {{ sales_counts|safe }},
borderColor: 'rgba(59, 130, 246, 1)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
tension: 0.3,
yAxisID: 'y'
},
{
label: 'Revenue ($)',
data: {{ revenue_data|safe }},
borderColor: 'rgba(16, 185, 129, 1)',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
tension: 0.3,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'Orders'
},
beginAtZero: true,
ticks: {
precision: 0
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: 'Revenue ($)'
},
beginAtZero: true,
grid: {
drawOnChartArea: false,
},
}
}
}
});
</script><!-- templates/dashboard/partials/sales_by_category.html -->
<div class="chart-container">
<canvas id="categoryChart"></canvas>
</div>
<script>
// Destroy existing chart if it exists
if (window.categoryChart) {
window.categoryChart.destroy();
}
// Create new chart
const ctx = document.getElementById('categoryChart').getContext('2d');
window.categoryChart = new Chart(ctx, {
type: 'pie',
data: {
labels: {{ labels|safe }},
datasets: [{
data: {{ data|safe }},
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)',
'rgba(255, 159, 64, 0.7)',
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = Math.round((value / total) * 100);
return `${label}: ${value} units (${percentage}%)`;
}
}
}
}
}
});
</script>Step 9: Create a Superuser
Create a superuser to access the admin panel:
python manage.py createsuperuserStep 10: Run Your Project
Now, let’s run the development server:
python manage.py runserverVisit http://127.0.0.1:8000/ in your browser to see your real-time sales dashboard!
Visit http://127.0.0.1:8000/admin/ to access the admin panel where you can manually add products and sales.
How It Works
- Initial Load: When you first load the dashboard, HTMX makes separate requests to load each component.
- Automatic Updates: HTMX automatically refreshes each component every 5 seconds using
hx-trigger="load, every 5s". - Chart Updates: When new data arrives, the charts are destroyed and recreated with the new data.
- Demo Button: The “Simulate New Sale” button lets you add random sales to see the dashboard update in real-time.
Extending the Project
Here are some ways you could extend this project:
- Date Range Selector: Add the ability to view data from different date ranges.
- User Authentication: Add login functionality to restrict dashboard access.
- Export Functionality: Add buttons to export data as CSV or PDF.
- Inventory Tracking: Add inventory levels and alerts for low stock.
- Customer Insights: Add customer data and purchase patterns.
Conclusion
You’ve built a simple but effective real-time sales dashboard with Django, HTMX, and Chart.js! This project demonstrates how to:
- Create a data model for products and sales
- Display key business metrics in real-time
- Visualize sales data with charts
- Create a responsive dashboard layout with Tailwind CSS
- Update data in real-time without page refreshes
This dashboard provides a solid foundation that you can build upon for your specific business needs. The real-time updates make it a valuable tool for monitoring sales performance throughout the day.
Happy coding!