Merge external custom components into aircalldashboard directly

This commit is contained in:
2024-12-28 15:19:30 -05:00
parent 7291221154
commit 7ed6cac8f7
7 changed files with 66 additions and 977 deletions

1
.gitignore vendored
View File

@@ -26,6 +26,7 @@ dist-ssr
dashboard/build/**
dashboard-server/frontend/build/**
**/build/**
._*
# Build directories
build/

View File

@@ -16,7 +16,6 @@ import Navigation from "@/components/dashboard/Navigation";
import { ScrollProvider } from "@/contexts/ScrollContext";
import DateTimeWeatherDisplay from "@/components/dashboard/DateTime";
import AircallDashboard from "@/components/dashboard/AircallDashboard";
import KlaviyoApiTest from "@/components/dashboard/KlaviyoApiTest";
import EventFeed from "./components/dashboard/EventFeed";
import StatCards from "./components/dashboard/StatCards";
import ProductGrid from "./components/dashboard/ProductGrid";

View File

@@ -1,43 +0,0 @@
// components/dashboard/AgentStatsCard.jsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
export const AgentStatsCard = ({ agent, formatDuration }) => {
const answerRate = ((agent.answered / agent.total) * 100).toFixed(1);
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">{agent.name}</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Answer Rate</span>
<span className="font-medium">{answerRate}%</span>
</div>
<Progress value={parseFloat(answerRate)} className="h-2" />
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Total Calls</p>
<p className="text-sm font-medium">{agent.total}</p>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Avg Duration</p>
<p className="text-sm font-medium">
{formatDuration(agent.average_duration)}
</p>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Answered</p>
<p className="text-sm font-medium">{agent.answered}</p>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Missed</p>
<p className="text-sm font-medium">{agent.missed}</p>
</div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -34,8 +34,12 @@ import {
ArrowUpDown,
Timer,
Loader2,
Download,
Search,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import {
Tooltip,
TooltipContent,
@@ -54,8 +58,6 @@ import {
BarChart,
Bar,
} from "recharts";
import { AgentStatsCard } from "@/components/dashboard/AgentStatsCard";
import { TableActions } from "@/components/dashboard/TableActions";
const COLORS = {
inbound: "hsl(262.1 83.3% 57.8%)", // Purple
@@ -223,6 +225,67 @@ const AgentPerformanceTable = ({ agents, onSort }) => {
);
};
const TableActions = ({ onSearch, onExport }) => {
return (
<div className="flex items-center justify-between py-4">
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Filter agents..."
onChange={(e) => onSearch(e.target.value)}
className="pl-8"
/>
</div>
</div>
<Button variant="outline" onClick={onExport}>
<Download className="mr-2 h-4 w-4" />
Export
</Button>
</div>
);
};
const AgentStatsCard = ({ agent, formatDuration }) => {
const answerRate = ((agent.answered / agent.total) * 100).toFixed(1);
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">{agent.name}</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Answer Rate</span>
<span className="font-medium">{answerRate}%</span>
</div>
<Progress value={parseFloat(answerRate)} className="h-2" />
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Total Calls</p>
<p className="text-sm font-medium">{agent.total}</p>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Avg Duration</p>
<p className="text-sm font-medium">
{formatDuration(agent.average_duration)}
</p>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Answered</p>
<p className="text-sm font-medium">{agent.answered}</p>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Missed</p>
<p className="text-sm font-medium">{agent.missed}</p>
</div>
</div>
</CardContent>
</Card>
);
};
const AircallDashboard = () => {
const [timeRange, setTimeRange] = useState("last7days");
const [metrics, setMetrics] = useState(null);

View File

@@ -1,862 +0,0 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { DateTime } from 'luxon';
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Loader2 } from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
} from 'recharts';
const TIME_RANGES = [
{ value: 'today', label: 'Today' },
{ value: 'yesterday', label: 'Yesterday' },
{ value: 'thisWeek', label: 'This Week' },
{ value: 'lastWeek', label: 'Last Week' },
{ value: 'thisMonth', label: 'This Month' },
{ value: 'lastMonth', label: 'Last Month' }
];
const INTERVALS = [
{ value: 'hour', label: 'Hourly' },
{ value: 'day', label: 'Daily' },
{ value: 'week', label: 'Weekly' },
{ value: 'month', label: 'Monthly' }
];
const METRIC_IDS = {
PLACED_ORDER: 'Y8cqcF',
SHIPPED_ORDER: 'VExpdL',
ACCOUNT_CREATED: 'TeeypV',
CANCELED_ORDER: 'YjVMNg',
NEW_BLOG_POST: 'YcxeDr',
PAYMENT_REFUNDED: 'R7XUYh'
};
const KlaviyoApiTest = () => {
// State
const [loading, setLoading] = useState({});
const [error, setError] = useState(null);
const [activeEndpoint, setActiveEndpoint] = useState('metrics');
// Shared state
const [selectedTimeRange, setSelectedTimeRange] = useState('today');
const [selectedInterval, setSelectedInterval] = useState('hour');
const [customDateRange, setCustomDateRange] = useState({
startDate: '',
endDate: ''
});
// Metrics endpoint state
const [metrics, setMetrics] = useState([]);
const [selectedMetric, setSelectedMetric] = useState('');
// Events endpoint state
const [eventResults, setEventResults] = useState(null);
const [eventStats, setEventStats] = useState(null);
// Aggregation endpoint state
const [aggregatedData, setAggregatedData] = useState([]);
// Stats endpoint state
const [periodStats, setPeriodStats] = useState(null);
// Products endpoint state
const [productStats, setProductStats] = useState(null);
// Feed endpoint state
const [feedEvents, setFeedEvents] = useState([]);
const [selectedMetrics, setSelectedMetrics] = useState([METRIC_IDS.PLACED_ORDER]);
useEffect(() => {
setDefaultDates();
}, []);
const setDefaultDates = () => {
const now = DateTime.now().setZone('America/New_York');
const threeDaysAgo = now.minus({ days: 3 });
setCustomDateRange({
startDate: threeDaysAgo.toFormat("yyyy-MM-dd'T'HH:mm"),
endDate: now.toFormat("yyyy-MM-dd'T'HH:mm")
});
};
// Shared Components
const renderTimeRangeControls = ({ showInterval = false }) => (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Time Range</h3>
<div className={`grid ${showInterval ? 'grid-cols-2' : 'grid-cols-1'} gap-4`}>
<Select value={selectedTimeRange} onValueChange={setSelectedTimeRange}>
<SelectTrigger>
<SelectValue placeholder="Select time range" />
</SelectTrigger>
<SelectContent>
{TIME_RANGES.map((range) => (
<SelectItem key={range.value} value={range.value}>
{range.label}
</SelectItem>
))}
<SelectItem value="custom">Custom Range</SelectItem>
</SelectContent>
</Select>
{showInterval && (
<Select value={selectedInterval} onValueChange={setSelectedInterval}>
<SelectTrigger>
<SelectValue placeholder="Select interval" />
</SelectTrigger>
<SelectContent>
{INTERVALS.map((interval) => (
<SelectItem key={interval.value} value={interval.value}>
{interval.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
{selectedTimeRange === 'custom' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="startDate">Start Date (Eastern Time)</Label>
<Input
id="startDate"
type="datetime-local"
value={customDateRange.startDate}
onChange={(e) => setCustomDateRange(prev => ({
...prev,
startDate: e.target.value
}))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="endDate">End Date (Eastern Time)</Label>
<Input
id="endDate"
type="datetime-local"
value={customDateRange.endDate}
onChange={(e) => setCustomDateRange(prev => ({
...prev,
endDate: e.target.value
}))}
/>
</div>
</div>
)}
</div>
);
const renderMetricSelector = () => (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Select Metric</h3>
</div>
<Select value={selectedMetric} onValueChange={setSelectedMetric}>
<SelectTrigger>
<SelectValue placeholder="Select a metric" />
</SelectTrigger>
<SelectContent>
{metrics.map((metric) => (
<SelectItem key={metric.id} value={metric.id}>
{metric.attributes?.name || metric.id}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
// API Endpoint Functions
const fetchMetrics = async () => {
setLoading(prev => ({ ...prev, metrics: true }));
setError(null);
try {
const response = await axios.get('/api/klaviyo/metrics');
setMetrics(response.data.data || []);
} catch (error) {
console.error('Error fetching metrics:', error);
setError(error.message);
} finally {
setLoading(prev => ({ ...prev, metrics: false }));
}
};
const fetchEventsByTimeRange = async () => {
if (!selectedMetric) return;
setLoading(prev => ({ ...prev, events: true }));
setError(null);
try {
const response = await axios.get(`/api/klaviyo/events/by-time/${selectedTimeRange}`, {
params: {
metricId: selectedMetric,
includeStats: true,
startDate: customDateRange.startDate,
endDate: customDateRange.endDate
}
});
setEventResults(response.data);
setEventStats(response.data.stats);
} catch (error) {
console.error('Error fetching events:', error);
setError(error.message);
} finally {
setLoading(prev => ({ ...prev, events: false }));
}
};
const fetchAggregatedEvents = async () => {
if (!selectedMetric) return;
setLoading(prev => ({ ...prev, aggregation: true }));
setError(null);
try {
const params = selectedTimeRange === 'custom'
? { startDate: customDateRange.startDate, endDate: customDateRange.endDate }
: { timeRange: selectedTimeRange };
const response = await axios.get('/api/klaviyo/events/aggregate', {
params: {
...params,
metricId: selectedMetric,
interval: selectedInterval
}
});
setAggregatedData(response.data.data);
} catch (error) {
console.error('Error fetching aggregated events:', error);
setError(error.message);
} finally {
setLoading(prev => ({ ...prev, aggregation: false }));
}
};
const fetchPeriodStats = async () => {
setLoading(prev => ({ ...prev, stats: true }));
setError(null);
try {
const params = selectedTimeRange === 'custom'
? { startDate: customDateRange.startDate, endDate: customDateRange.endDate }
: { timeRange: selectedTimeRange };
const response = await axios.get('/api/klaviyo/events/stats', { params });
setPeriodStats(response.data);
} catch (error) {
console.error('Error fetching period stats:', error);
setError(error.message);
} finally {
setLoading(prev => ({ ...prev, stats: false }));
}
};
const fetchEventFeed = async () => {
setLoading(prev => ({ ...prev, feed: true }));
setError(null);
try {
const params = selectedTimeRange === 'custom'
? { startDate: customDateRange.startDate, endDate: customDateRange.endDate }
: { timeRange: selectedTimeRange };
const response = await axios.get('/api/klaviyo/events/feed', {
params: {
...params,
metricIds: JSON.stringify(selectedMetrics)
}
});
setFeedEvents(response.data.data);
} catch (error) {
console.error('Error fetching event feed:', error);
setError(error.message);
} finally {
setLoading(prev => ({ ...prev, feed: false }));
}
};
const fetchProductStats = async () => {
setLoading(prev => ({ ...prev, products: true }));
setError(null);
try {
const params = selectedTimeRange === 'custom'
? { startDate: customDateRange.startDate, endDate: customDateRange.endDate }
: { timeRange: selectedTimeRange };
const response = await axios.get('/api/klaviyo/events/products', { params });
setProductStats(response.data);
} catch (error) {
console.error('Error fetching product stats:', error);
setError(error.message);
} finally {
setLoading(prev => ({ ...prev, products: false }));
}
};
// Endpoint Content Components
const renderMetricsEndpoint = () => (
<div className="space-y-4">
<Button
onClick={fetchMetrics}
disabled={loading.metrics}
className="w-full"
>
{loading.metrics && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Fetch Available Metrics
</Button>
{metrics.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Available Metrics</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{metrics.map((metric) => (
<div key={metric.id} className="p-2 border rounded">
<p className="font-semibold">{metric.attributes?.name || 'Unnamed Metric'}</p>
<p className="text-sm text-gray-500">ID: {metric.id}</p>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
);
const renderEventsEndpoint = () => (
<div className="space-y-4">
{renderMetricSelector()}
{renderTimeRangeControls({ showInterval: false })}
<Button
onClick={fetchEventsByTimeRange}
disabled={loading.events || !selectedMetric}
className="w-full"
>
{loading.events && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Fetch Events
</Button>
{eventStats && (
<Card>
<CardHeader>
<CardTitle>Event Statistics</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<p className="font-semibold">Total Events</p>
<p className="text-2xl">{eventStats.total}</p>
</div>
<div>
<p className="font-semibold">Time Range</p>
<p className="text-sm">
{eventStats.timeRange?.displayStart} to {eventStats.timeRange?.displayEnd}
</p>
</div>
<div>
<p className="font-semibold">Average Per Day</p>
<p className="text-2xl">{eventStats.averagePerDay?.toFixed(1)}</p>
</div>
</div>
</CardContent>
</Card>
)}
{eventResults && (
<Card>
<CardHeader>
<CardTitle>Raw Event Data</CardTitle>
</CardHeader>
<CardContent>
<pre className="whitespace-pre-wrap overflow-auto max-h-96">
{JSON.stringify(eventResults, null, 2)}
</pre>
</CardContent>
</Card>
)}
</div>
);
const renderAggregateEndpoint = () => (
<div className="space-y-4">
{renderMetricSelector()}
{renderTimeRangeControls({ showInterval: true })}
<Button
onClick={fetchAggregatedEvents}
disabled={loading.aggregation || !selectedMetric}
className="w-full"
>
{loading.aggregation && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Fetch Aggregated Data
</Button>
{aggregatedData.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Aggregated Events</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={aggregatedData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="displayTime"
angle={-45}
textAnchor="end"
height={80}
/>
<YAxis />
<Tooltip />
<Bar dataKey="count" fill="#8884d8" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)}
</div>
);
const renderStatsEndpoint = () => (
<div className="space-y-4">
{renderTimeRangeControls({ showInterval: false })}
<Button
onClick={fetchPeriodStats}
disabled={loading.stats}
className="w-full"
>
{loading.stats && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Fetch Period Statistics
</Button>
{periodStats && (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Revenue Overview</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<p className="font-semibold">Total Revenue</p>
<p className="text-2xl">${periodStats.stats.revenue?.toFixed(2) || '0.00'}</p>
{periodStats.stats.prevPeriodRevenue && (
<p className="text-sm text-gray-500">
Previous: ${periodStats.stats.prevPeriodRevenue.toFixed(2)}
</p>
)}
</div>
<div>
<p className="font-semibold">Order Count</p>
<p className="text-2xl">{periodStats.stats.orderCount || 0}</p>
<p className="text-sm text-gray-500">
Items: {periodStats.stats.itemCount || 0}
</p>
</div>
<div>
<p className="font-semibold">Average Order Value</p>
<p className="text-2xl">${periodStats.stats.averageOrderValue?.toFixed(2) || '0.00'}</p>
<p className="text-sm text-gray-500">
Items per order: {periodStats.stats.averageItemsPerOrder?.toFixed(1) || '0.0'}
</p>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Order Types</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{Object.entries(periodStats.stats.orderTypes || {}).map(([type, data]) => (
<div key={type} className="flex justify-between">
<span className="capitalize">{type.replace(/([A-Z])/g, ' $1').trim()}</span>
<span>{data.count} ({data.percentage?.toFixed(1)}%)</span>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Order Value Range</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{periodStats.stats.orderValueRange && (
<>
<div className="flex justify-between">
<span>Largest Order</span>
<span>${periodStats.stats.orderValueRange.largest?.toFixed(2)} (#{periodStats.stats.orderValueRange.largestOrderId})</span>
</div>
<div className="flex justify-between">
<span>Smallest Order</span>
<span>${periodStats.stats.orderValueRange.smallest?.toFixed(2)} (#{periodStats.stats.orderValueRange.smallestOrderId})</span>
</div>
</>
)}
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Products ({periodStats.stats.products?.total || 0})</CardTitle>
</CardHeader>
<CardContent>
<div className="max-h-60 overflow-auto space-y-2">
{periodStats.stats.products?.list?.map(product => (
<div key={product.id} className="flex justify-between py-1 border-b">
<span className="truncate flex-1 mr-4">{product.name}</span>
<span className="whitespace-nowrap">{product.count} sold</span>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Categories ({periodStats.stats.categories?.total || 0})</CardTitle>
</CardHeader>
<CardContent>
<div className="max-h-60 overflow-auto space-y-2">
{periodStats.stats.categories?.list?.map(category => (
<div key={category.name} className="flex justify-between py-1 border-b">
<span className="truncate flex-1 mr-4">{category.name}</span>
<span className="whitespace-nowrap">{category.count} items</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Shipping</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-2">Shipped orders: {periodStats.stats.shipping?.shippedCount || 0}</p>
<div className="max-h-60 overflow-auto space-y-2">
{periodStats.stats.shipping?.locations?.map(location => (
<div key={location.name} className="flex justify-between py-1 border-b">
<span className="truncate flex-1 mr-4">{location.name}</span>
<span className="whitespace-nowrap">{location.count} orders</span>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Peak Times</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{periodStats.stats.peakOrderHour && (
<div className="flex justify-between">
<span>Peak Hour</span>
<span>{periodStats.stats.peakOrderHour.displayHour} ({periodStats.stats.peakOrderHour.count} orders)</span>
</div>
)}
{periodStats.stats.bestRevenueDay && (
<div className="flex justify-between">
<span>Best Day</span>
<span>{periodStats.stats.bestRevenueDay.displayDate} (${periodStats.stats.bestRevenueDay.amount?.toFixed(2)})</span>
</div>
)}
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Refunds</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span>Count</span>
<span>{periodStats.stats.refunds?.count || 0}</span>
</div>
<div className="flex justify-between">
<span>Total Amount</span>
<span>${periodStats.stats.refunds?.total?.toFixed(2) || '0.00'}</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Canceled Orders</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span>Count</span>
<span>{periodStats.stats.canceledOrders?.count || 0}</span>
</div>
<div className="flex justify-between">
<span>Total Amount</span>
<span>${periodStats.stats.canceledOrders?.total?.toFixed(2) || '0.00'}</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)}
</div>
);
const renderProductsEndpoint = () => (
<div className="space-y-4">
{renderTimeRangeControls({ showInterval: false })}
<Button
onClick={fetchProductStats}
disabled={loading.products}
className="w-full"
>
{loading.products && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Fetch Product Statistics
</Button>
{productStats && (
<div className="space-y-4">
{/* Products */}
<Card>
<CardHeader>
<CardTitle>Products ({productStats.stats.products?.total || 0})</CardTitle>
</CardHeader>
<CardContent>
<div className="max-h-96 overflow-auto space-y-2">
{productStats.stats.products?.list?.map(product => (
<div key={product.id} className="p-2 border rounded">
<div className="flex justify-between items-start gap-4">
{product.imageUrl && (
<div className="flex-shrink-0">
<img
src={product.imageUrl}
alt={product.name}
className="w-16 h-16 object-cover rounded"
onError={(e) => e.target.style.display = 'none'}
/>
</div>
)}
<div className="flex-1">
<p className="font-semibold">{product.name}</p>
<p className="text-sm text-gray-500">SKU: {product.sku}</p>
<p className="text-sm text-gray-500">Brand: {product.brand}</p>
<p className="text-sm text-gray-500">Price: ${product.price.toFixed(2)}</p>
</div>
<div className="text-right">
<p className="font-semibold">${product.totalRevenue.toFixed(2)}</p>
<p className="text-sm font-medium">{product.totalQuantity} units sold</p>
<p className="text-sm text-gray-500">{product.orderCount} orders</p>
<p className="text-sm text-gray-500">Avg: ${product.averageOrderValue.toFixed(2)}/order</p>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Brands */}
<Card>
<CardHeader>
<CardTitle>Brands ({productStats.stats.brands?.total || 0})</CardTitle>
</CardHeader>
<CardContent>
<div className="max-h-60 overflow-auto space-y-2">
{productStats.stats.brands?.list?.map(brand => (
<div key={brand.name} className="flex justify-between py-1 border-b">
<div className="flex-1">
<p className="font-semibold">{brand.name}</p>
<p className="text-sm text-gray-500">
{brand.productCount} products, {brand.orderCount} orders
</p>
</div>
<div className="text-right">
<p className="font-semibold">{brand.totalQuantity} units sold</p>
<p className="text-sm text-gray-500">${brand.totalRevenue.toFixed(2)} revenue</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Categories */}
<Card>
<CardHeader>
<CardTitle>Categories ({productStats.stats.categories?.total || 0})</CardTitle>
</CardHeader>
<CardContent>
<div className="max-h-60 overflow-auto space-y-2">
{productStats.stats.categories?.list?.map(category => (
<div key={category.name} className="flex justify-between py-1 border-b">
<div className="flex-1">
<p className="font-semibold">{category.name}</p>
<p className="text-sm text-gray-500">
{category.productCount} products, {category.orderCount} orders
</p>
</div>
<div className="text-right">
<p className="font-semibold">{category.totalQuantity} units sold</p>
<p className="text-sm text-gray-500">${category.totalRevenue.toFixed(2)} revenue</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
</div>
);
const renderFeedEndpoint = () => (
<div className="space-y-4">
{renderTimeRangeControls({ showInterval: false })}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Select Event Types</h3>
<div className="flex flex-wrap gap-2">
{Object.entries(METRIC_IDS).map(([key, value]) => (
<Button
key={value}
variant={selectedMetrics.includes(value) ? "default" : "outline"}
onClick={() => {
setSelectedMetrics(prev =>
prev.includes(value)
? prev.filter(id => id !== value)
: [...prev, value]
);
}}
>
{key}
</Button>
))}
</div>
</div>
<Button
onClick={fetchEventFeed}
disabled={loading.feed || selectedMetrics.length === 0}
className="w-full"
>
{loading.feed && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Fetch Event Feed
</Button>
{feedEvents.length > 0 && (
<div className="space-y-4">
{feedEvents.map(event => (
<Card key={event.id}>
<CardHeader>
<CardTitle>{event.type}</CardTitle>
</CardHeader>
<CardContent>
<pre className="whitespace-pre-wrap overflow-auto max-h-96">
{JSON.stringify(event.attributes, null, 2)}
</pre>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
return (
<Card className="w-full">
<CardHeader>
<CardTitle>Klaviyo API Endpoints</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{error && (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Tabs value={activeEndpoint} onValueChange={setActiveEndpoint}>
<TabsList className="grid w-full grid-cols-6">
<TabsTrigger value="metrics">Metrics</TabsTrigger>
<TabsTrigger value="events">Events</TabsTrigger>
<TabsTrigger value="aggregate">Aggregate</TabsTrigger>
<TabsTrigger value="stats">Stats</TabsTrigger>
<TabsTrigger value="products">Products</TabsTrigger>
<TabsTrigger value="feed">Feed</TabsTrigger>
</TabsList>
<TabsContent value="metrics">
{renderMetricsEndpoint()}
</TabsContent>
<TabsContent value="events">
{renderEventsEndpoint()}
</TabsContent>
<TabsContent value="aggregate">
{renderAggregateEndpoint()}
</TabsContent>
<TabsContent value="stats">
{renderStatsEndpoint()}
</TabsContent>
<TabsContent value="products">
{renderProductsEndpoint()}
</TabsContent>
<TabsContent value="feed">
{renderFeedEndpoint()}
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
};
export default KlaviyoApiTest;

View File

@@ -1,25 +0,0 @@
// components/TableActions.jsx
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Download, Search } from "lucide-react";
export const TableActions = ({ onSearch, onExport }) => {
return (
<div className="flex items-center justify-between py-4">
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Filter agents..."
onChange={(e) => onSearch(e.target.value)}
className="pl-8"
/>
</div>
</div>
<Button variant="outline" onClick={onExport}>
<Download className="mr-2 h-4 w-4" />
Export
</Button>
</div>
);
};

View File

@@ -1,44 +0,0 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const timeRangeOptions = [
{ value: "today", label: "Today" },
{ value: "yesterday", label: "Yesterday" },
{ value: "last7days", label: "Last 7 Days" },
{ value: "last30days", label: "Last 30 Days" },
{ value: "last90days", label: "Last 90 Days" },
];
export const TimeRangeSelect = ({
value,
onChange,
className,
allowedRanges = [],
}) => {
const filteredOptions = allowedRanges.length
? timeRangeOptions.filter((option) => allowedRanges.includes(option.value))
: timeRangeOptions;
console.log("Allowed Ranges prop:", allowedRanges); // Debugging line
console.log("Filtered Options:", filteredOptions); // Debugging line
return (
<Select value={value} onValueChange={onChange}>
<SelectTrigger className={className}>
<SelectValue placeholder="Select time range" />
</SelectTrigger>
<SelectContent>
{filteredOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};