From 7a224ee8704ed288aad42b06b6eecef0fdaf707b Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 27 Dec 2024 12:10:08 -0500 Subject: [PATCH] Add SMS campaigns --- .../klaviyo-server/routes/reporting.routes.js | 53 +----- .../services/reporting.service.js | 165 ++++++++++-------- .../components/dashboard/KlaviyoCampaigns.jsx | 124 ++++++++++--- 3 files changed, 200 insertions(+), 142 deletions(-) diff --git a/dashboard-server/klaviyo-server/routes/reporting.routes.js b/dashboard-server/klaviyo-server/routes/reporting.routes.js index 2e1b2da..c33ad77 100644 --- a/dashboard-server/klaviyo-server/routes/reporting.routes.js +++ b/dashboard-server/klaviyo-server/routes/reporting.routes.js @@ -11,52 +11,17 @@ export function createReportingRouter(apiKey, apiRevision) { router.get('/campaigns/:timeRange', async (req, res) => { try { const { timeRange } = req.params; - const { startDate, endDate } = req.query; - - let params = {}; - if (timeRange === 'custom' && startDate && endDate) { - const range = timeManager.getCustomRange(startDate, endDate); - if (!range) { - return res.status(400).json({ error: 'Invalid date range' }); - } - params = { startDate: range.start.toISO(), endDate: range.end.toISO() }; - } else { - params = { timeRange }; - } - - const data = await reportingService.getCampaignReports(params); + const { channel } = req.query; - // Transform the data to match the expected format - const transformedData = { - data: data.data.map(campaign => ({ - id: campaign.id, - name: campaign.name, - subject: campaign.subject, - send_time: campaign.send_time, - stats: { - delivery_rate: campaign.attributes?.statistics?.delivery_rate || 0, - delivered: campaign.attributes?.statistics?.delivered || 0, - recipients: campaign.attributes?.statistics?.recipients || 0, - open_rate: campaign.attributes?.statistics?.open_rate || 0, - opens_unique: campaign.attributes?.statistics?.opens_unique || 0, - opens: campaign.attributes?.statistics?.opens || 0, - click_rate: campaign.attributes?.statistics?.click_rate || 0, - clicks_unique: campaign.attributes?.statistics?.clicks_unique || 0, - click_to_open_rate: campaign.attributes?.statistics?.click_to_open_rate || 0, - conversion_value: campaign.attributes?.statistics?.conversion_value || 0, - conversion_uniques: campaign.attributes?.statistics?.conversion_uniques || 0 - } - })) - }; - - res.json(transformedData); - } catch (error) { - console.error('[Reporting Route] Error:', error); - res.status(500).json({ - status: 'error', - message: error.message, - details: error.response?.data || null + const reports = await reportingService.getCampaignReports({ + timeRange, + channel }); + + res.json(reports); + } catch (error) { + console.error('[ReportingRoutes] Error fetching campaign reports:', error); + res.status(500).json({ error: error.message }); } }); diff --git a/dashboard-server/klaviyo-server/services/reporting.service.js b/dashboard-server/klaviyo-server/services/reporting.service.js index 8731713..2332de3 100644 --- a/dashboard-server/klaviyo-server/services/reporting.service.js +++ b/dashboard-server/klaviyo-server/services/reporting.service.js @@ -43,78 +43,83 @@ export class ReportingService { const range = this.timeManager.getDateRange(params.timeRange || 'last30days'); - const payload = { - data: { - type: "campaign-values-report", - attributes: { - timeframe: { - start: range.start.toISO(), - end: range.end.toISO() - }, - statistics: [ - "delivery_rate", - "delivered", - "recipients", - "open_rate", - "opens_unique", - "opens", - "click_rate", - "clicks_unique", - "click_to_open_rate", - "conversion_value", - "conversion_uniques" - ], - conversion_metric_id: METRIC_IDS.PLACED_ORDER, - filter: 'equals(send_channel,"email")' - } - } - }; + // Determine which channels to fetch based on params + const channelsToFetch = params.channel === 'all' || !params.channel + ? ['email', 'sms'] + : [params.channel]; - const response = await fetch(`${this.baseUrl}/campaign-values-reports`, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Klaviyo-API-Key ${this.apiKey}`, - 'revision': this.apiRevision - }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - const errorData = await response.json(); - console.error('[ReportingService] API Error:', errorData); - throw new Error(`Klaviyo API error: ${response.status} ${response.statusText}`); - } - - const reportData = await response.json(); - console.log('[ReportingService] Raw report data:', JSON.stringify(reportData, null, 2)); + const allResults = []; - // Get campaign IDs from the report - const campaignIds = reportData.data?.attributes?.results?.map(result => - result.groupings?.campaign_id - ).filter(Boolean) || []; - - console.log('[ReportingService] Extracted campaign IDs:', campaignIds); - - // Get campaign details including send time and subject lines - const campaignDetails = await this.getCampaignDetails(campaignIds); - console.log('[ReportingService] Campaign details:', JSON.stringify(campaignDetails, null, 2)); - - // Merge campaign details with report data - const enrichedData = { - data: reportData.data.attributes.results.map(result => { - const campaignId = result.groupings.campaign_id; - const details = campaignDetails.find(detail => detail.id === campaignId); - const message = details?.included?.find(item => item.type === 'campaign-message'); - - return { - id: campaignId, - name: details.attributes.name, - subject: details.attributes.subject, - send_time: details.attributes.send_time, + // Fetch each channel + for (const channel of channelsToFetch) { + const payload = { + data: { + type: "campaign-values-report", attributes: { - statistics: { + timeframe: { + start: range.start.toISO(), + end: range.end.toISO() + }, + statistics: [ + "delivery_rate", + "delivered", + "recipients", + "open_rate", + "opens_unique", + "opens", + "click_rate", + "clicks_unique", + "click_to_open_rate", + "conversion_value", + "conversion_uniques" + ], + conversion_metric_id: METRIC_IDS.PLACED_ORDER, + filter: `equals(send_channel,"${channel}")` + } + } + }; + + const response = await fetch(`${this.baseUrl}/campaign-values-reports`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Klaviyo-API-Key ${this.apiKey}`, + 'revision': this.apiRevision + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error('[ReportingService] API Error:', errorData); + throw new Error(`Klaviyo API error: ${response.status} ${response.statusText}`); + } + + const reportData = await response.json(); + console.log(`[ReportingService] Raw ${channel} report data:`, JSON.stringify(reportData, null, 2)); + + // Get campaign IDs from the report + const campaignIds = reportData.data?.attributes?.results?.map(result => + result.groupings?.campaign_id + ).filter(Boolean) || []; + + if (campaignIds.length > 0) { + // Get campaign details including send time and subject lines + const campaignDetails = await this.getCampaignDetails(campaignIds); + + // Process results for this channel + const channelResults = reportData.data.attributes.results.map(result => { + const campaignId = result.groupings.campaign_id; + const details = campaignDetails.find(detail => detail.id === campaignId); + + return { + id: campaignId, + name: details.attributes.name, + subject: details.attributes.subject, + send_time: details.attributes.send_time, + channel: channel, // Use the channel we're currently processing + stats: { delivery_rate: result.statistics.delivery_rate, delivered: result.statistics.delivered, recipients: result.statistics.recipients, @@ -127,10 +132,16 @@ export class ReportingService { conversion_value: result.statistics.conversion_value, conversion_uniques: result.statistics.conversion_uniques } - } - }; - }) - .sort((a, b) => { + }; + }); + + allResults.push(...channelResults); + } + } + + // Sort all results by date + const enrichedData = { + data: allResults.sort((a, b) => { const dateA = new Date(a.send_time); const dateB = new Date(b.send_time); return dateB - dateA; // Sort by date descending @@ -195,6 +206,11 @@ export class ReportingService { } const message = data.included?.find(item => item.type === 'campaign-message'); + + console.log('[ReportingService] Campaign details for ID:', campaignId, { + send_channel: data.data.attributes.send_channel, + raw_attributes: data.data.attributes + }); return { id: data.data.id, @@ -203,7 +219,8 @@ export class ReportingService { ...data.data.attributes, name: data.data.attributes.name, send_time: data.data.attributes.send_time, - subject: message?.attributes?.content?.subject + subject: message?.attributes?.content?.subject, + send_channel: data.data.attributes.send_channel || 'email' } }; } catch (error) { diff --git a/dashboard/src/components/dashboard/KlaviyoCampaigns.jsx b/dashboard/src/components/dashboard/KlaviyoCampaigns.jsx index fa7df76..7570e04 100644 --- a/dashboard/src/components/dashboard/KlaviyoCampaigns.jsx +++ b/dashboard/src/components/dashboard/KlaviyoCampaigns.jsx @@ -7,9 +7,19 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { DateTime } from "luxon"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { TIME_RANGES } from "@/lib/constants"; +import { Mail, MessageSquare } from "lucide-react"; // Helper functions for formatting -const formatRate = (value) => { +const formatRate = (value, isSMS = false, hideForSMS = false) => { + if (isSMS && hideForSMS) return "N/A"; if (typeof value !== "number") return "0.0%"; return `${(value * 100).toFixed(1)}%`; }; @@ -50,25 +60,46 @@ const MetricCell = ({ isMonetary = false, showConversionRate = false, totalRecipients = 0, -}) => ( - -
- {isMonetary ? formatCurrency(value) : formatRate(value)} -
-
- {count?.toLocaleString() || 0} {count === 1 ? "recipient" : "recipients"} - {showConversionRate && - totalRecipients > 0 && - ` (${((count / totalRecipients) * 100).toFixed(2)}%)`} -
- -); + isSMS = false, + hideForSMS = false, +}) => { + if (isSMS && hideForSMS) { + return ( + +
N/A
+
-
+ + ); + } -const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => { + return ( + +
+ {isMonetary ? formatCurrency(value) : formatRate(value, isSMS, hideForSMS)} +
+
+ {count?.toLocaleString() || 0} {count === 1 ? "recipient" : "recipients"} + {showConversionRate && + totalRecipients > 0 && + ` (${((count / totalRecipients) * 100).toFixed(2)}%)`} +
+ + ); +}; + +const CHANNEL_OPTIONS = [ + { value: "all", label: "All Campaigns" }, + { value: "email", label: "Email Only" }, + { value: "sms", label: "SMS Only" }, +]; + +const KlaviyoCampaigns = ({ className }) => { const [campaigns, setCampaigns] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(""); + const [selectedChannel, setSelectedChannel] = useState("all"); + const [selectedTimeRange, setSelectedTimeRange] = useState("last7days"); const [sortConfig, setSortConfig] = useState({ key: "send_time", direction: "desc", @@ -77,7 +108,9 @@ const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => { const fetchCampaigns = async () => { try { setIsLoading(true); - const response = await fetch(`/api/klaviyo/reporting/campaigns/${timeRange}`); + const response = await fetch( + `/api/klaviyo/reporting/campaigns/${selectedTimeRange}${selectedChannel !== 'all' ? `?channel=${selectedChannel}` : ''}` + ); if (!response.ok) { throw new Error(`Failed to fetch campaigns: ${response.status}`); @@ -98,7 +131,7 @@ const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => { fetchCampaigns(); const interval = setInterval(fetchCampaigns, 10 * 60 * 1000); // Refresh every 10 minutes return () => clearInterval(interval); - }, [timeRange]); + }, [selectedTimeRange, selectedChannel]); // Sort campaigns const sortedCampaigns = [...campaigns].sort((a, b) => { @@ -109,10 +142,11 @@ const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => { return direction * (a[sortConfig.key] - b[sortConfig.key]); }); - // Filter campaigns by search term + // Filter campaigns by search term and channel const filteredCampaigns = sortedCampaigns.filter( (campaign) => - campaign?.name?.toLowerCase().includes((searchTerm || "").toLowerCase()) + campaign?.name?.toLowerCase().includes((searchTerm || "").toLowerCase()) && + (selectedChannel === "all" || campaign?.channel === selectedChannel) ); if (isLoading) { @@ -132,9 +166,37 @@ const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => { {error && } - - Email Campaigns - +
+ + Campaigns + +
+ + +
+
@@ -170,8 +232,15 @@ const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => { ))}
-
- {campaign.name} +
+
+ {campaign.name} +
+ {campaign.channel === 'sms' ? ( + + ) : ( + + )}
{campaign.subject} @@ -201,21 +270,27 @@ const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => { value={campaign.stats.delivery_rate} count={campaign.stats.delivered} totalRecipients={campaign.stats.recipients} + isSMS={campaign.channel === 'sms'} /> { isMonetary={true} showConversionRate={true} totalRecipients={campaign.stats.recipients} + isSMS={campaign.channel === 'sms'} />