Back to Blog
·10 min

How to Build a Time Tracking Dashboard with Next.js

Time tracking is one of the most requested features in SaaS products. Whether you are building a freelancer platform, a project management tool, or an internal operations dashboard, a time tracker is almost always on the feature list.

In this guide, we will build a production-ready time tracking dashboard using Next.js, Prisma, and PostgreSQL. By the end, you'll have a solid foundation that supports start/stop timers, manual time entry, project-based tracking, and basic reporting.

Why Build Your Own Time Tracker?

Off-the-shelf time tracking tools like Toggl and Harvest are great, but they don't integrate with your specific workflow. Building your own means you can:

  • Embed time tracking directly into your existing app
  • Customize the data model for your business logic
  • Keep all data within your infrastructure
  • Add team-specific features without waiting for a vendor update

Setting Up the Database Schema

The heart of any time tracker is the database. Here is a Prisma schema that covers the essentials:

model Project {
  id        String   @id @default(cuid())
  name      String
  color     String   @default("#6366f1")
  billable  Boolean  @default(true)
  rate      Decimal? @default(0)
  entries   TimeEntry[]
  createdAt DateTime @default(now())
}

model TimeEntry {
  id        String   @id @default(cuid())
  projectId String
  project   Project  @relation(fields: [projectId], references: [id])
  userId    String
  startTime DateTime
  endTime   DateTime?
  duration  Int?
  notes     String?
  billable  Boolean  @default(true)
  createdAt DateTime @default(now())
}

Building the Timer Component

The core timer component needs to handle start, stop, and real-time display. Here is a simplified version:

'use client'

import { useState, useEffect, useCallback } from 'react'

export function Timer({ projectId, onEntry }: { projectId: string; onEntry: (entry: any) => void }) {
  const [running, setRunning] = useState(false)
  const [elapsed, setElapsed] = useState(0)
  const [startTime, setStartTime] = useState<Date | null>(null)

  useEffect(() => {
    if (!running) return
    const interval = setInterval(() => {
      setElapsed(prev => prev + 1)
    }, 1000)
    return () => clearInterval(interval)
  }, [running])

  const handleStart = useCallback(async () => {
    const now = new Date()
    setStartTime(now)
    setRunning(true)
    setElapsed(0)
  }, [projectId])

  const handleStop = useCallback(async () => {
    setRunning(false)
    const entry = {
      projectId,
      startTime: startTime!.toISOString(),
      endTime: new Date().toISOString(),
      duration: elapsed,
    }
    onEntry(entry)
  }, [projectId, startTime, elapsed, onEntry])

  return (
    <div className="flex items-center gap-4 rounded-xl bg-gray-900 p-4">
      <div className="text-3xl font-mono font-bold text-white">
        {String(Math.floor(elapsed / 3600)).padStart(2, '0')}:
        {String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0')}:
        {String(elapsed % 60).padStart(2, '0')}
      </div>
      <button
        onClick={running ? handleStop : handleStart}
        className="rounded-lg px-4 py-2 text-sm font-semibold text-white bg-brand-600 hover:bg-brand-700"
      >
        {running ? 'Stop' : 'Start'}
      </button>
    </div>
  )
}

Building the Timesheet View

Once you have time entries, you need a way to display and manage them. A timesheet view should show entries grouped by day with running totals:

export function Timesheet({ entries }: { entries: TimeEntry[] }) {
  const grouped = groupBy(entries, e => format(e.startTime, 'yyyy-MM-dd'))

  return (
    <div className="space-y-6">
      {Object.entries(grouped).map(([date, dayEntries]) => (
        <div key={date} className="rounded-xl bg-gray-900 p-4">
          <div className="flex items-center justify-between mb-3">
            <h3 className="text-sm font-semibold text-white">{format(new Date(date), 'EEEE, MMM d')}</h3>
            <span className="text-sm text-gray-400">
              {formatDuration(dayEntries.reduce((sum, e) => sum + (e.duration || 0), 0))}
            </span>
          </div>
          {dayEntries.map(entry => (
            <div key={entry.id} className="flex items-center justify-between py-2 border-b border-gray-800 last:border-0">
              <div>
                <p className="text-sm text-white">{entry.project.name}</p>
                {entry.notes && <p className="text-xs text-gray-400">{entry.notes}</p>}
              </div>
              <span className="text-sm text-gray-300">{formatDuration(entry.duration || 0)}</span>
            </div>
          ))}
        </div>
      ))}
    </div>
  )
}

Adding Reports

Reporting transforms raw time data into actionable insights. A weekly summary with billable vs non-billable hours, project breakdowns, and export functionality makes the tool enterprise-ready.

Cross-Selling: Supercharge Your Time Tracker

Our Time Tracker Pro template includes all of this and more — start/stop timer, manual entry, timesheet reporting, project management, team dashboards, and CSV export. It is a complete, production-ready implementation that you can customize and deploy in hours, not weeks.

Pair it with our OKR Goal Tracker to connect time tracking to company objectives, or use Freelancer Finance Hub to turn tracked hours into invoices automatically. The combination creates a powerful business operations suite.

Browse the full collection of templates to find everything you need for your next project.

Ready to Build?

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

Browse Starter Kits