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
/dashboardroute 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
taskscollection 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 statsOr 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:
API call
GET /api/items/tasks
?aggregate[count]=*
&groupBy[]=statusResponse:
[
{ "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]=doneIn a React component:
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-cardThen render the cards with your aggregated data:
"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):
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:
// 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
groupByparameter can group by multiple fields:&groupBy[]=status&groupBy[]=prioritygives you counts per status-priority combination. - Use
filterparameters to scope aggregates to the current user’s data whenusePermissionswould 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.