// The reports overview: one period and location scope every report destination. import { useEffect, useMemo, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { CalendarDays, ChevronRight } from 'lucide-react'; import { Alert, AlertDescription } from '../../../components/ui/alert'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../components/ui/select'; import { relayServerFailure } from '../../../utils/fetchFailureMessage'; import { ALL_LOCATIONS, PRESET_LABELS, filtersFromSearch, presetRange, searchFromFilters, type ReportFilters, type ReportPreset, } from './reportPeriod'; import { useSchedulingReport, type SchedulingReport } from './schedulingReport'; import { formatValue, usePracticeReport, type PracticeReport } from './practiceReport'; import { REPORT_DEFINITIONS, type ReportDefinition, type ReportMeasure } from './reportDefinitions'; import './ReportsPage.css'; const MAX_DAYS = 366; const DAY_MS = 86400000; export function rangeProblem(filters: ReportFilters): string | undefined { if (!filters.start || !filters.end) return 'Choose a start and an end date.'; if (filters.start > filters.end) return 'The end date must be on or after the start date.'; const days = (Date.parse(filters.end) - Date.parse(filters.start)) / DAY_MS + 1; if (days > MAX_DAYS) return 'Choose a period of a year or less.'; return undefined; } const count = (report: SchedulingReport | undefined, value: (report: SchedulingReport) => number) => report ? value(report).toLocaleString() : String.fromCharCode(0x2014); const DOT = ' ' + String.fromCharCode(0x00b7) + ' '; // Each destination reads its own headline counts, so one slow or refused report does not hold up the rest. const PracticeReportRow = ({ definition, filters }: { definition: ReportDefinition; filters: ReportFilters }) => { const navigate = useNavigate(); const loadReport = usePracticeReport(); const [report, setReport] = useState(); const [failed, setFailed] = useState(false); useEffect(() => { let live = true; setReport(undefined); setFailed(false); loadReport(definition.kind, { ...filters, compare: 'none' }) .then((result) => { if (live) setReport(result); }) .catch(() => { if (live) setFailed(true); }); return () => { live = false; }; }, [loadReport, definition, filters]); const Icon = definition.icon; const value = (measure: ReportMeasure) => formatValue(report ? report.totals[measure.key] : null, measure.format); return ( ); }; const ReportsPage = () => { const navigate = useNavigate(); const [params, setParams] = useSearchParams(); const search = params.toString(); const applied = useMemo(() => filtersFromSearch(new URLSearchParams(search)), [search]); const [draft, setDraft] = useState(applied); const [problem, setProblem] = useState(); const loadScheduling = useSchedulingReport(); const [report, setReport] = useState(); const [error, setError] = useState(); useEffect(() => setDraft(applied), [applied]); useEffect(() => { let live = true; setError(undefined); loadScheduling({ ...applied, compare: 'none' }) .then((result) => { if (live) setReport(result); }) .catch((failure) => { if (!live) return; setReport(undefined); setError(relayServerFailure(failure, 'The reports could not be loaded. Please try again.')); }); return () => { live = false; }; }, [loadScheduling, applied]); const choosePreset = (preset: ReportPreset) => { const range = presetRange(preset); setDraft((current) => ({ ...current, preset, ...(range ?? {}) })); }; const chooseDate = (key: 'start' | 'end', value: string) => setDraft((current) => ({ ...current, preset: 'custom', [key]: value })); const update = () => { const reason = rangeProblem(draft); setProblem(reason); if (!reason) setParams(searchFromFilters(draft)); }; return (

Reports

See how care moves through the practice, where work is waiting and how the organization is performing.

{ event.preventDefault(); update(); }} >
chooseDate('start', event.target.value)} />
chooseDate('end', event.target.value)} />
{problem ?
{problem}
: null} {error ? ( {error} ) : null}
{REPORT_DEFINITIONS.map((definition) => ( ))}
About these numbers: reports use records saved in Akashic. Scheduled coverage is not a payroll timecard.
); }; export default ReportsPage;