import { PrismaClient } from '@prisma/client'; import { ExportFormat } from '../../constants/export.constants'; import { BookingExportData, UserExportData, PaymentExportData, TournamentResultExportData, ExcelWorkbookData, ExportFilters } from '../../types/analytics.types'; import { formatDateForExport } from '../../utils/export'; const prisma = new PrismaClient(); /** * Exporta reservas a CSV/JSON */ export async function exportBookings( startDate: Date, endDate: Date, format: ExportFormat = ExportFormat.CSV ): Promise<{ data: string | Buffer; filename: string }> { const bookings = await prisma.booking.findMany({ where: { date: { gte: startDate, lte: endDate } }, include: { user: { select: { firstName: true, lastName: true, email: true } }, court: { select: { name: true } } }, orderBy: { date: 'asc' } }); const exportData: BookingExportData[] = bookings.map(booking => ({ id: booking.id, user: `${booking.user.firstName} ${booking.user.lastName}`, userEmail: booking.user.email, court: booking.court.name, date: formatDateForExport(booking.date), time: booking.startTime, price: booking.price, status: booking.status, createdAt: formatDateForExport(booking.createdAt) })); const filename = `reservas_${startDate.toISOString().split('T')[0]}_${endDate.toISOString().split('T')[0]}`; if (format === ExportFormat.JSON) { return { data: JSON.stringify(exportData, null, 2), filename: `${filename}.json` }; } // CSV por defecto const headers = { id: 'ID', user: 'Usuario', userEmail: 'Email', court: 'Pista', date: 'Fecha', time: 'Hora', price: 'Precio', status: 'Estado', createdAt: 'Fecha Creación' }; const csv = convertToCSV(exportData, headers); return { data: csv, filename: `${filename}.csv` }; } /** * Exporta usuarios a CSV/JSON */ export async function exportUsers( format: ExportFormat = ExportFormat.CSV, filters: ExportFilters = {} ): Promise<{ data: string | Buffer; filename: string }> { const where: Record = {}; if (filters.level) { where.level = filters.level; } if (filters.city) { where.city = filters.city; } const users = await prisma.user.findMany({ where, include: { _count: { select: { bookings: true } } }, orderBy: { createdAt: 'desc' } }); const exportData: UserExportData[] = users.map(user => ({ id: user.id, name: `${user.firstName} ${user.lastName}`, email: user.email, level: user.level || 'Sin nivel', city: user.city || 'Sin ciudad', joinedAt: formatDateForExport(user.createdAt), bookingsCount: user._count.bookings })); const filename = `usuarios_${new Date().toISOString().split('T')[0]}`; if (format === ExportFormat.JSON) { return { data: JSON.stringify(exportData, null, 2), filename: `${filename}.json` }; } const headers = { id: 'ID', name: 'Nombre', email: 'Email', level: 'Nivel', city: 'Ciudad', joinedAt: 'Fecha Registro', bookingsCount: 'Total Reservas' }; const csv = convertToCSV(exportData, headers); return { data: csv, filename: `${filename}.csv` }; } /** * Exporta pagos a CSV/JSON */ export async function exportPayments( startDate: Date, endDate: Date, format: ExportFormat = ExportFormat.CSV ): Promise<{ data: string | Buffer; filename: string }> { // Asumiendo que existe un modelo Payment o similar // Aquí usamos bookings como referencia de pagos const bookings = await prisma.booking.findMany({ where: { createdAt: { gte: startDate, lte: endDate }, status: { in: ['PAID', 'CONFIRMED', 'COMPLETED'] } }, include: { user: { select: { firstName: true, lastName: true, email: true } } }, orderBy: { createdAt: 'asc' } }); const exportData: PaymentExportData[] = bookings.map(booking => ({ id: booking.id, user: `${booking.user.firstName} ${booking.user.lastName}`, userEmail: booking.user.email, type: 'Reserva', amount: booking.price, status: booking.status, date: formatDateForExport(booking.createdAt) })); const filename = `pagos_${startDate.toISOString().split('T')[0]}_${endDate.toISOString().split('T')[0]}`; if (format === ExportFormat.JSON) { return { data: JSON.stringify(exportData, null, 2), filename: `${filename}.json` }; } const headers = { id: 'ID', user: 'Usuario', userEmail: 'Email', type: 'Tipo', amount: 'Monto', status: 'Estado', date: 'Fecha' }; const csv = convertToCSV(exportData, headers); return { data: csv, filename: `${filename}.csv` }; } /** * Exporta resultados de un torneo */ export async function exportTournamentResults( tournamentId: string, format: ExportFormat = ExportFormat.CSV ): Promise<{ data: string | Buffer; filename: string }> { const tournament = await prisma.tournament.findUnique({ where: { id: tournamentId }, include: { participants: { include: { user: { select: { firstName: true, lastName: true, email: true } } } }, matches: { where: { confirmed: true } } } }); if (!tournament) { throw new Error('Torneo no encontrado'); } // Calcular estadísticas por participante const participantStats = new Map(); for (const match of tournament.matches) { // Actualizar estadísticas para cada jugador del partido const matchParticipants = await prisma.tournamentParticipant.findMany({ where: { tournamentId, userId: { in: [match.player1Id, match.player2Id].filter(Boolean) as string[] } } }); for (const participant of matchParticipants) { const stats = participantStats.get(participant.userId) || { matchesPlayed: 0, wins: 0, losses: 0, points: 0 }; stats.matchesPlayed++; if (match.winnerId === participant.userId) { stats.wins++; stats.points += 3; // 3 puntos por victoria } else if (match.winnerId) { stats.losses++; stats.points += 1; // 1 punto por participar } participantStats.set(participant.userId, stats); } } const exportData: TournamentResultExportData[] = tournament.participants .map((participant, index) => { const stats = participantStats.get(participant.userId) || { matchesPlayed: 0, wins: 0, losses: 0, points: 0 }; return { position: index + 1, player: `${participant.user.firstName} ${participant.user.lastName}`, email: participant.user.email, matchesPlayed: stats.matchesPlayed, wins: stats.wins, losses: stats.losses, points: stats.points }; }) .sort((a, b) => b.points - a.points) .map((item, index) => ({ ...item, position: index + 1 })); const filename = `torneo_${tournament.name.replace(/\s+/g, '_').toLowerCase()}_${tournamentId}`; if (format === ExportFormat.JSON) { return { data: JSON.stringify(exportData, null, 2), filename: `${filename}.json` }; } const headers = { position: 'Posición', player: 'Jugador', email: 'Email', matchesPlayed: 'PJ', wins: 'PG', losses: 'PP', points: 'Puntos' }; const csv = convertToCSV(exportData, headers); return { data: csv, filename: `${filename}.csv` }; } /** * Genera un reporte completo en Excel con múltiples hojas */ export async function generateExcelReport( startDate: Date, endDate: Date ): Promise<{ data: Buffer; filename: string }> { // Importar xlsx dinámicamente para evitar problemas si no está instalado const xlsx = await import('xlsx'); // 1. Resumen General const totalBookings = await prisma.booking.count({ where: { date: { gte: startDate, lte: endDate } } }); const totalRevenue = await prisma.booking.aggregate({ where: { date: { gte: startDate, lte: endDate }, status: { in: ['PAID', 'CONFIRMED', 'COMPLETED'] } }, _sum: { price: true } }); const newUsers = await prisma.user.count({ where: { createdAt: { gte: startDate, lte: endDate } } }); const activeUsers = await prisma.user.count({ where: { bookings: { some: { date: { gte: startDate, lte: endDate } } } } }); const summaryData = [{ metrica: 'Total Reservas', valor: totalBookings }, { metrica: 'Ingresos Totales', valor: totalRevenue._sum.price || 0 }, { metrica: 'Nuevos Usuarios', valor: newUsers }, { metrica: 'Usuarios Activos', valor: activeUsers }]; // 2. Datos de Ingresos por Día const bookingsByDay = await prisma.booking.groupBy({ by: ['date'], where: { date: { gte: startDate, lte: endDate }, status: { in: ['PAID', 'CONFIRMED', 'COMPLETED'] } }, _sum: { price: true }, _count: { id: true } }); const revenueData = bookingsByDay.map(day => ({ fecha: day.date.toISOString().split('T')[0], reservas: day._count.id, ingresos: day._sum.price || 0 })); // 3. Datos de Ocupación por Pista const courts = await prisma.court.findMany({ include: { bookings: { where: { date: { gte: startDate, lte: endDate } } } } }); const occupancyData = courts.map(court => ({ pista: court.name, tipo: court.surface || 'No especificado', totalReservas: court.bookings.length, tasaOcupacion: calculateOccupancyRate(court.bookings.length, startDate, endDate) })); // 4. Datos de Usuarios const topUsers = await prisma.user.findMany({ take: 20, include: { bookings: { where: { date: { gte: startDate, lte: endDate } } }, _count: { select: { bookings: true } } }, orderBy: { bookings: { _count: 'desc' } } }); const usersData = topUsers.map(user => ({ nombre: `${user.firstName} ${user.lastName}`, email: user.email, nivel: user.level || 'Sin nivel', reservasPeriodo: user.bookings.length, reservasTotales: user._count.bookings })); // Crear workbook const workbook = xlsx.utils.book_new(); // Hoja 1: Resumen const summarySheet = xlsx.utils.json_to_sheet(summaryData); xlsx.utils.book_append_sheet(workbook, summarySheet, 'Resumen'); // Hoja 2: Ingresos const revenueSheet = xlsx.utils.json_to_sheet(revenueData); xlsx.utils.book_append_sheet(workbook, revenueSheet, 'Ingresos'); // Hoja 3: Ocupación const occupancySheet = xlsx.utils.json_to_sheet(occupancyData); xlsx.utils.book_append_sheet(workbook, occupancySheet, 'Ocupación'); // Hoja 4: Usuarios const usersSheet = xlsx.utils.json_to_sheet(usersData); xlsx.utils.book_append_sheet(workbook, usersSheet, 'Usuarios'); const buffer = xlsx.write(workbook, { type: 'buffer', bookType: 'xlsx' }); const filename = `reporte_completo_${startDate.toISOString().split('T')[0]}_${endDate.toISOString().split('T')[0]}.xlsx`; return { data: buffer, filename }; } /** * Convierte datos a formato CSV */ function convertToCSV(data: Record[], headers: Record): string { const CSV_SEPARATOR = ';'; if (data.length === 0) { return Object.values(headers).join(CSV_SEPARATOR); } const headerRow = Object.values(headers).join(CSV_SEPARATOR); const keys = Object.keys(headers); const rows = data.map(item => { return keys.map(key => { const value = item[key]; if (value === null || value === undefined) { return ''; } if (typeof value === 'string' && (value.includes(CSV_SEPARATOR) || value.includes('"') || value.includes('\n'))) { return `"${value.replace(/"/g, '""')}"`; } return String(value); }).join(CSV_SEPARATOR); }); return [headerRow, ...rows].join('\n'); } /** * Calcula la tasa de ocupación estimada */ function calculateOccupancyRate( bookingCount: number, startDate: Date, endDate: Date ): string { const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); const maxPossibleBookings = daysDiff * 10; // Asumiendo 10 franjas horarias por día const rate = maxPossibleBookings > 0 ? (bookingCount / maxPossibleBookings) * 100 : 0; return `${rate.toFixed(1)}%`; }