// The admin reports other than Scheduling: one server route, one response shape, counts only. import { useCallback } from 'react'; import { environment } from '../../../environments/environment.ts'; import { useHttpService } from '../../../services/HttpService.tsx'; import { formatRange, reportQuery, type ReportComparison, type ReportFilters } from './reportPeriod'; export type PracticeReportKind = 'referrals' | 'orders' | 'tasks' | 'communication' | 'billing' | 'staff-time' | 'compliance'; export type ReportValue = number | string | null; export interface PracticeReportRow { key: string; label: string; values: Record; } export interface PracticeReport { report: PracticeReportKind; range: { start: string; end: string }; compare: ReportComparison; locationId: string | null; timezone: string; locations: Array<{ id: string; name: string }>; totals: Record; rows: PracticeReportRow[]; chart?: PracticeReportRow[]; weeks?: Array<{ start: string; end: string; values: Record }>; comparison: { range: { start: string; end: string }; totals: Record } | null; } export const usePracticeReport = () => { const { get } = useHttpService(); return useCallback( (kind: PracticeReportKind, filters: ReportFilters) => get(environment.fhirBaseApi + '/PracticeReport?report=' + kind + '&' + reportQuery(filters)), [get], ); }; export type ValueFormat = 'count' | 'money' | 'hours' | 'text' | 'datetime'; const EM_DASH = String.fromCharCode(0x2014); /** Money arrives in cents, hours in minutes and times as instants shown in the practice's zone. */ export function formatValue(value: ReportValue | undefined, format: ValueFormat = 'count', timezone?: string): string { if (value === null || value === undefined || value === '') return EM_DASH; if (format === 'datetime' && typeof value === 'string') { const time = Date.parse(value); if (Number.isNaN(time)) return value; return new Intl.DateTimeFormat('en-US', { timeZone: timezone, month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }).format(time); } if (typeof value === 'string') return value; if (format === 'money') { return (value / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: value % 100 === 0 ? 0 : 2 }); } if (format === 'hours') return (Math.round((value / 60) * 10) / 10).toLocaleString() + ' h'; return value.toLocaleString(); } // A spreadsheet reads a leading = + - @ as a formula; names in a report are typed by the practice. const cell = (value: string | number) => { const text = String(value); const safe = /^[=+\-@]/.test(text) ? "'" + text : text; return /[",\r\n]/.test(safe) ? '"' + safe.replace(/"/g, '""') + '"' : safe; }; export interface ReportColumn { key: string; label: string; format?: ValueFormat; } /** The CSV carries the numbers the page shows, with the filters that produced them. */ export function practiceReportCsv( title: string, report: PracticeReport, locationName: string, measures: ReportColumn[], table: { first: string; columns: ReportColumn[] }, chart: string[][] = [], ): string { const rows: Array> = [ [title], ['Period', formatRange(report.range.start, report.range.end)], ['Location', locationName], ['Time zone', report.timezone], ]; if (report.comparison) rows.push(['Compared with', formatRange(report.comparison.range.start, report.comparison.range.end)]); rows.push([], report.comparison ? ['Measure', 'This period', 'Comparison period'] : ['Measure', 'This period']); for (const measure of measures) { const now = formatValue(report.totals[measure.key], measure.format); rows.push(report.comparison ? [measure.label, now, formatValue(report.comparison.totals[measure.key], measure.format)] : [measure.label, now]); } if (chart.length) rows.push([], ...chart); rows.push([], [table.first, ...table.columns.map((column) => column.label)]); for (const row of report.rows) { rows.push([row.label, ...table.columns.map((column) => formatValue(row.values[column.key], column.format, report.timezone))]); } return rows.map((row) => row.map(cell).join(',')).join('\r\n') + '\r\n'; } export const practiceReportCsvFilename = (kind: PracticeReportKind, report: PracticeReport) => kind + '-report-' + report.range.start + '-to-' + report.range.end + '.csv';