Analytics.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /**
  2. * Analytics — Track chatbot resolution rate, interactions, and category breakdown
  3. * Design: Warm Showroom palette with clean dashboard cards
  4. */
  5. import { useState, useMemo } from "react";
  6. import { trpc } from "@/lib/trpc";
  7. import { Button } from "@/components/ui/button";
  8. import {
  9. BarChart3, TrendingUp, CheckCircle2, AlertTriangle,
  10. Users, MessageSquare, Bot, Headphones, ThumbsUp, ThumbsDown,
  11. Package, Truck, RotateCw, XCircle, ArrowUpRight, ArrowDownRight,
  12. Calendar, RefreshCw, MousePointerClick,
  13. } from "lucide-react";
  14. /* ─── Stat Card ─── */
  15. function StatCard({
  16. label, value, icon, color, subtext, trend,
  17. }: {
  18. label: string; value: string | number; icon: React.ReactNode; color: string;
  19. subtext?: string; trend?: { value: number; positive: boolean };
  20. }) {
  21. return (
  22. <div className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
  23. <div className="flex items-start justify-between">
  24. <div>
  25. <p className="text-[11px] font-medium uppercase tracking-wider" style={{ color: "#a8a29e" }}>{label}</p>
  26. <p className="text-2xl font-bold mt-1" style={{ color: "#292524", fontFamily: "'Playfair Display', serif" }}>{value}</p>
  27. {subtext && <p className="text-[11px] mt-0.5" style={{ color: "#78716C" }}>{subtext}</p>}
  28. {trend && (
  29. <div className="flex items-center gap-1 mt-1">
  30. {trend.positive ? <ArrowUpRight className="w-3 h-3" style={{ color: "#14532D" }} /> : <ArrowDownRight className="w-3 h-3" style={{ color: "#dc2626" }} />}
  31. <span className="text-[10px] font-medium" style={{ color: trend.positive ? "#14532D" : "#dc2626" }}>
  32. {trend.value}% vs last period
  33. </span>
  34. </div>
  35. )}
  36. </div>
  37. <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: `${color}14`, color }}>
  38. {icon}
  39. </div>
  40. </div>
  41. </div>
  42. );
  43. }
  44. /* ─── Category Bar ─── */
  45. function CategoryBar({ category, count, resolved, total, icon, color }: {
  46. category: string; count: number; resolved: number; total: number;
  47. icon: React.ReactNode; color: string;
  48. }) {
  49. const pct = total > 0 ? Math.round((count / total) * 100) : 0;
  50. const resPct = count > 0 ? Math.round((resolved / count) * 100) : 0;
  51. return (
  52. <div className="p-3 rounded-lg border" style={{ borderColor: "#e7e0d5", background: "#fff" }}>
  53. <div className="flex items-center justify-between mb-2">
  54. <div className="flex items-center gap-2">
  55. <div className="w-7 h-7 rounded-md flex items-center justify-center" style={{ background: `${color}14`, color }}>
  56. {icon}
  57. </div>
  58. <div>
  59. <span className="text-sm font-semibold capitalize" style={{ color: "#292524" }}>{category}</span>
  60. <span className="text-[10px] ml-2" style={{ color: "#a8a29e" }}>{count} interactions</span>
  61. </div>
  62. </div>
  63. <span className="text-xs font-bold" style={{ color }}>{pct}%</span>
  64. </div>
  65. {/* Progress bar */}
  66. <div className="h-2 rounded-full overflow-hidden" style={{ background: "#f5f0e8" }}>
  67. <div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: color }} />
  68. </div>
  69. <div className="flex items-center justify-between mt-1.5">
  70. <span className="text-[10px]" style={{ color: "#a8a29e" }}>Resolution rate</span>
  71. <span className="text-[10px] font-medium" style={{ color: resPct >= 70 ? "#14532D" : resPct >= 40 ? "#ca8a04" : "#dc2626" }}>
  72. {resPct}%
  73. </span>
  74. </div>
  75. </div>
  76. );
  77. }
  78. /* ─── Resolution Donut ─── */
  79. function ResolutionDonut({ botRate, agentRate, escalatedRate, abandonedRate }: {
  80. botRate: number; agentRate: number; escalatedRate: number; abandonedRate: number;
  81. }) {
  82. const total = botRate + agentRate + escalatedRate + abandonedRate;
  83. const segments = [
  84. { label: "Bot Resolved", pct: botRate, color: "#14532D" },
  85. { label: "Agent Resolved", pct: agentRate, color: "#0369a1" },
  86. { label: "Escalated", pct: escalatedRate, color: "#ca8a04" },
  87. { label: "Abandoned", pct: abandonedRate, color: "#dc2626" },
  88. ];
  89. return (
  90. <div className="p-4 rounded-xl border" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
  91. <h3 className="text-xs font-bold uppercase tracking-wider mb-4" style={{ color: "#78716C" }}>Resolution Breakdown</h3>
  92. <div className="flex items-center gap-6">
  93. {/* Simple visual bars */}
  94. <div className="flex-1 space-y-2">
  95. {segments.map(seg => (
  96. <div key={seg.label}>
  97. <div className="flex items-center justify-between mb-0.5">
  98. <span className="text-[11px] font-medium" style={{ color: "#292524" }}>{seg.label}</span>
  99. <span className="text-[11px] font-bold" style={{ color: seg.color }}>{seg.pct}%</span>
  100. </div>
  101. <div className="h-2.5 rounded-full overflow-hidden" style={{ background: "#f5f0e8" }}>
  102. <div className="h-full rounded-full transition-all" style={{ width: `${seg.pct}%`, background: seg.color }} />
  103. </div>
  104. </div>
  105. ))}
  106. </div>
  107. {/* Overall score */}
  108. <div className="text-center shrink-0">
  109. <div
  110. className="w-20 h-20 rounded-full flex items-center justify-center border-4"
  111. style={{ borderColor: "#14532D", background: "#14532D08" }}
  112. >
  113. <div>
  114. <div className="text-xl font-bold" style={{ color: "#14532D", fontFamily: "'Playfair Display', serif" }}>
  115. {botRate + agentRate}%
  116. </div>
  117. <div className="text-[8px] uppercase tracking-wider" style={{ color: "#78716C" }}>Resolved</div>
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. );
  124. }
  125. export default function Analytics() {
  126. const [dateRange, setDateRange] = useState<"7d" | "30d" | "90d" | "all">("30d");
  127. const dateFilters = useMemo(() => {
  128. const now = new Date();
  129. if (dateRange === "all") return {};
  130. const days = dateRange === "7d" ? 7 : dateRange === "30d" ? 30 : 90;
  131. const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
  132. return { startDate: start.toISOString(), endDate: now.toISOString() };
  133. }, [dateRange]);
  134. const { data: summary, isLoading, refetch } = trpc.analytics.summary.useQuery(dateFilters);
  135. const categoryIcons: Record<string, { icon: React.ReactNode; color: string }> = {
  136. orders: { icon: <Package className="w-3.5 h-3.5" />, color: "#14532D" },
  137. shipping: { icon: <Truck className="w-3.5 h-3.5" />, color: "#0369a1" },
  138. returning: { icon: <RotateCw className="w-3.5 h-3.5" />, color: "#ca8a04" },
  139. cancelling: { icon: <XCircle className="w-3.5 h-3.5" />, color: "#dc2626" },
  140. };
  141. // Compute rates for donut
  142. const totalOutcomes = summary ? (summary.resolvedByBot + summary.resolvedByAgent + summary.escalated + summary.abandoned) : 0;
  143. const botRate = totalOutcomes > 0 ? Math.round(((summary?.resolvedByBot ?? 0) / totalOutcomes) * 100) : 0;
  144. const agentRate = totalOutcomes > 0 ? Math.round(((summary?.resolvedByAgent ?? 0) / totalOutcomes) * 100) : 0;
  145. const escalatedRate = totalOutcomes > 0 ? Math.round(((summary?.escalated ?? 0) / totalOutcomes) * 100) : 0;
  146. const abandonedRate = totalOutcomes > 0 ? Math.round(((summary?.abandoned ?? 0) / totalOutcomes) * 100) : 0;
  147. return (
  148. <div className="flex flex-col" style={{ height: "calc(100vh - 4rem)" }}>
  149. {/* Top toolbar */}
  150. <div className="border-b px-4 h-12 flex items-center justify-between shrink-0" style={{ background: "#fff", borderColor: "#e7e0d5" }}>
  151. <div className="flex items-center gap-3">
  152. <div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: "#ca8a04" }}>
  153. <BarChart3 className="w-3.5 h-3.5 text-white" />
  154. </div>
  155. <span className="text-sm font-bold" style={{ color: "#ca8a04", fontFamily: "'Playfair Display', serif" }}>Analytics</span>
  156. </div>
  157. <div className="flex items-center gap-2">
  158. {/* Date range selector */}
  159. <div className="flex gap-0.5 p-0.5 rounded-lg" style={{ background: "#f5f0e8" }}>
  160. {(["7d", "30d", "90d", "all"] as const).map(range => (
  161. <button
  162. key={range}
  163. onClick={() => setDateRange(range)}
  164. className="px-2.5 py-1 rounded-md text-[11px] font-medium transition-colors"
  165. style={{
  166. background: dateRange === range ? "#fff" : "transparent",
  167. color: dateRange === range ? "#292524" : "#a8a29e",
  168. boxShadow: dateRange === range ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
  169. }}
  170. >
  171. {range === "all" ? "All" : range}
  172. </button>
  173. ))}
  174. </div>
  175. <Button onClick={() => refetch()} variant="outline" size="sm" className="text-xs">
  176. <RefreshCw className="w-3 h-3 mr-1" /> Refresh
  177. </Button>
  178. </div>
  179. </div>
  180. {/* Content */}
  181. <div className="flex-1 overflow-y-auto p-4 space-y-4" style={{ background: "#FFFBEB" }}>
  182. {isLoading ? (
  183. <div className="flex items-center justify-center py-20">
  184. <RefreshCw className="w-6 h-6 animate-spin" style={{ color: "#ca8a04" }} />
  185. <span className="ml-3 text-sm" style={{ color: "#78716C" }}>Loading analytics...</span>
  186. </div>
  187. ) : !summary ? (
  188. <div className="text-center py-20">
  189. <BarChart3 className="w-12 h-12 mx-auto mb-3" style={{ color: "#d6d3d1" }} />
  190. <p className="text-sm" style={{ color: "#78716C" }}>No analytics data available yet</p>
  191. <p className="text-xs mt-1" style={{ color: "#a8a29e" }}>Data will appear as customers interact with the chatbot</p>
  192. </div>
  193. ) : (
  194. <>
  195. {/* Top-level stats */}
  196. <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
  197. <StatCard
  198. label="Total Sessions"
  199. value={summary.totalSessions}
  200. icon={<Users className="w-5 h-5" />}
  201. color="#14532D"
  202. subtext={`${summary.totalEvents} total events`}
  203. />
  204. <StatCard
  205. label="Resolution Rate"
  206. value={`${summary.resolutionRate}%`}
  207. icon={<CheckCircle2 className="w-5 h-5" />}
  208. color={summary.resolutionRate >= 70 ? "#14532D" : summary.resolutionRate >= 40 ? "#ca8a04" : "#dc2626"}
  209. subtext={`${summary.resolvedByBot + summary.resolvedByAgent} resolved`}
  210. />
  211. <StatCard
  212. label="Bot Resolution"
  213. value={`${summary.botResolutionRate}%`}
  214. icon={<Bot className="w-5 h-5" />}
  215. color="#0369a1"
  216. subtext={`${summary.resolvedByBot} by bot`}
  217. />
  218. <StatCard
  219. label="Escalated"
  220. value={summary.escalated}
  221. icon={<Headphones className="w-5 h-5" />}
  222. color="#C2410C"
  223. subtext={`${summary.abandoned} abandoned`}
  224. />
  225. </div>
  226. {/* Second row */}
  227. <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
  228. <StatCard
  229. label="Messages Sent"
  230. value={summary.messagesSent}
  231. icon={<MessageSquare className="w-5 h-5" />}
  232. color="#7c3aed"
  233. />
  234. <StatCard
  235. label="Messages Received"
  236. value={summary.messagesReceived}
  237. icon={<MessageSquare className="w-5 h-5" />}
  238. color="#059669"
  239. />
  240. <StatCard
  241. label="Button Clicks"
  242. value={summary.buttonClicks}
  243. icon={<MousePointerClick className="w-5 h-5" />}
  244. color="#ca8a04"
  245. />
  246. <StatCard
  247. label="Feedback"
  248. value={`${summary.positiveFeedback}/${summary.negativeFeedback}`}
  249. icon={summary.positiveFeedback >= summary.negativeFeedback ? <ThumbsUp className="w-5 h-5" /> : <ThumbsDown className="w-5 h-5" />}
  250. color={summary.positiveFeedback >= summary.negativeFeedback ? "#14532D" : "#dc2626"}
  251. subtext="Positive / Negative"
  252. />
  253. </div>
  254. {/* Resolution breakdown */}
  255. {totalOutcomes > 0 && (
  256. <ResolutionDonut
  257. botRate={botRate}
  258. agentRate={agentRate}
  259. escalatedRate={escalatedRate}
  260. abandonedRate={abandonedRate}
  261. />
  262. )}
  263. {/* Category breakdown */}
  264. <div>
  265. <h3 className="text-xs font-bold uppercase tracking-wider mb-3" style={{ color: "#78716C" }}>
  266. Category Breakdown
  267. </h3>
  268. <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
  269. {summary.categoryBreakdown.map(cat => {
  270. const catConfig = categoryIcons[cat.category] || { icon: <Package className="w-3.5 h-3.5" />, color: "#78716C" };
  271. return (
  272. <CategoryBar
  273. key={cat.category}
  274. category={cat.category}
  275. count={cat.count}
  276. resolved={cat.resolved}
  277. total={summary.totalEvents}
  278. icon={catConfig.icon}
  279. color={catConfig.color}
  280. />
  281. );
  282. })}
  283. </div>
  284. </div>
  285. </>
  286. )}
  287. </div>
  288. </div>
  289. );
  290. }