Files
avbeat-com/src/app/dashboard/adops/page.tsx

710 lines
34 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
type Tab = 'overview' | 'campaigns' | 'distributions' | 'newsletters' | 'documents' | 'invoices' | 'reports';
interface Campaign {
id: string;
campaign_name: string;
client_name: string;
publication_id: string;
placement_zone: string;
start_date: string;
end_date: string;
status: string;
agreed_rate: number;
impressions: number;
clicks: number;
ctr: number;
mooninvoice_invoice_id?: string;
blackmagic_excluded: boolean;
}
interface EmailCampaign {
id: string;
campaign_name: string;
client_name: string;
publication_id: string;
subject: string;
send_date: string;
status: string;
sent_count: number;
agreed_rate?: number;
is_newsletter: boolean;
email_lists?: { list_name: string; subscriber_count: number };
}
interface Stats {
activeCampaigns: number;
pendingApprovals: number;
endingSoon: number;
emailsSentThisMonth: number;
revenueThisMonth: number;
recentActivity: Campaign[];
}
const PUBLICATIONS = [
{ id: 'avbeat', name: 'AV Beat', short: 'AVB' },
{ id: 'av-beat', name: 'AV Beat', short: 'AV' },
];
const STATUS_COLORS: Record<string, string> = {
pending_info: 'bg-yellow-900/50 text-yellow-300 border-yellow-700',
pending_approval: 'bg-blue-900/50 text-blue-300 border-blue-700',
active: 'bg-green-900/50 text-green-300 border-green-700',
paused: 'bg-gray-700/50 text-gray-300 border-gray-600',
ended: 'bg-gray-800/50 text-gray-400 border-gray-700',
cancelled: 'bg-red-900/50 text-red-300 border-red-700',
draft: 'bg-gray-700/50 text-gray-300 border-gray-600',
scheduled: 'bg-purple-900/50 text-purple-300 border-purple-700',
sent: 'bg-green-900/50 text-green-300 border-green-700',
held: 'bg-red-900/50 text-red-300 border-red-700',
};
export default function AdOpsDashboard() {
const { user, loading } = useAuth();
const router = useRouter();
const [activeTab, setActiveTab] = useState<Tab>('overview');
const [isAdmin, setIsAdmin] = useState(false);
const [authChecked, setAuthChecked] = useState(false);
const [stats, setStats] = useState<Stats | null>(null);
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
const [emailCampaigns, setEmailCampaigns] = useState<EmailCampaign[]>([]);
const [filterPub, setFilterPub] = useState('');
const [filterStatus, setFilterStatus] = useState('');
const [filterClient, setFilterClient] = useState('');
const [showNewCampaign, setShowNewCampaign] = useState(false);
const [newCampaign, setNewCampaign] = useState({
campaign_name: '', client_name: '', publication_id: '', placement_zone: '',
destination_url: '', start_date: '', end_date: '', agreed_rate: '', notes: '',
});
const [saving, setSaving] = useState(false);
const [inboxLogs, setInboxLogs] = useState<any[]>([]);
useEffect(() => {
if (loading) return;
if (!user) { router.replace('/'); return; }
fetch('/api/adops/stats')
.then(r => {
if (r.status === 404) { router.replace('/'); return null; }
return r.json();
})
.then(data => {
if (!data) return;
setIsAdmin(true);
setStats(data);
setAuthChecked(true);
})
.catch(() => router.replace('/'));
}, [user, loading, router]);
useEffect(() => {
if (!isAdmin) return;
loadCampaigns();
loadEmailCampaigns();
}, [isAdmin, filterPub, filterStatus, filterClient]);
async function loadCampaigns() {
const params = new URLSearchParams();
if (filterPub) params.set('publication', filterPub);
if (filterStatus) params.set('status', filterStatus);
if (filterClient) params.set('client', filterClient);
const res = await fetch(`/api/adops/campaigns?${params}`);
if (res.ok) {
const data = await res.json();
setCampaigns(data.campaigns || []);
}
}
async function loadEmailCampaigns() {
const res = await fetch('/api/adops/email-campaigns');
if (res.ok) {
const data = await res.json();
setEmailCampaigns(data.campaigns || []);
}
}
async function loadInboxLogs() {
const res = await fetch('/api/adops/poll');
if (res.ok) {
const data = await res.json();
setInboxLogs(data.logs || []);
}
}
async function handleCreateCampaign() {
setSaving(true);
const res = await fetch('/api/adops/campaigns', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...newCampaign,
agreed_rate: parseFloat(newCampaign.agreed_rate) || null,
campaign_type: 'banner',
}),
});
if (res.ok) {
setShowNewCampaign(false);
setNewCampaign({ campaign_name: '', client_name: '', publication_id: '', placement_zone: '', destination_url: '', start_date: '', end_date: '', agreed_rate: '', notes: '' });
loadCampaigns();
}
setSaving(false);
}
async function handleStatusChange(campaignId: string, newStatus: string) {
await fetch('/api/adops/campaigns', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: campaignId, status: newStatus }),
});
loadCampaigns();
}
async function triggerPoll() {
await fetch('/api/adops/poll', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
loadInboxLogs();
}
if (loading || !authChecked) {
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
);
}
if (!isAdmin) return null;
const tabs: { id: Tab; label: string }[] = [
{ id: 'overview', label: 'Overview' },
{ id: 'campaigns', label: 'Banner Campaigns' },
{ id: 'distributions', label: 'Email Distributions' },
{ id: 'newsletters', label: 'Newsletters' },
{ id: 'documents', label: 'Documents' },
{ id: 'invoices', label: 'Invoices' },
{ id: 'reports', label: 'Reports' },
];
const newsletters = emailCampaigns.filter(c => c.is_newsletter);
const distributions = emailCampaigns.filter(c => !c.is_newsletter);
return (
<div className="min-h-screen bg-gray-950 text-gray-100">
{/* Header */}
<div className="border-b border-gray-800 bg-gray-900">
<div className="max-w-screen-xl mx-auto px-6 py-4">
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-orange-600 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" />
</svg>
</div>
<div>
<h1 className="text-lg font-bold text-white">Ad Ops & Email Marketing</h1>
<p className="text-xs text-gray-400">Ryan Salazar Admin Only</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={triggerPoll}
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-200 text-xs rounded-md transition-colors"
>
Poll Inbox
</button>
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-orange-900/50 text-orange-300 border border-orange-700">
<span className="w-1.5 h-1.5 rounded-full bg-orange-400" />
adops@avbeat.com
</span>
</div>
</div>
{/* Tabs */}
<div className="flex gap-1 mt-4 border-b border-gray-800 -mb-px overflow-x-auto">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => { setActiveTab(tab.id); if (tab.id === 'overview') loadInboxLogs(); }}
className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === tab.id
? 'border-orange-500 text-orange-400' :'border-transparent text-gray-400 hover:text-gray-200'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
</div>
{/* Tab Content */}
<div className="max-w-screen-xl mx-auto px-6 py-6">
{/* OVERVIEW TAB */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{[
{ label: 'Active Campaigns', value: stats?.activeCampaigns || 0, color: 'text-green-400' },
{ label: 'Pending Approvals', value: stats?.pendingApprovals || 0, color: 'text-yellow-400' },
{ label: 'Ending in 7 Days', value: stats?.endingSoon || 0, color: 'text-orange-400' },
{ label: 'Emails This Month', value: stats?.emailsSentThisMonth || 0, color: 'text-blue-400' },
{ label: 'Revenue This Month', value: `$${(stats?.revenueThisMonth || 0).toLocaleString()}`, color: 'text-emerald-400' },
].map(card => (
<div key={card.label} className="bg-gray-900 border border-gray-800 rounded-lg p-4">
<p className="text-xs text-gray-400 mb-1">{card.label}</p>
<p className={`text-2xl font-bold ${card.color}`}>{card.value}</p>
</div>
))}
</div>
{/* Recent Activity */}
<div className="bg-gray-900 border border-gray-800 rounded-lg p-4">
<h3 className="text-sm font-semibold text-gray-200 mb-3">Recent Campaign Activity</h3>
{stats?.recentActivity?.length ? (
<div className="space-y-2">
{stats.recentActivity.map(c => (
<div key={c.id} className="flex items-center justify-between py-2 border-b border-gray-800 last:border-0">
<div>
<p className="text-sm text-gray-200">{c.campaign_name || c.client_name}</p>
<p className="text-xs text-gray-500">{c.publication_id} {c.placement_zone}</p>
</div>
<span className={`text-xs px-2 py-0.5 rounded-full border ${STATUS_COLORS[c.status] || 'bg-gray-800 text-gray-400 border-gray-700'}`}>
{c.status.replace('_', ' ')}
</span>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500">No recent activity</p>
)}
</div>
{/* Inbox Log */}
<div className="bg-gray-900 border border-gray-800 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-200">Inbox Log</h3>
<button onClick={loadInboxLogs} className="text-xs text-gray-400 hover:text-gray-200">Refresh</button>
</div>
{inboxLogs.length ? (
<div className="space-y-2">
{inboxLogs.slice(0, 10).map(log => (
<div key={log.id} className="flex items-center justify-between py-2 border-b border-gray-800 last:border-0">
<div>
<p className="text-sm text-gray-200">{log.subject || '(no subject)'}</p>
<p className="text-xs text-gray-500">{log.from_email} {new Date(log.received_at).toLocaleString()}</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400">{log.request_type}</span>
<span className={`text-xs px-2 py-0.5 rounded-full border ${
log.processing_status === 'completed' ? 'bg-green-900/50 text-green-300 border-green-700' :
log.processing_status === 'error'? 'bg-red-900/50 text-red-300 border-red-700' : 'bg-gray-700/50 text-gray-300 border-gray-600'
}`}>
{log.processing_status}
</span>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500">No inbox logs yet. Click "Poll Inbox" to check for new emails.</p>
)}
</div>
</div>
)}
{/* BANNER CAMPAIGNS TAB */}
{activeTab === 'campaigns' && (
<div className="space-y-4">
{/* Filters + New Campaign */}
<div className="flex flex-wrap items-center gap-3">
<select
value={filterPub}
onChange={e => setFilterPub(e.target.value)}
className="bg-gray-800 border border-gray-700 text-gray-200 text-sm rounded-md px-3 py-2"
>
<option value="">All Publications</option>
{PUBLICATIONS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
<select
value={filterStatus}
onChange={e => setFilterStatus(e.target.value)}
className="bg-gray-800 border border-gray-700 text-gray-200 text-sm rounded-md px-3 py-2"
>
<option value="">All Statuses</option>
{['pending_info','pending_approval','active','paused','ended','cancelled'].map(s => (
<option key={s} value={s}>{s.replace('_', ' ')}</option>
))}
</select>
<input
type="text"
placeholder="Search client..."
value={filterClient}
onChange={e => setFilterClient(e.target.value)}
className="bg-gray-800 border border-gray-700 text-gray-200 text-sm rounded-md px-3 py-2 w-48"
/>
<button
onClick={() => setShowNewCampaign(true)}
className="ml-auto px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white text-sm font-medium rounded-md transition-colors"
>
+ New Campaign
</button>
</div>
{/* New Campaign Form */}
{showNewCampaign && (
<div className="bg-gray-900 border border-gray-700 rounded-lg p-5">
<h3 className="text-sm font-semibold text-gray-200 mb-4">New Banner Campaign</h3>
<div className="grid grid-cols-2 gap-4">
{[
{ key: 'campaign_name', label: 'Campaign Name', type: 'text' },
{ key: 'client_name', label: 'Client Name', type: 'text' },
{ key: 'destination_url', label: 'Click-Through URL', type: 'url' },
{ key: 'agreed_rate', label: 'Agreed Rate ($)', type: 'number' },
{ key: 'start_date', label: 'Start Date', type: 'date' },
{ key: 'end_date', label: 'End Date', type: 'date' },
].map(field => (
<div key={field.key}>
<label className="block text-xs text-gray-400 mb-1">{field.label}</label>
<input
type={field.type}
value={(newCampaign as any)[field.key]}
onChange={e => setNewCampaign(prev => ({ ...prev, [field.key]: e.target.value }))}
className="w-full bg-gray-800 border border-gray-700 text-gray-200 text-sm rounded-md px-3 py-2"
/>
</div>
))}
<div>
<label className="block text-xs text-gray-400 mb-1">Publication</label>
<select
value={newCampaign.publication_id}
onChange={e => setNewCampaign(prev => ({ ...prev, publication_id: e.target.value }))}
className="w-full bg-gray-800 border border-gray-700 text-gray-200 text-sm rounded-md px-3 py-2"
>
<option value="">Select publication</option>
{PUBLICATIONS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Placement Zone</label>
<input
type="text"
placeholder="e.g. leaderboard_top"
value={newCampaign.placement_zone}
onChange={e => setNewCampaign(prev => ({ ...prev, placement_zone: e.target.value }))}
className="w-full bg-gray-800 border border-gray-700 text-gray-200 text-sm rounded-md px-3 py-2"
/>
</div>
</div>
<div className="flex gap-3 mt-4">
<button
onClick={handleCreateCampaign}
disabled={saving}
className="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white text-sm font-medium rounded-md transition-colors disabled:opacity-50"
>
{saving ? 'Creating...' : 'Create Campaign'}
</button>
<button
onClick={() => setShowNewCampaign(false)}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-200 text-sm rounded-md transition-colors"
>
Cancel
</button>
</div>
</div>
)}
{/* Campaigns Table */}
<div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-800 bg-gray-800/50">
{['Client', 'Publication', 'Zone', 'Dates', 'Status', 'Impressions', 'Clicks', 'CTR', 'Revenue', 'Invoice', 'Actions'].map(h => (
<th key={h} className="text-left px-4 py-3 text-xs font-medium text-gray-400 uppercase tracking-wide">{h}</th>
))}
</tr>
</thead>
<tbody>
{campaigns.length === 0 ? (
<tr><td colSpan={11} className="px-4 py-8 text-center text-gray-500">No campaigns found</td></tr>
) : campaigns.map(c => (
<tr key={c.id} className="border-b border-gray-800 hover:bg-gray-800/30">
<td className="px-4 py-3">
<p className="font-medium text-gray-200">{c.client_name}</p>
{c.blackmagic_excluded && <span className="text-xs text-yellow-400"> BMD excluded from email</span>}
</td>
<td className="px-4 py-3 text-gray-300">{PUBLICATIONS.find(p => p.id === c.publication_id)?.short || c.publication_id}</td>
<td className="px-4 py-3 text-gray-400 text-xs">{c.placement_zone}</td>
<td className="px-4 py-3 text-gray-400 text-xs">
<div>{c.start_date}</div>
<div>{c.end_date}</div>
</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full border ${STATUS_COLORS[c.status] || 'bg-gray-800 text-gray-400 border-gray-700'}`}>
{c.status.replace(/_/g, ' ')}
</span>
</td>
<td className="px-4 py-3 text-gray-300">{c.impressions?.toLocaleString() || 0}</td>
<td className="px-4 py-3 text-gray-300">{c.clicks?.toLocaleString() || 0}</td>
<td className="px-4 py-3 text-gray-300">{((c.ctr || 0) * 100).toFixed(2)}%</td>
<td className="px-4 py-3 text-emerald-400 font-medium">${(c.agreed_rate || 0).toLocaleString()}</td>
<td className="px-4 py-3 text-xs text-gray-400">{c.mooninvoice_invoice_id || '—'}</td>
<td className="px-4 py-3">
<div className="flex gap-1">
{c.status === 'pending_approval' && (
<button
onClick={() => handleStatusChange(c.id, 'active')}
className="px-2 py-1 bg-green-700 hover:bg-green-600 text-white text-xs rounded"
>
Approve
</button>
)}
{c.status === 'active' && (
<button
onClick={() => handleStatusChange(c.id, 'paused')}
className="px-2 py-1 bg-yellow-700 hover:bg-yellow-600 text-white text-xs rounded"
>
Pause
</button>
)}
{c.status === 'paused' && (
<button
onClick={() => handleStatusChange(c.id, 'active')}
className="px-2 py-1 bg-blue-700 hover:bg-blue-600 text-white text-xs rounded"
>
Resume
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* EMAIL DISTRIBUTIONS TAB */}
{activeTab === 'distributions' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold text-gray-200">Email Distributions</h2>
<button className="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white text-sm font-medium rounded-md transition-colors">
+ New Distribution
</button>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-800 bg-gray-800/50">
{['Campaign', 'Publication', 'List', 'Send Date', 'Sent', 'Status', 'Revenue'].map(h => (
<th key={h} className="text-left px-4 py-3 text-xs font-medium text-gray-400 uppercase tracking-wide">{h}</th>
))}
</tr>
</thead>
<tbody>
{distributions.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-gray-500">No distributions yet</td></tr>
) : distributions.map(c => (
<tr key={c.id} className="border-b border-gray-800 hover:bg-gray-800/30">
<td className="px-4 py-3">
<p className="font-medium text-gray-200">{c.campaign_name}</p>
<p className="text-xs text-gray-500">{c.client_name}</p>
</td>
<td className="px-4 py-3 text-gray-300">{PUBLICATIONS.find(p => p.id === c.publication_id)?.short || c.publication_id}</td>
<td className="px-4 py-3 text-gray-400 text-xs">{c.email_lists?.list_name || '—'}</td>
<td className="px-4 py-3 text-gray-300">{c.send_date || '—'}</td>
<td className="px-4 py-3 text-gray-300">{c.sent_count?.toLocaleString() || 0}</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full border ${STATUS_COLORS[c.status] || 'bg-gray-800 text-gray-400 border-gray-700'}`}>
{c.status}
</span>
</td>
<td className="px-4 py-3 text-emerald-400">${(c.agreed_rate || 0).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* NEWSLETTERS TAB */}
{activeTab === 'newsletters' && (
<div className="space-y-4">
<h2 className="text-base font-semibold text-gray-200">Bi-Weekly Newsletters</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{PUBLICATIONS.map(pub => {
const pubNewsletters = newsletters.filter(n => n.publication_id === pub.id);
const latest = pubNewsletters[0];
return (
<div key={pub.id} className="bg-gray-900 border border-gray-800 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-gray-200">{pub.name}</h3>
<span className="text-xs text-gray-400">Every other Thursday</span>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Next send:</span>
<span className="text-gray-200">Thursday 11:00 AM ET</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Total issues:</span>
<span className="text-gray-200">{pubNewsletters.length}</span>
</div>
{latest && (
<div className="flex justify-between">
<span className="text-gray-400">Last status:</span>
<span className={`text-xs px-2 py-0.5 rounded-full border ${STATUS_COLORS[latest.status] || 'bg-gray-800 text-gray-400 border-gray-700'}`}>
{latest.status}
</span>
</div>
)}
</div>
{latest?.status === 'pending_approval' && (
<button className="mt-3 w-full px-3 py-2 bg-green-700 hover:bg-green-600 text-white text-sm font-medium rounded-md transition-colors">
Approve Newsletter
</button>
)}
</div>
);
})}
</div>
{/* Newsletter History */}
<div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-gray-800">
<h3 className="text-sm font-semibold text-gray-200">Newsletter History</h3>
</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-800 bg-gray-800/50">
{['Publication', 'Issue', 'Send Date', 'Sent', 'Status'].map(h => (
<th key={h} className="text-left px-4 py-3 text-xs font-medium text-gray-400 uppercase tracking-wide">{h}</th>
))}
</tr>
</thead>
<tbody>
{newsletters.length === 0 ? (
<tr><td colSpan={5} className="px-4 py-8 text-center text-gray-500">No newsletters yet</td></tr>
) : newsletters.map(n => (
<tr key={n.id} className="border-b border-gray-800 hover:bg-gray-800/30">
<td className="px-4 py-3 text-gray-200">{PUBLICATIONS.find(p => p.id === n.publication_id)?.name || n.publication_id}</td>
<td className="px-4 py-3 text-gray-400">#{n.newsletter_issue_number || '—'}</td>
<td className="px-4 py-3 text-gray-300">{n.send_date || '—'}</td>
<td className="px-4 py-3 text-gray-300">{n.sent_count?.toLocaleString() || 0}</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full border ${STATUS_COLORS[n.status] || 'bg-gray-800 text-gray-400 border-gray-700'}`}>
{n.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* DOCUMENTS TAB */}
{activeTab === 'documents' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold text-gray-200">Insertion Orders & Documents</h2>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-lg p-8 text-center">
<svg className="w-12 h-12 text-gray-600 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p className="text-gray-400 text-sm">Documents are attached via email and stored automatically.</p>
<p className="text-gray-500 text-xs mt-1">Jeff attaches IOs to campaign emails they appear here once processed.</p>
</div>
</div>
)}
{/* INVOICES TAB */}
{activeTab === 'invoices' && (
<div className="space-y-4">
<h2 className="text-base font-semibold text-gray-200">Campaign Invoices</h2>
<div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-800 bg-gray-800/50">
{['Campaign', 'Client', 'Publication', 'Amount', 'Invoice #', 'Status'].map(h => (
<th key={h} className="text-left px-4 py-3 text-xs font-medium text-gray-400 uppercase tracking-wide">{h}</th>
))}
</tr>
</thead>
<tbody>
{campaigns.filter(c => c.mooninvoice_invoice_id || c.agreed_rate).length === 0 ? (
<tr><td colSpan={6} className="px-4 py-8 text-center text-gray-500">No invoiced campaigns yet</td></tr>
) : campaigns.filter(c => c.agreed_rate).map(c => (
<tr key={c.id} className="border-b border-gray-800 hover:bg-gray-800/30">
<td className="px-4 py-3 text-gray-200">{c.campaign_name || c.client_name}</td>
<td className="px-4 py-3 text-gray-300">{c.client_name}</td>
<td className="px-4 py-3 text-gray-400">{PUBLICATIONS.find(p => p.id === c.publication_id)?.short || c.publication_id}</td>
<td className="px-4 py-3 text-emerald-400 font-medium">${(c.agreed_rate || 0).toLocaleString()}</td>
<td className="px-4 py-3 text-gray-400 text-xs">{c.mooninvoice_invoice_id || 'Pending'}</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full border ${STATUS_COLORS[c.status] || 'bg-gray-800 text-gray-400 border-gray-700'}`}>
{c.status.replace(/_/g, ' ')}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* REPORTS TAB */}
{activeTab === 'reports' && (
<div className="space-y-4">
<h2 className="text-base font-semibold text-gray-200">Performance Reports</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{PUBLICATIONS.map(pub => {
const pubCampaigns = campaigns.filter(c => c.publication_id === pub.id);
const totalRevenue = pubCampaigns.reduce((sum, c) => sum + (c.agreed_rate || 0), 0);
const activeCnt = pubCampaigns.filter(c => c.status === 'active').length;
const totalImpressions = pubCampaigns.reduce((sum, c) => sum + (c.impressions || 0), 0);
return (
<div key={pub.id} className="bg-gray-900 border border-gray-800 rounded-lg p-4">
<h3 className="font-semibold text-gray-200 mb-3">{pub.name}</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between"><span className="text-gray-400">Active campaigns</span><span className="text-gray-200">{activeCnt}</span></div>
<div className="flex justify-between"><span className="text-gray-400">Total campaigns</span><span className="text-gray-200">{pubCampaigns.length}</span></div>
<div className="flex justify-between"><span className="text-gray-400">Total impressions</span><span className="text-gray-200">{totalImpressions.toLocaleString()}</span></div>
<div className="flex justify-between"><span className="text-gray-400">Total revenue</span><span className="text-emerald-400 font-medium">${totalRevenue.toLocaleString()}</span></div>
</div>
</div>
);
})}
</div>
{/* Export */}
<div className="bg-gray-900 border border-gray-800 rounded-lg p-4">
<h3 className="text-sm font-semibold text-gray-200 mb-3">Export Reports</h3>
<div className="flex gap-3">
<button className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-200 text-sm rounded-md transition-colors">
Export Campaigns CSV
</button>
<button className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-200 text-sm rounded-md transition-colors">
Export Revenue Report
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}