import { Types } from 'mongoose'; import { Trip, ITrip } from '../models/Trip'; import { User } from '../models/User'; import { DriverProfile } from '../models/DriverProfile'; import { whatsAppNotificationService } from './whatsapp-notification.service'; import { pushNotificationService } from './push-notification.service'; export interface ScheduleTripParams { tripId: string; adminId: string; conductorId: string; fechaHoraSalida: Date | string; minutosEntreParadas?: number; // Tiempo estimado entre recogida y recogida (default: 15 min) } export class TripSchedulingService { private static instance: TripSchedulingService; private constructor() {} public static getInstance(): TripSchedulingService { if (!TripSchedulingService.instance) { TripSchedulingService.instance = new TripSchedulingService(); } return TripSchedulingService.instance; } /** * Obtiene todos los viajes que tienen cupo completo (4/4) o están listos para ser programados */ public async getViajesListosParaProgramar(): Promise { return Trip.find({ estado: { $in: ['POOL_COMPLETED', 'PENDING_APPROVAL'] }, tipoServicio: 'TRANSPORTE', }) .populate('pasajeros.usuarioId', 'nombre telefono') .sort({ cupoCompleto: -1, createdAt: 1 }) .exec(); } /** * Programa formalmente la hora de salida del viaje, asigna conductor y calcula * los horarios escalonados de recogida para cada pasajero según la ruta Waze */ public async programarViaje(params: ScheduleTripParams): Promise { const { tripId, adminId, conductorId, minutosEntreParadas = 15 } = params; const horaSalida = new Date(params.fechaHoraSalida); const trip = await Trip.findById(tripId); if (!trip) throw new Error(`Viaje ${tripId} no encontrado.`); const conductor = await User.findById(conductorId); if (!conductor || conductor.rol !== 'CONDUCTOR') { throw new Error('Conductor no válido o no registrado con rol CONDUCTOR.'); } const driverProfile = await DriverProfile.findOne({ userId: conductor._id }); if (!driverProfile) { throw new Error('El conductor seleccionado no tiene perfil vehicular registrado.'); } const admin = await User.findById(adminId); // 1. Asignar y actualizar campos de programación trip.conductorId = conductor._id; trip.fechaHoraSalidaProgramada = horaSalida; trip.estado = 'SCHEDULED'; trip.aprobadoPorAdmin = { adminId: admin ? admin._id : new Types.ObjectId(), fechaAprobacion: new Date(), }; // 2. Calcular los horarios de recogida escalonados por pasajero // Si la salida final hacia la autopista es a la horaSalida, calculamos hacia atrás para cada parada const totalParadas = trip.pasajeros.length; const itinerarioParaConductor: Array<{ orden: number; nombre: string; telefono: string; direccionRecogida: string; estadoPago: string; valor: number; horaProgramada: string; }> = []; trip.pasajeros.forEach((p, idx) => { // El orden de recogida (1 a 4): La primera parada se realiza (totalParadas - idx) * minutos antes const minutosAntes = (totalParadas - idx) * minutosEntreParadas; const horaRecogidaDate = new Date(horaSalida.getTime() - minutosAntes * 60 * 1000); const horaRecogidaStr = horaRecogidaDate.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true, }); p.horaEstimadaRecogida = horaRecogidaStr; itinerarioParaConductor.push({ orden: idx + 1, nombre: p.nombre, telefono: p.telefono, direccionRecogida: p.direccionRecogida, estadoPago: p.estadoPago === 'PAGADO_BOLD' ? 'Pagado vía Bold' : p.estadoPago === 'PAGADO_NEQUI' ? 'Pagado vía Nequi' : 'Pago pendiente en efectivo', valor: p.valor, horaProgramada: horaRecogidaStr, }); }); trip.horaEstimadaRecogida = new Date( horaSalida.getTime() - totalParadas * minutosEntreParadas * 60 * 1000 ); await trip.save(); console.log( `🗓️ [Viaje Programado] #${trip._id} programado para salir a las ${horaSalida.toLocaleString( 'es-CO' )} con Conductor ${conductor.nombre}` ); const horaSalidaFormateada = horaSalida.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true, }); const fechaFormateada = horaSalida.toLocaleDateString('es-CO', { weekday: 'long', day: 'numeric', month: 'long', }); // 3. Disparar notificaciones WhatsApp personalizadas a CADA UNO DE LOS PASAJEROS for (const p of trip.pasajeros) { const msgPasajero = `✅ *¡TU VIAJE HA SIDO PROGRAMADO OFICIALMENTE!*\n\n` + `Hola *${p.nombre}*, la central de despacho ha fijado la programación de tu viaje *${trip.origen.ciudad} ➔ ${trip.destino.ciudad}* (#${trip._id .toString() .slice(-6)}).\n\n` + `📅 *Fecha:* ${fechaFormateada}\n` + `⏰ *Tu hora exacta de recogida:* ${p.horaEstimadaRecogida}\n` + `📍 *Dirección:* ${p.direccionRecogida}\n\n` + `🚗 *Datos del Vehículo Asignado:*\n` + `• *Conductor:* ${conductor.nombre}\n` + `• *Vehículo:* ${driverProfile.modeloVehiculo} (${driverProfile.colorVehiculo || 'Blanco'})\n` + `• *Placa:* ${driverProfile.placaVehiculo}\n` + `• *Teléfono:* +${conductor.telefono.replace(/\D/g, '')}\n\n` + `💳 *Estado de Pago:* ${p.estadoPago === 'PAGADO_BOLD' ? 'Pagado vía Bold' : 'Pendiente en efectivo'}\n\n` + `_Por favor permanece listo 5 minutos antes de tu hora de recogida._`; await whatsAppNotificationService.sendTextMessage(p.telefono, msgPasajero); } // 4. Disparar notificación WhatsApp al CONDUCTOR con itinerario cronológico y Waze let cuerpoItinerario = ''; itinerarioParaConductor.forEach((item) => { cuerpoItinerario += `\n⏰ *${item.horaProgramada}* - Parada ${item.orden}: *${item.nombre}*\n` + `• Tel: +${item.telefono.replace(/\D/g, '')}\n` + `• Dir: ${item.direccionRecogida}\n` + `• Pago: ${item.estadoPago} ($${item.valor.toLocaleString('es-CO')})\n`; }); const waze1 = trip.secuenciaRutaWaze?.[0]?.wazeDeepLink || ''; const msgConductor = `🗓️ *ITINERARIO DE VIAJE PROGRAMADO*\n\n` + `¡Hola *${conductor.nombre}*! Se ha programado tu viaje *${trip.origen.ciudad} ➔ ${trip.destino.ciudad}* (#${trip._id .toString() .slice(-6)}).\n\n` + `📅 *Fecha:* ${fechaFormateada}\n` + `🚀 *Hora Inicio Primera Recogida:* ${itinerarioParaConductor[0]?.horaProgramada || horaSalidaFormateada}\n` + `🏁 *Hora Estimada Salida Vía Principal:* ${horaSalidaFormateada}\n\n` + `📋 *Secuencia de Paradas (4 Pasajeros):*` + cuerpoItinerario + `\n💰 *Tu Pago Neto:* $${trip.pagoConductor.toLocaleString('es-CO')} COP\n\n` + `🗺️ *Iniciar Waze a la Primera Parada:*\n👉 ${waze1}`; await whatsAppNotificationService.sendTextMessage(conductor.telefono, msgConductor); return trip; } /** * Sugiere conductores disponibles ordenados según qué tan cerca está su lugar de residencia * de los domicilios de recogida de los pasajeros en Cali */ public async sugerirConductoresPorCercaniaResidencia(tripId: string): Promise< Array<{ conductorId: string; nombre: string; telefono: string; placaVehiculo: string; modeloVehiculo: string; colorVehiculo?: string; distanciaKmAResidencia: number; direccionPrimeraParada: string; }> > { const trip = await Trip.findById(tripId); if (!trip) throw new Error('Viaje no encontrado'); const primeraParada = trip.secuenciaRutaWaze?.[0] || { lat: trip.pasajeros[0]?.coordenadas.coordinates[1], lng: trip.pasajeros[0]?.coordenadas.coordinates[0], direccion: trip.pasajeros[0]?.direccionRecogida, }; if (!primeraParada.lat || !primeraParada.lng) { return []; } const conductores = await DriverProfile.find({ estaActivo: true }) .populate('userId', 'nombre telefono') .lean(); const ranking = []; for (const d of conductores as any[]) { if (!d.userId) continue; const coords = d.zonaResidencia?.coordinates || d.coordenadasActuales?.coordinates; if (!coords) continue; const distMetros = this.calcularDistanciaMetros(primeraParada.lat, primeraParada.lng, coords[1], coords[0]); ranking.push({ conductorId: d.userId._id.toString(), nombre: d.userId.nombre, telefono: d.userId.telefono, placaVehiculo: d.placaVehiculo, modeloVehiculo: d.modeloVehiculo, colorVehiculo: d.colorVehiculo, distanciaKmAResidencia: +(distMetros / 1000).toFixed(2), direccionPrimeraParada: primeraParada.direccion, }); } // Ordenar de menor a mayor distancia: el conductor que vive más cerca de la primera parada va de primero ranking.sort((a, b) => a.distanciaKmAResidencia - b.distanciaKmAResidencia); return ranking; } private calcularDistanciaMetros(lat1: number, lon1: number, lat2: number, lon2: number): number { const R = 6371e3; const phi1 = (lat1 * Math.PI) / 180; const phi2 = (lat2 * Math.PI) / 180; const deltaPhi = ((lat2 - lat1) * Math.PI) / 180; const deltaLambda = ((lon2 - lon1) * Math.PI) / 180; const a = Math.sin(deltaPhi / 2) ** 2 + Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } } export const tripSchedulingService = TripSchedulingService.getInstance();