---
title: "ReportPage"
description: "A finished report or dashboard page from data: the question and its exact days, the period (presets, a custom range and the comparison with the previous period), the report’s filters and its export; then the headline figures, their reading and the chart cards, each with its loading and empty states. The app passes data and callbacks; every part is also public."
---

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

# ReportPage

## An overview

The period starts on the last 7 days and compares with the 7 before; turning the comparison off hides every change at once.

```tsx
import { getLocalTimeZone, today } from '@internationalized/date';
import { useState } from 'react';
import { presetRange } from '@arbol-ai/ui';
import type { Kpi } from '@arbol-ai/ui';
import { ReportPage } from '@arbol-ai/ui/charts';
import type { ReportChart } from '@arbol-ai/ui/charts';

const OVERVIEW_KPIS: Kpi[] = [
  { id: 'live', label: 'Live now', value: 18, isLive: true, hint: '12 by phone · 6 on WhatsApp' },
  {
    id: 'rate',
    label: 'Success rate',
    value: 0.81,
    formatOptions: { style: 'percent' },
    change: 3.2,
    changeUnit: 'points',
    info: 'Conversations that reached their goal, out of every conversation with a result.',
  },
  {
    id: 'review',
    label: 'To review',
    value: 23,
    change: -0.12,
    changeTone: 'inverse',
    link: { href: '#/report-page', label: 'Review them' },
  },
];

const DAILY = [312, 298, 341, 356, 330, 142, 118];

const DAILY_BEFORE = [290, 305, 322, 318, 301, 130, 121];

const CHANNELS = [
  { value: 'voice', label: 'Phone' },
  { value: 'whatsapp', label: 'WhatsApp' },
  { value: 'web', label: 'Web chat' },
];

const OverviewExample = () => {
  const [period, setPeriod] = useState(() => presetRange(7));
  const [isComparing, setIsComparing] = useState(true);
  const [channel, setChannel] = useState<string | null>(null);
  const zone = getLocalTimeZone();
  const charts: ReportChart[] = [
    {
      id: 'daily',
      title: 'Conversations',
      span: 'wide',
      value: 1897,
      change: 0.06,
      chart: {
        kind: 'trend',
        data: DAILY.map((value, index) => ({
          date: period.start.add({ days: index }).toDate(zone),
          value,
          previous: DAILY_BEFORE[index] ?? null,
        })),
      },
    },
    {
      id: 'outcomes',
      title: 'How they ended',
      span: 'narrow',
      chart: {
        kind: 'breakdown',
        showShare: true,
        items: [
          { id: 'success', label: 'Resolved', value: 1203, color: 'bg-chart-1', href: '#/report-page' },
          { id: 'failure', label: 'Not resolved', value: 284, color: 'bg-red-9', href: '#/report-page' },
          { id: 'unknown', label: 'Undecided', value: 410, color: 'bg-gray-6' },
        ],
      },
    },
  ];
  return (
    <ReportPage
      charts={charts}
      className="px-0 md:px-0"
      description="How your agents did, and what is waiting for your team."
      filters={[
        {
          id: 'channel',
          label: 'Channel',
          allLabel: 'All channels',
          options: CHANNELS,
          value: channel,
          onChange: setChannel,
        },
      ]}
      isComparing={isComparing}
      kpis={OVERVIEW_KPIS}
      maxValue={today(zone)}
      maxWidth="full"
      onComparingChange={setIsComparing}
      onExport={() => undefined}
      onPeriodChange={setPeriod}
      period={period}
      reading="81% of the conversations with a result reached their goal, 3 points more than the week before."
      title="Overview"
    />
  );
};

<OverviewExample />
```

## A report that narrows its list

A scheduling report: the days ahead among the presets, and reasons that filter the list below.

```tsx
import { useState } from 'react';
import { presetRange } from '@arbol-ai/ui';
import { ReportPage } from '@arbol-ai/ui/charts';

const REASONS = [
  { id: 'forgot', label: 'Forgot', value: 18, caption: 'no-show', color: 'bg-red-9' },
  { id: 'transport', label: 'No transport', value: 11, caption: 'no-show', color: 'bg-red-9' },
  { id: 'rescheduled', label: 'Rescheduled elsewhere', value: 7, caption: 'cancelled', color: 'bg-gray-a6' },
];

const NoShowsExample = () => {
  const [period, setPeriod] = useState(() => presetRange(30));
  const [reason, setReason] = useState<string | null>(null);
  const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
  return (
    <ReportPage
      charts={[
        {
          id: 'reasons',
          title: 'Why they missed it',
          description: 'Press one to see its appointments.',
          span: 'half',
          chart: { kind: 'breakdown', items: REASONS, selectedId: reason, onSelectionChange: setReason },
        },
        {
          id: 'weekdays',
          title: 'By weekday',
          span: 'half',
          chart: {
            kind: 'bars',
            categories: days,
            isStacked: true,
            series: [
              { label: 'No-show', values: [9, 6, 4, 7, 15], color: 'chart-negative' },
              { label: 'Cancelled', values: [3, 2, 1, 4, 5], color: 'chart-neutral' },
            ],
          },
        },
      ]}
      className="px-0 md:px-0"
      description="Who missed their appointment, and why?"
      kpis={[
        {
          id: 'lost',
          label: 'Lost',
          value: 105,
          change: 0.04,
          changeTone: 'inverse',
          hint: '8% of 1,284 appointments',
        },
        { id: 'no-show', label: 'No-shows', value: 41, change: -0.1, changeTone: 'inverse' },
        { id: 'cancelled', label: 'Cancelled', value: 64, change: 0.12, changeTone: 'inverse' },
      ]}
      maxWidth="full"
      onExport={() => undefined}
      onPeriodChange={setPeriod}
      period={period}
      presets={[7, 30, 90, 'this-month', 'last-month', { next: 7 }, { next: 30 }]}
      reading="41 patients missed their appointment (3%) and 64 cancelled (5%). The most common reason: “Forgot”."
      title="No-shows"
    />
  );
};

<NoShowsExample />
```

## Loading, empty and failed

```tsx
import { CalendarXIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { Button, EmptyState, SegmentedControl, presetRange } from '@arbol-ai/ui';
import { ReportPage } from '@arbol-ai/ui/charts';
import type { ReportPageProps } from '@arbol-ai/ui/charts';

const STATES: Pick<ReportPageProps, 'charts' | 'kpis' | 'title'> = {
  title: 'Appointments',
  kpis: [{ id: 'total', label: 'Appointments', value: 0, change: null }],
  charts: [{ id: 'daily', title: 'Appointments per day', chart: { kind: 'trend', data: [] } }],
};

type ReportState = 'loading' | 'empty' | 'failed';

const StatesExample = () => {
  const [period, setPeriod] = useState(() => presetRange(7));
  const [state, setState] = useState<ReportState>('loading');
  return (
    <div className="grid gap-2">
      <SegmentedControl
        label="State"
        onChange={setState}
        options={[
          { value: 'loading', label: 'Loading' },
          { value: 'empty', label: 'Empty' },
          { value: 'failed', label: 'Failed' },
        ]}
        size="sm"
        value={state}
      />
      <ReportPage
        {...STATES}
        className="px-0 md:px-0"
        empty={
          state === 'empty' ? (
            <EmptyState
              actions={
                <Button
                  onPress={() => {
                    setPeriod(presetRange(90));
                  }}
                  variant="outline"
                >
                  See the last 90 days
                </Button>
              }
              description="Try a longer period."
              icon={<CalendarXIcon aria-hidden />}
              title="No appointments in this period"
            />
          ) : undefined
        }
        error={
          state === 'failed' ? 'We couldn’t load the report. Check your connection and try again.' : undefined
        }
        isLoading={state === 'loading'}
        onPeriodChange={setPeriod}
        onRetry={() => {
          setState('loading');
        }}
        period={period}
        warning={
          state === 'failed'
            ? 'Counts stop at 10,000 appointments: narrow the period to see them all.'
            : undefined
        }
      />
    </div>
  );
};

<StatesExample />
```

## Source

The examples above are [`src/charts/report-page.examples.tsx`](https://github.com/getarbol/project-arbol-ui/blob/main/src/charts/report-page.examples.tsx), which the library's tests render too.

Source: https://epidermis.getarbol.com/components/blocks/report-page/index.mdx
