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/build/**
|
||||||
dashboard-server/frontend/build/**
|
dashboard-server/frontend/build/**
|
||||||
**/build/**
|
**/build/**
|
||||||
|
._*
|
||||||
|
|
||||||
# Build directories
|
# Build directories
|
||||||
build/
|
build/
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import Navigation from "@/components/dashboard/Navigation";
|
|||||||
import { ScrollProvider } from "@/contexts/ScrollContext";
|
import { ScrollProvider } from "@/contexts/ScrollContext";
|
||||||
import DateTimeWeatherDisplay from "@/components/dashboard/DateTime";
|
import DateTimeWeatherDisplay from "@/components/dashboard/DateTime";
|
||||||
import AircallDashboard from "@/components/dashboard/AircallDashboard";
|
import AircallDashboard from "@/components/dashboard/AircallDashboard";
|
||||||
import KlaviyoApiTest from "@/components/dashboard/KlaviyoApiTest";
|
|
||||||
import EventFeed from "./components/dashboard/EventFeed";
|
import EventFeed from "./components/dashboard/EventFeed";
|
||||||
import StatCards from "./components/dashboard/StatCards";
|
import StatCards from "./components/dashboard/StatCards";
|
||||||
import ProductGrid from "./components/dashboard/ProductGrid";
|
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,
|
ArrowUpDown,
|
||||||
Timer,
|
Timer,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Download,
|
||||||
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -54,8 +58,6 @@ import {
|
|||||||
BarChart,
|
BarChart,
|
||||||
Bar,
|
Bar,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { AgentStatsCard } from "@/components/dashboard/AgentStatsCard";
|
|
||||||
import { TableActions } from "@/components/dashboard/TableActions";
|
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
inbound: "hsl(262.1 83.3% 57.8%)", // Purple
|
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 AircallDashboard = () => {
|
||||||
const [timeRange, setTimeRange] = useState("last7days");
|
const [timeRange, setTimeRange] = useState("last7days");
|
||||||
const [metrics, setMetrics] = useState(null);
|
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