Skip to Content
TutorialsAdvanced Topics1. Dashboard & Analytics

1. Dashboard & Analytics

Your Project Management App has pages for managing tasks and projects, but no home screen that tells you what’s actually going on. In this tutorial you’ll build a stats dashboard powered by DaaS aggregate queries — counting, grouping, and summarising data at the API level without loading every record into memory.

By the end you’ll have:

  • A /dashboard route with live stat cards (total tasks, tasks by status, overdue count)
  • A “Tasks by Status” breakdown using groupBy
  • A chart visualising task completion over time
  • The dashboard wired directly to DaaS aggregate endpoints

Prerequisites

  • Completed Tutorial 8: Event Hooks & Extensions — or at minimum Tutorial 3 with a working tasks collection and some data.
  • At least 10 tasks in your database across different statuses and due dates to see meaningful stats.

DaaS aggregate API — The /api/items/:collection endpoint accepts an aggregate parameter that computes count, sum, avg, min, max and a groupBy parameter that buckets results. Computation happens in the database, not in your app code. See the DaaS items documentation  for the full filter syntax.

Scaffold the Dashboard Page

Ask your AI assistant to create the dashboard:

/create-feature dashboard page with task stats

Or create src/app/(protected)/dashboard/page.tsx manually. The page will fetch stats from three API calls and render them as cards.

Fetch Total Task Count by Status

Use the groupBy aggregate to get task counts split by status in a single query:

GET /api/items/tasks ?aggregate[count]=* &groupBy[]=status

Response:

[ { "status": "todo", "count": 8 }, { "status": "in_progress", "count": 3 }, { "status": "in_review", "count": 2 }, { "status": "done", "count": 14 } ]

Fetch Overdue Task Count

Combine a filter with an aggregate to count only tasks that are overdue and not yet done:

GET /api/items/tasks ?aggregate[count]=* &filter[due_date][_lt]=$NOW &filter[status][_nin]=done

In a React component:

src/app/(protected)/dashboard/page.tsx
const today = new Date().toISOString(); const { data: overdueResult } = useApiCall<{ count: number }[]>( `/api/items/tasks?aggregate[count]=*&filter[due_date][_lt]=${today}&filter[status][_nin]=done` ); const overdueCount = overdueResult?.[0]?.count ?? 0;

Fetch Tasks Completed This Month

const startOfMonth = new Date(); startOfMonth.setDate(1); startOfMonth.setHours(0, 0, 0, 0); const { data: completedResult } = useApiCall<{ count: number }[]>( `/api/items/tasks?aggregate[count]=*&filter[status][_eq]=done&filter[date_updated][_gte]=${startOfMonth.toISOString()}` );

Build the Stats Cards

Install the Buildpad stats layout components if you haven’t already:

npx @buildpad/cli@latest add stats-card

Then render the cards with your aggregated data:

src/app/(protected)/dashboard/page.tsx
"use client"; import { SimpleGrid, Paper, Text, Title } from "@mantine/core"; import { useApiCall } from "@/lib/buildpad/hooks/use-api-call"; export default function DashboardPage() { const today = new Date().toISOString(); const startOfMonth = new Date(); startOfMonth.setDate(1); const { data: statusBreakdown } = useApiCall<{ status: string; count: number }[]>( "/api/items/tasks?aggregate[count]=*&groupBy[]=status" ); const { data: overdueResult } = useApiCall<{ count: number }[]>( `/api/items/tasks?aggregate[count]=*&filter[due_date][_lt]=${today}&filter[status][_nin]=done` ); const { data: completedResult } = useApiCall<{ count: number }[]>( `/api/items/tasks?aggregate[count]=*&filter[status][_eq]=done&filter[date_updated][_gte]=${startOfMonth.toISOString()}` ); const totalTasks = statusBreakdown?.reduce((sum, s) => sum + s.count, 0) ?? 0; const doneTasks = statusBreakdown?.find((s) => s.status === "done")?.count ?? 0; const overdueCount = overdueResult?.[0]?.count ?? 0; const completedThisMonth = completedResult?.[0]?.count ?? 0; return ( <div> <Title order={2} mb="lg">Dashboard</Title> <SimpleGrid cols={{ base: 2, md: 4 }} mb="xl"> <StatCard label="Total Tasks" value={totalTasks} /> <StatCard label="Completed" value={doneTasks} color="green" /> <StatCard label="Overdue" value={overdueCount} color="red" /> <StatCard label="Done This Month" value={completedThisMonth} color="blue" /> </SimpleGrid> </div> ); } function StatCard({ label, value, color }: { label: string; value: number; color?: string }) { return ( <Paper p="md" withBorder> <Text size="sm" c="dimmed">{label}</Text> <Text size="xl" fw={700} c={color}>{value}</Text> </Paper> ); }

Add a Tasks by Status Bar Chart

Install Mantine Charts (already a dependency in Buildpad projects):

src/app/(protected)/dashboard/page.tsx
import { BarChart } from "@mantine/charts"; // Inside your component, below the stat cards: const chartData = (statusBreakdown ?? []).map((s) => ({ status: s.status.replace("_", " "), count: s.count, })); return ( // ...stat cards above... <Paper p="md" withBorder> <Title order={4} mb="md">Tasks by Status</Title> <BarChart h={240} data={chartData} dataKey="status" series={[{ name: "count", color: "blue" }]} /> </Paper> );

Add the Dashboard to Navigation

Update your sidebar navigation to include the dashboard link:

src/components/AppSidebar.tsx
// Add to your navigation items array: { label: "Dashboard", href: "/dashboard", icon: IconLayoutDashboard }

Performance Notes

  • Aggregate queries return a single result row regardless of how many tasks exist — use them for any stat that would otherwise require fetching and counting records in JavaScript.
  • The groupBy parameter can group by multiple fields: &groupBy[]=status&groupBy[]=priority gives you counts per status-priority combination.
  • Use filter parameters to scope aggregates to the current user’s data when usePermissions would apply row-level filtering anyway.

What’s Next

Your app now has a data-driven dashboard. When you’re ready to share it with real users, see 2. Deploying to Production.

Last updated on