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

536 lines (476 loc) 16.5 kB
import { NextApiRequest, NextApiResponse } from 'next'; import { PrismaClient } from '@prisma/client'; import { getSession } from 'next-auth/react'; import { z } from 'zod'; const prisma = new PrismaClient(); // Validation schemas const createProjectSchema = z.object({ name: z.string().min(1, 'Project name is required').max(100, 'Name too long'), description: z.string().max(2000, 'Description too long').optional(), key: z.string().min(2, 'Project key too short').max(10, 'Project key too long').regex(/^[A-Z][A-Z0-9]*$/, 'Key must be uppercase letters and numbers'), avatar: z.string().url().optional(), color: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid color format').optional(), // Project settings projectType: z.enum(['SOFTWARE', 'MARKETING', 'DESIGN', 'RESEARCH', 'BUSINESS', 'PERSONAL', 'OTHER']).default('SOFTWARE'), methodology: z.enum(['AGILE', 'SCRUM', 'KANBAN', 'WATERFALL', 'HYBRID', 'CUSTOM']).default('AGILE'), visibility: z.enum(['PRIVATE', 'TEAM', 'ORGANIZATION', 'PUBLIC']).default('PRIVATE'), priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).default('MEDIUM'), // Timeline startDate: z.string().datetime().optional(), endDate: z.string().datetime().optional(), estimatedHours: z.number().min(0).optional(), // Business budget: z.number().min(0).optional(), client: z.string().max(100).optional(), // Organization organizationId: z.string().optional(), // Project template template: z.record(z.any()).optional(), settings: z.record(z.any()).optional() }); const updateProjectSchema = createProjectSchema.partial(); async function hasProjectPermission(userId: string, projectId?: string, organizationId?: string, action: string = 'read') { // If creating a new project if (!projectId && organizationId) { const membership = await prisma.organizationMember.findUnique({ where: { organizationId_userId: { organizationId, userId } } }); return membership && ['OWNER', 'ADMIN', 'MANAGER'].includes(membership.role); } // If accessing existing project if (projectId) { const project = await prisma.project.findUnique({ where: { id: projectId }, include: { members: { where: { userId } }, organization: { include: { members: { where: { userId } } } } } }); if (!project) return false; // Project owner has all permissions if (project.ownerId === userId) return true; // Check project membership const projectMember = project.members[0]; if (projectMember) { switch (action) { case 'read': return true; case 'update': return ['OWNER', 'ADMIN', 'MANAGER'].includes(projectMember.role); case 'delete': return ['OWNER', 'ADMIN'].includes(projectMember.role); case 'manage_members': return ['OWNER', 'ADMIN', 'MANAGER'].includes(projectMember.role); default: return false; } } // Check organization membership for organization projects if (project.organizationId && project.organization?.members[0]) { const orgMember = project.organization.members[0]; switch (action) { case 'read': return project.visibility === 'ORGANIZATION' || project.visibility === 'PUBLIC'; case 'update': case 'delete': return ['OWNER', 'ADMIN'].includes(orgMember.role); default: return false; } } // Public projects are readable by everyone if (project.visibility === 'PUBLIC' && action === 'read') { return true; } } return false; } 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 } = req; switch (method) { case 'GET': return await getProjects(req, res); case 'POST': return await createProject(req, res); default: res.setHeader('Allow', ['GET', 'POST']); return res.status(405).end('Method Not Allowed'); } } catch (error) { console.error('Projects API error:', error); return res.status(500).json({ error: 'Internal server error' }); } finally { await prisma.$disconnect(); } } async function getProjects(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { organizationId, status = 'ACTIVE', type, visibility, limit = '20', page = '1', search, sortBy = 'updatedAt', sortOrder = 'desc' } = req.query; try { const pageNum = parseInt(page as string); const limitNum = parseInt(limit as string); const offset = (pageNum - 1) * limitNum; // Build where clause const whereClause: any = { OR: [ // User's own projects { ownerId: session.user.id }, // Projects where user is a member { members: { some: { userId: session.user.id, isActive: true } } } ] }; // Add filters if (status && status !== 'ALL') { whereClause.status = status; } if (type) { whereClause.projectType = type; } if (visibility) { whereClause.visibility = visibility; } if (organizationId && typeof organizationId === 'string') { whereClause.organizationId = organizationId; } if (search && typeof search === 'string') { whereClause.OR = [ ...whereClause.OR, { name: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, { key: { contains: search, mode: 'insensitive' } } ]; } // Get projects with comprehensive data const projects = await prisma.project.findMany({ where: whereClause, include: { owner: { select: { id: true, name: true, email: true, image: true } }, organization: { select: { id: true, name: true, slug: true } }, members: { where: { isActive: true }, include: { user: { select: { id: true, name: true, email: true, image: true } } }, take: 10 }, _count: { select: { tasks: true, sprints: true, milestones: true, timeEntries: true } } }, orderBy: { [sortBy as string]: sortOrder }, skip: offset, take: limitNum }); // Calculate project statistics const enrichedProjects = await Promise.all( projects.map(async (project) => { // Get task statistics const taskStats = await prisma.task.groupBy({ by: ['status'], where: { projectId: project.id }, _count: true }); const tasksByStatus = taskStats.reduce((acc: any, stat) => { acc[stat.status.toLowerCase()] = stat._count; return acc; }, {}); // Get recent activity count const recentActivityCount = await prisma.projectActivity.count({ where: { projectId: project.id, createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Last 7 days } } }); // Calculate progress const totalTasks = Object.values(tasksByStatus).reduce((sum: number, count: any) => sum + count, 0); const completedTasks = tasksByStatus.done || 0; const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; return { ...project, stats: { totalTasks, completedTasks, progress, tasksByStatus, totalMembers: project.members.length, recentActivity: recentActivityCount, totalSprints: project._count.sprints, totalMilestones: project._count.milestones, totalTimeEntries: project._count.timeEntries } }; }) ); // Get total count for pagination const total = await prisma.project.count({ where: whereClause }); res.status(200).json({ projects: enrichedProjects, pagination: { page: pageNum, limit: limitNum, total, pages: Math.ceil(total / limitNum) } }); } catch (error) { console.error('Error fetching projects:', error); res.status(500).json({ error: 'Failed to fetch projects' }); } } async function createProject(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); try { const validatedData = createProjectSchema.parse(req.body); // Check organization permissions if specified if (validatedData.organizationId) { const hasAccess = await hasProjectPermission(session.user.id, undefined, validatedData.organizationId, 'create'); if (!hasAccess) { return res.status(403).json({ error: 'Insufficient permissions to create project in this organization' }); } } // Check if project key is unique const existingProject = await prisma.project.findUnique({ where: { key: validatedData.key } }); if (existingProject) { return res.status(400).json({ error: 'Project key already exists' }); } // Check user project limits const userProjectCount = await prisma.project.count({ where: { ownerId: session.user.id, status: { not: 'ARCHIVED' } } }); const maxProjects = validatedData.organizationId ? 100 : 10; // Higher limit for organizations if (userProjectCount >= maxProjects) { return res.status(400).json({ error: `Maximum project limit reached (${maxProjects})` }); } // Create the project const project = await prisma.project.create({ data: { ...validatedData, ownerId: session.user.id, startDate: validatedData.startDate ? new Date(validatedData.startDate) : undefined, endDate: validatedData.endDate ? new Date(validatedData.endDate) : undefined }, include: { owner: { select: { id: true, name: true, email: true, image: true } }, organization: { select: { id: true, name: true, slug: true } } } }); // Add creator as project owner await prisma.projectMember.create({ data: { projectId: project.id, userId: session.user.id, role: 'OWNER' } }); // Create default workflow if methodology is specified if (validatedData.methodology) { await createDefaultWorkflow(project.id, validatedData.methodology); } // Create default categories and labels await createDefaultProjectStructure(project.id, validatedData.projectType); // Log project creation activity await prisma.projectActivity.create({ data: { projectId: project.id, userId: session.user.id, action: 'project_created', entityType: 'project', entityId: project.id, description: `Project "${project.name}" was created` } }); res.status(201).json({ project, message: 'Project created successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error creating project:', error); res.status(500).json({ error: 'Failed to create project' }); } } // Helper function to create default workflow async function createDefaultWorkflow(projectId: string, methodology: string) { const workflowConfigs: Record<string, any> = { AGILE: { steps: ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'TESTING', 'DONE'], transitions: [ { from: 'TODO', to: 'IN_PROGRESS' }, { from: 'IN_PROGRESS', to: 'IN_REVIEW' }, { from: 'IN_REVIEW', to: 'TESTING' }, { from: 'TESTING', to: 'DONE' }, { from: 'IN_REVIEW', to: 'IN_PROGRESS' }, { from: 'TESTING', to: 'IN_REVIEW' } ] }, KANBAN: { steps: ['TODO', 'IN_PROGRESS', 'DONE'], transitions: [ { from: 'TODO', to: 'IN_PROGRESS' }, { from: 'IN_PROGRESS', to: 'DONE' }, { from: 'IN_PROGRESS', to: 'TODO' } ] }, SCRUM: { steps: ['BACKLOG', 'SPRINT_PLANNING', 'IN_PROGRESS', 'IN_REVIEW', 'TESTING', 'DONE'], transitions: [ { from: 'BACKLOG', to: 'SPRINT_PLANNING' }, { from: 'SPRINT_PLANNING', to: 'IN_PROGRESS' }, { from: 'IN_PROGRESS', to: 'IN_REVIEW' }, { from: 'IN_REVIEW', to: 'TESTING' }, { from: 'TESTING', to: 'DONE' } ] } }; const config = workflowConfigs[methodology] || workflowConfigs.AGILE; await prisma.workflow.create({ data: { projectId, name: `${methodology} Workflow`, description: `Default ${methodology.toLowerCase()} workflow`, config, isDefault: true, isActive: true } }); } // Helper function to create default project structure async function createDefaultProjectStructure(projectId: string, projectType: string) { const categoryConfigs: Record<string, any[]> = { SOFTWARE: [ { name: 'Frontend', description: 'Frontend development tasks', color: '#3B82F6', icon: '🎨' }, { name: 'Backend', description: 'Backend development tasks', color: '#10B981', icon: '⚙️' }, { name: 'DevOps', description: 'DevOps and infrastructure tasks', color: '#F59E0B', icon: '🚀' }, { name: 'Testing', description: 'Testing and QA tasks', color: '#EF4444', icon: '🧪' }, { name: 'Documentation', description: 'Documentation tasks', color: '#8B5CF6', icon: '📚' } ], MARKETING: [ { name: 'Content', description: 'Content creation and marketing', color: '#EC4899', icon: '✍️' }, { name: 'Social Media', description: 'Social media management', color: '#3B82F6', icon: '📱' }, { name: 'SEO', description: 'Search engine optimization', color: '#10B981', icon: '🔍' }, { name: 'Analytics', description: 'Marketing analytics and reporting', color: '#F59E0B', icon: '📊' } ], DESIGN: [ { name: 'UI Design', description: 'User interface design', color: '#3B82F6', icon: '🎨' }, { name: 'UX Research', description: 'User experience research', color: '#10B981', icon: '🔬' }, { name: 'Prototyping', description: 'Prototyping and wireframing', color: '#F59E0B', icon: '🖼️' }, { name: 'Branding', description: 'Brand identity and guidelines', color: '#EF4444', icon: '🎯' } ] }; const categories = categoryConfigs[projectType] || categoryConfigs.SOFTWARE; for (const category of categories) { await prisma.taskCategory.create({ data: { projectId, ...category } }); } // Create default labels const defaultLabels = [ { name: 'urgent', color: '#EF4444' }, { name: 'bug', color: '#F59E0B' }, { name: 'feature', color: '#10B981' }, { name: 'enhancement', color: '#3B82F6' }, { name: 'research', color: '#8B5CF6' } ]; for (const label of defaultLabels) { await prisma.taskLabel.create({ data: { projectId, ...label } }); } }