Back to Blog
·11 min

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.

Ready to Build?

Get started with our production-ready starter kits and ship your project faster.

Browse Starter Kits