diff --git a/.DS_Store b/.DS_Store index 1e57e80..abae5ca 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/src/controllers/chatController.js b/src/controllers/chatController.js index 23238b9..2201888 100644 --- a/src/controllers/chatController.js +++ b/src/controllers/chatController.js @@ -1,4 +1,4 @@ -const Chat = require('../models/Chat'); +const Chat = require('../models/viaggi/Chat'); const Message = require('../models/Message'); const { User } = require('../models/user'); diff --git a/src/controllers/feedbackController.js b/src/controllers/feedbackController.js index 6b28d88..3007a2f 100644 --- a/src/controllers/feedbackController.js +++ b/src/controllers/feedbackController.js @@ -1,7 +1,7 @@ const mongoose = require('mongoose'); -const Feedback = require('../models/Feedback'); -const Ride = require('../models/Ride'); -const RideRequest = require('../models/RideRequest'); +const Feedback = require('../models/viaggi/Feedback'); +const Ride = require('../models/viaggi/Ride'); +const RideRequest = require('../models/viaggi/RideRequest'); const { User } = require('../models/user'); // ============================================================ @@ -338,7 +338,7 @@ const getUserFeedback = async (req, res) => { const [feedbacks, total, stats] = await Promise.all([ Feedback.find(query) .populate('fromUserId', 'username name surname profile.img') - .populate('rideId', 'departure destination dateTime') + .populate('rideId', 'departure destination departureDate') .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)), @@ -598,7 +598,7 @@ const canLeaveFeedback = async (req, res) => { _id: ride._id, departure: ride.departure, destination: ride.destination, - dateTime: ride.dateTime, + departureDate: ride.departureDate, }, }, }); @@ -823,7 +823,7 @@ const getMyGivenFeedback = async (req, res) => { const [feedbacks, total] = await Promise.all([ Feedback.find({ idapp, fromUserId: userId }) .populate('toUserId', 'username name surname profile.img') - .populate('rideId', 'departure destination dateTime') + .populate('rideId', 'departure destination departureDate') .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)), @@ -872,7 +872,7 @@ const getMyReceivedFeedback = async (req, res) => { const [feedbacks, total, stats] = await Promise.all([ Feedback.find({ idapp, toUserId: userId }) .populate('fromUserId', 'username name surname profile.img') - .populate('rideId', 'departure destination dateTime') + .populate('rideId', 'departure destination departureDate') .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)), diff --git a/src/controllers/rideController.js b/src/controllers/rideController.js index 29fb38c..3feaee6 100644 --- a/src/controllers/rideController.js +++ b/src/controllers/rideController.js @@ -1,8 +1,10 @@ -const Ride = require('../models/Ride'); +const Ride = require('../models/viaggi/Ride'); const { User } = require('../models/user'); -const RideRequest = require('../models/RideRequest'); -const Feedback = require('../models/Feedback'); -const Chat = require('../models/Chat'); +const RideRequest = require('../models/viaggi/RideRequest'); +const Feedback = require('../models/viaggi/Feedback'); +const Chat = require('../models/viaggi/Chat'); + +const { getRecurrenceDates, isRideActiveOnDate } = require('../helpers/recurrenceHelper'); /** * @desc Crea un nuovo viaggio (offerta o richiesta) @@ -19,7 +21,7 @@ const createRide = async (req, res) => { departure, destination, waypoints, - dateTime, + departureDate, flexibleTime, flexibleMinutes, recurrence, @@ -28,35 +30,35 @@ const createRide = async (req, res) => { vehicle, preferences, contribution, - notes + notes, } = req.body; // Validazione base if (!type || !['offer', 'request'].includes(type)) { return res.status(400).json({ success: false, - message: 'Tipo viaggio non valido. Usa "offer" o "request"' + message: 'Tipo viaggio non valido. Usa "offer" o "request"', }); } if (!departure || !departure.city || !departure.coordinates) { return res.status(400).json({ success: false, - message: 'Città di partenza richiesta con coordinate' + message: 'Città di partenza richiesta con coordinate', }); } if (!destination || !destination.city || !destination.coordinates) { return res.status(400).json({ success: false, - message: 'Città di destinazione richiesta con coordinate' + message: 'Città di destinazione richiesta con coordinate', }); } - if (!dateTime) { + if (!departureDate) { return res.status(400).json({ success: false, - message: 'Data e ora di partenza richieste' + message: 'Data e ora di partenza richieste', }); } @@ -65,7 +67,7 @@ const createRide = async (req, res) => { if (!passengers || !passengers.max || passengers.max < 1) { return res.status(400).json({ success: false, - message: 'Numero massimo passeggeri richiesto per le offerte' + message: 'Numero massimo passeggeri richiesto per le offerte', }); } } @@ -75,7 +77,7 @@ const createRide = async (req, res) => { if (waypoints && waypoints.length > 0) { orderedWaypoints = waypoints.map((wp, index) => ({ ...wp, - order: wp.order !== undefined ? wp.order : index + 1 + order: wp.order !== undefined ? wp.order : index + 1, })); } @@ -87,21 +89,21 @@ const createRide = async (req, res) => { departure, destination, waypoints: orderedWaypoints, - dateTime: new Date(dateTime), + departureDate: new Date(departureDate), flexibleTime: flexibleTime || false, flexibleMinutes: flexibleMinutes || 30, recurrence: recurrence || { type: 'once' }, preferences: preferences || {}, contribution: contribution || { contribTypes: [] }, notes, - status: 'active' + status: 'active', }; // Aggiungi campi specifici per tipo if (type === 'offer') { rideData.passengers = { available: passengers.max, - max: passengers.max + max: passengers.max, }; rideData.vehicle = vehicle || {}; } else { @@ -122,15 +124,14 @@ const createRide = async (req, res) => { res.status(201).json({ success: true, message: type === 'offer' ? 'Offerta passaggio creata!' : 'Richiesta passaggio creata!', - data: ride + data: ride, }); - } catch (error) { console.error('Errore creazione viaggio:', error); res.status(500).json({ success: false, message: 'Errore durante la creazione del viaggio', - error: error.message + error: error.message, }); } }; @@ -143,7 +144,7 @@ const createRide = async (req, res) => { const getRides = async (req, res) => { try { const idapp = req.query.idapp; - + const { type, departureCity, @@ -158,15 +159,15 @@ const getRides = async (req, res) => { status, page = 1, limit = 20, - sortBy = 'dateTime', - sortOrder = 'asc' + sortBy = 'departureDate', + sortOrder = 'asc', } = req.query; // Costruisci query const query = { idapp, status: status ? status : { $in: ['active', 'full'] }, - dateTime: { $gte: new Date() } + departureDate: { $gte: new Date() }, }; // Filtro tipo @@ -190,14 +191,14 @@ const getRides = async (req, res) => { startOfDay.setHours(0, 0, 0, 0); const endOfDay = new Date(date); endOfDay.setHours(23, 59, 59, 999); - query.dateTime = { $gte: startOfDay, $lte: endOfDay }; + query.departureDate = { $gte: startOfDay, $lte: endOfDay }; } // Filtro range date if (dateFrom || dateTo) { - query.dateTime = {}; - if (dateFrom) query.dateTime.$gte = new Date(dateFrom); - if (dateTo) query.dateTime.$lte = new Date(dateTo); + query.departureDate = {}; + if (dateFrom) query.departureDate.$gte = new Date(dateFrom); + if (dateTo) query.departureDate.$lte = new Date(dateTo); } // Filtro posti minimi disponibili @@ -235,12 +236,15 @@ const getRides = async (req, res) => { query.$or = [ { 'departure.city': cityRegex }, { 'destination.city': cityRegex }, - { 'waypoints.location.city': cityRegex } + { 'waypoints.location.city': cityRegex }, ]; } rides = await Ride.find(query) - .populate('userId', 'username name surname profile.img profile.driverProfile.averageRating profile.driverProfile.ridesCompletedAsDriver') + .populate( + 'userId', + 'username name surname profile.img profile.driverProfile.averageRating profile.driverProfile.ridesCompletedAsDriver' + ) .populate('contribution.contribTypes.contribTypeId') .sort(sortOptions) .skip(skip) @@ -256,16 +260,15 @@ const getRides = async (req, res) => { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } + pages: Math.ceil(total / parseInt(limit)), + }, }); - } catch (error) { console.error('Errore recupero viaggi:', error); res.status(500).json({ success: false, message: 'Errore durante il recupero dei viaggi', - error: error.message + error: error.message, }); } }; @@ -287,7 +290,7 @@ const getRideById = async (req, res) => { if (!ride) { return res.status(404).json({ success: false, - message: 'Viaggio non trovato' + message: 'Viaggio non trovato', }); } @@ -297,15 +300,14 @@ const getRideById = async (req, res) => { res.status(200).json({ success: true, - data: ride + data: ride, }); - } catch (error) { console.error('Errore recupero viaggio:', error); res.status(500).json({ success: false, message: 'Errore durante il recupero del viaggio', - error: error.message + error: error.message, }); } }; @@ -328,7 +330,7 @@ const updateRide = async (req, res) => { if (!ride) { return res.status(404).json({ success: false, - message: 'Viaggio non trovato' + message: 'Viaggio non trovato', }); } @@ -336,7 +338,7 @@ const updateRide = async (req, res) => { if (!ride.userId.equals(userId)) { return res.status(403).json({ success: false, - message: 'Non sei autorizzato a modificare questo viaggio' + message: 'Non sei autorizzato a modificare questo viaggio', }); } @@ -345,12 +347,12 @@ const updateRide = async (req, res) => { // Permetti solo alcune modifiche const allowedFields = ['notes', 'preferences', 'flexibleTime', 'flexibleMinutes']; const updateKeys = Object.keys(updateData); - const hasDisallowedFields = updateKeys.some(key => !allowedFields.includes(key)); - + const hasDisallowedFields = updateKeys.some((key) => !allowedFields.includes(key)); + if (hasDisallowedFields) { return res.status(400).json({ success: false, - message: 'Non puoi modificare percorso/orario con passeggeri confermati. Contattali prima.' + message: 'Non puoi modificare percorso/orario con passeggeri confermati. Contattali prima.', }); } } @@ -364,7 +366,7 @@ const updateRide = async (req, res) => { if (updateData.waypoints) { updateData.waypoints = updateData.waypoints.map((wp, index) => ({ ...wp, - order: wp.order !== undefined ? wp.order : index + 1 + order: wp.order !== undefined ? wp.order : index + 1, })); } @@ -374,26 +376,21 @@ const updateRide = async (req, res) => { updateData.passengers.available = updateData.passengers.max - bookedSeats; } - const updatedRide = await Ride.findByIdAndUpdate( - id, - { $set: updateData }, - { new: true, runValidators: true } - ) - .populate('userId', 'username name surname profile.img') - .populate('contribution.contribTypes.contribTypeId'); + const updatedRide = await Ride.findByIdAndUpdate(id, { $set: updateData }, { new: true, runValidators: true }) + .populate('userId', 'username name surname profile.img') + .populate('contribution.contribTypes.contribTypeId'); res.status(200).json({ success: true, message: 'Viaggio aggiornato con successo', - data: updatedRide + data: updatedRide, }); - } catch (error) { console.error('Errore aggiornamento viaggio:', error); res.status(500).json({ success: false, - message: 'Errore durante l\'aggiornamento del viaggio', - error: error.message + message: "Errore durante l'aggiornamento del viaggio", + error: error.message, }); } }; @@ -415,7 +412,7 @@ const deleteRide = async (req, res) => { if (!ride) { return res.status(404).json({ success: false, - message: 'Viaggio non trovato' + message: 'Viaggio non trovato', }); } @@ -423,7 +420,7 @@ const deleteRide = async (req, res) => { if (!ride.userId.equals(userId)) { return res.status(403).json({ success: false, - message: 'Non sei autorizzato a cancellare questo viaggio' + message: 'Non sei autorizzato a cancellare questo viaggio', }); } @@ -432,11 +429,11 @@ const deleteRide = async (req, res) => { // Aggiorna le richieste associate await RideRequest.updateMany( { rideId: id, status: 'accepted' }, - { + { status: 'cancelled', cancelledBy: 'driver', cancellationReason: reason || 'Viaggio cancellato dal conducente', - cancelledAt: new Date() + cancelledAt: new Date(), } ); } @@ -449,15 +446,14 @@ const deleteRide = async (req, res) => { res.status(200).json({ success: true, - message: 'Viaggio cancellato con successo' + message: 'Viaggio cancellato con successo', }); - } catch (error) { console.error('Errore cancellazione viaggio:', error); res.status(500).json({ success: false, message: 'Errore durante la cancellazione del viaggio', - error: error.message + error: error.message, }); } }; @@ -474,7 +470,7 @@ const getMyRides = async (req, res) => { const { type, role, status, page = 1, limit = 20 } = req.query; let query = { idapp }; - + if (role === 'driver') { // Viaggi dove sono il conducente query.userId = userId; @@ -483,10 +479,7 @@ const getMyRides = async (req, res) => { query['confirmedPassengers.userId'] = userId; } else { // Tutti i miei viaggi (come driver o passenger) - query.$or = [ - { userId }, - { 'confirmedPassengers.userId': userId } - ]; + query.$or = [{ userId }, { 'confirmedPassengers.userId': userId }]; } if (type) { @@ -503,7 +496,7 @@ const getMyRides = async (req, res) => { .populate('userId', 'username name surname profile.img profile.driverProfile.averageRating') .populate('confirmedPassengers.userId', 'username name surname profile.img') .populate('contribution.contribTypes.contribTypeId') - .sort({ dateTime: -1 }) + .sort({ departureDate: -1 }) .skip(skip) .limit(parseInt(limit)); @@ -511,30 +504,30 @@ const getMyRides = async (req, res) => { // Separa in passati e futuri const now = new Date(); - const upcoming = rides.filter(r => new Date(r.dateTime) >= now); - const past = rides.filter(r => new Date(r.dateTime) < now); + const upcoming = rides.filter((r) => new Date(r.departureDate) >= now && r.status !== 'cancelled'); + + const past = rides.filter((r) => new Date(r.departureDate) < now); res.status(200).json({ success: true, data: { all: rides, upcoming, - past + past, }, pagination: { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } + pages: Math.ceil(total / parseInt(limit)), + }, }); - } catch (error) { console.error('Errore recupero miei viaggi:', error); res.status(500).json({ success: false, message: 'Errore durante il recupero dei tuoi viaggi', - error: error.message + error: error.message, }); } }; @@ -547,22 +540,13 @@ const getMyRides = async (req, res) => { const searchRides = async (req, res) => { try { const idapp = req.query.idapp; - - const { - from, - to, - date, - seats = 1, - type = 'offer', - radius = 20, // km di raggio per ricerca geografica - page = 1, - limit = 20 - } = req.query; + + const { from, to, date, seats = 1, type = 'offer', radius = 20, page = 1, limit = 20 } = req.query; if (!from && !to) { return res.status(400).json({ success: false, - message: 'Specifica almeno una città di partenza o destinazione' + message: 'Specifica almeno una città di partenza o destinazione', }); } @@ -570,10 +554,9 @@ const searchRides = async (req, res) => { idapp, type, status: 'active', - dateTime: { $gte: new Date() } }; - // Costruisci condizioni di ricerca + // Costruisci condizioni di ricerca per città const orConditions = []; if (from) { @@ -588,38 +571,99 @@ const searchRides = async (req, res) => { orConditions.push({ 'waypoints.location.city': toRegex }); } - // Se ci sono condizioni OR if (orConditions.length > 0) { if (from && to) { - // Deve matchare sia partenza che destinazione (anche waypoints) const fromRegex = new RegExp(from, 'i'); const toRegex = new RegExp(to, 'i'); query.$and = [ { - $or: [ - { 'departure.city': fromRegex }, - { 'waypoints.location.city': fromRegex } - ] + $or: [{ 'departure.city': fromRegex }, { 'waypoints.location.city': fromRegex }], }, { - $or: [ - { 'destination.city': toRegex }, - { 'waypoints.location.city': toRegex } - ] - } + $or: [{ 'destination.city': toRegex }, { 'waypoints.location.city': toRegex }], + }, ]; } else { query.$or = orConditions; } } - // Filtro data + // Filtro data con supporto ricorrenze if (date) { - const startOfDay = new Date(date); + const targetDate = new Date(date); + const startOfDay = new Date(targetDate); startOfDay.setHours(0, 0, 0, 0); - const endOfDay = new Date(date); + const endOfDay = new Date(targetDate); endOfDay.setHours(23, 59, 59, 999); - query.dateTime = { $gte: startOfDay, $lte: endOfDay }; + const dayOfWeek = targetDate.getDay(); + + // Query che include sia viaggi singoli che ricorrenti + query.$or = [ + // Viaggio singolo (once) nella data specifica + { + 'recurrence.type': 'once', + departureDate: { $gte: startOfDay, $lte: endOfDay }, + }, + // Viaggio ricorrente settimanale/custom_days che cade in quel giorno + { + 'recurrence.type': { $in: ['weekly', 'custom_days'] }, + 'recurrence.daysOfWeek': dayOfWeek, + $and: [ + { + $or: [ + { 'recurrence.startDate': { $lte: endOfDay } }, + { 'recurrence.startDate': { $exists: false } }, + { 'recurrence.startDate': null }, + ], + }, + { + $or: [ + { 'recurrence.endDate': { $gte: startOfDay } }, + { 'recurrence.endDate': { $exists: false } }, + { 'recurrence.endDate': null }, + ], + }, + ], + // Escludi se la data è nelle excludedDates + 'recurrence.excludedDates': { + $not: { + $elemMatch: { + $gte: startOfDay, + $lte: endOfDay, + }, + }, + }, + }, + // Viaggio con date custom che include quella data + { + 'recurrence.type': 'custom_dates', + 'recurrence.customDates': { + $elemMatch: { + $gte: startOfDay, + $lte: endOfDay, + }, + }, + }, + ]; + } else { + // Senza filtro data, mostra viaggi futuri o con ricorrenze attive + const now = new Date(); + query.$or = [ + // Viaggi singoli futuri + { + 'recurrence.type': 'once', + departureDate: { $gte: now }, + }, + // Viaggi ricorrenti attivi + { + 'recurrence.type': { $in: ['weekly', 'custom_days', 'custom_dates'] }, + $or: [ + { 'recurrence.endDate': { $gte: now } }, + { 'recurrence.endDate': { $exists: false } }, + { 'recurrence.endDate': null }, + ], + }, + ]; } // Filtro posti @@ -627,34 +671,88 @@ const searchRides = async (req, res) => { query['passengers.available'] = { $gte: parseInt(seats) }; } - const skip = (parseInt(page) - 1) * parseInt(limit); - - const rides = await Ride.find(query) - .populate('userId', 'username name surname profile.img profile.driverProfile.averageRating profile.driverProfile.ridesCompletedAsDriver') + // Esegui query + let rides = await Ride.find(query) + .populate( + 'userId', + 'username name surname profile.img profile.driverProfile.averageRating profile.driverProfile.ridesCompletedAsDriver' + ) .populate('contribution.contribTypes.contribTypeId') - .sort({ dateTime: 1 }) - .skip(skip) - .limit(parseInt(limit)); + .sort({ departureDate: 1 }); - const total = await Ride.countDocuments(query); + // Post-processing: espandi ricorrenze se c'è un filtro data + if (date) { + const targetDate = new Date(date); + + // Filtra e verifica che il viaggio sia effettivamente attivo nella data target + rides = rides.filter((ride) => { + return isRideActiveOnDate(ride, targetDate); + }); + + // Aggiungi info sulla data specifica visualizzata + rides = rides.map((ride) => { + const rideObj = ride.toObject(); + if (ride.recurrence?.type !== 'once') { + rideObj._displayDate = targetDate; + rideObj._isRecurrence = true; + } + return rideObj; + }); + } else { + // Senza filtro data, espandi le ricorrenze per i prossimi 30 giorni + const expandedRides = []; + const now = new Date(); + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); + + rides.forEach((ride) => { + if (ride.recurrence?.type === 'once') { + expandedRides.push(ride.toObject()); + } else { + // Espandi le ricorrenze + const dates = getRecurrenceDates(ride, now, futureDate); + dates.forEach((date) => { + const rideObj = ride.toObject(); + rideObj._displayDate = date; + rideObj._isRecurrence = true; + // rideObj._id = `${ride._id}`; + // rideObj._id = `${ride._id}_${date.getTime()}`; // ID univoco per ogni istanza + expandedRides.push(rideObj); + }); + } + }); + + rides = expandedRides; + } + + // Ordina per data + rides.sort((a, b) => { + const dateA = a._displayDate || a.departureDate; + const dateB = b._displayDate || b.departureDate; + return new Date(dateA) - new Date(dateB); + }); + + // Paginazione manuale sui risultati espansi + const total = rides.length; + const skip = (parseInt(page) - 1) * parseInt(limit); + const paginatedRides = rides.slice(skip, skip + parseInt(limit)); res.status(200).json({ success: true, - data: rides, + data: paginatedRides, pagination: { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } + pages: Math.ceil(total / parseInt(limit)), + }, }); - } catch (error) { console.error('Errore ricerca viaggi:', error); res.status(500).json({ success: false, message: 'Errore durante la ricerca dei viaggi', - error: error.message + error: error.message, }); } }; @@ -675,14 +773,14 @@ const completeRide = async (req, res) => { if (!ride) { return res.status(404).json({ success: false, - message: 'Viaggio non trovato' + message: 'Viaggio non trovato', }); } if (!ride.userId.equals(userId)) { return res.status(403).json({ success: false, - message: 'Solo il conducente può completare il viaggio' + message: 'Solo il conducente può completare il viaggio', }); } @@ -691,34 +789,30 @@ const completeRide = async (req, res) => { // Aggiorna contatori utente await User.findByIdAndUpdate(userId, { - $inc: { 'profile.driverProfile.ridesCompletedAsDriver': 1 } + $inc: { 'profile.driverProfile.ridesCompletedAsDriver': 1 }, }); // Aggiorna contatori passeggeri - const passengerIds = ride.confirmedPassengers.map(p => p.userId); + const passengerIds = ride.confirmedPassengers.map((p) => p.userId); await User.updateMany( { _id: { $in: passengerIds } }, { $inc: { 'profile.driverProfile.ridesCompletedAsPassenger': 1 } } ); // Aggiorna richieste associate - await RideRequest.updateMany( - { rideId: id, status: 'accepted' }, - { status: 'completed', completedAt: new Date() } - ); + await RideRequest.updateMany({ rideId: id, status: 'accepted' }, { status: 'completed', completedAt: new Date() }); res.status(200).json({ success: true, message: 'Viaggio completato! Ora puoi lasciare un feedback.', - data: ride + data: ride, }); - } catch (error) { console.error('Errore completamento viaggio:', error); res.status(500).json({ success: false, message: 'Errore durante il completamento del viaggio', - error: error.message + error: error.message, }); } }; @@ -742,38 +836,43 @@ const getRidesStats = async (req, res) => { todayRides, myUpcomingAsDriver, myUpcomingAsPassenger, - pendingRequests + pendingRequests, ] = await Promise.all([ // Offerte attive - Ride.countDocuments({ idapp, type: 'offer', status: 'active', dateTime: { $gte: now } }), + Ride.countDocuments({ idapp, type: 'offer', status: 'active', departureDate: { $gte: now } }), // Richieste attive - Ride.countDocuments({ idapp, type: 'request', status: 'active', dateTime: { $gte: now } }), + Ride.countDocuments({ idapp, type: 'request', status: 'active', departureDate: { $gte: now } }), // Viaggi di oggi Ride.countDocuments({ idapp, status: { $in: ['active', 'full'] }, - dateTime: { + departureDate: { $gte: new Date(now.setHours(0, 0, 0, 0)), - $lte: new Date(now.setHours(23, 59, 59, 999)) - } + $lte: new Date(now.setHours(23, 59, 59, 999)), + }, }), // I miei prossimi come conducente - Ride.countDocuments({ idapp, userId, status: { $in: ['active', 'full'] }, dateTime: { $gte: new Date() } }), + Ride.countDocuments({ idapp, userId, status: { $in: ['active', 'full'] }, departureDate: { $gte: new Date() } }), // I miei prossimi come passeggero - Ride.countDocuments({ idapp, 'confirmedPassengers.userId': userId, status: { $in: ['active', 'full'] }, dateTime: { $gte: new Date() } }), + Ride.countDocuments({ + idapp, + 'confirmedPassengers.userId': userId, + status: { $in: ['active', 'full'] }, + departureDate: { $gte: new Date() }, + }), // Richieste in attesa per me - RideRequest.countDocuments({ idapp, driverId: userId, status: 'pending' }) + RideRequest.countDocuments({ idapp, driverId: userId, status: 'pending' }), ]); // Ultimi viaggi attivi const recentRides = await Ride.find({ idapp, status: 'active', - dateTime: { $gte: new Date() } + departureDate: { $gte: new Date() }, }) - .populate('userId', 'username name profile.img') - .sort({ createdAt: -1 }) - .limit(5); + .populate('userId', 'username name profile.img') + .sort({ createdAt: -1 }) + .limit(5); res.status(200).json({ success: true, @@ -784,18 +883,17 @@ const getRidesStats = async (req, res) => { todayRides, myUpcomingAsDriver, myUpcomingAsPassenger, - pendingRequests + pendingRequests, }, - recentRides - } + recentRides, + }, }); - } catch (error) { console.error('Errore recupero statistiche:', error); res.status(500).json({ success: false, message: 'Errore durante il recupero delle statistiche', - error: error.message + error: error.message, }); } }; @@ -818,48 +916,45 @@ const getWidgetData = async (req, res) => { myActiveRequests, pendingRequestsForMyRides, recentFeedback, - unreadMessages + unreadMessages, ] = await Promise.all([ // Active rides as driver Ride.countDocuments({ driverId: userId, status: 'active', - departureDate: { $gte: now } + departureDate: { $gte: now }, }), // Upcoming rides as passenger (accepted requests) RideRequest.countDocuments({ userId: userId, status: 'accepted', - 'rideInfo.departureDate': { $gte: now } + 'rideInfo.departureDate': { $gte: now }, }), // Past rides in last 30 days Ride.countDocuments({ - $or: [ - { driverId: userId }, - { 'passengers.userId': userId } - ], + $or: [{ driverId: userId }, { 'confirmedPassengers.userId': userId }], status: 'completed', - departureDate: { $gte: thirtyDaysAgo, $lt: now } + departureDate: { $gte: thirtyDaysAgo, $lt: now }, }), // My active requests (pending/accepted) RideRequest.countDocuments({ userId: userId, - status: { $in: ['pending', 'accepted'] } + status: { $in: ['pending', 'accepted'] }, }), // Pending requests for my rides RideRequest.countDocuments({ driverId: userId, - status: 'pending' + status: 'pending', }), // Recent feedback (last 7 days) Feedback.find({ toUserId: userId, - createdAt: { $gte: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) } + createdAt: { $gte: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) }, }) .select('rating createdAt') .sort({ createdAt: -1 }) @@ -870,14 +965,13 @@ const getWidgetData = async (req, res) => { Chat.countDocuments({ participants: userId, 'lastMessage.senderId': { $ne: userId }, - [`unreadCount.${userId}`]: { $gt: 0 } - }) + [`unreadCount.${userId}`]: { $gt: 0 }, + }), ]); // Calculate average recent rating - const avgRecentRating = recentFeedback.length > 0 - ? recentFeedback.reduce((sum, f) => sum + f.rating, 0) / recentFeedback.length - : null; + const avgRecentRating = + recentFeedback.length > 0 ? recentFeedback.reduce((sum, f) => sum + f.rating, 0) / recentFeedback.length : null; // Get trending routes (most searched in last 7 days) const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); @@ -885,46 +979,46 @@ const getWidgetData = async (req, res) => { { $match: { createdAt: { $gte: sevenDaysAgo }, - status: { $in: ['active', 'completed'] } - } + status: { $in: ['active', 'completed'] }, + }, }, { $group: { _id: { departure: '$departure.city', - destination: '$destination.city' + destination: '$destination.city', }, - count: { $sum: 1 } - } + count: { $sum: 1 }, + }, }, { - $sort: { count: -1 } + $sort: { count: -1 }, }, { - $limit: 3 + $limit: 3, }, { $project: { _id: 0, departure: '$_id.departure', destination: '$_id.destination', - count: 1 - } - } + count: 1, + }, + }, ]); // Get user stats const userStats = await Feedback.aggregate([ { - $match: { toUserId: userId } + $match: { toUserId: userId }, }, { $group: { _id: null, averageRating: { $avg: '$rating' }, - totalFeedbacks: { $sum: 1 } - } - } + totalFeedbacks: { $sum: 1 }, + }, + }, ]); const stats = userStats[0] || { averageRating: 0, totalFeedbacks: 0 }; @@ -935,26 +1029,26 @@ const getWidgetData = async (req, res) => { activeRides: myActiveRides, upcomingTrips: myUpcomingRides, pastRidesThisMonth: myPastRidesCount, - activeRequests: myActiveRequests + activeRequests: myActiveRequests, }, notifications: { pendingRequests: pendingRequestsForMyRides, unreadMessages: unreadMessages, - newFeedbacks: recentFeedback.length + newFeedbacks: recentFeedback.length, }, userStats: { averageRating: stats.averageRating ? parseFloat(stats.averageRating.toFixed(1)) : null, totalFeedbacks: stats.totalFeedbacks, - recentRating: avgRecentRating ? parseFloat(avgRecentRating.toFixed(1)) : null + recentRating: avgRecentRating ? parseFloat(avgRecentRating.toFixed(1)) : null, }, trending: { - routes: trendingRoutes + routes: trendingRoutes, }, quickActions: { hasVehicle: false, // Will be populated if vehicle model exists canCreateRide: myActiveRides < 5, // Limit active rides - hasCompletedProfile: true // Could check user profile completeness - } + hasCompletedProfile: true, // Could check user profile completeness + }, }; // Optional: Check if user has a vehicle (if Vehicle model exists) @@ -968,454 +1062,14 @@ const getWidgetData = async (req, res) => { return res.status(200).json({ success: true, - data: widgetData + data: widgetData, }); - } catch (error) { console.error('Error getting widget data:', error); return res.status(500).json({ success: false, message: 'Errore nel caricamento dei dati del widget', - error: error.message - }); - } -}; - -/** - * Get comprehensive statistics summary for user - * GET /api/viaggi/stats/summary - */ -const getStatsSummary = async (req, res) => { - try { - const userId = req.user._id; - const now = new Date(); - - // Time periods - const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); - const thisYear = new Date(now.getFullYear(), 0, 1); - const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - - // Parallel queries for all statistics - const [ - ridesAsDriver, - ridesAsPassenger, - totalDistance, - feedback, - monthlyRides, - yearlyRides, - recentActivity, - vehicleStats, - savingsEstimate - ] = await Promise.all([ - // Rides as driver - Ride.aggregate([ - { - $match: { - driverId: userId, - status: 'completed' - } - }, - { - $group: { - _id: null, - total: { $sum: 1 }, - thisMonth: { - $sum: { - $cond: [{ $gte: ['$departureDate', thisMonth] }, 1, 0] - } - }, - lastMonth: { - $sum: { - $cond: [ - { - $and: [ - { $gte: ['$departureDate', lastMonth] }, - { $lt: ['$departureDate', thisMonth] } - ] - }, - 1, - 0 - ] - } - }, - totalPassengers: { $sum: '$currentPassengers' } - } - } - ]), - - // Rides as passenger - RideRequest.aggregate([ - { - $match: { - userId: userId, - status: 'completed' - } - }, - { - $group: { - _id: null, - total: { $sum: 1 }, - thisMonth: { - $sum: { - $cond: [{ $gte: ['$createdAt', thisMonth] }, 1, 0] - } - } - } - } - ]), - - // Total distance traveled - Ride.aggregate([ - { - $match: { - $or: [ - { driverId: userId }, - { 'passengers.userId': userId } - ], - status: 'completed', - distance: { $exists: true, $gt: 0 } - } - }, - { - $group: { - _id: null, - totalKm: { $sum: '$distance' }, - avgDistance: { $avg: '$distance' } - } - } - ]), - - // Feedback statistics - Feedback.aggregate([ - { - $match: { toUserId: userId } - }, - { - $facet: { - overall: [ - { - $group: { - _id: null, - avgRating: { $avg: '$rating' }, - total: { $sum: 1 }, - fiveStars: { - $sum: { $cond: [{ $eq: ['$rating', 5] }, 1, 0] } - }, - fourStars: { - $sum: { $cond: [{ $eq: ['$rating', 4] }, 1, 0] } - }, - threeStars: { - $sum: { $cond: [{ $eq: ['$rating', 3] }, 1, 0] } - } - } - } - ], - asDriver: [ - { - $match: { toUserRole: 'driver' } - }, - { - $group: { - _id: null, - avgRating: { $avg: '$rating' }, - total: { $sum: 1 } - } - } - ], - asPassenger: [ - { - $match: { toUserRole: 'passenger' } - }, - { - $group: { - _id: null, - avgRating: { $avg: '$rating' }, - total: { $sum: 1 } - } - } - ], - recent: [ - { - $match: { - createdAt: { $gte: thirtyDaysAgo } - } - }, - { - $group: { - _id: null, - avgRating: { $avg: '$rating' }, - total: { $sum: 1 } - } - } - ] - } - } - ]), - - // Monthly breakdown (last 6 months) - Ride.aggregate([ - { - $match: { - $or: [ - { driverId: userId }, - { 'passengers.userId': userId } - ], - status: 'completed', - departureDate: { - $gte: new Date(now.getFullYear(), now.getMonth() - 5, 1) - } - } - }, - { - $group: { - _id: { - year: { $year: '$departureDate' }, - month: { $month: '$departureDate' } - }, - count: { $sum: 1 }, - asDriver: { - $sum: { - $cond: [{ $eq: ['$driverId', userId] }, 1, 0] - } - }, - asPassenger: { - $sum: { - $cond: [{ $ne: ['$driverId', userId] }, 1, 0] - } - } - } - }, - { - $sort: { '_id.year': 1, '_id.month': 1 } - } - ]), - - // Yearly summary - Ride.aggregate([ - { - $match: { - $or: [ - { driverId: userId }, - { 'passengers.userId': userId } - ], - status: 'completed', - departureDate: { $gte: thisYear } - } - }, - { - $group: { - _id: null, - total: { $sum: 1 }, - asDriver: { - $sum: { - $cond: [{ $eq: ['$driverId', userId] }, 1, 0] - } - }, - asPassenger: { - $sum: { - $cond: [{ $ne: ['$driverId', userId] }, 1, 0] - } - } - } - } - ]), - - // Recent activity (last 30 days) - Ride.find({ - $or: [ - { driverId: userId }, - { 'passengers.userId': userId } - ], - status: { $in: ['completed', 'active'] }, - departureDate: { $gte: thirtyDaysAgo } - }) - .select('departure.city destination.city departureDate status') - .sort({ departureDate: -1 }) - .limit(10) - .lean(), - - // Vehicle statistics (if vehicle exists) - (async () => { - try { - const Vehicle = require('../models/Vehicle'); - const vehicles = await Vehicle.find({ userId: userId }).lean(); - - if (vehicles.length === 0) return null; - - const vehicleRides = await Ride.aggregate([ - { - $match: { - driverId: userId, - vehicleId: { $exists: true }, - status: 'completed' - } - }, - { - $group: { - _id: '$vehicleId', - totalRides: { $sum: 1 }, - totalKm: { $sum: '$distance' } - } - } - ]); - - return { - totalVehicles: vehicles.length, - ridesPerVehicle: vehicleRides - }; - } catch (err) { - return null; - } - })(), - - // Estimated savings (CO2 and money) - Ride.aggregate([ - { - $match: { - 'passengers.userId': userId, - status: 'completed', - distance: { $exists: true, $gt: 0 } - } - }, - { - $group: { - _id: null, - totalKm: { $sum: '$distance' }, - totalRides: { $sum: 1 } - } - } - ]) - ]); - - // Process results - const driverStats = ridesAsDriver[0] || { - total: 0, - thisMonth: 0, - lastMonth: 0, - totalPassengers: 0 - }; - - const passengerStats = ridesAsPassenger[0] || { - total: 0, - thisMonth: 0 - }; - - const distanceStats = totalDistance[0] || { - totalKm: 0, - avgDistance: 0 - }; - - const feedbackData = feedback[0] || { - overall: [{ avgRating: 0, total: 0, fiveStars: 0, fourStars: 0, threeStars: 0 }], - asDriver: [{ avgRating: 0, total: 0 }], - asPassenger: [{ avgRating: 0, total: 0 }], - recent: [{ avgRating: 0, total: 0 }] - }; - - const yearlyStats = yearlyRides[0] || { total: 0, asDriver: 0, asPassenger: 0 }; - - // Calculate savings (assumptions: 0.15€/km fuel, 120g CO2/km) - const savingsData = savingsEstimate[0] || { totalKm: 0, totalRides: 0 }; - const estimatedCO2Saved = Math.round(savingsData.totalKm * 120); // grams - const estimatedMoneySaved = Math.round(savingsData.totalKm * 0.15 * 100) / 100; // euros - - // Calculate growth percentage - const monthGrowth = driverStats.lastMonth > 0 - ? Math.round(((driverStats.thisMonth - driverStats.lastMonth) / driverStats.lastMonth) * 100) - : 0; - - // Build response - const summary = { - overview: { - totalRides: driverStats.total + passengerStats.total, - asDriver: driverStats.total, - asPassenger: passengerStats.total, - totalPassengersTransported: driverStats.totalPassengers, - totalDistance: Math.round(distanceStats.totalKm), - avgDistance: Math.round(distanceStats.avgDistance) - }, - - thisMonth: { - ridesAsDriver: driverStats.thisMonth, - ridesAsPassenger: passengerStats.thisMonth, - total: driverStats.thisMonth + passengerStats.thisMonth, - growth: monthGrowth - }, - - thisYear: { - total: yearlyStats.total, - asDriver: yearlyStats.asDriver, - asPassenger: yearlyStats.asPassenger - }, - - ratings: { - overall: { - average: feedbackData.overall[0]?.avgRating - ? parseFloat(feedbackData.overall[0].avgRating.toFixed(2)) - : 0, - total: feedbackData.overall[0]?.total || 0, - distribution: { - fiveStars: feedbackData.overall[0]?.fiveStars || 0, - fourStars: feedbackData.overall[0]?.fourStars || 0, - threeStars: feedbackData.overall[0]?.threeStars || 0 - } - }, - asDriver: { - average: feedbackData.asDriver[0]?.avgRating - ? parseFloat(feedbackData.asDriver[0].avgRating.toFixed(2)) - : 0, - total: feedbackData.asDriver[0]?.total || 0 - }, - asPassenger: { - average: feedbackData.asPassenger[0]?.avgRating - ? parseFloat(feedbackData.asPassenger[0].avgRating.toFixed(2)) - : 0, - total: feedbackData.asPassenger[0]?.total || 0 - }, - recent: { - average: feedbackData.recent[0]?.avgRating - ? parseFloat(feedbackData.recent[0].avgRating.toFixed(2)) - : 0, - total: feedbackData.recent[0]?.total || 0 - } - }, - - monthlyBreakdown: monthlyRides.map(m => ({ - month: m._id.month, - year: m._id.year, - total: m.count, - asDriver: m.asDriver, - asPassenger: m.asPassenger - })), - - recentActivity: recentActivity.map(ride => ({ - id: ride._id, - route: `${ride.departure.city} → ${ride.destination.city}`, - date: ride.departureDate, - status: ride.status - })), - - impact: { - co2Saved: estimatedCO2Saved, // grams - moneySaved: estimatedMoneySaved, // euros - ridesShared: savingsData.totalRides - }, - - vehicles: vehicleStats - }; - - return res.status(200).json({ - success: true, - data: summary - }); - - } catch (error) { - console.error('Error getting stats summary:', error); - return res.status(500).json({ - success: false, - message: 'Errore nel caricamento delle statistiche', - error: error.message + error: error.message, }); } }; @@ -1434,8 +1088,8 @@ const getCitySuggestions = async (req, res) => { success: true, data: { suggestions: [], - message: 'Inserisci almeno 2 caratteri per la ricerca' - } + message: 'Inserisci almeno 2 caratteri per la ricerca', + }, }); } @@ -1447,71 +1101,68 @@ const getCitySuggestions = async (req, res) => { { $match: { status: { $in: ['active', 'completed'] }, - $or: [ - { 'departure.city': regex }, - { 'destination.city': regex } - ] - } + $or: [{ 'departure.city': regex }, { 'destination.city': regex }], + }, }, { $facet: { departures: [ { - $match: { 'departure.city': regex } + $match: { 'departure.city': regex }, }, { $group: { _id: { city: '$departure.city', region: '$departure.region', - country: '$departure.country' + country: '$departure.country', }, - count: { $sum: 1 } - } - } + count: { $sum: 1 }, + }, + }, ], destinations: [ { - $match: { 'destination.city': regex } + $match: { 'destination.city': regex }, }, { $group: { _id: { city: '$destination.city', region: '$destination.region', - country: '$destination.country' + country: '$destination.country', }, - count: { $sum: 1 } - } - } - ] - } + count: { $sum: 1 }, + }, + }, + ], + }, }, { $project: { combined: { - $concatArrays: ['$departures', '$destinations'] - } - } + $concatArrays: ['$departures', '$destinations'], + }, + }, }, { - $unwind: '$combined' + $unwind: '$combined', }, { $group: { _id: { city: '$combined._id.city', region: '$combined._id.region', - country: '$combined._id.country' + country: '$combined._id.country', }, - totalCount: { $sum: '$combined.count' } - } + totalCount: { $sum: '$combined.count' }, + }, }, { - $sort: { totalCount: -1 } + $sort: { totalCount: -1 }, }, { - $limit: 10 + $limit: 10, }, { $project: { @@ -1519,9 +1170,9 @@ const getCitySuggestions = async (req, res) => { city: '$_id.city', region: '$_id.region', country: '$_id.country', - popularity: '$totalCount' - } - } + popularity: '$totalCount', + }, + }, ]); // Italian cities database (fallback/supplement) @@ -1547,18 +1198,18 @@ const getCitySuggestions = async (req, res) => { { city: 'Prato', region: 'Toscana', country: 'Italia', type: 'major' }, { city: 'Modena', region: 'Emilia-Romagna', country: 'Italia', type: 'major' }, { city: 'Reggio Calabria', region: 'Calabria', country: 'Italia', type: 'major' }, - + // Regional capitals { city: 'Ancona', region: 'Marche', country: 'Italia', type: 'capital' }, - { city: 'Aosta', region: 'Valle d\'Aosta', country: 'Italia', type: 'capital' }, + { city: 'Aosta', region: "Valle d'Aosta", country: 'Italia', type: 'capital' }, { city: 'Cagliari', region: 'Sardegna', country: 'Italia', type: 'capital' }, { city: 'Campobasso', region: 'Molise', country: 'Italia', type: 'capital' }, { city: 'Catanzaro', region: 'Calabria', country: 'Italia', type: 'capital' }, - { city: 'L\'Aquila', region: 'Abruzzo', country: 'Italia', type: 'capital' }, + { city: "L'Aquila", region: 'Abruzzo', country: 'Italia', type: 'capital' }, { city: 'Perugia', region: 'Umbria', country: 'Italia', type: 'capital' }, { city: 'Potenza', region: 'Basilicata', country: 'Italia', type: 'capital' }, { city: 'Trento', region: 'Trentino-Alto Adige', country: 'Italia', type: 'capital' }, - + // Other important cities { city: 'Bergamo', region: 'Lombardia', country: 'Italia', type: 'city' }, { city: 'Bolzano', region: 'Trentino-Alto Adige', country: 'Italia', type: 'city' }, @@ -1590,33 +1241,33 @@ const getCitySuggestions = async (req, res) => { { city: 'Treviso', region: 'Veneto', country: 'Italia', type: 'city' }, { city: 'Udine', region: 'Friuli-Venezia Giulia', country: 'Italia', type: 'city' }, { city: 'Varese', region: 'Lombardia', country: 'Italia', type: 'city' }, - { city: 'Vicenza', region: 'Veneto', country: 'Italia', type: 'city' } + { city: 'Vicenza', region: 'Veneto', country: 'Italia', type: 'city' }, ]; // Filter Italian cities by query const matchingItalianCities = italianCities - .filter(c => c.city.toLowerCase().startsWith(searchQuery.toLowerCase())) - .map(c => ({ + .filter((c) => c.city.toLowerCase().startsWith(searchQuery.toLowerCase())) + .map((c) => ({ ...c, popularity: 0, - source: 'database' + source: 'database', })); // Merge results - prioritize cities from actual rides const cityMap = new Map(); // Add popular cities from rides first - popularCities.forEach(city => { + popularCities.forEach((city) => { const key = `${city.city}-${city.region}`.toLowerCase(); cityMap.set(key, { ...city, source: 'rides', - verified: true + verified: true, }); }); // Add Italian cities that aren't already in the map - matchingItalianCities.forEach(city => { + matchingItalianCities.forEach((city) => { const key = `${city.city}-${city.region}`.toLowerCase(); if (!cityMap.has(key)) { cityMap.set(key, city); @@ -1651,13 +1302,13 @@ const getCitySuggestions = async (req, res) => { suggestions = suggestions.slice(0, 10); // Format suggestions - const formattedSuggestions = suggestions.map(s => ({ + const formattedSuggestions = suggestions.map((s) => ({ city: s.city, region: s.region, country: s.country || 'Italia', fullName: `${s.city}, ${s.region}`, popularity: s.popularity || 0, - verified: s.verified || false + verified: s.verified || false, })); return res.status(200).json({ @@ -1665,16 +1316,15 @@ const getCitySuggestions = async (req, res) => { data: { query: searchQuery, suggestions: formattedSuggestions, - count: formattedSuggestions.length - } + count: formattedSuggestions.length, + }, }); - } catch (error) { console.error('Error getting city suggestions:', error); return res.status(500).json({ success: false, message: 'Errore nel caricamento dei suggerimenti', - error: error.message + error: error.message, }); } }; @@ -1691,30 +1341,27 @@ const getRecentCities = async (req, res) => { const recentDepartures = await Ride.aggregate([ { $match: { - $or: [ - { driverId: userId }, - { 'passengers.userId': userId } - ], - status: 'completed' - } + $or: [{ driverId: userId }, { 'confirmedPassengers.userId': userId }], + status: 'completed', + }, }, { - $sort: { departureDate: -1 } + $sort: { departureDate: -1 }, }, { $group: { _id: { city: '$departure.city', region: '$departure.region', - country: '$departure.country' + country: '$departure.country', }, lat: { $first: '$departure.lat' }, lng: { $first: '$departure.lng' }, - lastUsed: { $first: '$departureDate' } - } + lastUsed: { $first: '$departureDate' }, + }, }, { - $limit: 2 + $limit: 2, }, { $project: { @@ -1725,39 +1372,36 @@ const getRecentCities = async (req, res) => { lat: 1, lng: 1, lastUsed: 1, - type: { $literal: 'departure' } - } - } + type: { $literal: 'departure' }, + }, + }, ]); // Get last 2 unique destination cities const recentDestinations = await Ride.aggregate([ { $match: { - $or: [ - { driverId: userId }, - { 'passengers.userId': userId } - ], - status: 'completed' - } + $or: [{ driverId: userId }, { 'confirmedPassengers.userId': userId }], + status: 'completed', + }, }, { - $sort: { departureDate: -1 } + $sort: { departureDate: -1 }, }, { $group: { _id: { city: '$destination.city', region: '$destination.region', - country: '$destination.country' + country: '$destination.country', }, lat: { $first: '$destination.lat' }, lng: { $first: '$destination.lng' }, - lastUsed: { $first: '$departureDate' } - } + lastUsed: { $first: '$departureDate' }, + }, }, { - $limit: 2 + $limit: 2, }, { $project: { @@ -1768,16 +1412,16 @@ const getRecentCities = async (req, res) => { lat: 1, lng: 1, lastUsed: 1, - type: { $literal: 'destination' } - } - } + type: { $literal: 'destination' }, + }, + }, ]); // Combine and remove duplicates const allRecent = [...recentDepartures, ...recentDestinations]; const uniqueCities = new Map(); - allRecent.forEach(city => { + allRecent.forEach((city) => { const key = `${city.city}-${city.region}`; if (!uniqueCities.has(key)) { uniqueCities.set(key, city); @@ -1791,16 +1435,615 @@ const getRecentCities = async (req, res) => { return res.status(200).json({ success: true, data: { - cities - } + cities, + }, }); - } catch (error) { console.error('Error getting recent cities:', error); return res.status(500).json({ success: false, message: 'Errore nel caricamento delle città recenti', - error: error.message + error: error.message, + }); + } +}; + +// Backend Controller Additions for Trasporti Solidali +// Add these routes and methods to your existing rides controller + +// ============================================ +// CANCELLED RIDES +// ============================================ + +/** + * GET /api/viaggi/rides/cancelled + * Get user's cancelled rides + */ +const getCancelledRides = async (req, res) => { + try { + const userId = req.user._id; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + const skip = (page - 1) * limit; + + // Find cancelled rides where user is driver OR passenger + const cancelledRides = await Ride.find({ + status: 'cancelled', + $or: [{ userId: userId }, { 'confirmedPassengers.userId': userId }], + }) + .populate('userId', 'name surname profile.img username') + .populate('confirmedPassengers.userId', 'name surname profile.img username') + .sort({ updatedAt: -1 }) + .skip(skip) + .limit(limit) + .lean(); + + const total = await Ride.countDocuments({ + status: 'cancelled', + $or: [{ userId: userId }, { 'confirmedPassengers.userId': userId }], + }); + + return res.json({ + success: true, + data: { + rides: cancelledRides, + pagination: { + page, + limit, + total, + pages: Math.ceil(total / limit), + hasMore: page * limit < total, + }, + }, + }); + } catch (error) { + console.error('Error fetching cancelled rides:', error); + return res.status(500).json({ + success: false, + message: 'Errore nel recupero dei viaggi cancellati', + error: error.message, + }); + } +}; + +// ============================================ +// COMMUNITY RIDES - STATISTICS +// ============================================ + +/** + * GET /api/viaggi/rides/stats + * Get community rides statistics + */ +const getCommunityStatsComm = async (req, res) => { + try { + const idapp = req.query.idapp; + + const now = new Date(); + const startOfWeek = new Date(now); + startOfWeek.setDate(now.getDate() - now.getDay() + 1); // Monday + startOfWeek.setHours(0, 0, 0, 0); + + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 7); + + const next7Days = new Date(now); + next7Days.setDate(now.getDate() + 7); + next7Days.setHours(23, 59, 59, 999); + + // Total upcoming rides (include ricorrenze attive) + const allActiveRides = await Ride.find({ + idapp, + status: 'active', + $or: [ + { + 'recurrence.type': 'once', + departureDate: { $gte: now }, + }, + { + 'recurrence.type': { $in: ['weekly', 'custom_days', 'custom_dates'] }, + $or: [ + { 'recurrence.endDate': { $gte: now } }, + { 'recurrence.endDate': { $exists: false } }, + { 'recurrence.endDate': null }, + ], + }, + ], + }).lean(); + + // Espandi ricorrenze per questa settimana + const thisWeekExpanded = []; + const byDayOfWeek = {}; + + allActiveRides.forEach((ride) => { + const rideDates = getRecurrenceDates(ride, startOfWeek, endOfWeek); + + rideDates.forEach((date) => { + const dayKey = date.toISOString().split('T')[0]; + const dayName = date.toLocaleDateString('it-IT', { weekday: 'long' }); + + if (!byDayOfWeek[dayKey]) { + byDayOfWeek[dayKey] = { + date: dayKey, + dayName: dayName.charAt(0).toUpperCase() + dayName.slice(1), + count: 0, + rides: [], + }; + } + byDayOfWeek[dayKey].count++; + byDayOfWeek[dayKey].rides.push(ride._id); + thisWeekExpanded.push(ride); + }); + }); + + // Espandi ricorrenze per prossimi 7 giorni + let next7DaysExpanded = 0; + allActiveRides.forEach((ride) => { + const rideDates = getRecurrenceDates(ride, now, next7Days); + next7DaysExpanded += rideDates.length; + }); + + // Total upcoming (espandi prossimi 30 giorni come esempio) + const next30Days = new Date(now); + next30Days.setDate(now.getDate() + 30); + let totalUpcoming = 0; + allActiveRides.forEach((ride) => { + const rideDates = getRecurrenceDates(ride, now, next30Days); + totalUpcoming += rideDates.length; + }); + + // Popular routes (conta viaggi unici, non occorrenze) + const routes = await Ride.aggregate([ + { + $match: { + idapp, + status: 'active', + $or: [ + { + 'recurrence.type': 'once', + departureDate: { $gte: now }, + }, + { + 'recurrence.type': { $in: ['weekly', 'custom_days', 'custom_dates'] }, + $or: [ + { 'recurrence.endDate': { $gte: now } }, + { 'recurrence.endDate': { $exists: false } }, + { 'recurrence.endDate': null }, + ], + }, + ], + }, + }, + { + $group: { + _id: { + departure: '$departure', + destination: '$destination', + }, + count: { $sum: 1 }, + avgPrice: { $avg: '$contribution.contribTypes.0.price' }, + availableSeats: { $sum: '$passengers.available' }, + }, + }, + { + $sort: { count: -1 }, + }, + { + $limit: 10, + }, + ]); + + // Popular destinations + const destinations = await Ride.aggregate([ + { + $match: { + idapp, + status: 'active', + $or: [ + { + 'recurrence.type': 'once', + departureDate: { $gte: now }, + }, + { + 'recurrence.type': { $in: ['weekly', 'custom_days', 'custom_dates'] }, + $or: [ + { 'recurrence.endDate': { $gte: now } }, + { 'recurrence.endDate': { $exists: false } }, + { 'recurrence.endDate': null }, + ], + }, + ], + }, + }, + { + $group: { + _id: '$destination', + count: { $sum: 1 }, + }, + }, + { + $sort: { count: -1 }, + }, + { + $limit: 5, + }, + ]); + + // Total available seats (conta viaggi unici * posti) + const seatsAgg = await Ride.aggregate([ + { + $match: { + idapp, + status: 'active', + $or: [ + { + 'recurrence.type': 'once', + departureDate: { $gte: now }, + }, + { + 'recurrence.type': { $in: ['weekly', 'custom_days', 'custom_dates'] }, + $or: [ + { 'recurrence.endDate': { $gte: now } }, + { 'recurrence.endDate': { $exists: false } }, + { 'recurrence.endDate': null }, + ], + }, + ], + }, + }, + { + $group: { + _id: null, + totalSeats: { $sum: '$passengers.available' }, + }, + }, + ]); + + return res.json({ + success: true, + data: { + total: totalUpcoming, + thisWeek: thisWeekExpanded.length, + next7Days: next7DaysExpanded, + totalSeatsAvailable: seatsAgg[0]?.totalSeats || 0, + byDayOfWeek: Object.values(byDayOfWeek).sort((a, b) => new Date(a.date) - new Date(b.date)), + popularRoutes: routes.map((r) => ({ + departure: r._id.departure.city, + destination: r._id.destination.city, + count: r.count, + avgPrice: r.avgPrice ? r.avgPrice.toFixed(2) : null, + availableSeats: r.availableSeats, + })), + popularDestinations: destinations.map((d) => ({ + destination: d._id, + count: d.count, + })), + }, + }); + } catch (error) { + console.error('Error fetching community stats:', error); + return res.status(500).json({ + success: false, + message: 'Errore nel recupero delle statistiche', + error: error.message, + }); + } +}; + +// ============================================ +// COMMUNITY RIDES - LIST +// ============================================ + +/** + * GET /api/viaggi/rides/community + * Get all available community rides with filters + */ +const getCommunityRides = async (req, res) => { + try { + const userId = req.user._id; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + + const now = new Date(); + + // Build base query + const query = { + idapp: req.query.idapp, + type: 'offer', + status: 'active', + 'passengers.available': { $gt: 0 }, + }; + + // Filters per città + if (req.query.departure) { + query['departure.city'] = new RegExp(req.query.departure, 'i'); + } + + if (req.query.destination) { + query['destination.city'] = new RegExp(req.query.destination, 'i'); + } + + if (req.query.vehicleType) { + query['vehicle.type'] = req.query.vehicleType; + } + + if (req.query.maxPrice) { + query['contribution.contribTypes.0.price'] = { $lte: parseFloat(req.query.maxPrice) }; + } + + if (req.query.minSeats) { + query['passengers.available'] = { $gte: parseInt(req.query.minSeats) }; + } + + // Date filters con ricorrenze + let dateFrom = req.query.dateFrom ? new Date(req.query.dateFrom) : now; + let dateTo = req.query.dateTo ? new Date(req.query.dateTo) : null; + + if (dateTo) { + dateTo.setHours(23, 59, 59, 999); + } else { + // Default: prossimi 30 giorni + dateTo = new Date(now); + dateTo.setDate(dateTo.getDate() + 30); + } + + // Query per ricorrenze + query.$or = [ + // Viaggi singoli nel range + { + 'recurrence.type': 'once', + departureDate: { $gte: dateFrom, $lte: dateTo }, + }, + // Viaggi ricorrenti attivi + { + 'recurrence.type': { $in: ['weekly', 'custom_days', 'custom_dates'] }, + $or: [ + { 'recurrence.endDate': { $gte: dateFrom } }, + { 'recurrence.endDate': { $exists: false } }, + { 'recurrence.endDate': null }, + ], + }, + ]; + + // Fetch rides + let rides = await Ride.find(query) + .populate({ + path: 'userId', + select: 'name surname profile username', + }) + .populate('confirmedPassengers.userId', 'name surname profile') + .populate('contribution.contribTypes.contribTypeId', 'name description') + .lean(); + + // Espandi ricorrenze + const expandedRides = []; + rides.forEach((ride) => { + const rideDates = getRecurrenceDates(ride, dateFrom, dateTo); + + rideDates.forEach((date) => { + const rideInstance = { + ...ride, + _displayDate: date, + _isRecurrence: ride.recurrence?.type !== 'once', + price: ride.contribution?.contribTypes?.[0]?.price || 0, + }; + expandedRides.push(rideInstance); + }); + }); + + // Sorting + let sortFn; + if (req.query.sortBy === 'price_asc') { + sortFn = (a, b) => a.price - b.price; + } else if (req.query.sortBy === 'price_desc') { + sortFn = (a, b) => b.price - a.price; + } else if (req.query.sortBy === 'seats') { + sortFn = (a, b) => b.passengers.available - a.passengers.available; + } else { + sortFn = (a, b) => { + const dateA = a._displayDate || a.departureDate; + const dateB = b._displayDate || b.departureDate; + return new Date(dateA) - new Date(dateB); + }; + } + expandedRides.sort(sortFn); + + // Paginazione + const total = expandedRides.length; + const skip = (page - 1) * limit; + const paginatedRides = expandedRides.slice(skip, skip + limit); + + const originalRideIds = [...new Set(paginatedRides.map((r) => r._id))]; + + const userRequests = await RideRequest.find({ + rideId: { $in: originalRideIds }, + passengerId: userId, + }) + .select('rideId status') + .lean(); + + const requestMap = userRequests.reduce((acc, req) => { + acc[req.rideId.toString()] = req.status; + return acc; + }, {}); + + // Add request status + const ridesWithStatus = paginatedRides.map((ride) => { + return { + ...ride, + userRequestStatus: requestMap[ride._id.toString()] || null, + }; + }); + + return res.json({ + success: true, + data: { + rides: ridesWithStatus, + pagination: { + page, + limit, + total, + pages: Math.ceil(total / limit), + hasMore: page * limit < total, + }, + appliedFilters: { + departure: req.query.departure || null, + destination: req.query.destination || null, + dateFrom: req.query.dateFrom || null, + dateTo: req.query.dateTo || null, + maxPrice: req.query.maxPrice || null, + minSeats: req.query.minSeats || null, + vehicleType: req.query.vehicleType || null, + sortBy: req.query.sortBy || 'date', + }, + }, + }); + } catch (error) { + console.error('Error fetching community rides:', error); + return res.status(500).json({ + success: false, + message: 'Errore nel recupero dei viaggi', + error: error.message, + }); + } +}; + +// ============================================ +// COMMUNITY RIDES - CALENDAR DATA +// ============================================ + +/** + * GET /api/viaggi/rides/calendar + * Get rides grouped by date for calendar view + */ +const getCalendarRides = async (req, res) => { + try { + const userId = req.user._id; + const idapp = req.query.idapp; + const month = parseInt(req.query.month) ?? new Date().getMonth(); + const year = parseInt(req.query.year) ?? new Date().getFullYear(); + + const startDate = new Date(year, month, 1); + const endDate = new Date(year, month + 1, 0, 23, 59, 59); + + // Query più ampia per includere anche viaggi ricorrenti + const rides = await Ride.find({ + idapp, + status: 'active', + 'passengers.available': { $gt: 0 }, + $or: [ + // Viaggi singoli nel mese + { + 'recurrence.type': 'once', + departureDate: { $gte: startDate, $lte: endDate }, + }, + // Viaggi ricorrenti che potrebbero cadere nel mese + { + 'recurrence.type': { $in: ['weekly', 'custom_days', 'custom_dates'] }, + $or: [ + { 'recurrence.endDate': { $gte: startDate } }, + { 'recurrence.endDate': { $exists: false } }, + { 'recurrence.endDate': null }, + ], + }, + ], + }) + .populate('userId', 'name surname profile.img stats.averageRating') + .lean(); + + // Espandi le ricorrenze e raggruppa per data + const ridesByDate = {}; + + rides.forEach((ride) => { + // Ottieni tutte le date del viaggio nel mese corrente + const rideDates = getRecurrenceDates(ride, startDate, endDate); + + rideDates.forEach((date) => { + const dateKey = date.toISOString().split('T')[0]; + + if (!ridesByDate[dateKey]) { + ridesByDate[dateKey] = { + date: dateKey, + count: 0, + rides: [], + totalSeats: 0, + }; + } + + // Crea l'orario dalla data di partenza originale + const originalDate = new Date(ride.departureDate); + const timeString = originalDate.toLocaleTimeString('it-IT', { + hour: '2-digit', + minute: '2-digit', + }); + + ridesByDate[dateKey].count++; + ridesByDate[dateKey].rides.push({ + _id: ride._id, + departure: ride.departure, + destination: ride.destination, + time: timeString, + price: ride.price, + availableSeats: ride.passengers?.available || 0, + driver: ride.userId, + _isRecurrence: ride.recurrence?.type !== 'once', + _displayDate: date, + }); + ridesByDate[dateKey].totalSeats += ride.passengers?.available || 0; + }); + }); + + const totalRides = Object.values(ridesByDate).reduce((sum, day) => sum + day.count, 0); + + return res.json({ + success: true, + data: { + month, + year, + ridesByDate, + totalRides, + datesWithRides: Object.keys(ridesByDate).length, + }, + }); + } catch (error) { + console.error('Error fetching calendar rides:', error); + return res.status(500).json({ + success: false, + message: 'Errore nel recupero del calendario', + error: error.message, + }); + } +}; + +const toggleFavoriteRide = async (req, res) => { + try { + const userId = req.user._id; + const { rideId } = req.params; + + const user = await User.findById(userId); + const favoriteIndex = user.favoriteRides?.indexOf(rideId); + + if (favoriteIndex > -1) { + // Remove from favorites + user.favoriteRides.splice(favoriteIndex, 1); + } else { + // Add to favorites + if (!user.favoriteRides) user.favoriteRides = []; + user.favoriteRides.push(rideId); + } + + await user.save(); + + return res.json({ + success: true, + data: { + isFavorite: favoriteIndex === -1, + favoriteRides: user.favoriteRides, + }, + }); + } catch (error) { + return res.status(500).json({ + success: false, + message: 'Errore nel salvare il preferito', }); } }; @@ -1816,7 +2059,11 @@ module.exports = { completeRide, getRidesStats, getWidgetData, - getStatsSummary, getCitySuggestions, getRecentCities, + getCancelledRides, + getCommunityStatsComm, + getCommunityRides, + getCalendarRides, + toggleFavoriteRide, }; diff --git a/src/controllers/rideRequestController.js b/src/controllers/rideRequestController.js index 7025683..0e97121 100644 --- a/src/controllers/rideRequestController.js +++ b/src/controllers/rideRequestController.js @@ -1,9 +1,11 @@ const mongoose = require('mongoose'); -const RideRequest = require('../models/RideRequest'); -const Ride = require('../models/Ride'); -const Chat = require('../models/Chat'); +const RideRequest = require('../models/viaggi/RideRequest'); +const Ride = require('../models/viaggi/Ride'); +const Chat = require('../models/viaggi/Chat'); const Message = require('../models/Message'); +const TrasportiNotifications = require('./viaggi/TrasportiNotifications'); // Aggiungi import + /** * Helper per convertire ID in ObjectId */ @@ -126,12 +128,12 @@ const createRequest = async (req, res) => { // Popola i dati per la risposta await rideRequest.populate('passengerId', 'username name surname profile.img'); - await rideRequest.populate('rideId', 'departure destination dateTime'); + await rideRequest.populate('rideId', 'departure destination departureDate'); // Crea o recupera la chat tra passeggero e conducente let chat; let chatId = null; - + try { chat = await Chat.findOrCreateDirect(idapp, passengerId, ride.userId, rideId); chatId = chat._id; @@ -183,6 +185,116 @@ const createRequest = async (req, res) => { } }; +/** + * Crea richiesta passaggio da community ride + */ +const createRequestFromRide = async (req, res) => { + try { + const { rideId } = req.params; + const passengerId = req.user._id; + const { seats = 1, message, pickupPoint, dropoffPoint } = req.body; + + // Verifica che il viaggio esista + const ride = await Ride.findById(rideId); + + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato', + }); + } + + // Non puoi richiedere il tuo stesso viaggio + if (ride.userId.toString() === passengerId.toString()) { + return res.status(400).json({ + success: false, + message: 'Non puoi richiedere il tuo stesso viaggio', + }); + } + + // Verifica che ci siano posti disponibili + if (ride.passengers.available < seats) { + return res.status(400).json({ + success: false, + message: 'Non ci sono abbastanza posti disponibili', + }); + } + + // Verifica che non ci sia già una richiesta + const existingRequest = await RideRequest.findOne({ + rideId, + passengerId, + status: { $in: ['pending', 'accepted'] }, + }); + + if (existingRequest) { + return res.status(400).json({ + success: false, + message: 'Hai già una richiesta attiva per questo viaggio', + }); + } + + // Crea la richiesta + const request = new RideRequest({ + rideId, + passengerId, + driverId: ride.userId, + seats, + message, + pickupPoint: pickupPoint || ride.departure, + dropoffPoint: dropoffPoint || ride.destination, + status: 'pending', + }); + + await request.save(); + + // Popola i dati per la risposta + await request.populate([ + { path: 'passengerId', select: 'name surname profile.img' }, + { path: 'driverId', select: 'name surname profile.img' }, + { path: 'rideId', select: 'departure destination departureDate' }, + ]); + + // ============================================================ + // NOTIFICA AL CONDUCENTE + // ============================================================ + try { + // Ottieni dati completi del driver (per email, telegram, push) + const driver = await User.findById(ride.userId).select( + 'name surname email lang profile notificationPreferences idapp' + ); + + // Ottieni dati passeggero + const passenger = req.user; // o await User.findById(passengerId) + + // Invia notifica su tutti i canali abilitati + await TrasportiNotifications.notifyNewRideRequest( + driver, // destinatario + passenger, // chi ha fatto la richiesta + ride, // il viaggio + request, // la richiesta creata + driver.idapp, // idapp per Telegram + ); + } catch (notifError) { + // Log errore ma non bloccare la risposta + console.error('Errore invio notifica:', notifError); + } + + return res.status(201).json({ + success: true, + message: 'Richiesta inviata con successo', + data: request, + }); + } catch (error) { + console.error('Error creating ride request:', error); + return res.status(500).json({ + success: false, + message: 'Errore nella creazione della richiesta', + error: error.message, + }); + } +}; + /** * @desc Ottieni le richieste per un viaggio (per il conducente) * @route GET /api/viaggi/requests/ride/:rideId @@ -327,7 +439,7 @@ const getPendingRequests = async (req, res) => { status: 'pending', }) .populate('passengerId', 'username name surname profile.img profile.driverProfile.averageRating') - .populate('rideId', 'departure destination dateTime passengers') + .populate('rideId', 'departure destination departureDate passengers') .sort({ createdAt: -1 }); res.json({ @@ -415,7 +527,7 @@ const acceptRequest = async (req, res) => { if (!ride.confirmedPassengers) { ride.confirmedPassengers = []; } - + ride.confirmedPassengers.push({ userId: request.passengerId, seats: request.seatsRequested, @@ -429,10 +541,7 @@ const acceptRequest = async (req, res) => { await ride.updateAvailableSeats(); } else { // Fallback manuale - const totalConfirmed = ride.confirmedPassengers.reduce( - (sum, p) => sum + (p.seats || 1), - 0 - ); + const totalConfirmed = ride.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0); if (ride.passengers) { ride.passengers.available = Math.max(0, (ride.passengers.total || 0) - totalConfirmed); } @@ -462,7 +571,7 @@ const acceptRequest = async (req, res) => { timestamp: new Date(), type: 'ride_accepted', }; - + // Incrementa unread per il passeggero if (!chat.unreadCount) { chat.unreadCount = new Map(); @@ -471,7 +580,7 @@ const acceptRequest = async (req, res) => { const current = chat.unreadCount.get(passengerIdStr) || 0; chat.unreadCount.set(passengerIdStr, current + 1); chat.markModified('unreadCount'); - + await chat.save(); } catch (chatError) { console.error('Errore invio messaggio chat:', chatError); @@ -480,7 +589,7 @@ const acceptRequest = async (req, res) => { // TODO: Inviare notifica push al passeggero await request.populate('passengerId', 'username name surname profile.img'); - await request.populate('rideId', 'departure destination dateTime'); + await request.populate('rideId', 'departure destination departureDate'); res.json({ success: true, @@ -570,7 +679,7 @@ const rejectRequest = async (req, res) => { timestamp: new Date(), type: 'ride_rejected', }; - + if (!chat.unreadCount) { chat.unreadCount = new Map(); } @@ -578,7 +687,7 @@ const rejectRequest = async (req, res) => { const current = chat.unreadCount.get(passengerIdStr) || 0; chat.unreadCount.set(passengerIdStr, current + 1); chat.markModified('unreadCount'); - + await chat.save(); } catch (chatError) { console.error('Errore invio messaggio chat:', chatError); @@ -647,14 +756,11 @@ const cancelRequest = async (req, res) => { ride.confirmedPassengers = ride.confirmedPassengers.filter( (p) => p.userId.toString() !== request.passengerId.toString() ); - + if (typeof ride.updateAvailableSeats === 'function') { await ride.updateAvailableSeats(); } else { - const totalConfirmed = ride.confirmedPassengers.reduce( - (sum, p) => sum + (p.seats || 1), - 0 - ); + const totalConfirmed = ride.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0); if (ride.passengers) { ride.passengers.available = Math.max(0, (ride.passengers.total || 0) - totalConfirmed); } @@ -718,7 +824,7 @@ const getRequestById = async (req, res) => { // Verifica che l'utente sia coinvolto const passengerId = request.passengerId?._id || request.passengerId; const driverId = request.driverId?._id || request.driverId; - + const isPassenger = passengerId?.toString() === userId.toString(); const isDriver = driverId?.toString() === userId.toString(); @@ -768,7 +874,7 @@ const getReceivedRequests = async (req, res) => { // Fetch requests const requests = await RideRequest.find(query) .populate('passengerId', 'username name surname profile.img profile.driverProfile.averageRating') - .populate('rideId', 'departure destination dateTime passengers') + .populate('rideId', 'departure destination departureDate passengers') .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)) @@ -858,7 +964,7 @@ const getSentRequests = async (req, res) => { // Fetch requests const requests = await RideRequest.find(query) .populate('driverId', 'username name surname profile.img profile.driverProfile.averageRating') - .populate('rideId', 'departure destination dateTime passengers') + .populate('rideId', 'departure destination departureDate passengers') .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)) @@ -872,7 +978,7 @@ const getSentRequests = async (req, res) => { enriched.rideInfo = { departure: enriched.rideId.departure?.city || enriched.rideId.departure, destination: enriched.rideId.destination?.city || enriched.rideId.destination, - dateTime: enriched.rideId.dateTime, + departureDate: enriched.rideId.departureDate, availableSeats: enriched.rideId.passengers?.available || 0, }; } @@ -949,4 +1055,5 @@ module.exports = { getRequestById, getReceivedRequests, getSentRequests, -}; \ No newline at end of file + createRequestFromRide, +}; diff --git a/src/controllers/viaggi/TrasportiNotifications.js b/src/controllers/viaggi/TrasportiNotifications.js new file mode 100644 index 0000000..2380aba --- /dev/null +++ b/src/controllers/viaggi/TrasportiNotifications.js @@ -0,0 +1,847 @@ +/** + * TrasportiNotifications.js + * + * Servizio notifiche centralizzato per Trasporti Solidali. + * USA il telegrambot.js esistente per Telegram, AGGIUNGE Email e Push. + * + * NON MODIFICA telegrambot.js - lo importa e usa i suoi metodi. + */ + +const nodemailer = require('nodemailer'); +const webpush = require('web-push'); + +// Importa il tuo telegrambot esistente +const MyTelegramBot = require('../../telegram/telegrambot'); + +// ============================================================================= +// CONFIGURAZIONE +// ============================================================================= + +const config = { + // Email SMTP + smtp: { + host: process.env.SMTP_HOST || 'smtp.gmail.com', + port: parseInt(process.env.SMTP_PORT) || 465, + secure: true, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + } + }, + emailFrom: process.env.SMTP_FROM || 'noreply@trasporti.app', + + // Push VAPID + vapidPublicKey: process.env.VAPID_PUBLIC_KEY, + vapidPrivateKey: process.env.VAPID_PRIVATE_KEY, + vapidEmail: process.env.VAPID_EMAIL || 'admin@trasporti.app', + + // App + appName: process.env.APP_NAME || 'Trasporti Solidali', + appUrl: process.env.APP_URL || 'https://trasporti.app' +}; + +// Configura web-push se le chiavi sono presenti +if (config.vapidPublicKey && config.vapidPrivateKey) { + webpush.setVapidDetails( + `mailto:${config.vapidEmail}`, + config.vapidPublicKey, + config.vapidPrivateKey + ); +} + +// Crea transporter email +let emailTransporter = null; +if (config.smtp.auth.user && config.smtp.auth.pass) { + emailTransporter = nodemailer.createTransport(config.smtp); +} + +// ============================================================================= +// TIPI DI NOTIFICA +// ============================================================================= + +const NotificationType = { + // Viaggi + NEW_RIDE_REQUEST: 'new_ride_request', + REQUEST_ACCEPTED: 'request_accepted', + REQUEST_REJECTED: 'request_rejected', + RIDE_REMINDER_24H: 'ride_reminder_24h', + RIDE_REMINDER_2H: 'ride_reminder_2h', + RIDE_CANCELLED: 'ride_cancelled', + RIDE_MODIFIED: 'ride_modified', + + // Messaggi + NEW_MESSAGE: 'new_message', + + // Community + NEW_COMMUNITY_RIDE: 'new_community_ride', + + // Sistema + WEEKLY_DIGEST: 'weekly_digest', + TEST: 'test', + WELCOME: 'welcome' +}; + +// ============================================================================= +// EMOJI PER NOTIFICHE +// ============================================================================= + +const emo = { + CAR: '🚗', + PASSENGER: '🧑🤝🧑', + CHECK: '✅', + CROSS: '❌', + BELL: '🔔', + CLOCK: '⏰', + CALENDAR: '📅', + PIN: '📍', + ARROW: '➡️', + MESSAGE: '💬', + STAR: '⭐', + WARNING: '⚠️', + INFO: 'ℹ️', + WAVE: '👋', + HEART: '❤️' +}; + +// ============================================================================= +// TRADUZIONI NOTIFICHE +// ============================================================================= + +const translations = { + it: { + // Richieste + NEW_RIDE_REQUEST_TITLE: 'Nuova richiesta di passaggio', + NEW_RIDE_REQUEST_BODY: '{{passengerName}} chiede un passaggio per il viaggio {{departure}} → {{destination}} del {{date}}', + NEW_RIDE_REQUEST_ACTION: 'Visualizza richiesta', + + // Accettazione + REQUEST_ACCEPTED_TITLE: 'Richiesta accettata!', + REQUEST_ACCEPTED_BODY: '{{driverName}} ha accettato la tua richiesta per {{departure}} → {{destination}} del {{date}}', + REQUEST_ACCEPTED_ACTION: 'Visualizza viaggio', + + // Rifiuto + REQUEST_REJECTED_TITLE: 'Richiesta non accettata', + REQUEST_REJECTED_BODY: '{{driverName}} non ha potuto accettare la tua richiesta per {{departure}} → {{destination}}', + + // Promemoria + RIDE_REMINDER_24H_TITLE: 'Viaggio domani!', + RIDE_REMINDER_24H_BODY: 'Promemoria: domani hai un viaggio {{departure}} → {{destination}} alle {{time}}', + RIDE_REMINDER_2H_TITLE: 'Viaggio tra 2 ore!', + RIDE_REMINDER_2H_BODY: 'Il tuo viaggio {{departure}} → {{destination}} parte tra 2 ore alle {{time}}', + + // Cancellazione + RIDE_CANCELLED_TITLE: 'Viaggio cancellato', + RIDE_CANCELLED_BODY: 'Il viaggio {{departure}} → {{destination}} del {{date}} è stato cancellato', + RIDE_CANCELLED_REASON: 'Motivo: {{reason}}', + + // Modifica + RIDE_MODIFIED_TITLE: 'Viaggio modificato', + RIDE_MODIFIED_BODY: 'Il viaggio {{departure}} → {{destination}} è stato modificato. Verifica i nuovi dettagli.', + + // Messaggi + NEW_MESSAGE_TITLE: 'Nuovo messaggio', + NEW_MESSAGE_BODY: '{{senderName}}: {{preview}}', + + // Community + NEW_COMMUNITY_RIDE_TITLE: 'Nuovo viaggio nella tua zona', + NEW_COMMUNITY_RIDE_BODY: 'Nuovo viaggio disponibile: {{departure}} → {{destination}} il {{date}}', + + // Test + TEST_TITLE: 'Notifica di test', + TEST_BODY: 'Questa è una notifica di test da Trasporti Solidali. Se la vedi, tutto funziona!', + + // Welcome + WELCOME_TITLE: 'Benvenuto su Trasporti Solidali!', + WELCOME_BODY: 'Le notifiche sono state attivate correttamente. Riceverai aggiornamenti sui tuoi viaggi.', + + // Common + VIEW_DETAILS: 'Visualizza dettagli', + REPLY: 'Rispondi' + }, + + en: { + NEW_RIDE_REQUEST_TITLE: 'New ride request', + NEW_RIDE_REQUEST_BODY: '{{passengerName}} requests a ride for {{departure}} → {{destination}} on {{date}}', + NEW_RIDE_REQUEST_ACTION: 'View request', + + REQUEST_ACCEPTED_TITLE: 'Request accepted!', + REQUEST_ACCEPTED_BODY: '{{driverName}} accepted your request for {{departure}} → {{destination}} on {{date}}', + REQUEST_ACCEPTED_ACTION: 'View ride', + + REQUEST_REJECTED_TITLE: 'Request not accepted', + REQUEST_REJECTED_BODY: '{{driverName}} could not accept your request for {{departure}} → {{destination}}', + + RIDE_REMINDER_24H_TITLE: 'Ride tomorrow!', + RIDE_REMINDER_24H_BODY: 'Reminder: tomorrow you have a ride {{departure}} → {{destination}} at {{time}}', + RIDE_REMINDER_2H_TITLE: 'Ride in 2 hours!', + RIDE_REMINDER_2H_BODY: 'Your ride {{departure}} → {{destination}} leaves in 2 hours at {{time}}', + + RIDE_CANCELLED_TITLE: 'Ride cancelled', + RIDE_CANCELLED_BODY: 'The ride {{departure}} → {{destination}} on {{date}} has been cancelled', + RIDE_CANCELLED_REASON: 'Reason: {{reason}}', + + RIDE_MODIFIED_TITLE: 'Ride modified', + RIDE_MODIFIED_BODY: 'The ride {{departure}} → {{destination}} has been modified. Check the new details.', + + NEW_MESSAGE_TITLE: 'New message', + NEW_MESSAGE_BODY: '{{senderName}}: {{preview}}', + + NEW_COMMUNITY_RIDE_TITLE: 'New ride in your area', + NEW_COMMUNITY_RIDE_BODY: 'New ride available: {{departure}} → {{destination}} on {{date}}', + + TEST_TITLE: 'Test notification', + TEST_BODY: 'This is a test notification from Trasporti Solidali. If you see this, everything works!', + + WELCOME_TITLE: 'Welcome to Trasporti Solidali!', + WELCOME_BODY: 'Notifications have been enabled successfully. You will receive updates about your rides.', + + VIEW_DETAILS: 'View details', + REPLY: 'Reply' + } +}; + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +/** + * Ottiene traduzione con sostituzione variabili + */ +function getTranslation(lang, key, data = {}) { + const langTranslations = translations[lang] || translations['it']; + let text = langTranslations[key] || translations['it'][key] || key; + + // Sostituisci {{variabile}} + Object.keys(data).forEach(varName => { + const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g'); + text = text.replace(regex, data[varName] || ''); + }); + + return text; +} + +/** + * Mappa tipo notifica a chiave preferenze + */ +function getPreferenceKey(type) { + const map = { + [NotificationType.NEW_RIDE_REQUEST]: 'newRideRequest', + [NotificationType.REQUEST_ACCEPTED]: 'requestAccepted', + [NotificationType.REQUEST_REJECTED]: 'requestRejected', + [NotificationType.RIDE_REMINDER_24H]: 'rideReminder24h', + [NotificationType.RIDE_REMINDER_2H]: 'rideReminder2h', + [NotificationType.RIDE_CANCELLED]: 'rideCancelled', + [NotificationType.RIDE_MODIFIED]: 'rideCancelled', + [NotificationType.NEW_MESSAGE]: 'newMessage', + [NotificationType.NEW_COMMUNITY_RIDE]: 'newCommunityRide', + [NotificationType.WEEKLY_DIGEST]: 'weeklyDigest', + [NotificationType.TEST]: null, // Sempre inviato + [NotificationType.WELCOME]: null // Sempre inviato + }; + return map[type]; +} + +/** + * Verifica se inviare notifica su un canale + */ +function shouldSend(prefs, channel, type) { + if (!prefs) return false; + + const channelPrefs = prefs[channel]; + if (!channelPrefs || !channelPrefs.enabled) return false; + + // Test e Welcome sempre inviati se canale abilitato + if (type === NotificationType.TEST || type === NotificationType.WELCOME) { + return true; + } + + const prefKey = getPreferenceKey(type); + if (!prefKey) return true; // Se non mappato, invia + + return channelPrefs[prefKey] !== false; // Default true +} + +/** + * Tronca testo + */ +function truncate(text, maxLength = 100) { + if (!text || text.length <= maxLength) return text; + return text.substring(0, maxLength - 3) + '...'; +} + +// ============================================================================= +// EMAIL TEMPLATES +// ============================================================================= + +function buildEmailHtml(type, data, lang = 'it') { + const t = (key) => getTranslation(lang, key, data); + + const title = t(`${type.toUpperCase()}_TITLE`); + const body = t(`${type.toUpperCase()}_BODY`); + + // Colori per tipo + const colors = { + [NotificationType.NEW_RIDE_REQUEST]: '#667eea', + [NotificationType.REQUEST_ACCEPTED]: '#21ba45', + [NotificationType.REQUEST_REJECTED]: '#c10015', + [NotificationType.RIDE_REMINDER_24H]: '#f2711c', + [NotificationType.RIDE_REMINDER_2H]: '#db2828', + [NotificationType.RIDE_CANCELLED]: '#c10015', + [NotificationType.NEW_MESSAGE]: '#2185d0', + [NotificationType.NEW_COMMUNITY_RIDE]: '#a333c8', + [NotificationType.TEST]: '#667eea', + [NotificationType.WELCOME]: '#21ba45' + }; + + const color = colors[type] || '#667eea'; + + // Emoji per tipo + const emojis = { + [NotificationType.NEW_RIDE_REQUEST]: emo.PASSENGER, + [NotificationType.REQUEST_ACCEPTED]: emo.CHECK, + [NotificationType.REQUEST_REJECTED]: emo.CROSS, + [NotificationType.RIDE_REMINDER_24H]: emo.CALENDAR, + [NotificationType.RIDE_REMINDER_2H]: emo.CLOCK, + [NotificationType.RIDE_CANCELLED]: emo.WARNING, + [NotificationType.NEW_MESSAGE]: emo.MESSAGE, + [NotificationType.NEW_COMMUNITY_RIDE]: emo.CAR, + [NotificationType.TEST]: emo.BELL, + [NotificationType.WELCOME]: emo.WAVE + }; + + const emoji = emojis[type] || emo.BELL; + + // CTA button + let ctaHtml = ''; + if (data.actionUrl) { + const actionText = data.actionText || t('VIEW_DETAILS'); + ctaHtml = ` +
+ `; + } + + // Info viaggio + let rideInfoHtml = ''; + if (data.departure && data.destination) { + rideInfoHtml = ` ++ ${body} +
+ + ${rideInfoHtml} + + ${data.reason ? `${t('RIDE_CANCELLED_REASON')}
` : ''} + + ${ctaHtml} ++ ${config.appName} +
++ Ricevi questa email perché hai attivato le notifiche. + Gestisci preferenze +
+${err.stack || err.message}`;
- for (const id of ADMIN_GROUP_IDS) {
- await sendMessage(id, msg);
- }
-});
-
-module.exports = { reportError };
diff --git a/src/telegram/handlers/friendsHandler.js b/src/telegram/handlers/friendsHandler.js
deleted file mode 100644
index ffb366a..0000000
--- a/src/telegram/handlers/friendsHandler.js
+++ /dev/null
@@ -1,61 +0,0 @@
-// telegram/handlers/friendsHandler.js
-const shared_consts = require('../../tools/shared_nodejs');
-const tools = require('../../tools/general');
-const { User } = require('../../models/user');
-const printf = require('util').format;
-const { handleRegistration, InlineConferma } = require('./registrationHandler');
-
-async function handleFriends({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
- let notifyText = '';
-
- // SI -> amicizia
- if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_AMICIZIA) {
- if (userDest) {
- const req = tools.getReqByPar(idapp, username_action);
- const already = await User.isMyFriend(idapp, data.username, data.userDest);
- if (!already) await User.setFriendsCmd(req, idapp, data.username, data.userDest, shared_consts.FRIENDSCMD.SETFRIEND);
- await cl.sendMsg(msg.chat.id, '🤝 Amicizia confermata!');
- notifyText = 'Amicizia OK';
- }
- return notifyText;
- }
-
- // NO -> amicizia (rimuovi/nega)
- if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RICHIESTA_AMICIZIA) {
- if (userDest) {
- const req = tools.getReqByPar(idapp, username_action);
- const ris = await User.setFriendsCmd(req, idapp, data.username, data.userDest, shared_consts.FRIENDSCMD.REMOVE_FROM_MYFRIENDS);
- if (ris) {
- const msgDest = printf(tools.gettranslate('MSG_FRIENDS_NOT_ACCEPTED_CONFIRMED', user.lang), data.username);
- await localSendMsgByUsername(idapp, data.userDest, msgDest);
- }
- await cl.sendMsg(msg.chat.id, '🚫 Amicizia rifiutata.');
- notifyText = 'Rifiutata';
- }
- return notifyText;
- }
-
- // SI -> handshake
- if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_HANDSHAKE) {
- if (userDest) {
- const req = tools.getReqByPar(idapp, username_action);
- const already = await User.isMyHandShake(idapp, data.userDest, data.username);
- if (!already) await User.setFriendsCmd(req, idapp, data.userDest, data.username, shared_consts.FRIENDSCMD.SETHANDSHAKE);
- await cl.sendMsg(msg.chat.id, '🤝 Handshake confermato!');
- notifyText = 'Handshake OK';
- }
- return notifyText;
- }
-
- return 'OK';
-}
-
-// helper locale (equivalente del tuo local_sendMsgTelegram)
-async function localSendMsgByUsername(idapp, username, text) {
- const teleg_id = await User.TelegIdByUsername(idapp, username);
- const cl = require('../telegram.bot.init').getclTelegByidapp(idapp);
- if (cl && teleg_id) return await cl.sendMsg(teleg_id, text);
- return null;
-}
-
-module.exports = { handleFriends };
diff --git a/src/telegram/handlers/groupHandler.js b/src/telegram/handlers/groupHandler.js
deleted file mode 100644
index e30655d..0000000
--- a/src/telegram/handlers/groupHandler.js
+++ /dev/null
@@ -1,70 +0,0 @@
-// telegram/handlers/groupHandler.js
-const shared_consts = require('../../tools/shared_nodejs');
-const tools = require('../../tools/general');
-const { User } = require('../../models/user');
-
-const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
-
-/**
- * Gestisce conferma/rifiuto a richieste di GRUPPO
- * Payload data:
- * - action
- * - username (mittente originale)
- * - userDest (destinatario/utente da aggiungere)
- * - groupId (id o path del gruppo)
- * - groupname (nome del gruppo)
- */
-async function handleGroup({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
- let notifyText = '';
-
- // SI → accetta richiesta d'ingresso nel gruppo
- if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_GRUPPO) {
- // Se l’app ha funzioni di persistenza specifiche, usale se esistono
- // (non assumo nomi rigidi per non rompere il deploy)
- if (typeof User.setGroupCmd === 'function') {
- try {
- await User.setGroupCmd(idapp, data.userDest || data.username, data.groupId, shared_consts.GROUPCMD.ADDUSERTOGROUP, 1, username_action, { groupname: data.groupname });
- } catch (e) {
- console.warn('handleGroup:setGroupCmd non eseguito:', e.message);
- }
- }
-
- // Messaggi di conferma
- await cl.sendMsg(msg.chat.id, `✅ ${data.userDest || data.username} è stato aggiunto al gruppo ${data.groupname || data.groupId}.`);
- // Notifica anche l’utente interessato
- const targetUsername = data.userDest || data.username;
- const teleg_id = await User.TelegIdByUsername(idapp, targetUsername);
- if (teleg_id) {
- await cl.sendMsg(teleg_id, `👥 Sei stato aggiunto al gruppo: ${data.groupname || data.groupId}`);
- }
-
- notifyText = 'Gruppo: aggiunta OK';
- return notifyText;
- }
-
- // NO → rifiuta/annulla richiesta
- if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RICHIESTA_GRUPPO) {
- if (typeof User.setGroupCmd === 'function') {
- try {
- await User.setGroupCmd(idapp, data.userDest || data.username, data.groupId, shared_consts.GROUPCMD.REMOVEUSERFROMGROUP, 0, username_action, { groupname: data.groupname });
- } catch (e) {
- console.warn('handleGroup:setGroupCmd non eseguito:', e.message);
- }
- }
-
- await cl.sendMsg(msg.chat.id, '🚫 Richiesta gruppo rifiutata.');
- // Avvisa il richiedente
- const targetUsername = data.userDest || data.username;
- const teleg_id = await User.TelegIdByUsername(idapp, targetUsername);
- if (teleg_id) {
- await cl.sendMsg(teleg_id, `❌ La tua richiesta per il gruppo ${data.groupname || data.groupId} è stata rifiutata.`);
- }
-
- notifyText = 'Gruppo: rifiutata';
- return notifyText;
- }
-
- return 'OK';
-}
-
-module.exports = { handleGroup };
diff --git a/src/telegram/handlers/multiAppHandler.js b/src/telegram/handlers/multiAppHandler.js
deleted file mode 100644
index ef19d49..0000000
--- a/src/telegram/handlers/multiAppHandler.js
+++ /dev/null
@@ -1,47 +0,0 @@
-const { sendMessage } = require('../api');
-const { safeExec, eachSeries } = require('../helpers');
-const tools = require('../../tools/general');
-const {
- getAdminTelegramUsers,
- getManagersTelegramUsers,
- getAllTelegramUsersByApp,
-} = require('./userQuery');
-
-const sendMsgTelegramToTheAdminAllSites = safeExec(async (text, alsoGroups = false) => {
- const apps = await tools.getApps(); // deve restituire {idapp,...}
- await eachSeries(apps, async (app) => {
- const admins = await getAdminTelegramUsers(app.idapp);
- await eachSeries(admins, async (u) => sendMessage(u.telegram_id, text));
- if (alsoGroups && app?.telegram_admin_group_id) {
- await sendMessage(app.telegram_admin_group_id, text);
- }
- });
-});
-
-const sendMsgTelegramByIdApp = safeExec(async (idapp, text) => {
- const users = await getAllTelegramUsersByApp(idapp);
- await eachSeries(users, async (u) => sendMessage(u.telegram_id, text));
-});
-
-const sendMsgTelegramToTheManagers = safeExec(async (idapp, text) => {
- const managers = await getManagersTelegramUsers(idapp);
- await eachSeries(managers, async (u) => sendMessage(u.telegram_id, text));
-});
-
-const sendMsgTelegramToTheAdmin = safeExec(async (idapp, text) => {
- const admins = await getAdminTelegramUsers(idapp);
- await eachSeries(admins, async (u) => sendMessage(u.telegram_id, text));
-});
-
-const sendMsgTelegramToTheGroup = safeExec(async (chatId, text) => {
- if (!chatId) return null;
- return sendMessage(chatId, text);
-});
-
-module.exports = {
- sendMsgTelegramToTheAdminAllSites,
- sendMsgTelegramByIdApp,
- sendMsgTelegramToTheManagers,
- sendMsgTelegramToTheAdmin,
- sendMsgTelegramToTheGroup,
-};
diff --git a/src/telegram/handlers/notificationHandler.js b/src/telegram/handlers/notificationHandler.js
deleted file mode 100644
index 03290c8..0000000
--- a/src/telegram/handlers/notificationHandler.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const { sendMessage } = require('../api');
-const { safeExec } = require('../helpers');
-
-const sendNotification = safeExec(async (chatId, title, body) => {
- const msg = `🔔 ${title}\n${body}`;
- await sendMessage(chatId, msg);
-});
-
-module.exports = { sendNotification };
diff --git a/src/telegram/handlers/passwordHandler.js b/src/telegram/handlers/passwordHandler.js
deleted file mode 100644
index 097c2be..0000000
--- a/src/telegram/handlers/passwordHandler.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// telegram/handlers/passwordHandler.js
-const shared_consts = require('../../tools/shared_nodejs');
-const tools = require('../../tools/general');
-
-const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
-
-async function handlePassword({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
- let notifyText = '';
-
- if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RESET_PWD) {
- // Nel tuo codice usavi anche tools.sendNotificationToUser ecc.
- await tools.sendNotificationToUser(
- user?._id || msg.chat.id,
- '🔑 Reset Password',
- `La password di ${data.username} è stata resettata.`,
- '/',
- '',
- 'server',
- []
- );
- await cl.sendMsg(msg.chat.id, '✅ Password resettata.');
- notifyText = 'Reset OK';
- return notifyText;
- }
-
- if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RESET_PWD) {
- await cl.sendMsg(msg.chat.id, '❌ Reset password annullato.');
- notifyText = 'Annullato';
- return notifyText;
- }
-
- return 'OK';
-}
-
-module.exports = { handlePassword };
diff --git a/src/telegram/handlers/phaseHandler.js b/src/telegram/handlers/phaseHandler.js
deleted file mode 100644
index 6b5d0b9..0000000
--- a/src/telegram/handlers/phaseHandler.js
+++ /dev/null
@@ -1,65 +0,0 @@
-const messages = require('../messages');
-const { phase } = require('../constants');
-const { safeExec } = require('../helpers');
-const { sendMessage } = require('../api');
-const {
- getAdminTelegramUsers,
- getManagersTelegramUsers,
-} = require('./userQuery');
-
-// locals: { idapp, username, nomeapp, text, ... }
-const notifyToTelegram = safeExec(async (ph, locals = {}) => {
- const idapp = String(locals.idapp || '');
- let text = '';
-
- const templ = messages.byPhase[ph] || messages.byPhase.GENERIC;
- text = templ(locals);
-
- // router di default: manda agli admin dell'app
- const admins = await getAdminTelegramUsers(idapp);
- for (const a of admins) {
- if (a.telegram_id) await sendMessage(a.telegram_id, text);
- }
-});
-
-const askConfirmationUser = safeExec(async (idapp, phaseCode, user) => {
- const txt = messages.askConfirmationUser({
- idapp,
- username: user?.username,
- nomeapp: user?.nomeapp,
- });
- if (user?.telegram_id) await sendMessage(user.telegram_id, txt);
-});
-
-// helper semplici
-const sendNotifToAdmin = safeExec(async (idapp, title, body = '') => {
- const admins = await getAdminTelegramUsers(String(idapp));
- const txt = `📣 ${title}\n${body}`;
- for (const a of admins) {
- if (a.telegram_id) await sendMessage(a.telegram_id, txt);
- }
-});
-
-const sendNotifToManager = safeExec(async (idapp, title, body = '') => {
- const managers = await getManagersTelegramUsers(String(idapp));
- const txt = `📣 ${title}\n${body}`;
- for (const m of managers) {
- if (m.telegram_id) await sendMessage(m.telegram_id, txt);
- }
-});
-
-const sendNotifToAdminOrManager = safeExec(async (idapp, title, body = '', preferManagers = false) => {
- if (preferManagers) {
- return sendNotifToManager(idapp, title, body);
- }
- return sendNotifToAdmin(idapp, title, body);
-});
-
-module.exports = {
- notifyToTelegram,
- askConfirmationUser,
- sendNotifToAdmin,
- sendNotifToManager,
- sendNotifToAdminOrManager,
- phase, // re-export utile
-};
diff --git a/src/telegram/handlers/registrationHandler.js b/src/telegram/handlers/registrationHandler.js
deleted file mode 100644
index 502a702..0000000
--- a/src/telegram/handlers/registrationHandler.js
+++ /dev/null
@@ -1,50 +0,0 @@
-// telegram/handlers/registrationHandler.js
-const shared_consts = require('../../tools/shared_nodejs');
-const { User } = require('../../models/user');
-const telegrambot = require('../telegram.bot.init'); // per sendMsgTelegramToTheAdminAllSites
-const printf = require('util').format;
-
-const InlineConferma = {
- RISPOSTA_SI: 'SI_',
- RISPOSTA_NO: 'NO_',
-};
-
-async function handleRegistration({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
- let notifyText = '';
-
- // NO alla registrazione
- if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION) {
- await cl.sendMsg(msg.chat.id, '❌ Registrazione annullata.');
- notifyText = 'Annullata';
- return notifyText;
- }
-
- // SI alla registrazione standard
- if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION) {
- // set verified (come da tuo codice)
- await User.setVerifiedReg(idapp, data.username, data.userDest);
- await cl.sendMsg(msg.chat.id, '✅ Registrazione confermata.');
- await telegrambot.sendMsgTelegramToTheAdminAllSites(`🆕 Nuova registrazione confermata: ${data.userDest}`);
- notifyText = 'Registrazione OK';
- return notifyText;
- }
-
- // SI/NO alla REGISTRATION_FRIEND
- if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION_FRIEND) {
- await User.setVerifiedReg(idapp, data.username, data.userDest);
- await cl.sendMsg(msg.chat.id, '🤝 Conferma amicizia completata!');
- notifyText = 'Amicizia OK';
- return notifyText;
- }
-
- if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION_FRIEND) {
- await cl.sendMsg(msg.chat.id, '🚫 Invito amicizia rifiutato.');
- notifyText = 'Rifiutata';
- return notifyText;
- }
-
- // deleghe future (es. REGISTRATION_TOZOOM gestita in zoomHandler)
- return 'OK';
-}
-
-module.exports = { handleRegistration, InlineConferma };
diff --git a/src/telegram/handlers/userHandler.js b/src/telegram/handlers/userHandler.js
deleted file mode 100644
index 43f2d4a..0000000
--- a/src/telegram/handlers/userHandler.js
+++ /dev/null
@@ -1,15 +0,0 @@
-const { sendMessage, sendPhoto } = require('../api');
-const { formatUser, safeExec } = require('../helpers');
-
-const notifyUser = safeExec(async (user, text) => {
- if (!user?.telegram_id) return;
- const msg = `👋 Ciao ${formatUser(user)}\n${text}`;
- await sendMessage(user.telegram_id, msg);
-});
-
-const sendUserPhoto = safeExec(async (user, photoUrl, caption) => {
- if (!user?.telegram_id) return;
- await sendPhoto(user.telegram_id, photoUrl, caption);
-});
-
-module.exports = { notifyUser, sendUserPhoto };
diff --git a/src/telegram/handlers/userQuery.js b/src/telegram/handlers/userQuery.js
deleted file mode 100644
index e7bdf59..0000000
--- a/src/telegram/handlers/userQuery.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const { User } = require('../../models/user');
-
-async function getTelegramUsersByQuery(query = {}) {
- return User.find({
- ...query,
- telegram_id: { $exists: true, $ne: null },
- }).lean();
-}
-
-async function getAdminTelegramUsers(idapp) {
- return getTelegramUsersByQuery({ idapp, isAdmin: true });
-}
-
-async function getManagersTelegramUsers(idapp) {
- return getTelegramUsersByQuery({ idapp, isManager: true });
-}
-
-async function getFacilitatoriTelegramUsers(idapp) {
- return getTelegramUsersByQuery({ idapp, isFacilitatore: true });
-}
-
-async function getAllTelegramUsersByApp(idapp) {
- return getTelegramUsersByQuery({ idapp });
-}
-
-module.exports = {
- getTelegramUsersByQuery,
- getAdminTelegramUsers,
- getManagersTelegramUsers,
- getFacilitatoriTelegramUsers,
- getAllTelegramUsersByApp,
-};
diff --git a/src/telegram/handlers/zoomHandler.js b/src/telegram/handlers/zoomHandler.js
deleted file mode 100644
index 4f56fd2..0000000
--- a/src/telegram/handlers/zoomHandler.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// telegram/handlers/zoomHandler.js
-const shared_consts = require('../../tools/shared_nodejs');
-const { User } = require('../../models/user');
-
-const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
-
-async function handleZoom({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
- let notifyText = '';
-
- if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION_TOZOOM) {
- // nelle tue callback originale: conferma registrazione + messaggio
- await User.setVerifiedReg(idapp, data.username, data.userDest);
- await cl.sendMsg(msg.chat.id, '🟢 Accesso Zoom confermato!');
- notifyText = 'Zoom OK';
- return notifyText;
- }
-
- if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION_TOZOOM) {
- await cl.sendMsg(msg.chat.id, '🚫 Accesso Zoom rifiutato.');
- notifyText = 'Rifiutato';
- return notifyText;
- }
-
- return 'OK';
-}
-
-module.exports = { handleZoom };
diff --git a/src/telegram/helpers.js b/src/telegram/helpers.js
deleted file mode 100644
index 0d5e01a..0000000
--- a/src/telegram/helpers.js
+++ /dev/null
@@ -1,31 +0,0 @@
-function formatUser(user) {
- const u = user || {};
- const username = u.username || (u.profile && u.profile.username_telegram) || 'no_username';
- return `${u.name || ''} ${u.surname || ''} (@${username})`.trim();
-}
-
-function safeExec(fn) {
- return async (...args) => {
- try {
- return await fn(...args);
- } catch (e) {
- console.error('Telegram helper error:', e);
- return null;
- }
- };
-}
-
-function ensureArray(x) {
- if (!x) return [];
- return Array.isArray(x) ? x : [x];
-}
-
-// utility semplice per evitare flood (se ti serve rate-limit: usa bottleneck)
-async function eachSeries(arr, fn) {
- for (const item of arr) {
- // eslint-disable-next-line no-await-in-loop
- await fn(item);
- }
-}
-
-module.exports = { formatUser, safeExec, ensureArray, eachSeries };
diff --git a/src/telegram/index_new_check.js b/src/telegram/index_new_check.js
deleted file mode 100644
index 40d86fc..0000000
--- a/src/telegram/index_new_check.js
+++ /dev/null
@@ -1,66 +0,0 @@
-const messages = require('./messages');
-
-// base api/handlers già creati in precedenza
-const { sendMessage, sendPhoto } = require('./api');
-const { sendToAdmins } = require('./handlers/adminHandler');
-const { notifyUser, sendUserPhoto } = require('./handlers/userHandler');
-const { sendNotification } = require('./handlers/notificationHandler');
-const { reportError } = require('./handlers/errorHandler');
-
-// NUOVI HANDLER aggiunti ora
-const {
- sendMsgTelegram,
- sendMsgTelegramByIdTelegram,
- sendPhotoTelegram,
-} = require('./handlers/directHandler');
-
-const {
- sendMsgTelegramToTheAdminAllSites,
- sendMsgTelegramByIdApp,
- sendMsgTelegramToTheManagers,
- sendMsgTelegramToTheAdmin,
- sendMsgTelegramToTheGroup,
-} = require('./handlers/multiAppHandler');
-
-const {
- notifyToTelegram,
- askConfirmationUser,
- sendNotifToAdmin,
- sendNotifToManager,
- sendNotifToAdminOrManager,
- phase,
-} = require('./handlers/phaseHandler');
-
-module.exports = {
- // messaggi/template
- messages,
- phase,
-
- // API raw
- sendMessage,
- sendPhoto,
-
- // generico
- sendToAdmins,
- notifyUser,
- sendUserPhoto,
- sendNotification,
- reportError,
-
- // EQUIVALENTI del vecchio file
- sendMsgTelegram, // (user, text)
- sendMsgTelegramByIdTelegram, // (telegramId, text)
- sendPhotoTelegram, // (chatIdOrUser, photoUrl, caption)
-
- sendMsgTelegramToTheAdminAllSites, // (text, alsoGroups?)
- sendMsgTelegramByIdApp, // (idapp, text)
- sendMsgTelegramToTheManagers, // (idapp, text)
- sendMsgTelegramToTheAdmin, // (idapp, text)
- sendMsgTelegramToTheGroup, // (chatId, text)
-
- notifyToTelegram, // (phase, locals)
- askConfirmationUser, // (idapp, phase, user)
- sendNotifToAdmin, // (idapp, title, body)
- sendNotifToManager, // (idapp, title, body)
- sendNotifToAdminOrManager, // (idapp, title, body, preferManagers?)
-};
diff --git a/src/telegram/messages.js b/src/telegram/messages.js
deleted file mode 100644
index fda2701..0000000
--- a/src/telegram/messages.js
+++ /dev/null
@@ -1,25 +0,0 @@
-module.exports = {
- // messaggi generici
- serverStarted: (dbName) => `🚀 Il server ${dbName} è stato avviato con successo.`,
- userUnlocked: (user) => `⚠️ L'utente ${user.username} (${user.name} ${user.surname}) è stato sbloccato.`,
- errorOccurred: (context, err) =>
- `❌ Errore in ${context}\n${(err && err.message) || err}`,
- notifyAdmin: (msg) => `📢 Notifica Admin:\n${msg}`,
-
- // fasi logiche
- byPhase: {
- REGISTRATION: (locals = {}) =>
- `🆕 Nuova registrazione su ${locals.nomeapp || 'App'}\nUtente: ${locals.username}`,
- REGISTRATION_CONFIRMED: (locals = {}) =>
- `✅ Registrazione confermata su ${locals.nomeapp || 'App'} da ${locals.username}`,
- RESET_PWD: (locals = {}) =>
- `🔁 Reset password richiesto per ${locals.username}`,
- NOTIFICATION: (locals = {}) =>
- `🔔 Notifica: ${locals.text || ''}`,
- GENERIC: (locals = {}) =>
- `${locals.text || ''}`,
- },
-
- askConfirmationUser: (locals = {}) =>
- `👋 Ciao ${locals.username}!\nConfermi l'operazione su ${locals.nomeapp || 'App'}?`,
-};
diff --git a/src/telegram/prova.txt b/src/telegram/prova.txt
deleted file mode 100644
index 3939fbf..0000000
--- a/src/telegram/prova.txt
+++ /dev/null
@@ -1 +0,0 @@
-http://localhost:8084/signup/paoloar77/SuryaArena/5356627050
diff --git a/src/telegram/telegram.bot.init.js b/src/telegram/telegram.bot.init.js
deleted file mode 100644
index df4346e..0000000
--- a/src/telegram/telegram.bot.init.js
+++ /dev/null
@@ -1,488 +0,0 @@
-/**
- * ======================================================
- * TELEGRAM BOT INIT (derived from telegrambot_OLD.js)
- * ======================================================
- * - Gestione multi-bot per app (arrTelegram)
- * - Classe Telegram con funzioni core
- * - Invio messaggi, immagini, notifiche
- * - Callback Query, menu e inline keyboard
- * - API pubbliche per admin, manager, utenti
- * - Compatibile con tools, User, Circuit, ecc.
- * ======================================================
- */
-
-const TelegramBot = require('node-telegram-bot-api');
-const fs = require('fs');
-const path = require('path');
-const axios = require('axios');
-const sharp = require('sharp');
-
-const tools = require('../tools/general');
-const shared_consts = require('../tools/shared_nodejs');
-const server_constants = require('../tools/server_constants');
-const { Site } = require('../models/site');
-
-const { handleCallback } = require('./handlers/callbackHandler');
-
-
-// -----------------------------------------------------------------------------
-// 🔹 COSTANTI, ENUM, EMOJI E TEXT
-// -----------------------------------------------------------------------------
-const emo = {
- JOY: '😂',
- JOY2: '🤣',
- DANCER: '💃',
- STARS: '✨',
- FIRE: '🔥',
- SUN: '☀️',
- TV: '📺',
- NEWSPAPER: '🗞',
- KISS: '😘',
- PENCIL: '✏️',
- DREAM: '🏖',
- EYES: '😜',
- DIZZY: '💫',
- ONE_HUNDRED: '💯',
- SMILE_STAR: '🤩', // Star-struck
- LEFT_FACING_FIST: '🤛', // Left-facing fist
- CHECK_VERDE: '✅', // White check mark (verde)
- CHECK_GRIGIA: '☑️', // Ballot box with check (grigia)
- CROSS_ROSSA: '❌', // X (rossa)
- ENVELOPE: '✉️', // Envelope
- EXCLAMATION_MARK: '❗', // Exclamation mark
- QUESTION_MARK: '❓', // Question mark
- ARROW_RIGHT: '➡️', // Arrow pointing to the right
- INVITATI: '',
- HEART: '❤️',
- BLUE_HEART: '💙',
- GREEN_HEART: '💚',
- YELLOW_HEART: '💛',
- PURPLE_HEART: '💜',
- GIFT_HEART: '💝',
- GIFT: '🎁',
- ROBOT_FACE: '🤖',
- ADMIN: '💁',
- MALE: '💁♂️',
- FEMALE: '👩🦱',
- INNOCENT: '😇',
- CREDIT_CARD: '💳',
- PERSON: '🧑',
-};
-
-MsgBot = {
- OK: ['si', 'ok'],
- CUORE: ['❤️', '💚️', '💜'],
- CIAO: ['ciao', 'ciaoo', 'hola', 'holaa', 'hey', 'salve', 'buongiorno', 'buondi', 'ciao ❤️'],
- CI_SEI: ['ci sei', "c'è qualcuno", "c'è nessuno"],
- CHI_SONO_IO: ['chi sono io', 'chi sono'],
- COME_STAI: ['tutto bene', 'come stai', 'come stai', 'come va', 'come butta', 'come va oggi'],
- COME_TI_CHIAMI: [
- 'chi sei',
- 'come ti chiami',
- "qual'è il tuo nome",
- "qual'e' il tuo nome",
- 'che lavoro fai',
- 'di cosa ti occupi',
- ],
- COSA_FAI: ['cosa fai', 'cosa combini', 'che fai'],
- QUANTI_ANNI_HAI: ['quanti anni hai', 'che età hai'],
- SEI_LIBERO_STASERA: [
- 'sei libera stasera',
- 'sei libero stasera',
- 'usciamo insieme',
- "fare l'amore con me",
- 'fare sesso',
- 'vuoi scopare',
- 'vuoi trombare',
- ],
- MI_TROVI_UN_MOROSO: [
- 'trovi un moroso',
- 'una morosa',
- 'fidanzato',
- 'fidanzata',
- 'trovi un marito',
- 'trovi una moglie',
- ],
- CHAT_EMPOWER: ['chat empower'],
- MASCHIO_FEMMINA: ['sei uomo o donna', 'sei maschio o femmina', 'sei ragazzo o ragazza', 'che sesso hai'],
- DAMMI_UN_BACIO: ['dammi un bacio', 'baciami'],
- HAHA: ['hahaha', 'ahah', '😂'],
- MI_AMI: ['mi ami'],
- TI_AMO: ['ti amo', 'ti adoro', 'ti lovvo'],
- PREGO: ['prego', 'Prego ! 💋💋💋'],
- GRAZIE: ['grazie ainy', 'grazie', 'grazie mille', 'graziee', 'grazie ❤', 'grazie️❤', 'grazie 😘', 'grazie😘'],
- PRINCIPE_AZZURRO: ['principe azzurro'],
- START_INV: ['/start inv'],
- COSE_COVID: ["cos'è il covid", 'cosa è il covid'],
- COVID: ['covid'],
- SPOSAMI: ['sposami', 'vuoi sposar', 'sei sposat', 'ci sposiamo', 'ti sposo', 'sei sposat', 'mi sposi'],
- CHE_TEMPO_FA: ['che tempo'],
- NON_TROO_INVITATI: ['non trovo invitati', 'non riesco a trovare invitati'],
- TROVAMI_UN_UOMO_DONNA: ['trovami un uomo', 'trovami una donna', 'esiste una donna per me', 'esiste un uomo per me'],
- PAROLACCE: ['stronz', 'fanculo', 'fottiti', 'cagare', 'ammazzat', 'muori', 'cretino', 'stupido'],
- COME_SI_CHIAMA: ['come si chiama'],
- PROSSIMO_ZOOM: ['prossimo zoom', 'fare lo zoom', 'gli zoom', 'conferenz', 'zoom'],
- LAVAGNA: ['lavagna', 'Lavagna', 'LAVAGNA'],
- SEI_LIBERO_DI_RESPIRARE: ['sei libero di respirare'],
- SEI_LIBERO: ['sei liber', 'sei sposat', 'sei fidanzat', 'sei single'],
- AIUTO: [
- 'help',
- 'aiuto',
- 'ho bisogno di',
- 'ho problemi',
- 'non riesco',
- 'mi puoi aiutare',
- 'mi aiuti',
- 'aiutami',
- 'posso chiederti',
- 'puoi aiutarmi',
- ],
- UOMO: ['uomo', 'maschio'],
- SORPRESA: ['noo', 'davvero', 'sii', 'facciamo festa', 'è qui la festa', 'festa'],
- UGUALE: ['👍🏻', '✨', '❤🏻', '⭐', '❤', '❤❤', '🤩'],
- SI: ['si', 'yes'],
- NO: ['no', 'noo'],
- DONNA: ['donna', 'femmina'],
- FARE_DOMANDA: ['fare una domanda', 'posso farti una domanda'],
- DIVENTERO_RICCA: ['diventerò ricc'],
- DOVE_VUOI_VIVERE: ['dove vuoi vivere'],
- MA_ALLORA: ['ma allora'],
-};
-
-const MsgRisp = {
- CHAT_EMPOWER:
- 'Entra nella Chat EMPOWER !!!\n' +
- 'https://t.me/joinchat/C741mkx5QYXu-kyYCYvA8g ' +
- emo.PURPLE_HEART +
- emo.GIFT_HEART +
- emo.BLUE_HEART,
-};
-
-function getemojibynumber(number) {
- if (number === 0) {
- return '0️⃣'; // zero
- } else if (number === 1) {
- return '1️⃣'; // one
- } else if (number === 2) {
- return '2️⃣'; // two
- } else if (number === 3) {
- return '3️⃣'; // three
- } else if (number === 4) {
- return '4️⃣'; // four
- } else if (number === 5) {
- return '5️⃣'; // five
- } else if (number === 6) {
- return '6️⃣'; // six
- } else if (number === 7) {
- return '7️⃣'; // seven
- } else if (number === 8) {
- return '8️⃣'; // eight
- } else if (number === 9) {
- return '9️⃣'; // nine
- } else {
- return number;
- }
-}
-
-const Menu = {
- LANG_IT: '🇮🇹 Italiano', // Bandiera italiana
- LANG_EN: '🇬🇧 English', // Bandiera del Regno Unito
- LANG_ES: '🇪🇸 Español', // Bandiera spagnola
- LANG_FR: '🇫🇷 Français', // Bandiera francese
- LANG_SI: '🇸🇮 Slovenski', // Bandiera slovena
- LANG_PT: '🇵🇹 Português', // Bandiera portoghese
- LANG: '🌐 Language', // Globo con meridiani
- CHAT_PERSONALE: '👩💼💻', // Donna impiegata + computer
- EXIT_TELEGRAM: 'exittotelegram',
- MSG_TO_USER: 'sendmsgto',
- ADMIN: '💁♀️ Admin', // Persona al banco informazioni
- AIUTO: '🔮 Help', // Cristallo magico
- ALTRO: '📰 Altro', // Giornale
- SETPICPROFILE: '🖼 SetPicProfile', // Cornice con foto
- RESETPWD: '🔑 SetResetPwd', // Chiave
- MSG_SI_INVITATI_NO_7REQ_INVITATI: '📩Inv e NO 7 Req', // Busta
- MSGSTAFF: '📩 Invia a STAFF', // Busta
- MSGAPPARTIENE_CIRCUITI_RIS: 'Invia a Utenti dei Circuiti RIS',
- MSGPAOLO: '📩 Invia a SURYA', // Busta
- RESTART_SRV: '📩Restart-NodeJs', // Busta
- REBOOT_SRV: '📩Reboot-VPS!', // Busta
- EXECSH: '📩ExecSH', // Busta
- LOG_SRV: '🖥Logserver.sh', // Monitor
- MSGATUTTI: '📩 Invia a TUTTI', // Busta
- it: {
- ACCEDI: '👤 Accedi', // Persona
- LAVAGNA: '🕉 Lavagna', // Simbolo Om
- LINK_CONDIVIDERE: '🔗 Link da condividere', // Link
- ZOOM: 'ℹ️ Zoom (Conferenze)', // Informazione
- INFO: 'ℹ️ Informazioni', // Informazione
- ASSISTENZA: '👐 Le Chat', // Mani aperte
- INDIETRO: '🔙 Indietro', // Freccia indietro
- SI: '👍 SI', // Pollice su
- NO: '👎 NO', // Pollice giù
- ESCI_DA_CHAT: '📩 Esci dalla Conversazione', // Busta
- NUOVOSITO: '',
- },
- es: {
- ACCEDI: '👤 Entra',
- LAVAGNA: '🕉 Tablero',
- LINK_CONDIVIDERE: '🔗 Enlaces para compartir',
- ZOOM: 'ℹ️ Zoom (Conferencias)',
- INFO: 'ℹ️ Información',
- ASSISTENZA: '👐 Chats',
- INDIETRO: '🔙 Volver',
- SI: '👍 SÍ',
- NO: '👎 NO',
- ESCI_DA_CHAT: '📩 Salir de la conversación',
- },
- fr: {
- ACCEDI: '👤 Entrez',
- LAVAGNA: '🕉 Tableau de bord',
- LINK_CONDIVIDERE: '🔗 Liens à partager',
- ZOOM: 'ℹ️ Zoom (Conférences)',
- INFO: 'ℹ️ Informations',
- ASSISTENZA: '👐 Les chats',
- INDIETRO: '🔙 Retour',
- SI: '👍 OUI',
- NO: '👎 NON',
- ESCI_DA_CHAT: '📩 Quitter la conversation',
- },
- si: {
- ACCEDI: '👤 Prijava',
- LAVAGNA: '🕉 Tabla',
- LINK_CONDIVIDERE: '🔗 Link za vpis oseb',
- ZOOM: 'ℹ️ Zoom (Konference)',
- INFO: 'ℹ️ Informacije',
- ASSISTENZA: '👐 jev klepet',
- INDIETRO: '🔙 Nazaj',
- SI: '👍 DA',
- NO: '👎 NE',
- ESCI_DA_CHAT: '📩 Zaprite pogovor',
- },
- pt: {
- ACCEDI: '👤 Entre',
- LAVAGNA: '🕉 Tablero',
- LINK_CONDIVIDERE: '🔗 Links para compartilhar',
- ZOOM: 'ℹ️ Zoom (Conferências)',
- INFO: 'ℹ️ Informações',
- ASSISTENZA: '👐 Chats',
- INDIETRO: '🔙 Voltar',
- SI: '👍 SIM',
- NO: '👎 NÃO',
- ESCI_DA_CHAT: '📩 Sair da Conversa',
- },
- enUs: {
- ACCEDI: '👤 Enter',
- LAVAGNA: '🕉 DashBoard',
- LINK_CONDIVIDERE: '🔗 Link to Share',
- ZOOM: 'ℹ️ Zoom (Conference)',
- INFO: 'ℹ️ Info',
- ASSISTENZA: '👐 Chats',
- INDIETRO: '🔙 Back',
- SI: '👍 YES',
- NO: '👎 NO',
- ESCI_DA_CHAT: '📩 Exit to the Conversation',
- },
-};
-
-const CONTA_SOLO = 'contasolo';
-const RICEVI_EMAIL = 'riceviemail';
-const NOME_COGNOME = 'nomecognome';
-const CHEDI_SE_IMBARCARTI = 'chiedi_se_imbarcarti';
-
-const phase = {
- REGISTRATION: 'REGISTRATION',
- REGISTRATION_CONFIRMED: 'REGISTRATION_CONFIRMED',
- RESET_PWD: 'RESET_PWD',
- NOTIFICATION: 'NOTIFICATION',
-};
-
-const roles = {
- ADMIN: 'ADMIN',
- MANAGER: 'MANAGER',
- FACILITATORE: 'FACILITATORE',
- EDITOR: 'EDITOR',
- SOCIO: 'SOCIO',
-};
-
-// -----------------------------------------------------------------------------
-// 🔹 REGISTRY MULTI-BOT (arrTelegram)
-// -----------------------------------------------------------------------------
-const arrTelegram = [];
-
-function getclTelegByidapp(idapp) {
- const rec = arrTelegram.find((r) => String(r.idapp) === String(idapp));
- return rec ? rec.cl : null;
-}
-function getclTelegBytoken(token) {
- const rec = arrTelegram.find((r) => r.cl?.token === token);
- return rec ? rec.cl : null;
-}
-
-// -----------------------------------------------------------------------------
-// 🔹 FUNZIONI LOCALI
-// -----------------------------------------------------------------------------
-async function local_sendMsgTelegram(idapp, username, text) {
- const teleg_id = await User.TelegIdByUsername(idapp, username);
- const cl = getclTelegByidapp(idapp);
- if (cl && teleg_id) return await cl.sendMsg(teleg_id, text);
- return null;
-}
-
-async function local_sendMsgTelegramToTheManagers(idapp, text, msg, username_bo) {
- const managers = await User.getusersManagers(idapp);
- const username = username_bo || msg?.chat?.username;
- const fullmsg = `${emo.ROBOT_FACE}: Da ${tools.getNomeCognomeTelegram(msg)} (${username})\n${text}`;
- if (managers)
- for (const m of managers) {
- const cl = getclTelegByidapp(idapp);
- if (cl && m.profile?.teleg_id)
- await cl.sendMsg(m.profile.teleg_id, fullmsg, undefined, undefined, undefined, undefined, true);
- }
- return true;
-}
-
-// -----------------------------------------------------------------------------
-// 🔹 CLASSE TELEGRAM
-// -----------------------------------------------------------------------------
-class Telegram {
- constructor(idapp, bot) {
- this.idapp = idapp;
- this.bot = bot;
- this.token = bot.token;
- this.arrUsers = [];
- this.lastid = 0;
- }
-
- // Invia un messaggio base
- async sendMsg(chatId, text, parseMode = 'HTML', MyForm = null, message_id, chat_id, ripr_menuPrec, img) {
- if (!chatId) return;
- try {
- await this.bot.sendMessage(chatId, text, { parse_mode: parseMode });
- } catch (err) {
- console.error('❌ sendMsg error:', err.message);
- }
- }
-
- // Invia immagine con fallback
- async sendImageToTelegram(chatId, imgPath, caption = '') {
- if (!chatId || !imgPath) return;
- try {
- const buffer = fs.readFileSync(imgPath);
- const sharped = await sharp(buffer).resize({ width: 1280 }).toBuffer();
- await this.bot.sendPhoto(chatId, sharped, { caption });
- } catch (err) {
- console.error('⚠️ sendImageToTelegram fallback:', err.message);
- try {
- await this.bot.sendDocument(chatId, imgPath, { caption });
- } catch (e) {
- console.error('❌ sendDocument fallback:', e.message);
- }
- }
- }
-
- // Placeholder per ricezione messaggi
- async receiveMsg(msg) {
- console.log('💬', this.idapp, msg.text);
- }
-
- // Placeholder per start
- async start(msg) {
- await this.sendMsg(msg.chat.id, `${emo.ROBOT_FACE} Benvenuto nel bot ${tools.getNomeAppByIdApp(this.idapp)}!`);
- }
-
- // Inline keyboard esempio
- getInlineKeyboard(buttons) {
- return { reply_markup: { inline_keyboard: buttons } };
- }
-}
-
-// -----------------------------------------------------------------------------
-// 🔹 API PUBBLICHE
-// -----------------------------------------------------------------------------
-async function sendMsgTelegramToTheAdminAllSites(text, senzaintestazione) {
- const apps = await tools.getApps();
- for (const app of apps) {
- const filled = text.replace('{appname}', tools.getNomeAppByIdApp(app.idapp));
- await sendMsgTelegramToTheAdmin(app.idapp, filled, senzaintestazione);
- }
-}
-
-async function sendMsgTelegramToTheAdmin(idapp, text, senzaintestazione) {
- const usersadmin = await User.getusersAdmin(idapp);
- let head = emo.ROBOT_FACE + '[BOT-ADMIN]' + emo.ADMIN + ': ';
- if (senzaintestazione) head = '';
- if (usersadmin)
- for (const rec of usersadmin) {
- if (User.isAdmin(rec.perm)) {
- await sendMsgTelegramByIdTelegram(idapp, rec.profile.teleg_id, head + text);
- await tools.snooze(300);
- }
- }
- return true;
-}
-
-async function sendMsgTelegramByIdTelegram(idapp, idtelegram, text, message_id, chat_id, ripr_menuPrec, MyForm = null, img = '') {
- if (!idtelegram) return;
- const cl = getclTelegByidapp(idapp);
- if (cl) return await cl.sendMsg(idtelegram, text, null, MyForm, message_id, chat_id, ripr_menuPrec, img, { idapp });
-}
-
-async function sendMsgTelegramToTheManagers(idapp, text) {
- const managers = await User.getusersManagers(idapp);
- if (managers)
- for (const rec of managers) {
- await sendMsgTelegramByIdTelegram(idapp, rec.profile.teleg_id, text);
- await tools.snooze(300);
- }
- return true;
-}
-
-// -----------------------------------------------------------------------------
-// 🔹 CALLBACK QUERY HANDLER (semplificata ma compatibile)
-// -----------------------------------------------------------------------------
-function setupCallback(bot, cl) {
- bot.on('callback_query', async (callbackQuery) => {
- await handleCallback(bot, cl, callbackQuery);
- });
-}
-
-// -----------------------------------------------------------------------------
-// 🔹 INIZIALIZZAZIONE BOT MULTI-APP
-// -----------------------------------------------------------------------------
-async function initTelegramBots() {
- const arrApps = await tools.getApps();
- for (const app of arrApps) {
- const idapp = app.idapp;
- const token = tools.getTelegramKeyByIdApp(idapp);
- const nomebot = tools.getTelegramBotNameByIdApp(idapp);
- if (!token) continue;
-
- console.log(`🤖 Avvio BOT: ${nomebot} (${idapp})`);
- const bot = new TelegramBot(token, { polling: true });
- const cl = new Telegram(idapp, bot);
- arrTelegram.push({ idapp, cl });
-
- bot.onText(/\/start/, (msg) => cl.start(msg));
- bot.on('message', async (msg) => cl.receiveMsg(msg));
- setupCallback(bot, cl);
- }
-}
-
-// -----------------------------------------------------------------------------
-// 🔹 ESPORTAZIONI PUBBLICHE
-// -----------------------------------------------------------------------------
-module.exports = {
- Telegram,
- initTelegramBots,
- getclTelegByidapp,
- getclTelegBytoken,
- sendMsgTelegramToTheAdminAllSites,
- sendMsgTelegramToTheAdmin,
- sendMsgTelegramByIdTelegram,
- sendMsgTelegramToTheManagers,
- local_sendMsgTelegram,
- local_sendMsgTelegramToTheManagers,
- phase,
- roles,
- emo,
-};
diff --git a/src/tools/shared_nodejs.js b/src/tools/shared_nodejs.js
index cdc35ec..548a7e0 100755
--- a/src/tools/shared_nodejs.js
+++ b/src/tools/shared_nodejs.js
@@ -1330,5 +1330,6 @@ module.exports = {
JOB_TO_EXECUTE: {
MIGRATION_SECTORS_DIC25: 'Migration_Sectors_Dic_2025',
+ MIGRATION_TELEGRAM_30DIC25: 'Migration_Telegram_30Dic_2025',
},
};