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

761 lines (676 loc) 23 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 createAppointmentSchema = z.object({ doctorId: z.string(), scheduledAt: z.string().datetime(), duration: z.number().min(15).max(180).default(30), type: z.enum(['CONSULTATION', 'FOLLOW_UP', 'PHYSICAL_EXAM', 'VACCINATION', 'MENTAL_HEALTH', 'SPECIALIST_REFERRAL', 'EMERGENCY', 'TELEMEDICINE']).default('CONSULTATION'), method: z.enum(['IN_PERSON', 'TELEMEDICINE', 'PHONE_CALL', 'HOME_VISIT']).default('IN_PERSON'), reason: z.string().max(500).optional(), symptoms: z.string().max(1000).optional(), notes: z.string().max(1000).optional(), isFollowUp: z.boolean().default(false), followUpFor: z.string().optional() }); const updateAppointmentSchema = z.object({ status: z.enum(['SCHEDULED', 'CONFIRMED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'NO_SHOW', 'RESCHEDULED']).optional(), scheduledAt: z.string().datetime().optional(), reason: z.string().max(500).optional(), symptoms: z.string().max(1000).optional(), notes: z.string().max(1000).optional(), cancellationReason: z.string().max(500).optional() }); const consultationSchema = z.object({ appointmentId: z.string(), chiefComplaint: z.string().max(1000).optional(), historyOfPresentIllness: z.string().max(2000).optional(), pastMedicalHistory: z.string().max(2000).optional(), familyHistory: z.string().max(1000).optional(), socialHistory: z.string().max(1000).optional(), physicalExam: z.string().max(2000).optional(), vitalSigns: z.object({ systolicBP: z.number().optional(), diastolicBP: z.number().optional(), heartRate: z.number().optional(), temperature: z.number().optional(), respiratoryRate: z.number().optional(), oxygenSaturation: z.number().optional(), height: z.number().optional(), weight: z.number().optional(), painLevel: z.number().min(0).max(10).optional() }).optional(), assessment: z.string().max(2000).optional(), diagnosis: z.string().max(1000).optional(), treatmentPlan: z.string().max(2000).optional(), followUpInstructions: z.string().max(1000).optional(), followUpRequired: z.boolean().default(false), followUpDate: z.string().datetime().optional(), followUpType: z.enum(['CONSULTATION', 'FOLLOW_UP', 'PHYSICAL_EXAM', 'VACCINATION', 'MENTAL_HEALTH', 'SPECIALIST_REFERRAL', 'EMERGENCY', 'TELEMEDICINE']).optional(), consultationNotes: z.string().max(5000).optional(), privateNotes: z.string().max(2000).optional() }); async function hasAppointmentAccess(userId: string, appointmentId: string, action: string = 'read') { const appointment = await prisma.appointment.findUnique({ where: { id: appointmentId }, include: { patient: { include: { user: true } }, doctor: { include: { user: true } } } }); if (!appointment) return false; const isPatient = appointment.patient.userId === userId; const isDoctor = appointment.doctor.userId === userId; switch (action) { case 'read': return isPatient || isDoctor; case 'update': return isPatient || isDoctor; case 'cancel': return isPatient || isDoctor; case 'consult': return isDoctor; // Only doctors can create consultations default: 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; const { action } = req.query; switch (method) { case 'GET': return await getAppointments(req, res); case 'POST': if (action === 'consultation') { return await createConsultation(req, res); } return await createAppointment(req, res); case 'PUT': return await updateAppointment(req, res); case 'DELETE': return await cancelAppointment(req, res); default: res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE']); return res.status(405).end('Method Not Allowed'); } } catch (error) { console.error('Appointments API error:', error); return res.status(500).json({ error: 'Internal server error' }); } finally { await prisma.$disconnect(); } } async function getAppointments(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { doctorId, patientId, status, type, method: appointmentMethod, startDate, endDate, limit = '20', page = '1', upcoming = 'false', appointmentId } = req.query; try { // Get single appointment with detailed information if (appointmentId && typeof appointmentId === 'string') { if (!(await hasAppointmentAccess(session.user.id, appointmentId, 'read'))) { return res.status(403).json({ error: 'Access denied to this appointment' }); } const appointment = await prisma.appointment.findUnique({ where: { id: appointmentId }, include: { patient: { include: { user: { select: { id: true, name: true, email: true, image: true } } } }, doctor: { include: { user: { select: { id: true, name: true, email: true, image: true } } } }, consultation: { include: { prescriptions: true, labOrders: { include: { result: true } }, medicalRecord: true } }, billingRecord: true } }); if (!appointment) { return res.status(404).json({ error: 'Appointment not found' }); } return res.status(200).json({ appointment }); } const pageNum = parseInt(page as string); const limitNum = parseInt(limit as string); const offset = (pageNum - 1) * limitNum; // Build where clause based on user role and filters const whereClause: any = {}; // Get user's role (doctor or patient) const userProfile = await prisma.user.findUnique({ where: { id: session.user.id }, include: { doctorProfile: true, patientProfile: true } }); if (!userProfile?.doctorProfile && !userProfile?.patientProfile) { return res.status(403).json({ error: 'User must have a doctor or patient profile' }); } // Filter by user role if (userProfile.doctorProfile) { whereClause.doctorId = userProfile.doctorProfile.id; } else if (userProfile.patientProfile) { whereClause.patientId = userProfile.patientProfile.id; } // Override with specific doctor/patient if provided (for admin or cross-access) if (doctorId && typeof doctorId === 'string') { whereClause.doctorId = doctorId; } if (patientId && typeof patientId === 'string') { whereClause.patientId = patientId; } // Apply filters if (status && typeof status === 'string') { whereClause.status = status.toUpperCase(); } if (type && typeof type === 'string') { whereClause.type = type.toUpperCase(); } if (appointmentMethod && typeof appointmentMethod === 'string') { whereClause.method = appointmentMethod.toUpperCase(); } // Date range filtering if (startDate || endDate || upcoming === 'true') { whereClause.scheduledAt = {}; if (upcoming === 'true') { whereClause.scheduledAt.gte = new Date(); } else { if (startDate && typeof startDate === 'string') { whereClause.scheduledAt.gte = new Date(startDate); } if (endDate && typeof endDate === 'string') { whereClause.scheduledAt.lte = new Date(endDate); } } } const appointments = await prisma.appointment.findMany({ where: whereClause, include: { patient: { include: { user: { select: { id: true, name: true, email: true, image: true } } } }, doctor: { include: { user: { select: { id: true, name: true, email: true, image: true } } } }, consultation: { select: { id: true, startedAt: true, endedAt: true, duration: true, isTelemedicine: true } }, billingRecord: { select: { id: true, amount: true, paymentStatus: true, billingStatus: true } } }, orderBy: { scheduledAt: 'asc' }, skip: offset, take: limitNum }); // Get total count for pagination const total = await prisma.appointment.count({ where: whereClause }); res.status(200).json({ appointments, pagination: { page: pageNum, limit: limitNum, total, pages: Math.ceil(total / limitNum) } }); } catch (error) { console.error('Error fetching appointments:', error); res.status(500).json({ error: 'Failed to fetch appointments' }); } } async function createAppointment(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); try { const validatedData = createAppointmentSchema.parse(req.body); // Get patient profile for current user const userProfile = await prisma.user.findUnique({ where: { id: session.user.id }, include: { patientProfile: true } }); if (!userProfile?.patientProfile) { return res.status(403).json({ error: 'Only patients can book appointments' }); } // Verify doctor exists and is active const doctor = await prisma.doctor.findUnique({ where: { id: validatedData.doctorId }, include: { user: true } }); if (!doctor || !doctor.isActive || !doctor.isVerified) { return res.status(400).json({ error: 'Doctor not available for appointments' }); } // Check if appointment time is available const scheduledAt = new Date(validatedData.scheduledAt); const endTime = new Date(scheduledAt.getTime() + validatedData.duration * 60000); const conflictingAppointment = await prisma.appointment.findFirst({ where: { doctorId: validatedData.doctorId, status: { in: ['SCHEDULED', 'CONFIRMED', 'IN_PROGRESS'] }, AND: [ { scheduledAt: { lt: endTime } }, { scheduledAt: { gte: new Date(scheduledAt.getTime() - 30 * 60000) // 30 minutes buffer } } ] } }); if (conflictingAppointment) { return res.status(400).json({ error: 'Time slot not available' }); } // Create the appointment const appointment = await prisma.appointment.create({ data: { patientId: userProfile.patientProfile.id, doctorId: validatedData.doctorId, scheduledAt, duration: validatedData.duration, type: validatedData.type, method: validatedData.method, reason: validatedData.reason, symptoms: validatedData.symptoms, notes: validatedData.notes, isFollowUp: validatedData.isFollowUp, followUpFor: validatedData.followUpFor, status: 'SCHEDULED' }, include: { patient: { include: { user: { select: { name: true, email: true } } } }, doctor: { include: { user: { select: { name: true, email: true } } } } } }); // Update doctor's total patients count if first appointment const existingAppointments = await prisma.appointment.count({ where: { doctorId: validatedData.doctorId, patientId: userProfile.patientProfile.id, id: { not: appointment.id } } }); if (existingAppointments === 0) { await prisma.doctor.update({ where: { id: validatedData.doctorId }, data: { totalPatients: { increment: 1 } } }); } // Create billing record await prisma.billingRecord.create({ data: { patientId: userProfile.patientProfile.id, appointmentId: appointment.id, serviceDate: scheduledAt, serviceDescription: `${validatedData.type} - ${doctor.user.name}`, serviceCode: getServiceCode(validatedData.type), amount: doctor.consultationFee, currency: doctor.currency, patientResponsibility: doctor.consultationFee, billingStatus: 'DRAFT' } }); res.status(201).json({ appointment, message: 'Appointment scheduled successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error creating appointment:', error); res.status(500).json({ error: 'Failed to create appointment' }); } } async function updateAppointment(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { appointmentId } = req.query; if (!appointmentId || typeof appointmentId !== 'string') { return res.status(400).json({ error: 'Appointment ID required' }); } try { const validatedData = updateAppointmentSchema.parse(req.body); if (!(await hasAppointmentAccess(session.user.id, appointmentId, 'update'))) { return res.status(403).json({ error: 'Access denied to this appointment' }); } const updateData: any = {}; if (validatedData.status) { updateData.status = validatedData.status; if (validatedData.status === 'CANCELLED') { updateData.cancelledAt = new Date(); updateData.cancelledBy = session.user.id; updateData.cancellationReason = validatedData.cancellationReason; } } if (validatedData.scheduledAt) { updateData.scheduledAt = new Date(validatedData.scheduledAt); } if (validatedData.reason !== undefined) { updateData.reason = validatedData.reason; } if (validatedData.symptoms !== undefined) { updateData.symptoms = validatedData.symptoms; } if (validatedData.notes !== undefined) { updateData.notes = validatedData.notes; } const appointment = await prisma.appointment.update({ where: { id: appointmentId }, data: updateData, include: { patient: { include: { user: { select: { name: true, email: true } } } }, doctor: { include: { user: { select: { name: true, email: true } } } } } }); res.status(200).json({ appointment, message: 'Appointment updated successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error updating appointment:', error); res.status(500).json({ error: 'Failed to update appointment' }); } } async function cancelAppointment(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { appointmentId } = req.query; if (!appointmentId || typeof appointmentId !== 'string') { return res.status(400).json({ error: 'Appointment ID required' }); } try { if (!(await hasAppointmentAccess(session.user.id, appointmentId, 'cancel'))) { return res.status(403).json({ error: 'Access denied to this appointment' }); } const { reason } = req.body; const appointment = await prisma.appointment.update({ where: { id: appointmentId }, data: { status: 'CANCELLED', cancelledAt: new Date(), cancelledBy: session.user.id, cancellationReason: reason } }); // Update billing record await prisma.billingRecord.updateMany({ where: { appointmentId }, data: { billingStatus: 'CANCELLED' } }); res.status(200).json({ appointment, message: 'Appointment cancelled successfully' }); } catch (error) { console.error('Error cancelling appointment:', error); res.status(500).json({ error: 'Failed to cancel appointment' }); } } async function createConsultation(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); try { const validatedData = consultationSchema.parse(req.body); if (!(await hasAppointmentAccess(session.user.id, validatedData.appointmentId, 'consult'))) { return res.status(403).json({ error: 'Only the assigned doctor can create consultations' }); } // Get appointment details const appointment = await prisma.appointment.findUnique({ where: { id: validatedData.appointmentId }, include: { patient: true, doctor: true, consultation: true } }); if (!appointment) { return res.status(404).json({ error: 'Appointment not found' }); } if (appointment.consultation) { return res.status(400).json({ error: 'Consultation already exists for this appointment' }); } // Create consultation const consultation = await prisma.consultation.create({ data: { appointmentId: validatedData.appointmentId, patientId: appointment.patientId, doctorId: appointment.doctorId, chiefComplaint: validatedData.chiefComplaint, historyOfPresentIllness: validatedData.historyOfPresentIllness, pastMedicalHistory: validatedData.pastMedicalHistory, familyHistory: validatedData.familyHistory, socialHistory: validatedData.socialHistory, physicalExam: validatedData.physicalExam, vitalSigns: validatedData.vitalSigns ? JSON.stringify(validatedData.vitalSigns) : null, assessment: validatedData.assessment, diagnosis: validatedData.diagnosis, treatmentPlan: validatedData.treatmentPlan, followUpInstructions: validatedData.followUpInstructions, followUpRequired: validatedData.followUpRequired, followUpDate: validatedData.followUpDate ? new Date(validatedData.followUpDate) : null, followUpType: validatedData.followUpType, consultationNotes: validatedData.consultationNotes, privateNotes: validatedData.privateNotes, isTelemedicine: appointment.method === 'TELEMEDICINE' }, include: { patient: { include: { user: { select: { name: true, email: true } } } }, doctor: { include: { user: { select: { name: true, email: true } } } } } }); // Update appointment status await prisma.appointment.update({ where: { id: validatedData.appointmentId }, data: { status: 'COMPLETED' } }); // Update doctor consultation count await prisma.doctor.update({ where: { id: appointment.doctorId }, data: { totalConsultations: { increment: 1 } } }); // Create medical record if (validatedData.diagnosis || validatedData.assessment || validatedData.treatmentPlan) { await prisma.medicalRecord.create({ data: { patientId: appointment.patientId, doctorId: appointment.doctorId, consultationId: consultation.id, type: 'CONSULTATION_NOTE', title: `Consultation - ${new Date().toLocaleDateString()}`, content: ` Chief Complaint: ${validatedData.chiefComplaint || 'N/A'} Assessment: ${validatedData.assessment || 'N/A'} Diagnosis: ${validatedData.diagnosis || 'N/A'} Treatment Plan: ${validatedData.treatmentPlan || 'N/A'} `.trim(), summary: validatedData.assessment || 'Consultation completed', diagnosis: validatedData.diagnosis ? [validatedData.diagnosis] : [], symptoms: validatedData.chiefComplaint ? [validatedData.chiefComplaint] : [], treatments: validatedData.treatmentPlan ? [validatedData.treatmentPlan] : [] } }); } res.status(201).json({ consultation, message: 'Consultation created successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error creating consultation:', error); res.status(500).json({ error: 'Failed to create consultation' }); } } // Helper function to get service codes function getServiceCode(appointmentType: string): string { const serviceCodes: Record<string, string> = { 'CONSULTATION': '99213', 'FOLLOW_UP': '99212', 'PHYSICAL_EXAM': '99396', 'VACCINATION': '90471', 'MENTAL_HEALTH': '90834', 'SPECIALIST_REFERRAL': '99245', 'EMERGENCY': '99282', 'TELEMEDICINE': '99421' }; return serviceCodes[appointmentType] || '99213'; }