'use server'; // Sponsored Ads — Server Actions (localStorage-persisted mock) import type { Campaign, CampaignWithEvents, CampaignFormData, CampaignReport, CampaignStatus, PlacementDailyStats, } from '@/lib/types/ads'; import type { SurfaceKey } from '@/lib/types/ad-control'; import { MOCK_CAMPAIGNS, MOCK_DAILY_STATS, MOCK_TRACKING_EVENTS } from '@/features/ad-control/data/mockAdsData'; import { MOCK_PICKER_EVENTS, MOCK_SURFACES } from '@/features/ad-control/data/mockAdData'; import { computeDailyStats, getTrackingEvents, recordImpression, recordClick } from '@/lib/ads/tracking'; const CAMPAIGNS_KEY = 'ad_campaigns'; const CAMPAIGN_AUDIT_KEY = 'ad_campaign_audit'; // ===== Persistence ===== function getCampaignStore(): Campaign[] { if (typeof window === 'undefined') return MOCK_CAMPAIGNS; try { const raw = localStorage.getItem(CAMPAIGNS_KEY); return raw ? JSON.parse(raw) : MOCK_CAMPAIGNS; } catch { return MOCK_CAMPAIGNS; } } function saveCampaignStore(campaigns: Campaign[]) { if (typeof window !== 'undefined') { localStorage.setItem(CAMPAIGNS_KEY, JSON.stringify(campaigns)); } } function logCampaignAudit(campaignId: string, action: string, details?: Record) { if (typeof window === 'undefined') return; try { const entries = JSON.parse(localStorage.getItem(CAMPAIGN_AUDIT_KEY) || '[]'); entries.push({ id: `ca-${Date.now()}`, campaignId, actorId: 'admin-1', action, details: details || null, createdAt: new Date().toISOString(), }); localStorage.setItem(CAMPAIGN_AUDIT_KEY, JSON.stringify(entries.slice(-500))); } catch { } } // ===== Resolve Events ===== function resolveEvents(campaign: Campaign): CampaignWithEvents { const events = MOCK_PICKER_EVENTS.filter(e => campaign.eventIds.includes(e.id)); return { ...campaign, events }; } // ===== Auto-end expired campaigns ===== function autoEndCampaigns(campaigns: Campaign[]): Campaign[] { const now = new Date().toISOString(); return campaigns.map(c => { if (c.status === 'ACTIVE' && (c.endAt < now || c.spent >= c.totalBudget)) { return { ...c, status: 'ENDED' as CampaignStatus, updatedAt: now }; } return c; }); } // ===== QUERIES ===== export async function getCampaigns( status?: CampaignStatus | 'ALL', ): Promise<{ success: boolean; data: CampaignWithEvents[] }> { let campaigns = autoEndCampaigns(getCampaignStore()); saveCampaignStore(campaigns); if (status && status !== 'ALL') { campaigns = campaigns.filter(c => c.status === status); } // Sort: IN_REVIEW first, then ACTIVE, then by updatedAt desc const statusOrder: Record = { IN_REVIEW: 0, ACTIVE: 1, PAUSED: 2, DRAFT: 3, ENDED: 4, REJECTED: 5 }; campaigns.sort((a, b) => (statusOrder[a.status] ?? 9) - (statusOrder[b.status] ?? 9) || b.updatedAt.localeCompare(a.updatedAt)); return { success: true, data: campaigns.map(resolveEvents) }; } export async function getCampaign(id: string): Promise<{ success: boolean; data?: CampaignWithEvents; message: string }> { const campaigns = getCampaignStore(); const campaign = campaigns.find(c => c.id === id); if (!campaign) return { success: false, message: 'Campaign not found' }; return { success: true, data: resolveEvents(campaign), message: 'OK' }; } // ===== MUTATIONS ===== export async function createCampaign( data: CampaignFormData, ): Promise<{ success: boolean; message: string; data?: Campaign }> { const campaigns = getCampaignStore(); const now = new Date().toISOString(); const id = `camp-${Date.now().toString(36)}`; const newCampaign: Campaign = { id, partnerId: `partner-${data.partnerName.toLowerCase().replace(/\s+/g, '-').slice(0, 10)}`, partnerName: data.partnerName, name: data.name, objective: data.objective, status: 'DRAFT', startAt: data.startAt, endAt: data.endAt, billingModel: data.billingModel, totalBudget: data.totalBudget, dailyCap: data.dailyCap, spent: 0, targeting: data.targeting, surfaceKeys: data.surfaceKeys, eventIds: data.eventIds, frequencyCap: data.frequencyCap, approvedBy: null, rejectedReason: null, createdBy: 'admin-1', createdAt: now, updatedAt: now, }; campaigns.push(newCampaign); saveCampaignStore(campaigns); logCampaignAudit(id, 'CREATED', { name: data.name }); return { success: true, message: 'Campaign created as draft', data: newCampaign }; } export async function updateCampaign( id: string, patch: Partial, ): Promise<{ success: boolean; message: string }> { const campaigns = getCampaignStore(); const idx = campaigns.findIndex(c => c.id === id); if (idx === -1) return { success: false, message: 'Campaign not found' }; const campaign = campaigns[idx]; if (campaign.status !== 'DRAFT' && campaign.status !== 'PAUSED') { return { success: false, message: 'Can only edit draft or paused campaigns' }; } const updated: Campaign = { ...campaign, ...patch, updatedAt: new Date().toISOString(), } as Campaign; campaigns[idx] = updated; saveCampaignStore(campaigns); logCampaignAudit(id, 'UPDATED', patch); return { success: true, message: 'Campaign updated' }; } export async function submitCampaign( id: string, ): Promise<{ success: boolean; message: string }> { const campaigns = getCampaignStore(); const idx = campaigns.findIndex(c => c.id === id); if (idx === -1) return { success: false, message: 'Campaign not found' }; if (campaigns[idx].status !== 'DRAFT') return { success: false, message: 'Only draft campaigns can be submitted' }; // Validation checks const c = campaigns[idx]; if (c.eventIds.length === 0) return { success: false, message: 'At least one event is required' }; if (c.surfaceKeys.length === 0) return { success: false, message: 'At least one surface is required' }; if (c.totalBudget <= 0) return { success: false, message: 'Budget must be positive' }; if (!c.startAt || !c.endAt) return { success: false, message: 'Schedule dates are required' }; campaigns[idx] = { ...c, status: 'IN_REVIEW', updatedAt: new Date().toISOString() }; saveCampaignStore(campaigns); logCampaignAudit(id, 'SUBMITTED'); return { success: true, message: 'Campaign submitted for review' }; } export async function approveCampaign( id: string, ): Promise<{ success: boolean; message: string }> { const campaigns = getCampaignStore(); const idx = campaigns.findIndex(c => c.id === id); if (idx === -1) return { success: false, message: 'Campaign not found' }; if (campaigns[idx].status !== 'IN_REVIEW') return { success: false, message: 'Campaign is not in review' }; // Eligibility checks const c = campaigns[idx]; for (const eventId of c.eventIds) { const event = MOCK_PICKER_EVENTS.find(e => e.id === eventId); if (!event) return { success: false, message: `Event ${eventId} not found` }; if (event.approvalStatus !== 'APPROVED') return { success: false, message: `Event "${event.title}" is not approved` }; if (new Date(event.endDate) < new Date()) return { success: false, message: `Event "${event.title}" has ended` }; } campaigns[idx] = { ...c, status: 'ACTIVE', approvedBy: 'admin-1', updatedAt: new Date().toISOString() }; saveCampaignStore(campaigns); logCampaignAudit(id, 'APPROVED'); return { success: true, message: 'Campaign approved and activated' }; } export async function rejectCampaign( id: string, reason: string, ): Promise<{ success: boolean; message: string }> { const campaigns = getCampaignStore(); const idx = campaigns.findIndex(c => c.id === id); if (idx === -1) return { success: false, message: 'Campaign not found' }; if (campaigns[idx].status !== 'IN_REVIEW') return { success: false, message: 'Campaign is not in review' }; campaigns[idx] = { ...campaigns[idx], status: 'REJECTED', rejectedReason: reason, updatedAt: new Date().toISOString() }; saveCampaignStore(campaigns); logCampaignAudit(id, 'REJECTED', { reason }); return { success: true, message: 'Campaign rejected' }; } export async function pauseCampaign( id: string, ): Promise<{ success: boolean; message: string }> { const campaigns = getCampaignStore(); const idx = campaigns.findIndex(c => c.id === id); if (idx === -1) return { success: false, message: 'Campaign not found' }; if (campaigns[idx].status !== 'ACTIVE') return { success: false, message: 'Only active campaigns can be paused' }; campaigns[idx] = { ...campaigns[idx], status: 'PAUSED', updatedAt: new Date().toISOString() }; saveCampaignStore(campaigns); logCampaignAudit(id, 'PAUSED'); return { success: true, message: 'Campaign paused' }; } export async function resumeCampaign( id: string, ): Promise<{ success: boolean; message: string }> { const campaigns = getCampaignStore(); const idx = campaigns.findIndex(c => c.id === id); if (idx === -1) return { success: false, message: 'Campaign not found' }; if (campaigns[idx].status !== 'PAUSED') return { success: false, message: 'Only paused campaigns can be resumed' }; campaigns[idx] = { ...campaigns[idx], status: 'ACTIVE', updatedAt: new Date().toISOString() }; saveCampaignStore(campaigns); logCampaignAudit(id, 'RESUMED'); return { success: true, message: 'Campaign resumed' }; } // ===== REPORTING ===== export async function getCampaignReport( id: string, ): Promise<{ success: boolean; data?: CampaignReport; message: string }> { const campaigns = getCampaignStore(); const campaign = campaigns.find(c => c.id === id); if (!campaign) return { success: false, message: 'Campaign not found' }; // Get or compute daily stats let dailyStats: PlacementDailyStats[]; if (campaign.id === 'camp-001') { // Use pre-computed mock data for demo campaign dailyStats = MOCK_DAILY_STATS; } else { dailyStats = computeDailyStats( campaign.id, campaign.billingModel, campaign.totalBudget / (campaign.billingModel === 'CPM' ? 1000 : campaign.billingModel === 'CPC' ? 500 : 1), ); } // Totals const totalImpressions = dailyStats.reduce((s, d) => s + d.impressions, 0); const totalClicks = dailyStats.reduce((s, d) => s + d.clicks, 0); const totalSpend = dailyStats.reduce((s, d) => s + d.spend, 0); // By surface const surfaceMap = new Map(); for (const stat of dailyStats) { const existing = surfaceMap.get(stat.surfaceKey) || { impressions: 0, clicks: 0, spend: 0 }; existing.impressions += stat.impressions; existing.clicks += stat.clicks; existing.spend += stat.spend; surfaceMap.set(stat.surfaceKey, existing); } const bySurface = Array.from(surfaceMap.entries()).map(([surfaceKey, data]) => { const surface = MOCK_SURFACES.find(s => s.key === surfaceKey); return { surfaceKey: surfaceKey as SurfaceKey, surfaceName: surface?.name || surfaceKey, impressions: data.impressions, clicks: data.clicks, ctr: data.impressions > 0 ? Number((data.clicks / data.impressions).toFixed(4)) : 0, spend: Number(data.spend.toFixed(2)), }; }); const report: CampaignReport = { campaign: resolveEvents(campaign), totals: { impressions: totalImpressions, clicks: totalClicks, ctr: totalImpressions > 0 ? Number((totalClicks / totalImpressions).toFixed(4)) : 0, spend: Number(totalSpend.toFixed(2)), remaining: Number((campaign.totalBudget - totalSpend).toFixed(2)), }, dailyStats, bySurface, }; return { success: true, data: report, message: 'OK' }; } // ===== CSV EXPORT ===== export async function exportCampaignCSV(id: string): Promise<{ success: boolean; csv?: string; message: string }> { const res = await getCampaignReport(id); if (!res.success || !res.data) return { success: false, message: res.message }; const { campaign, totals, dailyStats, bySurface } = res.data; const lines: string[] = [ `Campaign Report: ${campaign.name}`, `Partner: ${campaign.partnerName}`, `Status: ${campaign.status}`, `Period: ${campaign.startAt.slice(0, 10)} to ${campaign.endAt.slice(0, 10)}`, `Billing: ${campaign.billingModel}`, `Budget: ₹${campaign.totalBudget.toLocaleString()}`, `Spent: ₹${totals.spend.toLocaleString()}`, ``, `Date,Surface,Impressions,Clicks,CTR,Spend`, ...dailyStats.map(d => `${d.date},${d.surfaceKey},${d.impressions},${d.clicks},${(d.ctr * 100).toFixed(2)}%,₹${d.spend.toFixed(2)}` ), ``, `Surface Summary`, `Surface,Impressions,Clicks,CTR,Spend`, ...bySurface.map(s => `${s.surfaceName},${s.impressions},${s.clicks},${(s.ctr * 100).toFixed(2)}%,₹${s.spend.toFixed(2)}` ), ``, `Totals,${totals.impressions},${totals.clicks},${(totals.ctr * 100).toFixed(2)}%,₹${totals.spend.toFixed(2)}`, ]; return { success: true, csv: lines.join('\n'), message: 'OK' }; } // ===== DASHBOARD STATS ===== export async function getSponsoredStats(): Promise<{ success: boolean; data: { activeCampaigns: number; todaySpend: number; impressions24h: number; clicks24h: number; ctr24h: number; }; }> { const campaigns = autoEndCampaigns(getCampaignStore()); const activeCampaigns = campaigns.filter(c => c.status === 'ACTIVE').length; // Last 24h stats from tracking events const cutoff24h = new Date(Date.now() - 86400000).toISOString(); // For pre-seeded data, use mock stats; for real-time, use tracking let impressions24h = 0; let clicks24h = 0; let todaySpend = 0; // Check real tracking events first const allEvents = getTrackingEvents(); const recent = allEvents.filter(e => e.timestamp > cutoff24h); if (recent.length > 0) { impressions24h = recent.filter(e => e.type === 'IMPRESSION').length; clicks24h = recent.filter(e => e.type === 'CLICK').length; todaySpend = Number(((impressions24h / 1000) * 250).toFixed(2)); // ₹250 CPM assumed } else { // Fallback to mock daily stats (last day) const lastDayStats = MOCK_DAILY_STATS.filter(d => d.date === '2026-02-09'); impressions24h = lastDayStats.reduce((s, d) => s + d.impressions, 0); clicks24h = lastDayStats.reduce((s, d) => s + d.clicks, 0); todaySpend = lastDayStats.reduce((s, d) => s + d.spend, 0); } const ctr24h = impressions24h > 0 ? Number((clicks24h / impressions24h).toFixed(4)) : 0; return { success: true, data: { activeCampaigns, todaySpend, impressions24h, clicks24h, ctr24h }, }; }