import { useState, useMemo } from "react" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, } from "@/components/ui/chart" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Area, AreaChart, Bar, BarChart, CartesianGrid, XAxis, YAxis, } from "recharts" import { Loader2, TrendingUp, ShoppingCart, Mail, MousePointerClick, DollarSign, Users, Ticket, Package, UserX, CheckCircle2, XCircle, ExternalLink, Eye, BarChart3, Download, } from "lucide-react" import { useAbandonedCartAnalytics } from "../../hooks/use-abandoned-cart" import type { DailyStat } from "../../types" import { toast } from "sonner" /* ───────────────── Helpers ───────────────── */ function formatCurrency(amount: number, currency: string) { try { return new Intl.NumberFormat(undefined, { style: "currency", currency: currency || "USD", }).format(amount) } catch { return `${currency} ${amount.toFixed(2)}` } } function formatDate(dateStr: string) { if (!dateStr) return "—" const date = new Date(dateStr) return date.toLocaleDateString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }) } function formatShortDate(dateStr: string) { const date = new Date(dateStr) return date.toLocaleDateString(undefined, { month: "short", day: "numeric" }) } /* ───────────────── Chart Configs ───────────────── */ const cartsChartConfig = { abandoned: { label: "Abandoned", color: "#f97316", }, recovered: { label: "Recovered", color: "#22c55e", }, } satisfies ChartConfig const revenueChartConfig = { revenue: { label: "Revenue", color: "#00226b", }, } satisfies ChartConfig const emailEngagementConfig = { opened: { label: "Opened", color: "#00226b", }, clicked: { label: "Clicked", color: "#22c55e", }, } satisfies ChartConfig /* ───────────────── Period Filter ───────────────── */ type Period = "7d" | "30d" function filterByPeriod(stats: DailyStat[], period: Period): DailyStat[] { if (!stats || stats.length === 0) return [] const days = period === "7d" ? 7 : 30 return stats.slice(-days) } /* ───────────────── Main Component ───────────────── */ export function AnalyticsTab() { const { analytics, loading } = useAbandonedCartAnalytics() const [period, setPeriod] = useState("30d") const [exporting, setExporting] = useState(false) const [exportingCarts, setExportingCarts] = useState(false) const filteredStats = useMemo( () => filterByPeriod(analytics?.dailyStats || [], period), [analytics?.dailyStats, period] ) const handleExportAnalytics = async () => { try { setExporting(true) const apiUrl = (window as any).swiftCommerceData?.apiUrl || '/wp-json/swift-commerce/v1' const nonce = (window as any).swiftCommerceData?.restNonce || '' const response = await fetch(`${apiUrl}/abandoned-cart/export-analytics`, { headers: { 'X-WP-Nonce': nonce }, }) const data = await response.json() if (data.success && data.csv) { const blob = new Blob([data.csv], { type: 'text/csv;charset=utf-8;' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = `cart-recovery-analytics-${new Date().toISOString().slice(0, 10)}.csv` document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) } } catch (error) { console.error('Failed to export analytics:', error) } finally { setExporting(false) } } const handleExportCarts = async () => { try { setExportingCarts(true) const apiUrl = (window as any).swiftCommerceData?.apiUrl || '/wp-json/swift-commerce/v1' const nonce = (window as any).swiftCommerceData?.restNonce || '' const response = await fetch(`${apiUrl}/abandoned-cart/export`, { headers: { 'X-WP-Nonce': nonce }, }) const data = await response.json() if (data.success && data.csv) { const blob = new Blob([data.csv], { type: 'text/csv;charset=utf-8;' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = `abandoned-carts-${new Date().toISOString().slice(0, 10)}.csv` document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) if (data.truncated) { toast.warning(`Exported the newest ${data.count} of ${data.total} carts.`) } } } catch (error) { console.error('Failed to export carts:', error) } finally { setExportingCarts(false) } } if (loading) { return (
) } if (!analytics) { return (

No analytics data yet

Analytics will populate once you start tracking abandoned carts and sending recovery emails.

) } const currency = analytics.currency || "USD" const revenueDescription = (analytics.revenueByCurrency || []).length > 1 ? (analytics.revenueByCurrency || []) .map((row) => formatCurrency(row.amount, row.currency)) .join(' · ') : "Paid order revenue in the store currency" return (
Overview Emails Coupons
{/* ══════════════════════ OVERVIEW TAB ══════════════════════ */} {/* Summary Cards */}
{/* Carts Trend Chart */} {filteredStats.length > 0 && (
Cart Recovery Trend Abandoned vs recovered carts over time.
} /> } />
)} {/* Revenue Trend Chart */} {filteredStats.length > 0 && (
Revenue Recovered Daily recovered revenue ({currency}).
formatCurrency(v, currency)} /> formatCurrency(Number(value), currency)} /> } />
)} {/* Customer Breakdown + Most Abandoned Products */}
{/* Customer Breakdown */} Customer Breakdown Guest vs registered customer activity.

Abandoned

Recovered

{analytics.totalUnsubscribed > 0 && (
Unsubscribed {analytics.totalUnsubscribed}
)}
{/* Popular Abandoned Products */} Most Abandoned Products Products most frequently left in abandoned carts. {analytics.popularProducts && analytics.popularProducts.length > 0 ? (
{analytics.popularProducts.map((product, idx) => (
{idx + 1}.
{product.image ? ( {product.name} ) : (
)}

{product.name}

{formatCurrency(parseFloat(product.price || "0"), currency)}

{product.count} {product.count === 1 ? "time" : "times"}
))}
) : (

No product data available yet.

)}
{/* ══════════════════════ EMAILS TAB ══════════════════════ */} {/* Email Summary Cards */}
{/* Email Engagement Funnel */} {analytics.emailsSent > 0 && ( Email Engagement Funnel How recipients interact with your recovery emails.
)} {/* Email Engagement Over Time Chart */} {analytics.recentEmails && analytics.recentEmails.length > 0 && (
Email Engagement Opens and clicks recorded from recovery emails.
} /> } />
)} {/* Recent Recovery Emails Table */} {analytics.recentEmails && analytics.recentEmails.length > 0 && ( Recent Recovery Emails Last 20 recovery emails sent with engagement status.
Recipient Subject Sent Opened Clicked Cart Status {analytics.recentEmails.map((email) => ( {email.customer_email} {email.subject} {formatDate(email.sent_at)} {email.opened_at ? (
{formatDate(email.opened_at)}
) : ( )}
{email.clicked_at ? (
{formatDate(email.clicked_at)}
) : ( )}
{email.cart_status}
))}
)}
{/* ══════════════════════ COUPONS TAB ══════════════════════ */} {/* Coupon Summary Cards */}
0 ? `${(((analytics.couponsUsed || 0) / analytics.totalCoupons) * 100).toFixed(1)}%` : "0%" } icon={TrendingUp} description="Of generated coupons used" />
{/* Coupon Funnel */} {(analytics.totalCoupons || 0) > 0 && ( Coupon Performance Funnel From generation to revenue — how your recovery coupons perform.

{formatCurrency(analytics.couponRevenue || 0, currency)}

Total Coupon Revenue

{(analytics.couponsUsed || 0) > 0 ? formatCurrency( (analytics.couponRevenue || 0) / (analytics.couponsUsed || 1), currency ) : formatCurrency(0, currency)}

Avg Order with Coupon

)} {(analytics.totalCoupons || 0) === 0 && (

No coupons generated yet

Enable coupon generation in your email sequence settings to automatically include discount codes in recovery emails.

)}
) } /* ───────────────── Sub-Components ───────────────── */ function StatCard({ title, value, icon: Icon, description, compact, }: { title: string value: string | number icon: React.ElementType description?: string compact?: boolean }) { if (compact) { return (

{title}

{value}

) } return ( {title}
{value}
{description && (

{description}

)}
) } function ProgressStat({ label, value, total, color, }: { label: string value: number total: number color: string }) { const pct = total > 0 ? (value / total) * 100 : 0 return (
{label} {value}
) } function FunnelBar({ label, value, total, color, }: { label: string value: number total: number color: string }) { const pct = total > 0 ? (value / total) * 100 : 0 return (
{label} {value} ({pct.toFixed(1)}%)
) } function PeriodSelector({ period, onChange, }: { period: Period onChange: (p: Period) => void }) { return (
) } /* ───────────────── Data Helpers ───────────────── */ interface EmailDayStat { date: string opened: number clicked: number } /** * Build daily aggregated open/click stats from recent emails for the engagement chart. */ function buildEmailDailyStats( emails: { sent_at: string; opened_at: string | null; clicked_at: string | null }[], period: Period ): EmailDayStat[] { const days = period === "7d" ? 7 : 30 const now = new Date() const map = new Map() // Seed all dates for (let i = days - 1; i >= 0; i--) { const d = new Date(now) d.setDate(d.getDate() - i) const key = d.toISOString().slice(0, 10) map.set(key, { date: key, opened: 0, clicked: 0 }) } // Tally opens and clicks by their respective dates for (const e of emails) { if (e.opened_at) { const key = new Date(e.opened_at).toISOString().slice(0, 10) const stat = map.get(key) if (stat) stat.opened++ } if (e.clicked_at) { const key = new Date(e.clicked_at).toISOString().slice(0, 10) const stat = map.get(key) if (stat) stat.clicked++ } } return Array.from(map.values()) }