UNPKG

sysrot-hub

Version:

CLI de nueva generación para proyectos Next.js 14+ con IA multi-modelo, Web3 integration, internacionalización completa y roadmap realista 2025-2026

541 lines (476 loc) 17.1 kB
import { NextApiRequest, NextApiResponse } from 'next'; import { PrismaClient } from '@prisma/client'; import { getSession } from 'next-auth/react'; const prisma = new PrismaClient(); async function hasPermission(userId: string, organizationId: string, requiredRole: string[] = ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER']) { const membership = await prisma.organizationMember.findUnique({ where: { organizationId_userId: { organizationId, userId } } }); return membership && requiredRole.includes(membership.role); } export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const session = await getSession({ req }); if (!session?.user) { return res.status(401).json({ error: 'Authentication required' }); } const { method, query } = req; const { organizationId, type } = query; if (!organizationId || typeof organizationId !== 'string') { return res.status(400).json({ error: 'Organization ID is required' }); } // Check permissions const hasAccess = await hasPermission(session.user.id, organizationId); if (!hasAccess) { return res.status(403).json({ error: 'Access denied' }); } if (method !== 'GET') { res.setHeader('Allow', ['GET']); return res.status(405).end('Method Not Allowed'); } switch (type) { case 'overview': return await getOverviewMetrics(req, res); case 'usage': return await getUsageMetrics(req, res); case 'members': return await getMemberMetrics(req, res); case 'activities': return await getActivityMetrics(req, res); case 'billing': return await getBillingMetrics(req, res); default: return await getDashboardMetrics(req, res); } } catch (error) { console.error('Analytics API error:', error); return res.status(500).json({ error: 'Internal server error' }); } finally { await prisma.$disconnect(); } } async function getDashboardMetrics(req: NextApiRequest, res: NextApiResponse) { const { organizationId } = req.query; try { const now = new Date(); const thisMonth = now.getMonth() + 1; const thisYear = now.getFullYear(); const lastMonth = thisMonth === 1 ? 12 : thisMonth - 1; const lastMonthYear = thisMonth === 1 ? thisYear - 1 : thisYear; // Get organization with subscription and plan const organization = await prisma.organization.findUnique({ where: { id: organizationId as string }, include: { plan: true, subscription: true, members: { select: { id: true, role: true, joinedAt: true, lastActiveAt: true } } } }); if (!organization) { return res.status(404).json({ error: 'Organization not found' }); } // Get current and previous month usage const [currentUsage, previousUsage] = await Promise.all([ prisma.usage.findUnique({ where: { organizationId_month_year: { organizationId: organizationId as string, month: thisMonth, year: thisYear } } }), prisma.usage.findUnique({ where: { organizationId_month_year: { organizationId: organizationId as string, month: lastMonth, year: lastMonthYear } } }) ]); // Get recent activities const recentActivities = await prisma.activity.findMany({ where: { organizationId: organizationId as string }, include: { user: { select: { name: true, email: true, image: true } } }, orderBy: { createdAt: 'desc' }, take: 10 }); // Calculate metrics const totalMembers = organization.members.length; const activeMembers = organization.members.filter(m => { if (!m.lastActiveAt) return false; const daysSinceActive = (now.getTime() - m.lastActiveAt.getTime()) / (1000 * 60 * 60 * 24); return daysSinceActive <= 30; // Active in last 30 days }).length; const currentMonthUsers = currentUsage?.users || 0; const previousMonthUsers = previousUsage?.users || 0; const userGrowth = previousMonthUsers > 0 ? ((currentMonthUsers - previousMonthUsers) / previousMonthUsers) * 100 : 0; const currentApiUsage = currentUsage?.apiRequests || 0; const apiLimit = organization.apiLimit; const apiUsagePercentage = (currentApiUsage / apiLimit) * 100; const currentStorage = currentUsage?.storage || 0; const storageLimit = organization.storageLimit; const storageUsagePercentage = (currentStorage / storageLimit) * 100; // Plan utilization const planUtilization = { users: { current: totalMembers, limit: organization.userLimit, percentage: (totalMembers / organization.userLimit) * 100 }, storage: { current: currentStorage, limit: storageLimit, percentage: storageUsagePercentage }, api: { current: currentApiUsage, limit: apiLimit, percentage: apiUsagePercentage } }; // Trial status let trialStatus = null; if (organization.trialEndsAt && organization.trialEndsAt > now) { const daysLeft = Math.ceil((organization.trialEndsAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); trialStatus = { isActive: true, daysLeft, endsAt: organization.trialEndsAt }; } const metrics = { organization: { id: organization.id, name: organization.name, status: organization.status, plan: organization.plan, subscription: organization.subscription, trialStatus }, overview: { totalMembers, activeMembers, userGrowth: Math.round(userGrowth * 100) / 100, planUtilization, healthScore: calculateHealthScore(planUtilization, activeMembers, totalMembers) }, usage: { current: currentUsage, previous: previousUsage, growth: { users: userGrowth, apiRequests: previousUsage?.apiRequests ? ((currentApiUsage - previousUsage.apiRequests) / previousUsage.apiRequests) * 100 : 0, storage: previousUsage?.storage ? ((currentStorage - previousUsage.storage) / previousUsage.storage) * 100 : 0 } }, recentActivities: recentActivities.slice(0, 5) // Top 5 for dashboard }; res.status(200).json({ metrics }); } catch (error) { console.error('Error fetching dashboard metrics:', error); res.status(500).json({ error: 'Failed to fetch dashboard metrics' }); } } async function getOverviewMetrics(req: NextApiRequest, res: NextApiResponse) { const { organizationId } = req.query; try { // Get 12 months of usage data const usageData = await prisma.usage.findMany({ where: { organizationId: organizationId as string }, orderBy: [{ year: 'desc' }, { month: 'desc' }], take: 12 }); // Get member growth over time const memberGrowth = await prisma.$queryRaw` SELECT DATE_TRUNC('month', "joinedAt") as month, COUNT(*) as new_members FROM "OrganizationMember" WHERE "organizationId" = ${organizationId} GROUP BY DATE_TRUNC('month', "joinedAt") ORDER BY month DESC LIMIT 12 `; // Activity trends const activityTrends = await prisma.$queryRaw` SELECT DATE_TRUNC('day', "createdAt") as day, COUNT(*) as activities FROM "Activity" WHERE "organizationId" = ${organizationId} AND "createdAt" >= NOW() - INTERVAL '30 days' GROUP BY DATE_TRUNC('day', "createdAt") ORDER BY day DESC `; res.status(200).json({ usageData: usageData.reverse(), // Oldest first for charts memberGrowth, activityTrends }); } catch (error) { console.error('Error fetching overview metrics:', error); res.status(500).json({ error: 'Failed to fetch overview metrics' }); } } async function getUsageMetrics(req: NextApiRequest, res: NextApiResponse) { const { organizationId, period = '12' } = req.query; try { const months = parseInt(period as string) || 12; const usageData = await prisma.usage.findMany({ where: { organizationId: organizationId as string }, orderBy: [{ year: 'desc' }, { month: 'desc' }], take: months }); // Calculate usage trends const trends = usageData.reverse().map((usage, index, arr) => { const previous = index > 0 ? arr[index - 1] : null; return { ...usage, trends: previous ? { users: previous.users > 0 ? ((usage.users - previous.users) / previous.users) * 100 : 0, apiRequests: previous.apiRequests > 0 ? ((usage.apiRequests - previous.apiRequests) / previous.apiRequests) * 100 : 0, storage: previous.storage > 0 ? ((usage.storage - previous.storage) / previous.storage) * 100 : 0, bandwidth: previous.bandwidth > 0 ? ((usage.bandwidth - previous.bandwidth) / previous.bandwidth) * 100 : 0 } : null }; }); res.status(200).json({ usageData: trends }); } catch (error) { console.error('Error fetching usage metrics:', error); res.status(500).json({ error: 'Failed to fetch usage metrics' }); } } async function getMemberMetrics(req: NextApiRequest, res: NextApiResponse) { const { organizationId } = req.query; try { const members = await prisma.organizationMember.findMany({ where: { organizationId: organizationId as string }, include: { user: { select: { id: true, name: true, email: true, image: true, createdAt: true } } }, orderBy: { joinedAt: 'desc' } }); // Role distribution const roleDistribution = members.reduce((acc, member) => { acc[member.role] = (acc[member.role] || 0) + 1; return acc; }, {} as Record<string, number>); // Activity levels const now = new Date(); const activityLevels = { active: 0, // Last 7 days recent: 0, // Last 30 days inactive: 0 // 30+ days or never }; members.forEach(member => { if (!member.lastActiveAt) { activityLevels.inactive++; } else { const daysSinceActive = (now.getTime() - member.lastActiveAt.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceActive <= 7) { activityLevels.active++; } else if (daysSinceActive <= 30) { activityLevels.recent++; } else { activityLevels.inactive++; } } }); // Member growth by month const membersByMonth = await prisma.$queryRaw` SELECT DATE_TRUNC('month', "joinedAt") as month, COUNT(*) as count FROM "OrganizationMember" WHERE "organizationId" = ${organizationId} GROUP BY DATE_TRUNC('month', "joinedAt") ORDER BY month DESC LIMIT 12 `; res.status(200).json({ members, roleDistribution, activityLevels, membersByMonth, totalMembers: members.length }); } catch (error) { console.error('Error fetching member metrics:', error); res.status(500).json({ error: 'Failed to fetch member metrics' }); } } async function getActivityMetrics(req: NextApiRequest, res: NextApiResponse) { const { organizationId, days = '30' } = req.query; try { const daysBack = parseInt(days as string) || 30; const startDate = new Date(); startDate.setDate(startDate.getDate() - daysBack); const activities = await prisma.activity.findMany({ where: { organizationId: organizationId as string, createdAt: { gte: startDate } }, include: { user: { select: { name: true, email: true, image: true } } }, orderBy: { createdAt: 'desc' } }); // Activity types distribution const actionDistribution = activities.reduce((acc, activity) => { acc[activity.action] = (acc[activity.action] || 0) + 1; return acc; }, {} as Record<string, number>); // Daily activity const dailyActivity = await prisma.$queryRaw` SELECT DATE_TRUNC('day', "createdAt") as day, COUNT(*) as count FROM "Activity" WHERE "organizationId" = ${organizationId} AND "createdAt" >= ${startDate} GROUP BY DATE_TRUNC('day', "createdAt") ORDER BY day DESC `; // Most active users const userActivity = await prisma.$queryRaw` SELECT u.name, u.email, u.image, COUNT(a.id) as activity_count FROM "Activity" a JOIN "User" u ON a."userId" = u.id WHERE a."organizationId" = ${organizationId} AND a."createdAt" >= ${startDate} GROUP BY u.id, u.name, u.email, u.image ORDER BY activity_count DESC LIMIT 10 `; res.status(200).json({ activities: activities.slice(0, 50), // Recent 50 activities actionDistribution, dailyActivity, userActivity, totalActivities: activities.length }); } catch (error) { console.error('Error fetching activity metrics:', error); res.status(500).json({ error: 'Failed to fetch activity metrics' }); } } async function getBillingMetrics(req: NextApiRequest, res: NextApiResponse) { const { organizationId } = req.query; try { const organization = await prisma.organization.findUnique({ where: { id: organizationId as string }, include: { plan: true, subscription: true } }); if (!organization) { return res.status(404).json({ error: 'Organization not found' }); } // Calculate MRR (Monthly Recurring Revenue) const mrr = organization.plan?.price || 0; // Usage costs (if any overages) const currentMonth = new Date().getMonth() + 1; const currentYear = new Date().getFullYear(); const currentUsage = await prisma.usage.findUnique({ where: { organizationId_month_year: { organizationId: organizationId as string, month: currentMonth, year: currentYear } } }); // Calculate overages const overages = { users: Math.max(0, (currentUsage?.users || 0) - organization.userLimit), storage: Math.max(0, (currentUsage?.storage || 0) - organization.storageLimit), api: Math.max(0, (currentUsage?.apiRequests || 0) - organization.apiLimit) }; // Subscription status let subscriptionStatus = 'unknown'; let nextBillingDate = null; if (organization.subscription) { subscriptionStatus = organization.subscription.status.toLowerCase(); nextBillingDate = organization.subscription.currentPeriodEnd; } const billingMetrics = { plan: organization.plan, subscription: organization.subscription, mrr, overages, subscriptionStatus, nextBillingDate, trialEndsAt: organization.trialEndsAt, isTrialing: organization.trialEndsAt && organization.trialEndsAt > new Date() }; res.status(200).json({ billingMetrics }); } catch (error) { console.error('Error fetching billing metrics:', error); res.status(500).json({ error: 'Failed to fetch billing metrics' }); } } // Helper function to calculate organization health score function calculateHealthScore(planUtilization: any, activeMembers: number, totalMembers: number): number { let score = 100; // Deduct points for high utilization (risk of hitting limits) if (planUtilization.users.percentage > 90) score -= 20; else if (planUtilization.users.percentage > 75) score -= 10; if (planUtilization.storage.percentage > 90) score -= 15; else if (planUtilization.storage.percentage > 75) score -= 8; if (planUtilization.api.percentage > 90) score -= 15; else if (planUtilization.api.percentage > 75) score -= 8; // Deduct points for low member activity const activityRate = totalMembers > 0 ? (activeMembers / totalMembers) * 100 : 0; if (activityRate < 30) score -= 25; else if (activityRate < 60) score -= 15; else if (activityRate < 80) score -= 5; return Math.max(0, Math.min(100, score)); }