diff --git a/dashboard/src/components/dashboard/MetaCampaigns.jsx b/dashboard/src/components/dashboard/MetaCampaigns.jsx
index 6ae7841..2e6ce10 100644
--- a/dashboard/src/components/dashboard/MetaCampaigns.jsx
+++ b/dashboard/src/components/dashboard/MetaCampaigns.jsx
@@ -13,7 +13,21 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { Instagram, Loader2 } from "lucide-react";
+import {
+ Instagram,
+ Loader2,
+ Users,
+ DollarSign,
+ Eye,
+ Repeat,
+ MousePointer,
+ BarChart,
+ Target,
+ ShoppingCart,
+ MessageCircle,
+ Hash,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
// Helper functions for formatting
const formatCurrency = (value, decimalPlaces = 2) =>
@@ -39,6 +53,8 @@ const summaryCard = (label, value, options = {}) => {
isMonetary = false,
isPercentage = false,
decimalPlaces = 0,
+ icon: Icon,
+ iconColor,
} = options;
let displayValue;
@@ -51,12 +67,19 @@ const summaryCard = (label, value, options = {}) => {
}
return (
-
-
{label}
-
- {displayValue}
-
-
+
+
+
+
+
{label}
+
{displayValue}
+
+ {Icon && (
+
+ )}
+
+
+
);
};
@@ -69,7 +92,7 @@ const MetricCell = ({
decimalPlaces = 0,
}) => (
-
+
{isMonetary
? formatCurrency(value, decimalPlaces)
: isPercentage
@@ -77,12 +100,12 @@ const MetricCell = ({
: formatNumber(value, decimalPlaces)}
{label && (
-
+
{label}
)}
{sublabel && (
-
+
{sublabel}
)}
@@ -115,12 +138,135 @@ const CampaignName = ({ name }) => {
return {name};
};
+const getObjectiveAction = (campaignObjective) => {
+ const objectiveMap = {
+ OUTCOME_AWARENESS: { action_type: "impressions", label: "Impressions" },
+ OUTCOME_ENGAGEMENT: { action_type: "post_engagement", label: "Post Engagements" },
+ OUTCOME_TRAFFIC: { action_type: "link_click", label: "Link Clicks" },
+ OUTCOME_LEADS: { action_type: "lead", label: "Leads" },
+ OUTCOME_SALES: { action_type: "purchase", label: "Purchases" },
+ MESSAGES: { action_type: "messages", label: "Messages" },
+ };
+
+ return objectiveMap[campaignObjective] || { action_type: "link_click", label: "Link Clicks" };
+};
+
+const calculateBudget = (campaign) => {
+ if (campaign.daily_budget) {
+ return { value: campaign.daily_budget / 100, type: "day" };
+ }
+ if (campaign.lifetime_budget) {
+ return { value: campaign.lifetime_budget / 100, type: "lifetime" };
+ }
+
+ const adsets = campaign.adsets?.data || [];
+ const dailyTotal = adsets.reduce((sum, adset) => sum + (adset.daily_budget || 0), 0);
+ const lifetimeTotal = adsets.reduce((sum, adset) => sum + (adset.lifetime_budget || 0), 0);
+
+ if (dailyTotal > 0) return { value: dailyTotal / 100, type: "day" };
+ if (lifetimeTotal > 0) return { value: lifetimeTotal / 100, type: "lifetime" };
+
+ return { value: 0, type: "day" };
+};
+
+const processMetrics = (campaign) => {
+ const insights = campaign.insights?.data?.[0] || {};
+ const spend = parseFloat(insights.spend || 0);
+ const impressions = parseInt(insights.impressions || 0);
+ const clicks = parseInt(insights.clicks || 0);
+ const reach = parseInt(insights.reach || 0);
+ const cpc = parseFloat(insights.cpc || 0);
+ const ctr = parseFloat(insights.ctr || 0);
+ const cpm = parseFloat(insights.cpm || 0);
+ const frequency = parseFloat(insights.frequency || 0);
+
+ // Purchase value and total purchases
+ const purchaseValue = (insights.action_values || [])
+ .filter(({ action_type }) => action_type === "purchase")
+ .reduce((sum, { value }) => sum + parseFloat(value || 0), 0);
+
+ const totalPurchases = (insights.actions || [])
+ .filter(({ action_type }) => action_type === "purchase")
+ .reduce((sum, { value }) => sum + parseInt(value || 0), 0);
+
+ // Aggregate unique actions
+ const actionMap = new Map();
+ (insights.actions || []).forEach(({ action_type, value }) => {
+ const currentValue = actionMap.get(action_type) || 0;
+ actionMap.set(action_type, currentValue + parseInt(value || 0));
+ });
+
+ const actions = Array.from(actionMap.entries()).map(([action_type, value]) => ({
+ action_type,
+ value,
+ }));
+
+ // Map of cost per action
+ const costPerActionMap = new Map();
+ (insights.cost_per_action_type || []).forEach(({ action_type, value }) => {
+ costPerActionMap.set(action_type, parseFloat(value || 0));
+ });
+
+ // Total post engagements
+ const totalPostEngagements = actionMap.get("post_engagement") || 0;
+
+ return {
+ spend,
+ impressions,
+ clicks,
+ reach,
+ frequency,
+ ctr,
+ cpm,
+ cpc,
+ actions,
+ costPerActionMap,
+ purchaseValue,
+ totalPurchases,
+ totalPostEngagements,
+ };
+};
+
+const processCampaignData = (campaign) => {
+ const metrics = processMetrics(campaign);
+ const budget = calculateBudget(campaign);
+ const { action_type, label } = getObjectiveAction(campaign.objective);
+
+ // Get cost per result from costPerActionMap
+ const costPerResult = metrics.costPerActionMap.get(action_type) || 0;
+
+ return {
+ id: campaign.id,
+ name: campaign.name,
+ status: campaign.status,
+ objective: label,
+ objectiveActionType: action_type,
+ budget: budget.value,
+ budgetType: budget.type,
+ metrics: {
+ ...metrics,
+ costPerResult,
+ },
+ };
+};
+
const MetaCampaigns = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [campaigns, setCampaigns] = useState([]);
- const [timeframe, setTimeframe] = useState("30");
+ const [timeframe, setTimeframe] = useState("7");
const [summaryMetrics, setSummaryMetrics] = useState(null);
+ const [sortConfig, setSortConfig] = useState({
+ key: "spend",
+ direction: "desc",
+ });
+
+ const handleSort = (key) => {
+ setSortConfig((prev) => ({
+ key,
+ direction: prev.key === key && prev.direction === "desc" ? "asc" : "desc",
+ }));
+ };
const computeDateRange = (timeframe) => {
// Create date in Eastern Time
@@ -168,35 +314,8 @@ const MetaCampaigns = () => {
accountInsights.json()
]);
- // Process campaigns to match the expected format
- const processedCampaigns = campaignsJson.map(campaign => {
- const insights = campaign.insights?.data?.[0] || {};
- const frequency = parseFloat(insights.frequency) || 0;
- return {
- id: campaign.id,
- name: campaign.name,
- objective: campaign.objective,
- status: campaign.status,
- budget: campaign.daily_budget / 100,
- budgetType: 'daily',
- metrics: {
- spend: parseFloat(insights.spend) || 0,
- impressions: parseInt(insights.impressions) || 0,
- reach: parseInt(insights.reach) || 0,
- clicks: parseInt(insights.clicks) || 0,
- ctr: parseFloat(insights.ctr) || 0,
- cpc: parseFloat(insights.cpc) || 0,
- cpm: parseFloat(insights.cpm) || 0,
- frequency,
- totalPurchases: parseInt(insights.purchase) || 0,
- purchaseValue: parseFloat(insights.purchase_value) || 0,
- totalPostEngagements: parseInt(insights.post_engagement) || 0,
- costPerResult: parseFloat(insights.cost_per_action_type?.find(c => c.action_type === 'purchase')?.value) || 0,
- actions: insights.actions || []
- }
- };
- });
-
+ // Process campaigns with the new processing logic
+ const processedCampaigns = campaignsJson.map(processCampaignData);
const activeCampaigns = processedCampaigns.filter(c => c.metrics.spend > 0);
setCampaigns(activeCampaigns);
@@ -241,6 +360,35 @@ const MetaCampaigns = () => {
fetchMetaAdsData();
}, [timeframe]);
+ // Sort campaigns
+ const sortedCampaigns = [...campaigns].sort((a, b) => {
+ const direction = sortConfig.direction === "desc" ? -1 : 1;
+
+ switch (sortConfig.key) {
+ case "date":
+ // Add date sorting using campaign ID (Meta IDs are chronological)
+ return direction * (parseInt(b.id) - parseInt(a.id));
+ case "spend":
+ return direction * ((a.metrics.spend || 0) - (b.metrics.spend || 0));
+ case "reach":
+ return direction * ((a.metrics.reach || 0) - (b.metrics.reach || 0));
+ case "impressions":
+ return direction * ((a.metrics.impressions || 0) - (b.metrics.impressions || 0));
+ case "cpm":
+ return direction * ((a.metrics.cpm || 0) - (b.metrics.cpm || 0));
+ case "ctr":
+ return direction * ((a.metrics.ctr || 0) - (b.metrics.ctr || 0));
+ case "results":
+ return direction * ((getActionValue(a, a.objectiveActionType) || 0) - (getActionValue(b, b.objectiveActionType) || 0));
+ case "value":
+ return direction * ((a.metrics.purchaseValue || 0) - (b.metrics.purchaseValue || 0));
+ case "engagements":
+ return direction * ((a.metrics.totalPostEngagements || 0) - (b.metrics.totalPostEngagements || 0));
+ default:
+ return 0;
+ }
+ });
+
if (loading) {
return (
@@ -264,7 +412,7 @@ const MetaCampaigns = () => {
return (
-
+
Meta Ads Performance
@@ -281,58 +429,70 @@ const MetaCampaigns = () => {
-
+
{[
{
label: "Active Campaigns",
value: summaryMetrics?.totalCampaigns,
+ options: { icon: Target, iconColor: "text-purple-500" },
},
{
label: "Total Spend",
value: summaryMetrics?.totalSpend,
- options: { isMonetary: true, decimalPlaces: 0 },
+ options: { isMonetary: true, decimalPlaces: 0, icon: DollarSign, iconColor: "text-green-500" },
+ },
+ {
+ label: "Total Reach",
+ value: summaryMetrics?.totalReach,
+ options: { icon: Users, iconColor: "text-blue-500" },
},
- { label: "Total Reach", value: summaryMetrics?.totalReach },
{
label: "Total Impressions",
value: summaryMetrics?.totalImpressions,
+ options: { icon: Eye, iconColor: "text-indigo-500" },
},
{
label: "Avg Frequency",
value: summaryMetrics?.avgFrequency,
- options: { decimalPlaces: 2 },
+ options: { decimalPlaces: 2, icon: Repeat, iconColor: "text-cyan-500" },
},
{
label: "Total Engagements",
value: summaryMetrics?.totalPostEngagements,
+ options: { icon: MessageCircle, iconColor: "text-pink-500" },
},
{
label: "Avg CPM",
value: summaryMetrics?.avgCpm,
- options: { isMonetary: true, decimalPlaces: 2 },
+ options: { isMonetary: true, decimalPlaces: 2, icon: DollarSign, iconColor: "text-emerald-500" },
},
{
label: "Avg CTR",
value: summaryMetrics?.avgCtr,
- options: { isPercentage: true, decimalPlaces: 2 },
+ options: { isPercentage: true, decimalPlaces: 2, icon: BarChart, iconColor: "text-orange-500" },
},
{
label: "Avg CPC",
value: summaryMetrics?.avgCpc,
- options: { isMonetary: true, decimalPlaces: 2 },
+ options: { isMonetary: true, decimalPlaces: 2, icon: MousePointer, iconColor: "text-rose-500" },
},
{
label: "Total Link Clicks",
value: summaryMetrics?.totalLinkClicks,
+ options: { icon: MousePointer, iconColor: "text-amber-500" },
+ },
+ {
+ label: "Total Purchases",
+ value: summaryMetrics?.totalPurchases,
+ options: { icon: ShoppingCart, iconColor: "text-teal-500" },
},
- { label: "Total Purchases", value: summaryMetrics?.totalPurchases },
{
label: "Purchase Value",
value: summaryMetrics?.totalPurchaseValue,
- options: { isMonetary: true, decimalPlaces: 0 },
+ options: { isMonetary: true, decimalPlaces: 0, icon: DollarSign, iconColor: "text-lime-500" },
},
].map((card) => (
-
+
{summaryCard(card.label, card.value, card.options)}
))}
@@ -345,37 +505,91 @@ const MetaCampaigns = () => {
- |
- Campaign
+ |
+
|
-
- Spend
+ |
+
|
-
- Reach
+ |
+
|
-
- Impressions
+ |
+
|
-
- CPM
+ |
+
|
-
- CTR
+ |
+
|
-
- Results
+ |
+
|
-
- Value
+ |
+
|
-
- Engagements
+ |
+
|
- {campaigns.map((campaign) => (
+ {sortedCampaigns.map((campaign) => (
{
}
/>
-
+
{
value={campaign.metrics.purchaseValue}
isMonetary
decimalPlaces={2}
- sublabel={`${formatCurrency(campaign.metrics.costPerResult)}/result`}
+ sublabel={campaign.metrics.costPerResult ? `${formatCurrency(campaign.metrics.costPerResult)}/result` : null}
/>
-
+
))}
diff --git a/examples DO NOT USE OR EDIT/EXAMPLE ONLY metaAdsService.js b/examples DO NOT USE OR EDIT/EXAMPLE ONLY metaAdsService.js
new file mode 100644
index 0000000..fde8418
--- /dev/null
+++ b/examples DO NOT USE OR EDIT/EXAMPLE ONLY metaAdsService.js
@@ -0,0 +1,176 @@
+import axios from "axios";
+
+class MetaAdsService {
+ constructor() {
+ this.client = axios.create({
+ baseURL: "/api/meta-ads",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ }
+
+ // metaAdsService.js
+
+ processMetrics(campaign) {
+ const insights = campaign.insights?.data?.[0] || {};
+ const spend = parseFloat(insights.spend || 0);
+ const impressions = parseInt(insights.impressions || 0);
+ const clicks = parseInt(insights.clicks || 0);
+ const reach = parseInt(insights.reach || 0);
+ const cpc = parseFloat(insights.cpc || 0);
+ const ctr = parseFloat(insights.ctr || 0);
+ const cpm = parseFloat(insights.cpm || 0);
+ const frequency = parseFloat(insights.frequency || 0);
+
+ // Purchase value and total purchases
+ const purchaseValue = (insights.action_values || [])
+ .filter(({ action_type }) => action_type === "purchase")
+ .reduce((sum, { value }) => sum + parseFloat(value || 0), 0);
+
+ const totalPurchases = (insights.actions || [])
+ .filter(({ action_type }) => action_type === "purchase")
+ .reduce((sum, { value }) => sum + parseInt(value || 0), 0);
+
+ // Aggregate unique actions
+ const actionMap = new Map();
+ (insights.actions || []).forEach(({ action_type, value }) => {
+ const currentValue = actionMap.get(action_type) || 0;
+ actionMap.set(action_type, currentValue + parseInt(value || 0));
+ });
+
+ const actions = Array.from(actionMap.entries()).map(
+ ([action_type, value]) => ({
+ action_type,
+ value,
+ })
+ );
+
+ // Map of cost per action
+ const costPerActionMap = new Map();
+ (insights.cost_per_action_type || []).forEach(({ action_type, value }) => {
+ costPerActionMap.set(action_type, parseFloat(value || 0));
+ });
+
+ // Total post engagements
+ const totalPostEngagements = actionMap.get("post_engagement") || 0;
+
+ return {
+ spend,
+ impressions,
+ clicks,
+ reach,
+ frequency,
+ ctr,
+ cpm,
+ cpc,
+ actions,
+ costPerActionMap, // Include cost per action map
+ purchaseValue,
+ totalPurchases,
+ totalPostEngagements, // Include total post engagements
+ };
+ }
+
+ getObjectiveAction(campaignObjective) {
+ const objectiveMap = {
+ OUTCOME_AWARENESS: { action_type: "impressions", label: "Impressions" },
+ OUTCOME_ENGAGEMENT: {
+ action_type: "post_engagement",
+ label: "Post Engagements",
+ },
+ OUTCOME_TRAFFIC: { action_type: "link_click", label: "Link Clicks" },
+ OUTCOME_LEADS: { action_type: "lead", label: "Leads" },
+ OUTCOME_SALES: { action_type: "purchase", label: "Purchases" },
+ MESSAGES: { action_type: "messages", label: "Messages" },
+ // Add other objectives as needed
+ };
+
+ return (
+ objectiveMap[campaignObjective] || {
+ action_type: "link_click",
+ label: "Link Clicks",
+ }
+ );
+ }
+ calculateBudget(campaign) {
+ if (campaign.daily_budget) {
+ return { value: campaign.daily_budget / 100, type: "day" };
+ }
+ if (campaign.lifetime_budget) {
+ return { value: campaign.lifetime_budget / 100, type: "lifetime" };
+ }
+
+ const adsets = campaign.adsets?.data || [];
+ const dailyTotal = adsets.reduce(
+ (sum, adset) => sum + (adset.daily_budget || 0),
+ 0
+ );
+ const lifetimeTotal = adsets.reduce(
+ (sum, adset) => sum + (adset.lifetime_budget || 0),
+ 0
+ );
+
+ if (dailyTotal > 0) return { value: dailyTotal / 100, type: "day" };
+ if (lifetimeTotal > 0)
+ return { value: lifetimeTotal / 100, type: "lifetime" };
+
+ return { value: 0, type: "day" };
+ }
+
+ processCampaignData(campaign) {
+ const metrics = this.processMetrics(campaign);
+ const budget = this.calculateBudget(campaign);
+ const { action_type, label } = this.getObjectiveAction(campaign.objective);
+
+ // Get cost per result from costPerActionMap
+ const costPerResult = metrics.costPerActionMap.get(action_type) || 0;
+
+ return {
+ id: campaign.id,
+ name: campaign.name,
+ status: campaign.status,
+ objective: label, // User-friendly objective label
+ objectiveActionType: action_type, // Action type for metrics
+ budget: budget.value,
+ budgetType: budget.type,
+ metrics: {
+ ...metrics,
+ costPerResult,
+ },
+ };
+ }
+ async getCampaigns({ since, until }) {
+ try {
+ const response = await this.client.get("/campaigns", {
+ params: { since, until },
+ });
+
+ return response.data.map((campaign) =>
+ this.processCampaignData(campaign)
+ );
+ } catch (error) {
+ console.error(
+ "Error fetching campaigns:",
+ error.response?.data || error.message
+ );
+ throw error;
+ }
+ }
+ async getAccountInsights({ since, until }) {
+ try {
+ const response = await this.client.get("/account_insights", {
+ params: { since, until },
+ });
+ return response.data; // This will be an object with reach and other fields
+ } catch (error) {
+ console.error(
+ "Error fetching account insights:",
+ error.response?.data || error.message
+ );
+ throw error;
+ }
+ }
+}
+
+export const metaAdsService = new MetaAdsService();
|