Merge external custom components into aircalldashboard directly
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,6 +26,7 @@ dist-ssr
|
||||
dashboard/build/**
|
||||
dashboard-server/frontend/build/**
|
||||
**/build/**
|
||||
._*
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user