How to Build a CRM Dashboard: Step-by-Step Development Guide
Customer Relationship Management (CRM) software is one of the most popular SaaS categories. Every business needs to manage contacts, track deals, and analyze sales performance. Building a CRM dashboard from scratch requires careful attention to data modeling, user workflows, and data visualization.
This guide walks through building a professional CRM dashboard using Next.js 14, TanStack Table, Recharts, and Supabase. You will learn how to build contact management, deal pipelines, activity tracking, and analytics dashboards.
CRM Data Model
A CRM system revolves around these core entities:
-- Contacts (people you do business with)
create table contacts (
id uuid default gen_random_uuid() primary key,
user_id uuid references auth.users not null,
name text not null,
email text,
phone text,
company text,
position text,
avatar_url text,
tags text[] default '{}',
created_at timestamptz default now()
);
-- Deals (sales opportunities)
create table deals (
id uuid default gen_random_uuid() primary key,
user_id uuid references auth.users not null,
contact_id uuid references contacts not null,
title text not null,
value integer not null,
stage text not null default 'lead',
probability integer default 10,
expected_close_date date,
notes text,
created_at timestamptz default now()
);
-- Activities (interactions with contacts)
create table activities (
id uuid default gen_random_uuid() primary key,
user_id uuid references auth.users not null,
contact_id uuid references contacts not null,
type text not null, -- email, call, meeting, note
subject text not null,
description text,
due_date timestamptz,
completed boolean default false,
created_at timestamptz default now()
);Kanban Deal Pipeline
The deal pipeline is the heart of any CRM. Here is a Kanban board implementation:
'use client'
import { useState } from 'react'
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
import { Plus } from 'lucide-react'
const STAGES = [
{ id: 'lead', label: 'Lead', color: 'bg-gray-100' },
{ id: 'qualified', label: 'Qualified', color: 'bg-blue-50' },
{ id: 'proposal', label: 'Proposal', color: 'bg-yellow-50' },
{ id: 'negotiation', label: 'Negotiation', color: 'bg-orange-50' },
{ id: 'closed-won', label: 'Closed Won', color: 'bg-green-50' },
{ id: 'closed-lost', label: 'Closed Lost', color: 'bg-red-50' },
]
interface Deal {
id: string
title: string
value: number
stage: string
contact: { name: string }
}
export function DealPipeline({ deals }: { deals: Deal[] }) {
const [columns, setColumns] = useState(() => {
const grouped: Record<string, Deal[]> = {}
STAGES.forEach(s => { grouped[s.id] = [] })
deals.forEach(d => { grouped[d.stage]?.push(d) })
return grouped
})
const handleDragEnd = (result: any) => {
if (!result.destination) return
const sourceCol = result.source.droppableId
const destCol = result.destination.droppableId
if (sourceCol === destCol) return
const sourceItems = [...columns[sourceCol]]
const destItems = [...columns[destCol]]
const [removed] = sourceItems.splice(result.source.index, 1)
removed.stage = destCol
destItems.splice(result.destination.index, 0, removed)
setColumns({ ...columns, [sourceCol]: sourceItems, [destCol]: destItems })
}
return (
<DragDropContext onDragEnd={handleDragEnd}>
<div className="flex gap-4 overflow-x-auto pb-4">
{STAGES.map(stage => (
<div key={stage.id} className="min-w-[280px] flex-shrink-0">
<div className="mb-3 flex items-center justify-between">
<h3 className="font-semibold text-sm">{stage.label}</h3>
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium">
{columns[stage.id]?.length ?? 0}
</span>
</div>
<Droppable droppableId={stage.id}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className={`min-h-[400px] rounded-xl ${stage.color} p-3 space-y-3`}
>
{columns[stage.id]?.map((deal, index) => (
<Draggable key={deal.id} draggableId={deal.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className="rounded-lg border bg-white p-3 shadow-sm"
>
<p className="font-medium text-sm">{deal.title}</p>
<p className="mt-1 text-xs text-gray-500">{deal.contact.name}</p>
<p className="mt-2 font-bold text-sm">${deal.value.toLocaleString()}</p>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</div>
))}
</div>
</DragDropContext>
)
}Sales Analytics Dashboard
'use client'
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer, PieChart, Pie, Cell,
} from 'recharts'
const COLORS = ['#6366f1', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6']
export function SalesAnalytics({ deals }: { deals: any[] }) {
const stageData = deals.reduce((acc: any[], deal) => {
const existing = acc.find(d => d.name === deal.stage)
if (existing) {
existing.value += deal.value
} else {
acc.push({ name: deal.stage, value: deal.value })
}
return acc
}, [])
const monthlyData = [
{ month: 'Jan', revenue: 12000, deals: 8 },
{ month: 'Feb', revenue: 15000, deals: 12 },
{ month: 'Mar', revenue: 18000, deals: 15 },
{ month: 'Apr', revenue: 14000, deals: 10 },
{ month: 'May', revenue: 22000, deals: 18 },
{ month: 'Jun', revenue: 25000, deals: 20 },
]
return (
<div className="grid gap-6 lg:grid-cols-2">
<div className="rounded-xl border bg-white p-6">
<h3 className="font-semibold">Revenue by Stage</h3>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={stageData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={100}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{stageData.map((_, i) => (
<Cell key={i} fill={COLORS[i % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
<div className="rounded-xl border bg-white p-6">
<h3 className="font-semibold">Monthly Revenue</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={monthlyData}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis dataKey="month" fontSize={12} />
<YAxis fontSize={12} />
<Tooltip />
<Bar dataKey="revenue" fill="#6366f1" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)
}The Bottom Line
Building a CRM dashboard requires careful attention to data modeling, user workflows, and analytics visualization. The CRM Dashboard Starter template provides all of this — contact management, Kanban deal pipeline, activity tracking, and sales analytics — ready to customize for your specific business needs.
Related Template
Try this production-ready starter kit to build your project faster.
Dashboard Overview
$45,231
↑ +20.1%
2,350
↑ +12.5%
1,423
↓ -3.2%
3.24%
↑ +4.3%
Revenue Overview
Recent Orders
| Order | Customer | Amount | Status |
|---|---|---|---|
| #1234 | Alice Johnson | $79 | Completed |
| #1235 | Bob Smith | $29 | Pending |
| #1236 | Carol White | $199 | Completed |
| #1237 | David Lee | $49 | Failed |
Admin Dashboard Pro
Beautiful admin panels at warp speed
Ready to Build?
Get started with our production-ready starter kits and ship your project faster.
Browse Starter Kits