import { NextRequest, NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { getGoalLabel, getLifecycleStageLabel, getRoleLabel, getSourceLabel, getTeamSizeLabel, getUseCaseLabel, } from '@/lib/revops'; import { db } from '@/lib/db'; import { getMetricSnapshot, getUpgradeCandidateBadges } from '@/lib/revops-server'; export const dynamic = 'force-dynamic'; type HydratedUser = { id: string; name: string | null; email: string; emailDomain: string | null; plan: string; lifecycleStage: string; fitScore: number; intentScore: number; leadScore: number; signupSource: string | null; signupSourceSelfReported: string | null; signupCampaign: string | null; signupLandingPath: string | null; primaryUseCase: string | null; primaryGoal: string | null; jobRole: string | null; companyName: string | null; companyWebsite: string | null; teamSizeBucket: string | null; createdAt: string; firstQrCreatedAt: string | null; activationAt: string | null; firstDynamicQrAt: string | null; qrCount: number; dynamicQrCount: number; scanCount: number; contentTypeCount: number; upgradeBadges: string[]; }; function hasAdminSession() { const adminCookie = cookies().get('newsletter-admin'); return adminCookie?.value === 'authenticated'; } function toIso(value: Date | null) { return value ? value.toISOString() : null; } function safeDate(value: string | null) { if (!value) return null; const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? null : parsed; } function applyUserFilters(users: HydratedUser[], request: NextRequest) { const stage = request.nextUrl.searchParams.get('stage'); const source = request.nextUrl.searchParams.get('source'); const campaign = request.nextUrl.searchParams.get('campaign'); const landingPath = request.nextUrl.searchParams.get('landingPath'); const useCase = request.nextUrl.searchParams.get('useCase'); const goal = request.nextUrl.searchParams.get('goal'); const role = request.nextUrl.searchParams.get('role'); const teamSize = request.nextUrl.searchParams.get('teamSize'); const plan = request.nextUrl.searchParams.get('plan'); const search = request.nextUrl.searchParams.get('search')?.toLowerCase().trim(); const from = safeDate(request.nextUrl.searchParams.get('from')); const to = safeDate(request.nextUrl.searchParams.get('to')); return users.filter((user) => { const createdAt = new Date(user.createdAt); const matchesSearch = !search || [ user.name, user.email, user.companyName, user.emailDomain, ].filter(Boolean).some((value) => value!.toLowerCase().includes(search)); return ( (!stage || user.lifecycleStage === stage) && (!source || user.signupSource === source) && (!campaign || user.signupCampaign === campaign) && (!landingPath || user.signupLandingPath === landingPath) && (!useCase || user.primaryUseCase === useCase) && (!goal || user.primaryGoal === goal) && (!role || user.jobRole === role) && (!teamSize || user.teamSizeBucket === teamSize) && (!plan || user.plan === plan) && (!from || createdAt >= from) && (!to || createdAt <= to) && matchesSearch ); }); } function sortUsers(users: HydratedUser[], sort: string) { const sorted = [...users]; sorted.sort((a, b) => { switch (sort) { case 'createdAt_asc': return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); case 'activationAt_desc': return new Date(b.activationAt || 0).getTime() - new Date(a.activationAt || 0).getTime(); case 'leadScore_asc': return a.leadScore - b.leadScore; case 'fitScore_desc': return b.fitScore - a.fitScore; case 'intentScore_desc': return b.intentScore - a.intentScore; case 'leadScore_desc': default: return b.leadScore - a.leadScore || new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); } }); return sorted; } function buildGroupedRows(users: HydratedUser[], key: keyof HydratedUser) { const rows = new Map(); users.forEach((user) => { const rawValue = (user[key] as string | null) || 'unknown'; const row = rows.get(rawValue) || { key: rawValue, signups: 0, firstQr: 0, activated: 0, hot: 0, upgradeCandidates: 0, paid: 0, }; row.signups += 1; if (user.firstQrCreatedAt) row.firstQr += 1; if (user.activationAt) row.activated += 1; if (user.lifecycleStage === 'hot') row.hot += 1; if (user.lifecycleStage === 'upgrade_candidate') row.upgradeCandidates += 1; if (user.lifecycleStage === 'paid') row.paid += 1; rows.set(rawValue, row); }); return Array.from(rows.values()).sort((a, b) => b.signups - a.signups); } function buildFunnel(users: HydratedUser[]) { return { signup: users.length, sourceConfirmed: users.filter((user) => Boolean(user.signupSourceSelfReported)).length, useCaseSelected: users.filter((user) => Boolean(user.primaryUseCase)).length, goalSelected: users.filter((user) => Boolean(user.primaryGoal)).length, profileCaptured: users.filter((user) => Boolean(user.jobRole && user.teamSizeBucket)).length, firstQrCreated: users.filter((user) => Boolean(user.firstQrCreatedAt)).length, firstDynamicQrCreated: users.filter((user) => Boolean(user.firstDynamicQrAt)).length, activated: users.filter((user) => Boolean(user.activationAt)).length, }; } function buildLifecycleSummary(users: HydratedUser[]) { return { cold: users.filter((user) => user.lifecycleStage === 'cold').length, activated: users.filter((user) => user.lifecycleStage === 'activated').length, warm: users.filter((user) => user.lifecycleStage === 'warm').length, hot: users.filter((user) => user.lifecycleStage === 'hot').length, upgrade_candidate: users.filter((user) => user.lifecycleStage === 'upgrade_candidate').length, paid: users.filter((user) => user.lifecycleStage === 'paid').length, }; } function buildCsv(rows: HydratedUser[]) { const headers = [ 'name', 'email', 'email_domain', 'plan', 'lifecycle_stage', 'fit_score', 'intent_score', 'lead_score', 'source', 'self_reported_source', 'campaign', 'landing_page', 'use_case', 'goal', 'role', 'company', 'team_size', 'created_at', 'first_qr_created_at', 'activation_at', 'qr_count', 'dynamic_qr_count', 'scan_count', ]; const escape = (value: string | number | null) => { const normalized = value == null ? '' : String(value); return `"${normalized.replace(/"/g, '""')}"`; }; const lines = rows.map((row) => [ row.name, row.email, row.emailDomain, row.plan, row.lifecycleStage, row.fitScore, row.intentScore, row.leadScore, row.signupSource, row.signupSourceSelfReported, row.signupCampaign, row.signupLandingPath, row.primaryUseCase, row.primaryGoal, row.jobRole, row.companyName, row.teamSizeBucket, row.createdAt, row.firstQrCreatedAt, row.activationAt, row.qrCount, row.dynamicQrCount, row.scanCount, ].map(escape).join(',')); return [headers.join(','), ...lines].join('\n'); } export async function GET(request: NextRequest) { try { if (!hasAdminSession()) { return NextResponse.json({ error: 'Unauthorized - Admin login required' }, { status: 401 }); } const rawUsers = await db.user.findMany({ select: { id: true, name: true, email: true, emailDomain: true, plan: true, lifecycleStage: true, fitScore: true, intentScore: true, leadScore: true, signupSource: true, signupSourceSelfReported: true, signupCampaign: true, signupLandingPath: true, primaryUseCase: true, primaryGoal: true, jobRole: true, companyName: true, companyWebsite: true, teamSizeBucket: true, createdAt: true, firstQrCreatedAt: true, firstDynamicQrAt: true, activationAt: true, qrCodes: { select: { type: true, contentType: true, createdAt: true, _count: { select: { scans: true, }, }, }, }, }, orderBy: { createdAt: 'desc', }, }); const recentBillingLogs = await db.userLifecycleLog.findMany({ where: { reason: { startsWith: 'subscription_', }, }, orderBy: { createdAt: 'desc', }, take: 10, select: { fromStage: true, toStage: true, reason: true, createdAt: true, user: { select: { id: true, name: true, email: true, plan: true, }, }, }, }); const users: HydratedUser[] = rawUsers.map((user) => { const metrics = getMetricSnapshot(user.qrCodes); return { id: user.id, name: user.name, email: user.email, emailDomain: user.emailDomain, plan: user.plan, lifecycleStage: user.lifecycleStage, fitScore: user.fitScore, intentScore: user.intentScore, leadScore: user.leadScore, signupSource: user.signupSource, signupSourceSelfReported: user.signupSourceSelfReported, signupCampaign: user.signupCampaign, signupLandingPath: user.signupLandingPath, primaryUseCase: user.primaryUseCase, primaryGoal: user.primaryGoal, jobRole: user.jobRole, companyName: user.companyName, companyWebsite: user.companyWebsite, teamSizeBucket: user.teamSizeBucket, createdAt: user.createdAt.toISOString(), firstQrCreatedAt: toIso(user.firstQrCreatedAt), activationAt: toIso(user.activationAt), firstDynamicQrAt: toIso(user.firstDynamicQrAt), qrCount: metrics.qrCount, dynamicQrCount: metrics.dynamicQrCount, scanCount: metrics.scanCount, contentTypeCount: metrics.contentTypeCount, upgradeBadges: getUpgradeCandidateBadges(user, metrics), }; }); const filteredUsers = sortUsers( applyUserFilters(users, request), request.nextUrl.searchParams.get('sort') || 'leadScore_desc' ); if (request.nextUrl.searchParams.get('format') === 'csv') { const csv = buildCsv(filteredUsers); return new NextResponse(csv, { headers: { 'Content-Type': 'text/csv; charset=utf-8', 'Content-Disposition': 'attachment; filename="qrmaster-revops-export.csv"', }, }); } const page = Number(request.nextUrl.searchParams.get('page') || '1'); const pageSize = Number(request.nextUrl.searchParams.get('pageSize') || '25'); const total = filteredUsers.length; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const paginatedUsers = filteredUsers.slice((page - 1) * pageSize, page * pageSize); const acquisitionBySource = buildGroupedRows(users, 'signupSource').map((row) => ({ ...row, label: getSourceLabel(row.key), activationRate: row.signups ? Math.round((row.activated / row.signups) * 100) : 0, })); const acquisitionByCampaign = buildGroupedRows(users, 'signupCampaign'); const acquisitionByLandingPath = buildGroupedRows(users, 'signupLandingPath'); const funnel = buildFunnel(users); const lifecycleSummary = buildLifecycleSummary(users); const mismatchCount = users.filter( (user) => user.signupSource && user.signupSourceSelfReported && user.signupSource !== user.signupSourceSelfReported ).length; const upgradeCandidates = users .filter((user) => user.plan === 'FREE' && user.lifecycleStage === 'upgrade_candidate') .sort((a, b) => b.leadScore - a.leadScore) .slice(0, 25); const recentBillingActivity = recentBillingLogs.map((log) => ({ userId: log.user.id, name: log.user.name, email: log.user.email, plan: log.user.plan, reason: log.reason, fromStage: log.fromStage, toStage: log.toStage, createdAt: log.createdAt.toISOString(), })); const filterOptions = { stages: ['cold', 'activated', 'warm', 'hot', 'upgrade_candidate', 'paid'], sources: Array.from(new Set(users.map((user) => user.signupSource).filter((value): value is string => Boolean(value)))), campaigns: Array.from(new Set(users.map((user) => user.signupCampaign).filter((value): value is string => Boolean(value)))), landingPaths: Array.from(new Set(users.map((user) => user.signupLandingPath).filter((value): value is string => Boolean(value)))), useCases: Array.from(new Set(users.map((user) => user.primaryUseCase).filter((value): value is string => Boolean(value)))), goals: Array.from(new Set(users.map((user) => user.primaryGoal).filter((value): value is string => Boolean(value)))), roles: Array.from(new Set(users.map((user) => user.jobRole).filter((value): value is string => Boolean(value)))), teamSizes: Array.from(new Set(users.map((user) => user.teamSizeBucket).filter((value): value is string => Boolean(value)))), plans: Array.from(new Set(users.map((user) => user.plan).filter((value): value is string => Boolean(value)))), }; return NextResponse.json({ overview: { totalUsers: users.length, mismatchCount, activatedUsers: funnel.activated, paidUsers: lifecycleSummary.paid, recentBillingEvents: recentBillingActivity.length, }, acquisition: { bySource: acquisitionBySource, byCampaign: acquisitionByCampaign.slice(0, 15), byLandingPath: acquisitionByLandingPath.slice(0, 15), }, funnel, funnelBreakdowns: { bySource: acquisitionBySource.slice(0, 10), byUseCase: buildGroupedRows(users, 'primaryUseCase').map((row) => ({ ...row, label: getUseCaseLabel(row.key) })), byRole: buildGroupedRows(users, 'jobRole').map((row) => ({ ...row, label: getRoleLabel(row.key) })), byTeamSize: buildGroupedRows(users, 'teamSizeBucket').map((row) => ({ ...row, label: getTeamSizeLabel(row.key) })), }, lifecycleSummary, recentBillingActivity, campaignSourceQuality: acquisitionBySource, upgradeCandidates, filterOptions, segments: { total, page, pageSize, totalPages, rows: paginatedUsers.map((user) => ({ ...user, lifecycleStageLabel: getLifecycleStageLabel(user.lifecycleStage), signupSourceLabel: getSourceLabel(user.signupSource), signupSourceSelfReportedLabel: getSourceLabel(user.signupSourceSelfReported), primaryUseCaseLabel: getUseCaseLabel(user.primaryUseCase), primaryGoalLabel: getGoalLabel(user.primaryGoal), jobRoleLabel: getRoleLabel(user.jobRole), teamSizeLabel: getTeamSizeLabel(user.teamSizeBucket), })), }, }); } catch (error) { console.error('Error fetching RevOps dashboard data:', error); return NextResponse.json({ error: 'Failed to fetch RevOps dashboard data' }, { status: 500 }); } }