Skip to content

WalkInAnalytics

A comprehensive analytics component displaying walk-in KPIs and trends. Shows total walk-ins, average wait time, no-show rate, service duration, walk-in vs scheduled ratio, and visual distribution charts for busiest hours and days.

Terminal window
npx thebookingkit add walk-in-analytics
import { WalkInAnalytics } from "@/components/walk-in-analytics";
const analyticsData = {
totalWalkIns: 42,
averageWaitMinutes: 18,
noShowCount: 3,
noShowRate: 0.071,
completedCount: 39,
cancelledCount: 0,
averageServiceDuration: 28,
walkInRatio: 0.35,
hourlyDistribution: {
9: 2, 10: 5, 11: 8, 12: 7, 13: 6, 14: 4, 15: 3, 16: 2,
},
dailyDistribution: {
0: 4, 1: 8, 2: 7, 3: 6, 4: 9, 5: 5, 6: 3,
},
};
export function AnalyticsDashboard() {
return (
<WalkInAnalytics
data={analyticsData}
totalBookings={120}
dateRangeLabel="Last 30 days"
/>
);
}
export interface WalkInAnalyticsProps {
/** Computed analytics data */
data: WalkInAnalyticsData;
/** Total bookings (all sources) for the same period */
totalBookings: number;
/** Date range label (e.g., "Last 7 days") */
dateRangeLabel?: string;
/** Additional CSS class name */
className?: string;
/** Inline styles */
style?: React.CSSProperties;
}
export interface WalkInAnalyticsData {
totalWalkIns: number;
averageWaitMinutes: number;
noShowCount: number;
noShowRate: number;
completedCount: number;
cancelledCount: number;
hourlyDistribution: Record<number, number>;
dailyDistribution: Record<number, number>;
walkInRatio: number;
averageServiceDuration: number;
}
import React from "react";
import { cn } from "../utils/cn.js";
/** Walk-in analytics data */
export interface WalkInAnalyticsData {
totalWalkIns: number;
averageWaitMinutes: number;
noShowCount: number;
noShowRate: number;
completedCount: number;
cancelledCount: number;
hourlyDistribution: Record<number, number>;
dailyDistribution: Record<number, number>;
walkInRatio: number;
averageServiceDuration: number;
}
/** Props for the WalkInAnalytics component */
export interface WalkInAnalyticsProps {
/** Computed analytics data */
data: WalkInAnalyticsData;
/** Total bookings (all sources) for the same period */
totalBookings: number;
/** Date range label (e.g., "Last 7 days") */
dateRangeLabel?: string;
/** Additional CSS class name */
className?: string;
/** Inline styles */
style?: React.CSSProperties;
}
const DAY_LABELS: Record<number, string> = {
0: "Sun",
1: "Mon",
2: "Tue",
3: "Wed",
4: "Thu",
5: "Fri",
6: "Sat",
};
/**
* Walk-in analytics summary with metrics and distribution charts.
*
* Shows key walk-in KPIs (total, wait time, no-show rate) alongside
* hourly and daily distribution bar charts.
*
* @example
* ```tsx
* <WalkInAnalytics
* data={analyticsData}
* totalBookings={150}
* dateRangeLabel="Last 30 days"
* />
* ```
*/
export function WalkInAnalytics({
data,
totalBookings,
dateRangeLabel = "Selected period",
className,
style,
}: WalkInAnalyticsProps) {
const scheduledBookings = totalBookings - data.totalWalkIns;
return (
<div className={cn("tbk-walkin-analytics", className)} style={style}>
<div className="tbk-analytics-header">
<h2 className="tbk-form-title">Walk-In Analytics</h2>
<span className="tbk-analytics-period">{dateRangeLabel}</span>
</div>
{/* KPI Cards */}
<div className="tbk-analytics-cards">
<div className="tbk-analytics-card">
<span className="tbk-analytics-card-value">
{data.totalWalkIns}
</span>
<span className="tbk-analytics-card-label">Total Walk-Ins</span>
</div>
<div className="tbk-analytics-card">
<span className="tbk-analytics-card-value">
{data.averageWaitMinutes} min
</span>
<span className="tbk-analytics-card-label">Avg Wait Time</span>
</div>
<div className="tbk-analytics-card">
<span className="tbk-analytics-card-value">
{data.averageServiceDuration} min
</span>
<span className="tbk-analytics-card-label">
Avg Service Duration
</span>
</div>
<div className="tbk-analytics-card">
<span className="tbk-analytics-card-value">
{(data.noShowRate * 100).toFixed(1)}%
</span>
<span className="tbk-analytics-card-label">No-Show Rate</span>
</div>
</div>
{/* Walk-In vs Scheduled */}
<div className="tbk-analytics-section">
<h3 className="tbk-section-title">Walk-In vs Scheduled</h3>
<div className="tbk-analytics-ratio-bar">
<div
className="tbk-analytics-ratio-walkin"
style={{
width: `${data.walkInRatio * 100}%`,
}}
title={`Walk-ins: ${data.totalWalkIns}`}
/>
<div
className="tbk-analytics-ratio-scheduled"
style={{
width: `${(1 - data.walkInRatio) * 100}%`,
}}
title={`Scheduled: ${scheduledBookings}`}
/>
</div>
<div className="tbk-analytics-ratio-legend">
<span className="tbk-legend-item">
<span
className="tbk-legend-dot"
style={{ backgroundColor: "#3b82f6" }}
/>
Walk-ins ({data.totalWalkIns})
</span>
<span className="tbk-legend-item">
<span
className="tbk-legend-dot"
style={{ backgroundColor: "#94a3b8" }}
/>
Scheduled ({scheduledBookings})
</span>
</div>
</div>
{/* Status Breakdown */}
<div className="tbk-analytics-section">
<h3 className="tbk-section-title">Status Breakdown</h3>
<div className="tbk-analytics-status-grid">
<div className="tbk-analytics-status-item">
<span className="tbk-analytics-status-count">
{data.completedCount}
</span>
<span className="tbk-analytics-status-label">Completed</span>
</div>
<div className="tbk-analytics-status-item">
<span className="tbk-analytics-status-count">
{data.noShowCount}
</span>
<span className="tbk-analytics-status-label">No-Shows</span>
</div>
<div className="tbk-analytics-status-item">
<span className="tbk-analytics-status-count">
{data.cancelledCount}
</span>
<span className="tbk-analytics-status-label">Cancelled</span>
</div>
</div>
</div>
{/* Hourly Distribution */}
<div className="tbk-analytics-section">
<h3 className="tbk-section-title">Busiest Hours</h3>
<div className="tbk-analytics-chart">
{renderHourlyChart(data.hourlyDistribution)}
</div>
</div>
{/* Daily Distribution */}
<div className="tbk-analytics-section">
<h3 className="tbk-section-title">Busiest Days</h3>
<div className="tbk-analytics-chart">
{renderDailyChart(data.dailyDistribution)}
</div>
</div>
</div>
);
}
function renderHourlyChart(distribution: Record<number, number>) {
const max = Math.max(1, ...Object.values(distribution));
const hours = Array.from({ length: 24 }, (_, i) => i);
// Only show hours 6-22 for readability
const displayHours = hours.filter((h) => h >= 6 && h <= 22);
return (
<div className="tbk-bar-chart" role="img" aria-label="Hourly walk-in distribution">
{displayHours.map((hour) => {
const count = distribution[hour] ?? 0;
const height = max > 0 ? (count / max) * 100 : 0;
return (
<div key={hour} className="tbk-bar-column">
<div
className="tbk-bar"
style={{ height: `${height}%` }}
title={`${hour}:00 — ${count} walk-ins`}
/>
<span className="tbk-bar-label">
{hour % 3 === 0 ? `${hour}:00` : ""}
</span>
</div>
);
})}
</div>
);
}
function renderDailyChart(distribution: Record<number, number>) {
const max = Math.max(1, ...Object.values(distribution));
const days = [0, 1, 2, 3, 4, 5, 6];
return (
<div className="tbk-bar-chart tbk-bar-chart-daily" role="img" aria-label="Daily walk-in distribution">
{days.map((day) => {
const count = distribution[day] ?? 0;
const height = max > 0 ? (count / max) * 100 : 0;
return (
<div key={day} className="tbk-bar-column">
<div
className="tbk-bar"
style={{ height: `${height}%` }}
title={`${DAY_LABELS[day]}${count} walk-ins`}
/>
<span className="tbk-bar-label">{DAY_LABELS[day]}</span>
</div>
);
})}
</div>
);
}