import { useState, useMemo, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useAuth } from '../../contexts/AuthContext';
import { AppLayout } from '../../components/layout/AppLayout';
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
import {
facebookAdsApi,
type FacebookAdsCampaign,
type FacebookAdsMetric,
type FacebookAdsAnalytics,
} from '../../api/facebookAds';
import {
BarChart3,
TrendingUp,
TrendingDown,
AlertTriangle,
AlertCircle,
CheckCircle2,
Eye,
MousePointerClick,
Target,
DollarSign,
RefreshCw,
Search,
Filter,
X,
Calendar,
ArrowUpRight,
ArrowDownRight,
Activity,
Monitor,
Zap,
} from 'lucide-react';
// โโโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function formatCurrency(value: number | null): string {
if (value == null) return 'โ';
if (Math.abs(value) >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`;
if (Math.abs(value) >= 1_000) return `$${(value / 1_000).toFixed(1)}K`;
return `$${value.toFixed(2)}`;
}
function formatNumber(value: number | null): string {
if (value == null) return 'โ';
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
return value.toLocaleString();
}
function formatPercent(value: number | null): string {
if (value == null) return 'โ';
return `${(value * 100).toFixed(2)}%`;
}
function statusColor(status: string): string {
const map: Record<string, string> = {
ACTIVE: 'bg-emerald-100 text-emerald-700',
PAUSED: 'bg-amber-100 text-amber-700',
DELETED: 'bg-gray-100 text-gray-500',
};
return map[status] || 'bg-gray-100 text-gray-600';
}
function statusLabel(status: string): string {
return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
}
function timeAgo(dateStr: string | null | undefined): string {
if (!dateStr) return 'โ';
const now = Date.now();
const then = new Date(dateStr).getTime();
const diff = Math.abs(now - then);
const days = Math.floor(diff / 86400000);
if (days === 0) return 'today';
if (days === 1) return 'yesterday';
if (days < 30) return `${days}d ago`;
return new Date(dateStr).toLocaleDateString();
}
function channelLabel(channel: string): string {
const map: Record<string, string> = {
FACEBOOK: 'Facebook',
INSTAGRAM: 'Instagram',
AUDIENCE_NETWORK: 'Audience Network',
MESSENGER: 'Messenger',
WHATSAPP: 'WhatsApp',
};
return map[channel] || channel;
}
// โโโ Tab Types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
type TabKey = 'overview' | 'campaigns' | 'metrics' | 'trends';
// โโโ Overview Tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function OverviewTab({ analytics }: { analytics: FacebookAdsAnalytics }) {
const stats = [
{ label: 'Active Campaigns', value: analytics.active_campaigns, icon: Monitor, color: 'text-emerald-600 bg-emerald-50' },
{ label: 'Spend (30d)', value: formatCurrency(analytics.total_spend_30d), icon: DollarSign, color: 'text-blue-600 bg-blue-50' },
{ label: 'Impressions (30d)', value: formatNumber(analytics.total_impressions_30d), icon: Eye, color: 'text-violet-600 bg-violet-50' },
{ label: 'Clicks (30d)', value: formatNumber(analytics.total_clicks_30d), icon: MousePointerClick, color: 'text-cyan-600 bg-cyan-50' },
{ label: 'Conversions', value: analytics.total_conversions_30d.toFixed(0), icon: Target, color: 'text-emerald-600 bg-emerald-50' },
{ label: 'Avg CTR', value: formatPercent(analytics.avg_ctr), icon: TrendingUp, color: 'text-green-600 bg-green-50' },
{ label: 'Avg CPC', value: formatCurrency(analytics.avg_cpc), icon: DollarSign, color: 'text-amber-600 bg-amber-50' },
{ label: 'Avg CPA', value: formatCurrency(analytics.avg_cpa), icon: TrendingDown, color: 'text-red-600 bg-red-50' },
{ label: 'ROAS', value: analytics.total_roas ? `${analytics.total_roas.toFixed(2)}x` : 'โ', icon: Zap, color: 'text-indigo-600 bg-indigo-50' },
];
return (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
{stats.map((stat) => (
<div key={stat.label} className="rounded-xl border border-surface-200 bg-white p-4">
<div className={`inline-flex rounded-lg p-2 mb-2 ${stat.color}`}>
<stat.icon className="h-4 w-4" />
</div>
<p className="text-xs text-surface-500">{stat.label}</p>
<p className="text-lg font-bold text-surface-900 mt-0.5">{stat.value}</p>
</div>
))}
</div>
{/* Spend Timeline */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Spend</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Last 30 days</span>
<span className="font-semibold text-blue-600">{formatCurrency(analytics.total_spend_30d)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Last 90 days</span>
<span className="font-semibold text-blue-600">{formatCurrency(analytics.total_spend_90d)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Year to date</span>
<span className="font-semibold text-blue-600">{formatCurrency(analytics.total_spend_ytd)}</span>
</div>
</div>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Performance</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Impressions (30d)</span>
<span className="font-semibold text-violet-600">{formatNumber(analytics.total_impressions_30d)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Clicks (30d)</span>
<span className="font-semibold text-cyan-600">{formatNumber(analytics.total_clicks_30d)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Conversions (30d)</span>
<span className="font-semibold text-emerald-600">{analytics.total_conversions_30d.toFixed(0)}</span>
</div>
</div>
</div>
</div>
{/* Campaigns by Status */}
{analytics.campaigns_by_status && analytics.campaigns_by_status.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Campaigns by Status</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{analytics.campaigns_by_status.map((item) => (
<div key={item.status} className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500 capitalize">{item.status}</p>
<p className="text-lg font-bold text-surface-900">{item.count}</p>
<p className="text-xs text-surface-400">Budget: {formatCurrency(item.total_budget)}</p>
</div>
))}
</div>
</div>
)}
{/* Campaigns by Channel */}
{analytics.campaigns_by_channel && analytics.campaigns_by_channel.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Campaigns by Channel</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{analytics.campaigns_by_channel.map((item) => (
<div key={item.channel_type} className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">{channelLabel(item.channel_type)}</p>
<p className="text-lg font-bold text-surface-900">{item.count}</p>
<p className="text-xs text-surface-400">Spend: {formatCurrency(item.total_spend)}</p>
</div>
))}
</div>
</div>
)}
{/* Top Campaigns */}
{analytics.top_campaigns && analytics.top_campaigns.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Top Campaigns</h3>
<div className="space-y-2">
{analytics.top_campaigns.map((campaign) => (
<div key={campaign.external_id} className="flex justify-between items-center py-2 border-b border-surface-100 last:border-0">
<div>
<p className="text-sm font-medium text-surface-900">{campaign.name}</p>
<p className="text-xs text-surface-400">
{campaign.clicks} clicks ยท {campaign.conversions?.toFixed(0) || 0} conversions
</p>
</div>
<div className="flex items-center gap-3">
<span className={`inline-block rounded-full px-2 py-0.5 text-xs font-semibold ${statusColor(campaign.status)}`}>
{statusLabel(campaign.status)}
</span>
<span className="font-semibold text-blue-600">{formatCurrency(campaign.spend)}</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
// โโโ Campaigns Tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function CampaignsTab({ companyId }: { companyId: string }) {
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [selectedCampaign, setSelectedCampaign] = useState<FacebookAdsCampaign | null>(null);
const { data, isLoading } = useQuery({
queryKey: ['facebook-ads-campaigns', companyId, search, statusFilter],
queryFn: () =>
facebookAdsApi.getCampaigns(companyId, {
search: search || undefined,
status: statusFilter || undefined,
}),
staleTime: 60_000,
});
const campaigns = data?.data?.items || [];
return (
<div>
{/* Filters */}
<div className="flex flex-wrap items-center gap-3 mb-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-surface-400" />
<input
type="text"
placeholder="Search campaigns..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-lg border border-surface-300 bg-white pl-10 pr-3 py-2 text-sm outline-none focus:border-brand-500"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="rounded-lg border border-surface-300 bg-white px-3 py-2 text-sm outline-none focus:border-brand-500"
>
<option value="">All Statuses</option>
<option value="ACTIVE">Active</option>
<option value="PAUSED">Paused</option>
<option value="DELETED">Deleted</option>
</select>
</div>
{/* Table */}
{isLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : (
<div className="overflow-x-auto rounded-xl border border-surface-200">
<table className="w-full text-sm">
<thead>
<tr className="bg-surface-50">
<th className="text-left px-4 py-3 font-medium text-surface-500">Campaign</th>
<th className="text-left px-4 py-3 font-medium text-surface-500">Status</th>
<th className="text-left px-4 py-3 font-medium text-surface-500">Channel</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Budget</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Spend</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Clicks</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Conversions</th>
</tr>
</thead>
<tbody>
{campaigns.length === 0 ? (
<tr><td colSpan={7} className="text-center py-8 text-surface-400">No campaigns found</td></tr>
) : (
campaigns.map((campaign) => (
<tr
key={campaign.id}
onClick={() => setSelectedCampaign(campaign)}
className="border-t border-surface-100 hover:bg-surface-50 cursor-pointer"
>
<td className="px-4 py-3 font-medium text-surface-900">{campaign.name}</td>
<td className="px-4 py-3">
<span className={`inline-block rounded-full px-2 py-0.5 text-xs font-semibold ${statusColor(campaign.status)}`}>
{statusLabel(campaign.status)}
</span>
</td>
<td className="px-4 py-3 text-surface-500">{channelLabel(campaign.channel_type)}</td>
<td className="px-4 py-3 text-right font-medium">{formatCurrency(campaign.budget)}</td>
<td className="px-4 py-3 text-right font-medium text-blue-600">{formatCurrency(campaign.total_spend)}</td>
<td className="px-4 py-3 text-right text-surface-600">{formatNumber(campaign.total_clicks)}</td>
<td className="px-4 py-3 text-right text-emerald-600 font-medium">{campaign.total_conversions.toFixed(0)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
{/* Detail Panel */}
{selectedCampaign && (
<div className="fixed inset-0 z-50 flex justify-end" role="dialog" aria-modal="true">
<div className="absolute inset-0 bg-black/30" onClick={() => setSelectedCampaign(null)} />
<div className="relative z-10 flex h-full w-full max-w-lg flex-col overflow-y-auto bg-white shadow-xl border-l border-surface-200">
<div className="flex items-center justify-between border-b border-surface-200 px-6 py-4">
<div>
<h2 className="text-lg font-semibold text-surface-900">{selectedCampaign.name}</h2>
<p className="text-sm text-surface-500">{channelLabel(selectedCampaign.channel_type)} ยท {selectedCampaign.budget_type} budget</p>
</div>
<button
onClick={() => setSelectedCampaign(null)}
className="rounded-lg p-2 text-surface-400 hover:bg-surface-100 min-h-[44px] min-w-[44px] flex items-center justify-center"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="grid grid-cols-2 gap-3 border-b border-surface-200 px-6 py-4">
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Status</p>
<span className={`inline-block mt-1 rounded-full px-2 py-0.5 text-xs font-semibold ${statusColor(selectedCampaign.status)}`}>
{statusLabel(selectedCampaign.status)}
</span>
</div>
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Budget</p>
<p className="text-lg font-bold text-surface-900">{formatCurrency(selectedCampaign.budget)}</p>
</div>
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Total Spend</p>
<p className="text-lg font-bold text-blue-600">{formatCurrency(selectedCampaign.total_spend)}</p>
</div>
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Conversions</p>
<p className="text-lg font-bold text-emerald-600">{selectedCampaign.total_conversions.toFixed(0)}</p>
</div>
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Clicks</p>
<p className="text-sm font-medium">{formatNumber(selectedCampaign.total_clicks)}</p>
</div>
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Channel</p>
<p className="text-sm font-medium">{channelLabel(selectedCampaign.channel_type)}</p>
</div>
</div>
{selectedCampaign.start_date && (
<div className="border-b border-surface-200 px-6 py-4">
<p className="text-xs font-medium text-surface-500 uppercase tracking-wide mb-2">Dates</p>
<div className="space-y-1 text-sm text-surface-600">
<div className="flex justify-between"><span>Start</span><span>{timeAgo(selectedCampaign.start_date)}</span></div>
{selectedCampaign.end_date && <div className="flex justify-between"><span>End</span><span>{timeAgo(selectedCampaign.end_date)}</span></div>}
</div>
</div>
)}
<div className="px-6 py-4">
<p className="text-xs font-medium text-surface-500 uppercase tracking-wide mb-2">Updated</p>
<p className="text-sm text-surface-600">{timeAgo(selectedCampaign.updated_at)}</p>
</div>
</div>
</div>
)}
</div>
);
}
// โโโ Metrics Tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function MetricsTab({ companyId }: { companyId: string }) {
const [campaignFilter, setCampaignFilter] = useState('');
const { data, isLoading } = useQuery({
queryKey: ['facebook-ads-metrics', companyId, campaignFilter],
queryFn: () =>
facebookAdsApi.getMetrics(companyId, {
campaign_id: campaignFilter || undefined,
per_page: 100,
}),
staleTime: 60_000,
});
const metrics = data?.data?.items || [];
return (
<div>
<div className="flex flex-wrap items-center gap-3 mb-4">
<select
value={campaignFilter}
onChange={(e) => setCampaignFilter(e.target.value)}
className="rounded-lg border border-surface-300 bg-white px-3 py-2 text-sm outline-none focus:border-brand-500"
>
<option value="">All Campaigns</option>
</select>
</div>
{isLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : (
<div className="overflow-x-auto rounded-xl border border-surface-200">
<table className="w-full text-sm">
<thead>
<tr className="bg-surface-50">
<th className="text-left px-4 py-3 font-medium text-surface-500">Date</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Spend</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Impressions</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Clicks</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">CTR</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">CPC</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Conversions</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">ROAS</th>
</tr>
</thead>
<tbody>
{metrics.length === 0 ? (
<tr><td colSpan={8} className="text-center py-8 text-surface-400">No metrics found</td></tr>
) : (
metrics.map((metric) => (
<tr key={metric.id} className="border-t border-surface-100 hover:bg-surface-50">
<td className="px-4 py-3 text-surface-600">{timeAgo(metric.metric_date)}</td>
<td className="px-4 py-3 text-right font-medium text-blue-600">{formatCurrency(metric.spend)}</td>
<td className="px-4 py-3 text-right text-surface-600">{formatNumber(metric.impressions)}</td>
<td className="px-4 py-3 text-right text-surface-600">{formatNumber(metric.clicks)}</td>
<td className="px-4 py-3 text-right text-surface-500">{formatPercent(metric.ctr)}</td>
<td className="px-4 py-3 text-right text-surface-500">{formatCurrency(metric.cpc)}</td>
<td className="px-4 py-3 text-right text-emerald-600 font-medium">{metric.conversions.toFixed(0)}</td>
<td className="px-4 py-3 text-right text-surface-500">{metric.roas ? `${metric.roas.toFixed(2)}x` : 'โ'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</div>
);
}
// โโโ Trends Tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function TrendsTab({ companyId }: { companyId: string }) {
const { data, isLoading } = useQuery({
queryKey: ['facebook-ads-analytics', companyId],
queryFn: () => facebookAdsApi.getAnalytics(companyId),
staleTime: 60_000,
});
const analytics = data?.data;
const trend = analytics?.daily_spend_trend || [];
const maxSpend = useMemo(() => Math.max(...trend.map((t) => t.spend || 0), 1), [trend]);
return (
<div className="space-y-6">
{isLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : (
<>
{/* Spend Trend Bars */}
{trend.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Daily Spend Trend</h3>
<div className="space-y-2">
{trend.slice(-30).map((day) => {
const pct = (day.spend / maxSpend) * 100;
return (
<div key={day.date} className="flex items-center gap-3">
<span className="text-xs text-surface-400 w-16 flex-shrink-0">{new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</span>
<div className="flex-1 bg-surface-100 rounded-full h-6 overflow-hidden">
<div
className="h-full bg-[#1877F2] rounded-full flex items-center justify-end pr-2 transition-all"
style={{ width: `${Math.max(pct, 2)}%` }}
>
{pct > 15 && (
<span className="text-xs text-white font-medium">{formatCurrency(day.spend)}</span>
)}
</div>
</div>
{pct <= 15 && <span className="text-xs text-surface-500">{formatCurrency(day.spend)}</span>}
</div>
);
})}
</div>
</div>
)}
{/* Conversions Trend */}
{trend.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Daily Conversions Trend</h3>
<div className="space-y-2">
{trend.slice(-30).map((day) => {
const maxConv = Math.max(...trend.map((t) => t.conversions || 0), 1);
const pct = (day.conversions / maxConv) * 100;
return (
<div key={day.date} className="flex items-center gap-3">
<span className="text-xs text-surface-400 w-16 flex-shrink-0">{new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</span>
<div className="flex-1 bg-surface-100 rounded-full h-6 overflow-hidden">
<div
className="h-full bg-emerald-500 rounded-full flex items-center justify-end pr-2 transition-all"
style={{ width: `${Math.max(pct, 2)}%` }}
>
{pct > 15 && (
<span className="text-xs text-white font-medium">{day.conversions.toFixed(0)}</span>
)}
</div>
</div>
{pct <= 15 && <span className="text-xs text-surface-500">{day.conversions.toFixed(0)}</span>}
</div>
);
})}
</div>
</div>
)}
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="rounded-xl border border-surface-200 bg-white p-4">
<p className="text-xs text-surface-500">Total Spend</p>
<p className="text-lg font-bold text-blue-600">{formatCurrency(analytics?.total_spend_30d)}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<p className="text-xs text-surface-500">Avg Daily Spend</p>
<p className="text-lg font-bold text-blue-600">
{trend.length > 0 ? formatCurrency(analytics?.total_spend_30d / trend.length) : 'โ'}
</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<p className="text-xs text-surface-500">Total Conversions</p>
<p className="text-lg font-bold text-emerald-600">{analytics?.total_conversions_30d?.toFixed(0) || '0'}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<p className="text-xs text-surface-500">ROAS</p>
<p className="text-lg font-bold text-indigo-600">{analytics?.total_roas ? `${analytics.total_roas.toFixed(2)}x` : 'โ'}</p>
</div>
</div>
</>
)}
</div>
);
}
// โโโ Main Page โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export function FacebookAdsPage() {
const { user } = useAuth();
const [activeTab, setActiveTab] = useState<TabKey>('overview');
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
const queryClient = useQueryClient();
const companyId = user?.tenantId ? String(user.tenantId) : '';
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 4000);
return () => clearTimeout(timer);
}
}, [toast]);
const { data: analyticsData, isLoading: analyticsLoading } = useQuery({
queryKey: ['facebook-ads-analytics', companyId],
queryFn: () => facebookAdsApi.getAnalytics(companyId),
staleTime: 120_000,
});
const syncMutation = useMutation({
mutationFn: () => facebookAdsApi.sync(companyId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['facebook-ads'] });
setToast({ message: 'Facebook Ads synced successfully', type: 'success' });
},
onError: (err: unknown) => {
const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error || 'Facebook Ads sync failed';
setToast({ message, type: 'error' });
},
});
const tabs: { key: TabKey; label: string; icon: React.FC }[] = [
{ key: 'overview', label: 'Overview', icon: BarChart3 },
{ key: 'campaigns', label: 'Campaigns', icon: Monitor },
{ key: 'metrics', label: 'Metrics', icon: Activity },
{ key: 'trends', label: 'Trends', icon: TrendingUp },
];
return (
<AppLayout>
{/* Toast */}
{toast && (
<div className="fixed top-4 right-4 z-[70] flex items-center gap-2 rounded-lg bg-surface-900 px-4 py-3 text-sm text-white shadow-lg">
{toast.type === 'success' ? (
<CheckCircle2 className="h-4 w-4 text-green-400" />
) : (
<AlertCircle className="h-4 w-4 text-red-400" />
)}
{toast.message}
</div>
)}
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-surface-900">Facebook Ads</h1>
<p className="text-sm text-surface-500 mt-1">Meta Ads Manager โ campaigns, metrics, and ROAS</p>
</div>
<button
onClick={() => syncMutation.mutate()}
disabled={syncMutation.isPending}
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[#1877F2] text-white text-sm font-medium hover:bg-[#166fe5] disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw className={`h-4 w-4 ${syncMutation.isPending ? 'animate-spin' : ''}`} />
{syncMutation.isPending ? 'Syncing...' : 'Sync Now'}
</button>
</div>
{/* Tabs */}
<div className="flex gap-1 border-b border-surface-200">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.key
? 'border-[#1877F2] text-[#1877F2]'
: 'border-transparent text-surface-500 hover:text-surface-700'
}`}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
{analyticsLoading && activeTab === 'overview' ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : activeTab === 'overview' && analyticsData?.data ? (
<OverviewTab analytics={analyticsData.data} />
) : activeTab === 'campaigns' ? (
<CampaignsTab companyId={companyId} />
) : activeTab === 'metrics' ? (
<MetricsTab companyId={companyId} />
) : activeTab === 'trends' ? (
<TrendsTab companyId={companyId} />
) : null}
</div>
</AppLayout>
);
}