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.
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.
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
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, which the library’s tests render too.