> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dashboards & reports

> Compute counts, sums, group-bys, and joins efficiently with zite.sql() instead of pulling rows into JavaScript.

`findAll` caps at 2,000 rows, so aggregating in JavaScript silently undercounts on large tables. Push the math into the database instead: call [`zite.sql()`](/framework/database-client#read-only-sql) from a backend [workflow](/framework/workflows).

## The pattern

One query per stat, returning typed results the frontend renders:

```typescript theme={null}
// apps/<app>/src/api/salesDashboard.ts
import { z } from 'zod';
import { createEndpoint } from 'zitejs/backend';
import { zite } from 'zitejs/db';

export default createEndpoint({
  description: 'Sales dashboard stats',
  inputSchema: z.object({}),
  outputSchema: z.object({
    totalOrders: z.number(),
    totalRevenue: z.number(),
    revenueByDay: z.array(z.object({ date: z.string(), revenue: z.number() })),
  }),
  execute: async () => {
    const totals = await zite.sql({
      query: `
        SELECT
          COUNT(DISTINCT o.id) AS "totalOrders",
          COALESCE(SUM(i."price"), 0) AS "totalRevenue"
        FROM "Orders" o
        LEFT JOIN "ItemsOrders" l ON l."ordersId" = o.id
        LEFT JOIN "Items" i ON i.id = l."itemsId"
      `,
    });

    const byDay = await zite.sql({
      query: `
        SELECT
          to_char(o."orderPlacedAt", 'YYYY-MM-DD') AS date,
          COALESCE(SUM(i."price"), 0) AS revenue
        FROM "Orders" o
        LEFT JOIN "ItemsOrders" l ON l."ordersId" = o.id
        LEFT JOIN "Items" i ON i.id = l."itemsId"
        WHERE o."orderPlacedAt" >= now() - interval '14 days'
        GROUP BY 1
        ORDER BY 1 ASC
      `,
    });

    return {
      totalOrders: Number(totals.rows[0]?.totalOrders ?? 0),
      totalRevenue: Number(totals.rows[0]?.totalRevenue ?? 0),
      revenueByDay: byDay.rows.map(r => ({ date: String(r.date), revenue: Number(r.revenue) })),
    };
  },
});
```

Render it on the frontend with `recharts` and the generated caller:

```tsx theme={null}
import { useEffect, useState } from 'react';
import { salesDashboard, SalesDashboardOutputType } from 'zitejs/api';
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer } from 'recharts';

export function Dashboard() {
  const [data, setData] = useState<SalesDashboardOutputType>();
  useEffect(() => { salesDashboard({}).then(setData); }, []);
  if (!data) return null;
  return (
    <ResponsiveContainer width="100%" height={240}>
      <BarChart data={data.revenueByDay}>
        <XAxis dataKey="date" /><YAxis />
        <Bar dataKey="revenue" />
      </BarChart>
    </ResponsiveContainer>
  );
}
```

## Rules

For the full `zite.sql()` rules (SDK-name quoting, link-table joins, date handling), see [the database client](/framework/database-client). Two that bite most on dashboards:

* Double-quote every identifier — `FROM "Orders"`, not `FROM Orders`.
* Join linked records through the link table (`"ItemsOrders"` with `"itemsId"` / `"ordersId"`), never on the JSONB field.
