"use client" import { Clock } from 'lucide-react' import { DOW_SHORT_MON_FIRST } from '@/lib/dashboard/date-utils' import type { ResponseTimeSummary } from '@/lib/dashboard/types' import { EmptyState } from './empty-state' import { Skeleton } from './skeleton' interface ResponseTimeChartProps { data: ResponseTimeSummary | null loading: boolean /** Minutes. Horizontal dashed line rendered at this height. */ thresholdMinutes?: number } const VB_W = 760 const VB_H = 220 const PADDING = { top: 24, right: 16, bottom: 32, left: 44 } export function ResponseTimeChart({ data, loading, thresholdMinutes = 5, }: ResponseTimeChartProps) { const hasData = data?.buckets.some((b) => b.avgMinutes != null) ?? false return ( Average First Response Time Minutes to reply to a customer's first unreplied message, by weekday {data && (data.thisWeekAvg != null || data.lastWeekAvg != null) && ( This week:{' '} {fmt(data.thisWeekAvg)} Last week:{' '} {fmt(data.lastWeekAvg)} )} {loading || !data ? ( ) : !hasData ? ( ) : ( )} ) } function Bars({ data, thresholdMinutes, }: { data: ResponseTimeSummary thresholdMinutes: number }) { const chartW = VB_W - PADDING.left - PADDING.right const chartH = VB_H - PADDING.top - PADDING.bottom const values = data.buckets.map((b) => b.avgMinutes ?? 0) const rawMax = Math.max(thresholdMinutes * 1.2, ...values) const maxY = niceCeil(rawMax) const yFor = (v: number) => maxY === 0 ? PADDING.top + chartH : PADDING.top + chartH - (v / maxY) * chartH const barSlot = chartW / 7 const barW = Math.min(44, barSlot * 0.55) const ticks = [0, maxY / 2, maxY].map((t) => Math.round(t)) return ( {/* Y grid */} {ticks.map((t) => { const y = yFor(t) return ( {t}m ) })} {/* Threshold line — rendered after grid so it sits on top, but we keep it muted so bars remain the visual focus. */} {thresholdMinutes > 0 && thresholdMinutes <= maxY && ( target {thresholdMinutes}m )} {/* Bars */} {data.buckets.map((b, i) => { const v = b.avgMinutes ?? 0 const x = PADDING.left + barSlot * i + (barSlot - barW) / 2 const y = yFor(v) const h = PADDING.top + chartH - y const muted = b.avgMinutes == null return ( {DOW_SHORT_MON_FIRST[i]}:{' '} {b.avgMinutes == null ? 'no samples' : `${b.avgMinutes.toFixed(1)} min avg`} {b.samples > 0 ? ` (${b.samples} sample${b.samples === 1 ? '' : 's'})` : ''} {DOW_SHORT_MON_FIRST[i]} ) })} ) } function fmt(mins: number | null): string { if (mins == null) return '—' if (mins < 1) return `${Math.max(1, Math.round(mins * 60))}s` if (mins < 60) return `${mins.toFixed(1)}m` return `${(mins / 60).toFixed(1)}h` } function niceCeil(max: number): number { if (max <= 0) return 10 const pow = Math.pow(10, Math.floor(Math.log10(max))) const n = max / pow let nice: number if (n <= 1) nice = 1 else if (n <= 2) nice = 2 else if (n <= 5) nice = 5 else nice = 10 return nice * pow }
Minutes to reply to a customer's first unreplied message, by weekday