| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- /**
- * Analytics — Track chatbot resolution rate, interactions, and category breakdown
- * Design: Warm Showroom palette with clean dashboard cards
- */
- import { useState, useMemo } from "react";
- import { trpc } from "@/lib/trpc";
- import { Button } from "@/components/ui/button";
- import {
- BarChart3, TrendingUp, CheckCircle2, AlertTriangle,
- Users, MessageSquare, Bot, Headphones, ThumbsUp, ThumbsDown,
- Package, Truck, RotateCw, XCircle, ArrowUpRight, ArrowDownRight,
- Calendar, RefreshCw, MousePointerClick,
- } from "lucide-react";
- /* ─── Stat Card ─── */
- function StatCard({
- label, value, icon, color, subtext, trend,
- }: {
- label: string; value: string | number; icon: React.ReactNode; color: string;
- subtext?: string; trend?: { value: number; positive: boolean };
- }) {
- return (
- <div className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
- <div className="flex items-start justify-between">
- <div>
- <p className="text-[11px] font-medium uppercase tracking-wider" style={{ color: "#a8a29e" }}>{label}</p>
- <p className="text-2xl font-bold mt-1" style={{ color: "#292524", fontFamily: "'Playfair Display', serif" }}>{value}</p>
- {subtext && <p className="text-[11px] mt-0.5" style={{ color: "#78716C" }}>{subtext}</p>}
- {trend && (
- <div className="flex items-center gap-1 mt-1">
- {trend.positive ? <ArrowUpRight className="w-3 h-3" style={{ color: "#14532D" }} /> : <ArrowDownRight className="w-3 h-3" style={{ color: "#dc2626" }} />}
- <span className="text-[10px] font-medium" style={{ color: trend.positive ? "#14532D" : "#dc2626" }}>
- {trend.value}% vs last period
- </span>
- </div>
- )}
- </div>
- <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: `${color}14`, color }}>
- {icon}
- </div>
- </div>
- </div>
- );
- }
- /* ─── Category Bar ─── */
- function CategoryBar({ category, count, resolved, total, icon, color }: {
- category: string; count: number; resolved: number; total: number;
- icon: React.ReactNode; color: string;
- }) {
- const pct = total > 0 ? Math.round((count / total) * 100) : 0;
- const resPct = count > 0 ? Math.round((resolved / count) * 100) : 0;
- return (
- <div className="p-3 rounded-lg border" style={{ borderColor: "#e7e0d5", background: "#fff" }}>
- <div className="flex items-center justify-between mb-2">
- <div className="flex items-center gap-2">
- <div className="w-7 h-7 rounded-md flex items-center justify-center" style={{ background: `${color}14`, color }}>
- {icon}
- </div>
- <div>
- <span className="text-sm font-semibold capitalize" style={{ color: "#292524" }}>{category}</span>
- <span className="text-[10px] ml-2" style={{ color: "#a8a29e" }}>{count} interactions</span>
- </div>
- </div>
- <span className="text-xs font-bold" style={{ color }}>{pct}%</span>
- </div>
- {/* Progress bar */}
- <div className="h-2 rounded-full overflow-hidden" style={{ background: "#f5f0e8" }}>
- <div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: color }} />
- </div>
- <div className="flex items-center justify-between mt-1.5">
- <span className="text-[10px]" style={{ color: "#a8a29e" }}>Resolution rate</span>
- <span className="text-[10px] font-medium" style={{ color: resPct >= 70 ? "#14532D" : resPct >= 40 ? "#ca8a04" : "#dc2626" }}>
- {resPct}%
- </span>
- </div>
- </div>
- );
- }
- /* ─── Resolution Donut ─── */
- function ResolutionDonut({ botRate, agentRate, escalatedRate, abandonedRate }: {
- botRate: number; agentRate: number; escalatedRate: number; abandonedRate: number;
- }) {
- const total = botRate + agentRate + escalatedRate + abandonedRate;
- const segments = [
- { label: "Bot Resolved", pct: botRate, color: "#14532D" },
- { label: "Agent Resolved", pct: agentRate, color: "#0369a1" },
- { label: "Escalated", pct: escalatedRate, color: "#ca8a04" },
- { label: "Abandoned", pct: abandonedRate, color: "#dc2626" },
- ];
- return (
- <div className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
- <h3 className="text-xs font-bold uppercase tracking-wider mb-4" style={{ color: "#78716C" }}>Resolution Breakdown</h3>
- <div className="flex items-center gap-6">
- {/* Simple visual bars */}
- <div className="flex-1 space-y-2">
- {segments.map(seg => (
- <div key={seg.label}>
- <div className="flex items-center justify-between mb-0.5">
- <span className="text-[11px] font-medium" style={{ color: "#292524" }}>{seg.label}</span>
- <span className="text-[11px] font-bold" style={{ color: seg.color }}>{seg.pct}%</span>
- </div>
- <div className="h-2.5 rounded-full overflow-hidden" style={{ background: "#f5f0e8" }}>
- <div className="h-full rounded-full transition-all" style={{ width: `${seg.pct}%`, background: seg.color }} />
- </div>
- </div>
- ))}
- </div>
- {/* Overall score */}
- <div className="text-center shrink-0">
- <div
- className="w-20 h-20 rounded-full flex items-center justify-center border-4"
- style={{ borderColor: "#14532D", background: "#14532D08" }}
- >
- <div>
- <div className="text-xl font-bold" style={{ color: "#14532D", fontFamily: "'Playfair Display', serif" }}>
- {botRate + agentRate}%
- </div>
- <div className="text-[8px] uppercase tracking-wider" style={{ color: "#78716C" }}>Resolved</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
- export default function Analytics() {
- const [dateRange, setDateRange] = useState<"7d" | "30d" | "90d" | "all">("30d");
- const dateFilters = useMemo(() => {
- const now = new Date();
- if (dateRange === "all") return {};
- const days = dateRange === "7d" ? 7 : dateRange === "30d" ? 30 : 90;
- const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
- return { startDate: start.toISOString(), endDate: now.toISOString() };
- }, [dateRange]);
- const { data: summary, isLoading, refetch } = trpc.analytics.summary.useQuery(dateFilters);
- const categoryIcons: Record<string, { icon: React.ReactNode; color: string }> = {
- orders: { icon: <Package className="w-3.5 h-3.5" />, color: "#14532D" },
- shipping: { icon: <Truck className="w-3.5 h-3.5" />, color: "#0369a1" },
- returning: { icon: <RotateCw className="w-3.5 h-3.5" />, color: "#ca8a04" },
- cancelling: { icon: <XCircle className="w-3.5 h-3.5" />, color: "#dc2626" },
- };
- // Compute rates for donut
- const totalOutcomes = summary ? (summary.resolvedByBot + summary.resolvedByAgent + summary.escalated + summary.abandoned) : 0;
- const botRate = totalOutcomes > 0 ? Math.round(((summary?.resolvedByBot ?? 0) / totalOutcomes) * 100) : 0;
- const agentRate = totalOutcomes > 0 ? Math.round(((summary?.resolvedByAgent ?? 0) / totalOutcomes) * 100) : 0;
- const escalatedRate = totalOutcomes > 0 ? Math.round(((summary?.escalated ?? 0) / totalOutcomes) * 100) : 0;
- const abandonedRate = totalOutcomes > 0 ? Math.round(((summary?.abandoned ?? 0) / totalOutcomes) * 100) : 0;
- return (
- <div className="flex flex-col" style={{ height: "calc(100vh - 4rem)" }}>
- {/* Top toolbar */}
- <div className="border-b px-4 h-12 flex items-center justify-between shrink-0" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
- <div className="flex items-center gap-3">
- <div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: "#ca8a04" }}>
- <BarChart3 className="w-3.5 h-3.5 text-white" />
- </div>
- <span className="text-sm font-bold" style={{ color: "#ca8a04", fontFamily: "'Playfair Display', serif" }}>Analytics</span>
- </div>
- <div className="flex items-center gap-2">
- {/* Date range selector */}
- <div className="flex gap-0.5 p-0.5 rounded-lg" style={{ background: "#f5f0e8" }}>
- {(["7d", "30d", "90d", "all"] as const).map(range => (
- <button
- key={range}
- onClick={() => setDateRange(range)}
- className="px-2.5 py-1 rounded-md text-[11px] font-medium transition-colors"
- style={{
- background: dateRange === range ? "#fff" : "transparent",
- color: dateRange === range ? "#292524" : "#a8a29e",
- boxShadow: dateRange === range ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
- }}
- >
- {range === "all" ? "All" : range}
- </button>
- ))}
- </div>
- <Button onClick={() => refetch()} variant="outline" size="sm" className="text-xs">
- <RefreshCw className="w-3 h-3 mr-1" /> Refresh
- </Button>
- </div>
- </div>
- {/* Content */}
- <div className="flex-1 overflow-y-auto p-4 space-y-4" style={{ background: "#FFFBEB" }}>
- {isLoading ? (
- <div className="flex items-center justify-center py-20">
- <RefreshCw className="w-6 h-6 animate-spin" style={{ color: "#ca8a04" }} />
- <span className="ml-3 text-sm" style={{ color: "#78716C" }}>Loading analytics...</span>
- </div>
- ) : !summary ? (
- <div className="text-center py-20">
- <BarChart3 className="w-12 h-12 mx-auto mb-3" style={{ color: "#d6d3d1" }} />
- <p className="text-sm" style={{ color: "#78716C" }}>No analytics data available yet</p>
- <p className="text-xs mt-1" style={{ color: "#a8a29e" }}>Data will appear as customers interact with the chatbot</p>
- </div>
- ) : (
- <>
- {/* Top-level stats */}
- <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
- <StatCard
- label="Total Sessions"
- value={summary.totalSessions}
- icon={<Users className="w-5 h-5" />}
- color="#14532D"
- subtext={`${summary.totalEvents} total events`}
- />
- <StatCard
- label="Resolution Rate"
- value={`${summary.resolutionRate}%`}
- icon={<CheckCircle2 className="w-5 h-5" />}
- color={summary.resolutionRate >= 70 ? "#14532D" : summary.resolutionRate >= 40 ? "#ca8a04" : "#dc2626"}
- subtext={`${summary.resolvedByBot + summary.resolvedByAgent} resolved`}
- />
- <StatCard
- label="Bot Resolution"
- value={`${summary.botResolutionRate}%`}
- icon={<Bot className="w-5 h-5" />}
- color="#0369a1"
- subtext={`${summary.resolvedByBot} by bot`}
- />
- <StatCard
- label="Escalated"
- value={summary.escalated}
- icon={<Headphones className="w-5 h-5" />}
- color="#C2410C"
- subtext={`${summary.abandoned} abandoned`}
- />
- </div>
- {/* Second row */}
- <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
- <StatCard
- label="Messages Sent"
- value={summary.messagesSent}
- icon={<MessageSquare className="w-5 h-5" />}
- color="#7c3aed"
- />
- <StatCard
- label="Messages Received"
- value={summary.messagesReceived}
- icon={<MessageSquare className="w-5 h-5" />}
- color="#059669"
- />
- <StatCard
- label="Button Clicks"
- value={summary.buttonClicks}
- icon={<MousePointerClick className="w-5 h-5" />}
- color="#ca8a04"
- />
- <StatCard
- label="Feedback"
- value={`${summary.positiveFeedback}/${summary.negativeFeedback}`}
- icon={summary.positiveFeedback >= summary.negativeFeedback ? <ThumbsUp className="w-5 h-5" /> : <ThumbsDown className="w-5 h-5" />}
- color={summary.positiveFeedback >= summary.negativeFeedback ? "#14532D" : "#dc2626"}
- subtext="Positive / Negative"
- />
- </div>
- {/* Resolution breakdown */}
- {totalOutcomes > 0 && (
- <ResolutionDonut
- botRate={botRate}
- agentRate={agentRate}
- escalatedRate={escalatedRate}
- abandonedRate={abandonedRate}
- />
- )}
- {/* Category breakdown */}
- <div>
- <h3 className="text-xs font-bold uppercase tracking-wider mb-3" style={{ color: "#78716C" }}>
- Category Breakdown
- </h3>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
- {summary.categoryBreakdown.map(cat => {
- const catConfig = categoryIcons[cat.category] || { icon: <Package className="w-3.5 h-3.5" />, color: "#78716C" };
- return (
- <CategoryBar
- key={cat.category}
- category={cat.category}
- count={cat.count}
- resolved={cat.resolved}
- total={summary.totalEvents}
- icon={catConfig.icon}
- color={catConfig.color}
- />
- );
- })}
- </div>
- </div>
- </>
- )}
- </div>
- </div>
- );
- }
|