Add missing columns and fix formatting

This commit is contained in:
2024-12-27 11:29:06 -05:00
parent 59fdb221cb
commit adbaa75499

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Tooltip, Tooltip,
@@ -8,24 +7,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { Loader2, AlertCircle } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { TIME_RANGES } from "@/lib/constants";
// Helper functions for formatting // Helper functions for formatting
const formatRate = (value) => { const formatRate = (value) => {
@@ -55,6 +36,13 @@ const TableSkeleton = () => (
</div> </div>
); );
// Error alert component
const ErrorAlert = ({ description }) => (
<div className="p-4 mb-4 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/10 rounded-lg">
{description}
</div>
);
// MetricCell component for displaying campaign metrics // MetricCell component for displaying campaign metrics
const MetricCell = ({ const MetricCell = ({
value, value,
@@ -76,45 +64,58 @@ const MetricCell = ({
</td> </td>
); );
const KlaviyoCampaigns = ({ const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => {
className,
timeRange = "last7days",
onTimeRangeChange,
title = "Email Campaigns",
description
}) => {
const [campaigns, setCampaigns] = useState([]); const [campaigns, setCampaigns] = useState([]);
const [loading, setLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [selectedTimeRange, setSelectedTimeRange] = useState(timeRange); const [searchTerm, setSearchTerm] = useState("");
const [sortConfig, setSortConfig] = useState({
useEffect(() => { key: "send_time",
fetchCampaigns(); direction: "desc",
}, [selectedTimeRange]); });
const fetchCampaigns = async () => { const fetchCampaigns = async () => {
try { try {
setLoading(true); setIsLoading(true);
const response = await fetch(`/api/klaviyo/reporting/campaigns/${timeRange}`);
if (!response.ok) {
throw new Error(`Failed to fetch campaigns: ${response.status}`);
}
const data = await response.json();
setCampaigns(data.data || []);
setError(null); setError(null);
} catch (err) {
const response = await axios.get(`/api/klaviyo/reporting/campaigns/${selectedTimeRange}`); console.error("Error fetching campaigns:", err);
setCampaigns(response.data.data || []); setError(err.message);
} catch (error) {
console.error("Error fetching campaigns:", error);
setError(error.message);
} finally { } finally {
setLoading(false); setIsLoading(false);
} }
}; };
const handleTimeRangeChange = (value) => { useEffect(() => {
setSelectedTimeRange(value); fetchCampaigns();
if (onTimeRangeChange) { const interval = setInterval(fetchCampaigns, 10 * 60 * 1000); // Refresh every 10 minutes
onTimeRangeChange(value); return () => clearInterval(interval);
} }, [timeRange]);
};
if (loading) { // Sort campaigns
const sortedCampaigns = [...campaigns].sort((a, b) => {
const direction = sortConfig.direction === "desc" ? -1 : 1;
if (sortConfig.key === "send_time") {
return direction * (DateTime.fromISO(a.send_time) - DateTime.fromISO(b.send_time));
}
return direction * (a[sortConfig.key] - b[sortConfig.key]);
});
// Filter campaigns by search term
const filteredCampaigns = sortedCampaigns.filter(
(campaign) =>
campaign?.name?.toLowerCase().includes((searchTerm || "").toLowerCase())
);
if (isLoading) {
return ( return (
<Card className="h-full bg-white dark:bg-gray-900"> <Card className="h-full bg-white dark:bg-gray-900">
<CardHeader> <CardHeader>
@@ -129,85 +130,73 @@ const KlaviyoCampaigns = ({
return ( return (
<Card className="h-full bg-white dark:bg-gray-900"> <Card className="h-full bg-white dark:bg-gray-900">
{error && ( {error && <ErrorAlert description={error} />}
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Failed to load campaigns: {error}</AlertDescription>
</Alert>
)}
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100"> <CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
{title} Email Campaigns
</CardTitle> </CardTitle>
<Select value={selectedTimeRange} onValueChange={handleTimeRangeChange}>
<SelectTrigger className="w-[130px]">
<SelectValue placeholder="Select time range" />
</SelectTrigger>
<SelectContent>
{TIME_RANGES.map((range) => (
<SelectItem key={range.value} value={range.value}>
{range.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardHeader> </CardHeader>
<CardContent className="overflow-y-auto pl-4 max-h-[350px]"> <CardContent className="overflow-y-auto pl-4 max-h-[350px] mb-4">
<Table> <table className="w-full">
<TableHeader> <thead>
<TableRow> <tr>
<TableHead className="font-medium">Campaign</TableHead> <th className="p-2 text-left font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
<TableHead className="text-center font-medium">Delivery</TableHead> Campaign
<TableHead className="text-center font-medium">Opens</TableHead> </th>
<TableHead className="text-center font-medium">Clicks</TableHead> <th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
<TableHead className="text-center font-medium">Orders</TableHead> Delivery
</TableRow> </th>
</TableHeader> <th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
<TableBody> Opens
{campaigns.map((campaign) => ( </th>
<TableRow key={campaign.id}> <th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
<TableCell className="align-top"> Clicks
</th>
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
CTR
</th>
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
Orders
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
{filteredCampaigns.map((campaign) => (
<tr
key={campaign.id}
className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div> <td className="p-2 align-top">
<div className="font-medium text-gray-900 dark:text-gray-100"> <div className="font-medium text-gray-900 dark:text-gray-100">
{campaign.name || "Unnamed Campaign"} {campaign.name}
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400 truncate max-w-[300px]"> <div className="text-sm text-gray-500 dark:text-gray-400 truncate max-w-[300px]">
{campaign.subject || "No subject"} {campaign.subject}
</div> </div>
<div className="text-xs text-gray-400 dark:text-gray-500"> <div className="text-xs text-gray-400 dark:text-gray-500">
{campaign.send_time {campaign.send_time
? DateTime.fromISO(campaign.send_time).toLocaleString( ? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED)
DateTime.DATETIME_MED
)
: "No date"} : "No date"}
</div> </div>
</div> </td>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
side="top" side="top"
className="break-words bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 border dark:border-gray-700" className="break-words bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 border dark:border-gray-700"
> >
<p className="font-medium"> <p className="font-medium">{campaign.name}</p>
{campaign.name || "Unnamed Campaign"} <p>{campaign.subject}</p>
</p>
<p>{campaign.subject || "No subject"}</p>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
{campaign.send_time {campaign.send_time
? DateTime.fromISO(campaign.send_time).toLocaleString( ? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED)
DateTime.DATETIME_MED
)
: "No date"} : "No date"}
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</TableCell>
<MetricCell <MetricCell
value={campaign.stats.delivery_rate} value={campaign.stats.delivery_rate}
count={campaign.stats.delivered} count={campaign.stats.delivered}
@@ -223,6 +212,11 @@ const KlaviyoCampaigns = ({
count={campaign.stats.clicks_unique} count={campaign.stats.clicks_unique}
totalRecipients={campaign.stats.recipients} totalRecipients={campaign.stats.recipients}
/> />
<MetricCell
value={campaign.stats.click_to_open_rate}
count={campaign.stats.clicks_unique}
totalRecipients={campaign.stats.opens_unique}
/>
<MetricCell <MetricCell
value={campaign.stats.conversion_value} value={campaign.stats.conversion_value}
count={campaign.stats.conversion_uniques} count={campaign.stats.conversion_uniques}
@@ -230,10 +224,10 @@ const KlaviyoCampaigns = ({
showConversionRate={true} showConversionRate={true}
totalRecipients={campaign.stats.recipients} totalRecipients={campaign.stats.recipients}
/> />
</TableRow> </tr>
))} ))}
</TableBody> </tbody>
</Table> </table>
</CardContent> </CardContent>
</Card> </Card>
); );