From fb407436945154d89f8298a1abadddb74c9a8701 Mon Sep 17 00:00:00 2001 From: Surya Paolo Date: Tue, 30 Dec 2025 11:36:42 +0100 Subject: [PATCH] - Aggiornamento Viaggi --- .DS_Store | Bin 14340 -> 14340 bytes src/controllers/chatController.js | 2 +- src/controllers/feedbackController.js | 14 +- src/controllers/rideController.js | 1657 ++++++++++------- src/controllers/rideRequestController.js | 159 +- .../viaggi/TrasportiNotifications.js | 847 +++++++++ src/controllers/viaggi/settingsController.js | 422 +++++ .../trasportiNotificationsController.js | 506 +++++ src/controllers/viaggi/widgetController.js | 219 +++ src/helpers/recurrenceHelper.js | 77 + src/models/user.js | 10 +- src/models/{ => viaggi}/Chat.js | 0 src/models/{ => viaggi}/Feedback.js | 2 +- src/models/{ => viaggi}/Ride.js | 17 +- src/models/{ => viaggi}/RideRequest.js | 2 +- src/models/viaggi/UserSettings.js | 310 +++ src/populate/migration-categories.js | 20 +- src/router/api_router.js | 7 + src/routes/viaggi/settingsRoutes.js | 100 + src/routes/viaggi/widgetRoutes.js | 32 + src/routes/viaggiRoutes.js | 36 +- src/server/wsShellHandler.js | 57 - src/telegram/api.js | 33 - src/telegram/config.js | 8 - src/telegram/constants.js | 18 - src/telegram/handlers/adminHandler.js | 11 - src/telegram/handlers/callbackHandler.js | 101 - src/telegram/handlers/circuitHandler.js | 54 - src/telegram/handlers/directHandler.js | 24 - src/telegram/handlers/errorHandler.js | 12 - src/telegram/handlers/friendsHandler.js | 61 - src/telegram/handlers/groupHandler.js | 70 - src/telegram/handlers/multiAppHandler.js | 47 - src/telegram/handlers/notificationHandler.js | 9 - src/telegram/handlers/passwordHandler.js | 35 - src/telegram/handlers/phaseHandler.js | 65 - src/telegram/handlers/registrationHandler.js | 50 - src/telegram/handlers/userHandler.js | 15 - src/telegram/handlers/userQuery.js | 32 - src/telegram/handlers/zoomHandler.js | 27 - src/telegram/helpers.js | 31 - src/telegram/index_new_check.js | 66 - src/telegram/messages.js | 25 - src/telegram/prova.txt | 1 - src/telegram/telegram.bot.init.js | 488 ----- src/tools/shared_nodejs.js | 1 + 46 files changed, 3676 insertions(+), 2104 deletions(-) create mode 100644 src/controllers/viaggi/TrasportiNotifications.js create mode 100644 src/controllers/viaggi/settingsController.js create mode 100644 src/controllers/viaggi/trasportiNotificationsController.js create mode 100644 src/controllers/viaggi/widgetController.js create mode 100644 src/helpers/recurrenceHelper.js rename src/models/{ => viaggi}/Chat.js (100%) rename src/models/{ => viaggi}/Feedback.js (99%) rename src/models/{ => viaggi}/Ride.js (96%) rename src/models/{ => viaggi}/RideRequest.js (99%) create mode 100644 src/models/viaggi/UserSettings.js create mode 100644 src/routes/viaggi/settingsRoutes.js create mode 100644 src/routes/viaggi/widgetRoutes.js delete mode 100644 src/server/wsShellHandler.js delete mode 100644 src/telegram/api.js delete mode 100644 src/telegram/config.js delete mode 100644 src/telegram/constants.js delete mode 100644 src/telegram/handlers/adminHandler.js delete mode 100644 src/telegram/handlers/callbackHandler.js delete mode 100644 src/telegram/handlers/circuitHandler.js delete mode 100644 src/telegram/handlers/directHandler.js delete mode 100644 src/telegram/handlers/errorHandler.js delete mode 100644 src/telegram/handlers/friendsHandler.js delete mode 100644 src/telegram/handlers/groupHandler.js delete mode 100644 src/telegram/handlers/multiAppHandler.js delete mode 100644 src/telegram/handlers/notificationHandler.js delete mode 100644 src/telegram/handlers/passwordHandler.js delete mode 100644 src/telegram/handlers/phaseHandler.js delete mode 100644 src/telegram/handlers/registrationHandler.js delete mode 100644 src/telegram/handlers/userHandler.js delete mode 100644 src/telegram/handlers/userQuery.js delete mode 100644 src/telegram/handlers/zoomHandler.js delete mode 100644 src/telegram/helpers.js delete mode 100644 src/telegram/index_new_check.js delete mode 100644 src/telegram/messages.js delete mode 100644 src/telegram/prova.txt delete mode 100644 src/telegram/telegram.bot.init.js diff --git a/.DS_Store b/.DS_Store index 1e57e80b8a58b40ed4d8b04e8f9d1ff111213994..abae5cac0bc1977c61f845c0552424ddfb449fcc 100644 GIT binary patch delta 1384 zcmb8uTTC2P7zgn0zuac#!0uU>!Y(ZqLQ8R>bhX>Us;v~lmP>Ub>9V`* zEZB<85`#&r(G=%N+i1M?K}}7Am(<3GCe}3iq;AyKYvPM0nix$@)J8qC19*u(I1lIc z%{TMSIlo!REM$&H*c=g+5OJcgdB*2iOnc4tV0k0Na%nLXp)iFr2d*EUOiyLRinzLn zv@fTp|*UT?`)4XPGuCuePp)+)(tzj+{44Lt|Y8*9BdDd4S6G2y`i+J+6+wQud*(H$O)6r3*ZRy@iYbE4IoAJbJW zmDBg@T2^*rj~q?Kr?i5byPUPm$3|xo`t*#RJ1p@gso-{T=el~PjH~gSc57O-vQ+-6 zBo`x%^^DzRMS8NDXjREh*x1aJs38jD@#P!+t%?#osHaqJ$ay<=OF&_<;BpA3L6zWJtb;3+-O@~-gygSd#zXRi5s~{MoWpZE?VywpV7nW z*_~Ewm#P+va&niOvHk%`;_r{;jRWzl7P&X2WedxJ+azg%yNc`9<9by+mtGAHSwXI+ zX7yY=Cn_5j-HahMbp@=GCu-{Q+k@fv%79`PHY8OKKXq+@kQT{M3(Bt$3ouC(J ziO$h^dY3+=kLWUeLSNE1^aK4!SLi4D9hGpx1r8rJq8XbIz&3QC6CreCH~KMvK}0cz zaoml)xCd!WBMSrf<3Sw7F+2nl&)_*ck5hOVXYmHk;jKI_;B8#Q2ly19;d6YCpYaQR zbvPX^hu6{QXtry2Sm3Q|)b3VVM~4_K(-l}VHde>{zhm9q*WWiFwr|GJhP0&Ezgf+E z)F1p$x(B68RTkE3Jk@Jz)?0!*Zr-(e{I-`BB7c=v{zqXA_;%I+@7|w&5|I`9mS~dF6aK7Z;tO|R3Nbb;OzdcLBs={vfLGE~8X zdNg5!@X`W5wjzk_LXBmn2fYZR54U4TxQSr|Bl{4?BvkAdcrt>Ih35$Jn8yQn7?0pM z7V#KP;3S^JQ+Qf%dJ(VStgT_y$l!_&yT?0AG4GWUgrBW!aGKw4mKRUDm#X;3So&>C Gx%3Bkvq#YY delta 136 zcmV;30C)d{aD;G>PYZbf0009301yBGXOr+A6_ITilQ1*{1pxp600ff>E*-J0UJn63 zvn4Ls0h8b_2a%c)vkeem1(O{Sbrd)@F)ScBG<_@}doeRGIV>PIG&hr`5i<}20CNCx q0AplxV{Bq?Z)^aQHWoLtAROxhvEac20Xd@#AP%wA`vJ2DF#QLfohKRq 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 = ` +
+
+ ${emo.PIN} +
+
Partenza
+
${data.departure}
+
+
+
${emo.ARROW}
+
+ ${emo.PIN} +
+
Destinazione
+
${data.destination}
+
+
+ ${data.date ? ` +
+ ${emo.CALENDAR} ${data.date} + ${data.time ? `${emo.CLOCK} ${data.time}` : ''} +
+ ` : ''} +
+ `; + } + + return ` + + + + + + + +
+ +
+
${emoji}
+

${title}

+
+ + +
+

+ ${body} +

+ + ${rideInfoHtml} + + ${data.reason ? `

${t('RIDE_CANCELLED_REASON')}

` : ''} + + ${ctaHtml} +
+ + +
+

+ ${config.appName} +

+

+ Ricevi questa email perché hai attivato le notifiche. + Gestisci preferenze +

+
+
+ + + `; +} + +// ============================================================================= +// TELEGRAM MESSAGE BUILDER +// ============================================================================= + +function buildTelegramMessage(type, data, lang = 'it') { + const t = (key) => getTranslation(lang, key, data); + + const title = t(`${type.toUpperCase()}_TITLE`); + const body = t(`${type.toUpperCase()}_BODY`); + + // 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; + + let message = `${emoji} ${title}\n\n${body}`; + + // Aggiungi info viaggio + if (data.departure && data.destination) { + message += `\n\n${emo.PIN} Percorso:\n${data.departure} ${emo.ARROW} ${data.destination}`; + if (data.date) { + message += `\n${emo.CALENDAR} ${data.date}`; + } + if (data.time) { + message += ` ${emo.CLOCK} ${data.time}`; + } + } + + // Motivo cancellazione + if (data.reason) { + message += `\n\n${t('RIDE_CANCELLED_REASON')}`; + } + + return message; +} + +// ============================================================================= +// PUSH NOTIFICATION BUILDER +// ============================================================================= + +function buildPushPayload(type, data, lang = 'it') { + const t = (key) => getTranslation(lang, key, data); + + const title = t(`${type.toUpperCase()}_TITLE`); + let body = t(`${type.toUpperCase()}_BODY`); + + // Tronca body per push + body = truncate(body, 150); + + // Icone per tipo + const icons = { + [NotificationType.NEW_RIDE_REQUEST]: '/icons/request.png', + [NotificationType.REQUEST_ACCEPTED]: '/icons/accepted.png', + [NotificationType.REQUEST_REJECTED]: '/icons/rejected.png', + [NotificationType.RIDE_REMINDER_24H]: '/icons/reminder.png', + [NotificationType.RIDE_REMINDER_2H]: '/icons/urgent.png', + [NotificationType.RIDE_CANCELLED]: '/icons/cancelled.png', + [NotificationType.NEW_MESSAGE]: '/icons/message.png', + [NotificationType.NEW_COMMUNITY_RIDE]: '/icons/community.png', + [NotificationType.TEST]: '/icons/notification.png', + [NotificationType.WELCOME]: '/icons/welcome.png' + }; + + return { + title, + body, + icon: icons[type] || '/icons/notification.png', + badge: '/icons/badge.png', + tag: type, + data: { + type, + url: data.actionUrl || config.appUrl, + ...data + }, + actions: data.actionUrl ? [ + { action: 'open', title: t('VIEW_DETAILS') } + ] : [] + }; +} + +// ============================================================================= +// MAIN SERVICE +// ============================================================================= + +const TrasportiNotifications = { + + // Esponi tipi e emoji + NotificationType, + emo, + + // Esponi config + config, + + /** + * Invia notifica su tutti i canali abilitati + * + * @param {Object} user - Utente destinatario (con notificationPreferences) + * @param {string} type - Tipo notifica (da NotificationType) + * @param {Object} data - Dati per template + * @param {string} idapp - ID app (per Telegram) + * @returns {Object} { success, results: { email, telegram, push } } + */ + async sendNotification(user, type, data, idapp) { + const results = { + email: null, + telegram: null, + push: null + }; + + const prefs = user.notificationPreferences || {}; + const lang = user.lang || 'it'; + + // Aggiungi URL azione se non presente + if (!data.actionUrl && data.rideId) { + data.actionUrl = `${config.appUrl}/trasporti/viaggio/${data.rideId}`; + } + if (!data.actionUrl && data.requestId) { + data.actionUrl = `${config.appUrl}/trasporti/richieste/${data.requestId}`; + } + if (!data.actionUrl && data.chatId) { + data.actionUrl = `${config.appUrl}/trasporti/chat/${data.chatId}`; + } + + // EMAIL + if (shouldSend(prefs, 'email', type) && user.email) { + results.email = await this.sendEmail(user.email, type, data, lang); + } + + // TELEGRAM (usa il tuo telegrambot.js esistente!) + const telegId = user.profile?.teleg_id || prefs.telegram?.chatId; + if (shouldSend(prefs, 'telegram', type) && telegId) { + results.telegram = await this.sendTelegram(idapp, telegId, type, data, lang); + } + + // PUSH + const pushSub = prefs.push?.subscription; + if (shouldSend(prefs, 'push', type) && pushSub) { + results.push = await this.sendPush(pushSub, type, data, lang); + } + + return { + success: Object.values(results).some(r => r?.success), + results + }; + }, + + /** + * Invia notifica a multipli utenti + */ + async sendNotificationToMany(users, type, data, idapp) { + const results = []; + + for (const user of users) { + try { + const result = await this.sendNotification(user, type, data, idapp); + results.push({ userId: user._id, ...result }); + + // Delay per evitare rate limiting + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + results.push({ userId: user._id, success: false, error: error.message }); + } + } + + return results; + }, + + // =========================================================================== + // EMAIL + // =========================================================================== + + async sendEmail(to, type, data, lang = 'it') { + if (!emailTransporter) { + return { success: false, error: 'Email not configured' }; + } + + try { + const t = (key) => getTranslation(lang, key, data); + const subject = `${config.appName} - ${t(`${type.toUpperCase()}_TITLE`)}`; + const html = buildEmailHtml(type, data, lang); + + const info = await emailTransporter.sendMail({ + from: `"${config.appName}" <${config.emailFrom}>`, + to, + subject, + html + }); + + return { success: true, messageId: info.messageId }; + } catch (error) { + console.error('Email send error:', error); + return { success: false, error: error.message }; + } + }, + + // =========================================================================== + // TELEGRAM (usa il tuo MyTelegramBot!) + // =========================================================================== + + async sendTelegram(idapp, chatId, type, data, lang = 'it') { + try { + const message = buildTelegramMessage(type, data, lang); + + // USA IL TUO METODO ESISTENTE! + const result = await MyTelegramBot.local_sendMsgTelegramByIdTelegram( + idapp, + chatId, + message, + null, // message_id + null, // chat_id reply + false, // ripr_menuPrec + null, // MyForm (bottoni) + '' // img + ); + + return { success: true, messageId: result?.message_id }; + } catch (error) { + console.error('Telegram send error:', error); + return { success: false, error: error.message }; + } + }, + + /** + * Invia notifica Telegram con bottoni inline + */ + async sendTelegramWithButtons(idapp, chatId, type, data, buttons, lang = 'it') { + try { + const message = buildTelegramMessage(type, data, lang); + + // Crea inline keyboard + const cl = MyTelegramBot.getclTelegByidapp(idapp); + if (!cl) { + return { success: false, error: 'Telegram client not found' }; + } + + const keyboard = cl.getInlineKeyboard(lang, buttons); + + const result = await MyTelegramBot.local_sendMsgTelegramByIdTelegram( + idapp, + chatId, + message, + null, + null, + false, + keyboard, + '' + ); + + return { success: true, messageId: result?.message_id }; + } catch (error) { + console.error('Telegram send error:', error); + return { success: false, error: error.message }; + } + }, + + // =========================================================================== + // PUSH + // =========================================================================== + + async sendPush(subscription, type, data, lang = 'it') { + if (!config.vapidPublicKey || !config.vapidPrivateKey) { + return { success: false, error: 'Push not configured' }; + } + + try { + const payload = JSON.stringify(buildPushPayload(type, data, lang)); + + await webpush.sendNotification(subscription, payload); + + return { success: true }; + } catch (error) { + console.error('Push send error:', error); + + // Subscription scaduta + if (error.statusCode === 410 || error.statusCode === 404) { + return { success: false, error: 'Subscription expired', expired: true }; + } + + return { success: false, error: error.message }; + } + }, + + // =========================================================================== + // METODI SPECIFICI PER TRASPORTI + // =========================================================================== + + /** + * Notifica nuova richiesta passaggio al conducente + */ + async notifyNewRideRequest(driver, passenger, ride, request, idapp) { + return this.sendNotification(driver, NotificationType.NEW_RIDE_REQUEST, { + passengerName: `${passenger.name} ${passenger.surname}`, + departure: ride.departure?.city || ride.departure?.address, + destination: ride.destination?.city || ride.destination?.address, + date: formatDate(ride.departureTime), + time: formatTime(ride.departureTime), + seats: request.seats || 1, + rideId: ride._id, + requestId: request._id, + actionUrl: `${config.appUrl}/trasporti/richieste/${request._id}` + }, idapp); + }, + + /** + * Notifica richiesta accettata al passeggero + */ + async notifyRequestAccepted(passenger, driver, ride, idapp) { + return this.sendNotification(passenger, NotificationType.REQUEST_ACCEPTED, { + driverName: `${driver.name} ${driver.surname}`, + departure: ride.departure?.city || ride.departure?.address, + destination: ride.destination?.city || ride.destination?.address, + date: formatDate(ride.departureTime), + time: formatTime(ride.departureTime), + rideId: ride._id, + actionUrl: `${config.appUrl}/trasporti/viaggio/${ride._id}` + }, idapp); + }, + + /** + * Notifica richiesta rifiutata al passeggero + */ + async notifyRequestRejected(passenger, driver, ride, reason, idapp) { + return this.sendNotification(passenger, NotificationType.REQUEST_REJECTED, { + driverName: `${driver.name} ${driver.surname}`, + departure: ride.departure?.city || ride.departure?.address, + destination: ride.destination?.city || ride.destination?.address, + reason + }, idapp); + }, + + /** + * Notifica promemoria viaggio + */ + async notifyRideReminder(user, ride, hoursBefor, idapp) { + const type = hoursBefor === 24 + ? NotificationType.RIDE_REMINDER_24H + : NotificationType.RIDE_REMINDER_2H; + + return this.sendNotification(user, type, { + departure: ride.departure?.city || ride.departure?.address, + destination: ride.destination?.city || ride.destination?.address, + date: formatDate(ride.departureTime), + time: formatTime(ride.departureTime), + rideId: ride._id, + actionUrl: `${config.appUrl}/trasporti/viaggio/${ride._id}` + }, idapp); + }, + + /** + * Notifica viaggio cancellato + */ + async notifyRideCancelled(user, ride, reason, idapp) { + return this.sendNotification(user, NotificationType.RIDE_CANCELLED, { + departure: ride.departure?.city || ride.departure?.address, + destination: ride.destination?.city || ride.destination?.address, + date: formatDate(ride.departureTime), + reason, + rideId: ride._id + }, idapp); + }, + + /** + * Notifica nuovo messaggio + */ + async notifyNewMessage(recipient, sender, message, chatId, idapp) { + return this.sendNotification(recipient, NotificationType.NEW_MESSAGE, { + senderName: `${sender.name} ${sender.surname}`, + preview: truncate(message.text, 100), + chatId, + actionUrl: `${config.appUrl}/trasporti/chat/${chatId}` + }, idapp); + }, + + /** + * Invia notifica di test + */ + async sendTestNotification(user, channel, idapp) { + const type = NotificationType.TEST; + const data = { + actionUrl: `${config.appUrl}/trasporti/impostazioni` + }; + const lang = user.lang || 'it'; + + if (channel === 'email' && user.email) { + return this.sendEmail(user.email, type, data, lang); + } + + const telegId = user.profile?.teleg_id || user.notificationPreferences?.telegram?.chatId; + if (channel === 'telegram' && telegId) { + return this.sendTelegram(idapp, telegId, type, data, lang); + } + + const pushSub = user.notificationPreferences?.push?.subscription; + if (channel === 'push' && pushSub) { + return this.sendPush(pushSub, type, data, lang); + } + + if (channel === 'all') { + return this.sendNotification(user, type, data, idapp); + } + + return { success: false, error: 'Invalid channel or not configured' }; + } +}; + +// ============================================================================= +// HELPER DATE +// ============================================================================= + +function formatDate(date) { + if (!date) return ''; + const d = new Date(date); + return d.toLocaleDateString('it-IT', { + weekday: 'short', + day: 'numeric', + month: 'short' + }); +} + +function formatTime(date) { + if (!date) return ''; + const d = new Date(date); + return d.toLocaleTimeString('it-IT', { + hour: '2-digit', + minute: '2-digit' + }); +} + +// ============================================================================= +// EXPORT +// ============================================================================= + +module.exports = TrasportiNotifications; \ No newline at end of file diff --git a/src/controllers/viaggi/settingsController.js b/src/controllers/viaggi/settingsController.js new file mode 100644 index 0000000..65e49cc --- /dev/null +++ b/src/controllers/viaggi/settingsController.js @@ -0,0 +1,422 @@ +// ============================================================ +// 🔧 SETTINGS CONTROLLER - Trasporti Solidali +// ============================================================ +// File: server/controllers/viaggi/settingsController.js + +const UserSettings = require('../../models/viaggi/UserSettings'); + +/** + * 📄 GET /api/viaggi/settings + * Ottieni le impostazioni dell'utente + */ +exports.getSettings = async (req, res) => { + try { + const { idapp } = req.query; + const userId = req.user._id; + + // Validazione + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + // Ottieni o crea impostazioni + const settings = await UserSettings.getOrCreateSettings(idapp, userId); + + return res.status(200).json({ + success: true, + data: settings.toClientJSON() + }); + + } catch (error) { + console.error('❌ Errore getSettings:', error); + return res.status(500).json({ + success: false, + message: 'Errore nel caricamento delle impostazioni', + error: error.message + }); + } +}; + +/** + * 📝 PUT /api/viaggi/settings + * Aggiorna le impostazioni dell'utente + */ +exports.updateSettings = async (req, res) => { + try { + const userId = req.user._id; + const idapp = req.user.idapp; + const updates = req.body; + + // Validazione + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + if (!updates || Object.keys(updates).length === 0) { + return res.status(400).json({ + success: false, + message: 'Nessuna modifica specificata' + }); + } + + // Aggiorna impostazioni + const settings = await UserSettings.updateSettings(idapp, userId, updates); + + return res.status(200).json({ + success: true, + message: 'Impostazioni aggiornate con successo', + data: settings.toClientJSON() + }); + + } catch (error) { + console.error('❌ Errore updateSettings:', error); + return res.status(500).json({ + success: false, + message: 'Errore nell\'aggiornamento delle impostazioni', + error: error.message + }); + } +}; + +/** + * 📝 PATCH /api/viaggi/settings/notifications + * Aggiorna solo le impostazioni notifiche + */ +exports.updateNotifications = async (req, res) => { + try { + const idapp = req.user.idapp; + const userId = req.user._id; + const { notifications } = req.body; + + // Validazione + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + if (!notifications) { + return res.status(400).json({ + success: false, + message: 'notifications è richiesto' + }); + } + + // Aggiorna solo notifiche + const settings = await UserSettings.updateSettings(idapp, userId, { notifications }); + + return res.status(200).json({ + success: true, + message: 'Notifiche aggiornate', + data: settings.notifications + }); + + } catch (error) { + console.error('❌ Errore updateNotifications:', error); + return res.status(500).json({ + success: false, + message: 'Errore nell\'aggiornamento delle notifiche', + error: error.message + }); + } +}; + +/** + * 📝 PATCH /api/viaggi/settings/privacy + * Aggiorna solo le impostazioni privacy + */ +exports.updatePrivacy = async (req, res) => { + try { + const idapp = req.user.idapp; + const userId = req.user._id; + const { privacy } = req.body; + + // Validazione + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + if (!privacy) { + return res.status(400).json({ + success: false, + message: 'privacy è richiesto' + }); + } + + // Aggiorna solo privacy + const settings = await UserSettings.updateSettings(idapp, userId, { privacy }); + + return res.status(200).json({ + success: true, + message: 'Privacy aggiornata', + data: settings.privacy + }); + + } catch (error) { + console.error('❌ Errore updatePrivacy:', error); + return res.status(500).json({ + success: false, + message: 'Errore nell\'aggiornamento della privacy', + error: error.message + }); + } +}; + +/** + * 📝 PATCH /api/viaggi/settings/ride-preferences + * Aggiorna solo le preferenze viaggi + */ +exports.updateRidePreferences = async (req, res) => { + try { + const idapp = req.user.idapp; + const userId = req.user._id; + const { ridePreferences } = req.body; + + // Validazione + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + if (!ridePreferences) { + return res.status(400).json({ + success: false, + message: 'ridePreferences è richiesto' + }); + } + + // Aggiorna preferenze viaggi + const settings = await UserSettings.updateSettings(idapp, userId, { ridePreferences }); + + return res.status(200).json({ + success: true, + message: 'Preferenze viaggi aggiornate', + data: settings.ridePreferences + }); + + } catch (error) { + console.error('❌ Errore updateRidePreferences:', error); + return res.status(500).json({ + success: false, + message: 'Errore nell\'aggiornamento delle preferenze', + error: error.message + }); + } +}; + +/** + * 📝 PATCH /api/viaggi/settings/interface + * Aggiorna solo le impostazioni interfaccia + */ +exports.updateInterface = async (req, res) => { + try { + const idapp = req.user.idapp; + const userId = req.user._id; + const { interface: interfaceSettings } = req.body; + + // Validazione + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + if (!interfaceSettings) { + return res.status(400).json({ + success: false, + message: 'interface è richiesto' + }); + } + + // Aggiorna interfaccia + const settings = await UserSettings.updateSettings(idapp, userId, { + interface: interfaceSettings + }); + + return res.status(200).json({ + success: true, + message: 'Interfaccia aggiornata', + data: settings.interface + }); + + } catch (error) { + console.error('❌ Errore updateInterface:', error); + return res.status(500).json({ + success: false, + message: 'Errore nell\'aggiornamento dell\'interfaccia', + error: error.message + }); + } +}; + +/** + * 🔄 POST /api/viaggi/settings/reset + * Reset impostazioni ai valori predefiniti + */ +exports.resetSettings = async (req, res) => { + try { + const idapp = req.user.idapp; + const userId = req.user._id; + const { section } = req.body; // Opzionale: resetta solo una sezione + + // Validazione + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + // Trova impostazioni esistenti + let settings = await UserSettings.findOne({ idapp, userId }); + + if (!settings) { + return res.status(404).json({ + success: false, + message: 'Impostazioni non trovate' + }); + } + + if (section) { + // Reset solo di una sezione specifica + const schema = UserSettings.schema.paths[section]; + if (!schema) { + return res.status(400).json({ + success: false, + message: 'Sezione non valida' + }); + } + + // Ottieni valori predefiniti dalla schema + settings[section] = schema.defaultValue || {}; + } else { + // Reset completo - cancella e ricrea + await UserSettings.deleteOne({ idapp, userId }); + settings = await UserSettings.getOrCreateSettings(idapp, userId); + } + + await settings.save(); + + return res.status(200).json({ + success: true, + message: section + ? `Sezione ${section} resettata` + : 'Impostazioni resettate ai valori predefiniti', + data: settings.toClientJSON() + }); + + } catch (error) { + console.error('❌ Errore resetSettings:', error); + return res.status(500).json({ + success: false, + message: 'Errore nel reset delle impostazioni', + error: error.message + }); + } +}; + +/** + * 📊 GET /api/viaggi/settings/export + * Esporta tutte le impostazioni (per backup o trasferimento) + */ +exports.exportSettings = async (req, res) => { + try { + const { idapp } = req.query; + const userId = req.user._id; + + // Validazione + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + const settings = await UserSettings.findOne({ idapp, userId }); + + if (!settings) { + return res.status(404).json({ + success: false, + message: 'Impostazioni non trovate' + }); + } + + // Esporta in formato JSON pulito + const exportData = { + exportDate: new Date().toISOString(), + userId: userId.toString(), + idapp, + settings: settings.toClientJSON() + }; + + return res.status(200).json({ + success: true, + data: exportData + }); + + } catch (error) { + console.error('❌ Errore exportSettings:', error); + return res.status(500).json({ + success: false, + message: 'Errore nell\'esportazione delle impostazioni', + error: error.message + }); + } +}; + +/** + * 📥 POST /api/viaggi/settings/import + * Importa impostazioni da backup + */ +exports.importSettings = async (req, res) => { + try { + const idapp = req.user.idapp; + const userId = req.user._id; + const { settings: importedSettings } = req.body; + + // Validazione + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + if (!importedSettings) { + return res.status(400).json({ + success: false, + message: 'settings è richiesto' + }); + } + + // Aggiorna con le impostazioni importate + const settings = await UserSettings.updateSettings(idapp, userId, importedSettings); + + return res.status(200).json({ + success: true, + message: 'Impostazioni importate con successo', + data: settings.toClientJSON() + }); + + } catch (error) { + console.error('❌ Errore importSettings:', error); + return res.status(500).json({ + success: false, + message: 'Errore nell\'importazione delle impostazioni', + error: error.message + }); + } +}; diff --git a/src/controllers/viaggi/trasportiNotificationsController.js b/src/controllers/viaggi/trasportiNotificationsController.js new file mode 100644 index 0000000..2fb5b8a --- /dev/null +++ b/src/controllers/viaggi/trasportiNotificationsController.js @@ -0,0 +1,506 @@ +/** + * trasportiNotificationsController.js + * + * Controller API per gestire le preferenze di notifica utente. + * Funziona insieme a TrasportiNotifications.js + */ + +const mongoose = require('mongoose'); +const TrasportiNotifications = require('./TrasportiNotifications'); + +// ============================================================================= +// SCHEMA PREFERENZE (da aggiungere al model User) +// ============================================================================= + +const notificationPreferencesSchema = new mongoose.Schema({ + email: { + enabled: { type: Boolean, default: true }, + newRideRequest: { type: Boolean, default: true }, + requestAccepted: { type: Boolean, default: true }, + requestRejected: { type: Boolean, default: true }, + rideReminder24h: { type: Boolean, default: true }, + rideReminder2h: { type: Boolean, default: true }, + rideCancelled: { type: Boolean, default: true }, + newMessage: { type: Boolean, default: true }, + newCommunityRide: { type: Boolean, default: false }, + weeklyDigest: { type: Boolean, default: false } + }, + telegram: { + enabled: { type: Boolean, default: false }, + chatId: { type: Number, default: 0 }, + username: { type: String, default: '' }, + connectedAt: { type: Date }, + newRideRequest: { type: Boolean, default: true }, + requestAccepted: { type: Boolean, default: true }, + requestRejected: { type: Boolean, default: true }, + rideReminder24h: { type: Boolean, default: true }, + rideReminder2h: { type: Boolean, default: true }, + rideCancelled: { type: Boolean, default: true }, + newMessage: { type: Boolean, default: true } + }, + push: { + enabled: { type: Boolean, default: false }, + subscription: { type: mongoose.Schema.Types.Mixed }, + subscribedAt: { type: Date }, + newRideRequest: { type: Boolean, default: true }, + requestAccepted: { type: Boolean, default: true }, + requestRejected: { type: Boolean, default: true }, + rideReminder24h: { type: Boolean, default: true }, + rideReminder2h: { type: Boolean, default: true }, + rideCancelled: { type: Boolean, default: true }, + newMessage: { type: Boolean, default: true } + } +}, { _id: false }); + +// ============================================================================= +// STORAGE CODICI TELEGRAM (in-memory, usa Redis in produzione) +// ============================================================================= + +const telegramConnectCodes = new Map(); + +// Pulizia codici scaduti ogni 5 minuti +setInterval(() => { + const now = Date.now(); + for (const [code, data] of telegramConnectCodes) { + if (now - data.createdAt > 10 * 60 * 1000) { // 10 minuti + telegramConnectCodes.delete(code); + } + } +}, 5 * 60 * 1000); + +/** + * Genera codice random 6 caratteri + */ +function generateCode() { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Escludo caratteri ambigui + let code = ''; + for (let i = 0; i < 6; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return code; +} + +// ============================================================================= +// CONTROLLER +// ============================================================================= + +const trasportiNotificationsController = { + + // Esponi schema per User model + notificationPreferencesSchema, + + /** + * GET /api/trasporti/notifications/preferences + * Ottiene preferenze notifiche utente + */ + async getNotificationPreferences(req, res) { + try { + const user = req.user; + + // Default preferences se non esistono + const defaultPrefs = { + email: { + enabled: true, + newRideRequest: true, + requestAccepted: true, + requestRejected: true, + rideReminder24h: true, + rideReminder2h: true, + rideCancelled: true, + newMessage: true, + newCommunityRide: false, + weeklyDigest: false + }, + telegram: { + enabled: false, + chatId: user.profile?.teleg_id || 0, + username: user.profile?.teleg_username || '', + newRideRequest: true, + requestAccepted: true, + requestRejected: true, + rideReminder24h: true, + rideReminder2h: true, + rideCancelled: true, + newMessage: true + }, + push: { + enabled: false, + newRideRequest: true, + requestAccepted: true, + requestRejected: true, + rideReminder24h: true, + rideReminder2h: true, + rideCancelled: true, + newMessage: true + } + }; + + // Merge con preferenze salvate + const prefs = user.notificationPreferences || {}; + const mergedPrefs = { + email: { ...defaultPrefs.email, ...prefs.email }, + telegram: { ...defaultPrefs.telegram, ...prefs.telegram }, + push: { ...defaultPrefs.push, ...prefs.push } + }; + + // Sync chatId da profile se presente + if (user.profile?.teleg_id && !mergedPrefs.telegram.chatId) { + mergedPrefs.telegram.chatId = user.profile.teleg_id; + mergedPrefs.telegram.enabled = true; + } + + res.json({ + success: true, + data: { + email: user.email, + preferences: mergedPrefs, + vapidPublicKey: TrasportiNotifications.config.vapidPublicKey, + telegramBotUsername: process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot' + } + }); + } catch (error) { + console.error('getNotificationPreferences error:', error); + res.status(500).json({ success: false, message: error.message }); + } + }, + + /** + * PUT /api/trasporti/notifications/preferences + * Aggiorna preferenze notifiche + */ + async updateNotificationPreferences(req, res) { + try { + const { User } = require('../../models/user'); + const { email, telegram, push } = req.body; + + const updateData = {}; + + // Email preferences + if (email) { + Object.keys(email).forEach(key => { + if (key !== 'enabled' || typeof email[key] === 'boolean') { + updateData[`notificationPreferences.email.${key}`] = email[key]; + } + }); + } + + // Telegram preferences (escludi chatId, username - gestiti via connect) + if (telegram) { + const allowedKeys = ['enabled', 'newRideRequest', 'requestAccepted', 'requestRejected', + 'rideReminder24h', 'rideReminder2h', 'rideCancelled', 'newMessage']; + Object.keys(telegram).forEach(key => { + if (allowedKeys.includes(key)) { + updateData[`notificationPreferences.telegram.${key}`] = telegram[key]; + } + }); + } + + // Push preferences (escludi subscription - gestito via subscribe) + if (push) { + const allowedKeys = ['enabled', 'newRideRequest', 'requestAccepted', 'requestRejected', + 'rideReminder24h', 'rideReminder2h', 'rideCancelled', 'newMessage']; + Object.keys(push).forEach(key => { + if (allowedKeys.includes(key)) { + updateData[`notificationPreferences.push.${key}`] = push[key]; + } + }); + } + + await User.updateOne( + { _id: req.user._id }, + { $set: updateData } + ); + + res.json({ success: true, message: 'Preferenze aggiornate' }); + } catch (error) { + console.error('updateNotificationPreferences error:', error); + res.status(500).json({ success: false, message: error.message }); + } + }, + + /** + * POST /api/trasporti/notifications/telegram/code + * Genera codice per connessione Telegram + */ + async generateTelegramCode(req, res) { + try { + const userId = req.user._id.toString(); + + // Rimuovi codici esistenti per questo utente + for (const [code, data] of telegramConnectCodes) { + if (data.userId === userId) { + telegramConnectCodes.delete(code); + } + } + + // Genera nuovo codice + let code; + do { + code = generateCode(); + } while (telegramConnectCodes.has(code)); + + // Salva + telegramConnectCodes.set(code, { + userId, + createdAt: Date.now(), + chatId: null, + username: null + }); + + res.json({ + success: true, + data: { + code, + expiresIn: 600, // 10 minuti + botUsername: process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot', + instructions: `Invia "${code}" al bot @${process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot'} su Telegram` + } + }); + } catch (error) { + console.error('generateTelegramCode error:', error); + res.status(500).json({ success: false, message: error.message }); + } + }, + + /** + * POST /api/trasporti/notifications/telegram/connect + * Completa connessione Telegram dopo validazione codice dal bot + */ + async connectTelegram(req, res) { + try { + const { User } = require('../../models/user'); + const { code } = req.body; + + if (!code) { + return res.status(400).json({ success: false, message: 'Codice richiesto' }); + } + + const codeData = telegramConnectCodes.get(code.toUpperCase()); + + if (!codeData) { + return res.status(400).json({ success: false, message: 'Codice non valido o scaduto' }); + } + + if (codeData.userId !== req.user._id.toString()) { + return res.status(400).json({ success: false, message: 'Codice non valido' }); + } + + // Verifica che il bot abbia validato il codice (impostando chatId) + if (!codeData.chatId) { + return res.status(400).json({ + success: false, + message: 'Invia prima il codice al bot su Telegram', + needsBotInteraction: true + }); + } + + // Aggiorna utente + await User.updateOne( + { _id: req.user._id }, + { + $set: { + 'notificationPreferences.telegram.enabled': true, + 'notificationPreferences.telegram.chatId': codeData.chatId, + 'notificationPreferences.telegram.username': codeData.username || '', + 'notificationPreferences.telegram.connectedAt': new Date(), + // Retrocompatibilità con profile.teleg_id + 'profile.teleg_id': codeData.chatId, + 'profile.teleg_username': codeData.username || '' + } + } + ); + + // Rimuovi codice usato + telegramConnectCodes.delete(code.toUpperCase()); + + // Invia messaggio benvenuto + const idapp = req.user.idapp; + await TrasportiNotifications.sendTelegram( + idapp, + codeData.chatId, + TrasportiNotifications.NotificationType.WELCOME, + {}, + req.user.lang || 'it' + ); + + res.json({ + success: true, + message: 'Telegram connesso!', + data: { + chatId: codeData.chatId, + username: codeData.username + } + }); + } catch (error) { + console.error('connectTelegram error:', error); + res.status(500).json({ success: false, message: error.message }); + } + }, + + /** + * DELETE /api/trasporti/notifications/telegram/disconnect + * Disconnette Telegram + */ + async disconnectTelegram(req, res) { + try { + const { User } = require('../../models/user'); + + // Ottieni chatId prima di disconnettere per inviare messaggio + const chatId = req.user.notificationPreferences?.telegram?.chatId || req.user.profile?.teleg_id; + const idapp = req.user.idapp; + + // Aggiorna utente + await User.updateOne( + { _id: req.user._id }, + { + $set: { + 'notificationPreferences.telegram.enabled': false, + 'notificationPreferences.telegram.chatId': 0, + 'notificationPreferences.telegram.username': '', + 'notificationPreferences.telegram.connectedAt': null, + 'profile.teleg_id': 0, + 'profile.teleg_username': '' + } + } + ); + + // Invia messaggio di disconnessione + if (chatId && idapp) { + const MyTelegramBot = require('./telegram/telegrambot'); + await MyTelegramBot.local_sendMsgTelegramByIdTelegram( + idapp, + chatId, + '👋 Telegram disconnesso da Trasporti Solidali.\n\nPuoi riconnettere in qualsiasi momento dalla pagina impostazioni.', + null, null, false, null, '' + ); + } + + res.json({ success: true, message: 'Telegram disconnesso' }); + } catch (error) { + console.error('disconnectTelegram error:', error); + res.status(500).json({ success: false, message: error.message }); + } + }, + + /** + * POST /api/trasporti/notifications/push/subscribe + * Registra subscription push + */ + async subscribePushNotifications(req, res) { + try { + const { User } = require('../../models/user'); + const { subscription } = req.body; + + if (!subscription || !subscription.endpoint) { + return res.status(400).json({ success: false, message: 'Subscription non valida' }); + } + + await User.updateOne( + { _id: req.user._id }, + { + $set: { + 'notificationPreferences.push.enabled': true, + 'notificationPreferences.push.subscription': subscription, + 'notificationPreferences.push.subscribedAt': new Date() + } + } + ); + + res.json({ success: true, message: 'Push notifications attivate' }); + } catch (error) { + console.error('subscribePushNotifications error:', error); + res.status(500).json({ success: false, message: error.message }); + } + }, + + /** + * DELETE /api/trasporti/notifications/push/unsubscribe + * Rimuove subscription push + */ + async unsubscribePushNotifications(req, res) { + try { + const { User } = require('../../models/user'); + + await User.updateOne( + { _id: req.user._id }, + { + $set: { + 'notificationPreferences.push.enabled': false, + 'notificationPreferences.push.subscription': null + } + } + ); + + res.json({ success: true, message: 'Push notifications disattivate' }); + } catch (error) { + console.error('unsubscribePushNotifications error:', error); + res.status(500).json({ success: false, message: error.message }); + } + }, + + /** + * POST /api/trasporti/notifications/test + * Invia notifica di test + */ + async sendTestNotification(req, res) { + try { + const { channel } = req.body; // 'email', 'telegram', 'push', 'all' + const idapp = req.user.idapp; + + const result = await TrasportiNotifications.sendTestNotification(req.user, channel, idapp); + + if (result.success) { + res.json({ success: true, message: `Notifica di test inviata su ${channel}` }); + } else { + res.status(400).json({ success: false, message: result.error }); + } + } catch (error) { + console.error('sendTestNotification error:', error); + res.status(500).json({ success: false, message: error.message }); + } + }, + + /** + * Handler per il bot Telegram quando riceve un codice + * Chiamare questa funzione dal tuo telegrambot.js + */ + handleTelegramCodeFromBot(code, chatId, username) { + const codeUpper = code.toUpperCase(); + const codeData = telegramConnectCodes.get(codeUpper); + + if (!codeData) { + return { success: false, error: 'Codice non valido o scaduto' }; + } + + // Aggiorna con chatId e username + codeData.chatId = chatId; + codeData.username = username; + telegramConnectCodes.set(codeUpper, codeData); + + return { success: true, userId: codeData.userId }; + }, + + /** + * Rimuovi subscription push scadute + * Chiamare quando si riceve errore 410/404 + */ + async removePushSubscription(userId) { + try { + const { User } = require('../../models/user'); + await User.updateOne( + { _id: userId }, + { + $set: { + 'notificationPreferences.push.subscription': null, + 'notificationPreferences.push.enabled': false + } + } + ); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } + } +}; + +module.exports = trasportiNotificationsController; \ No newline at end of file diff --git a/src/controllers/viaggi/widgetController.js b/src/controllers/viaggi/widgetController.js new file mode 100644 index 0000000..6a5f39f --- /dev/null +++ b/src/controllers/viaggi/widgetController.js @@ -0,0 +1,219 @@ +// ============================================================ +// 📊 WIDGET & STATS CONTROLLER - Trasporti Solidali +// ============================================================ +// File: server/controllers/viaggi/widgetController.js + +const Ride = require('../../models/viaggi/Ride'); +const RideRequest = require('../../models/viaggi/RideRequest'); +const Feedback = require('../../models/viaggi/Feedback'); +const Chat = require('../../models/viaggi/Chat'); +const mongoose = require('mongoose'); + +/** + * 📊 GET /api/viaggi/widget/data + * Ottieni dati per il widget dashboard + */ +exports.getWidgetData = async (req, res) => { + try { + const { idapp } = req.query; + const userId = req.user._id; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + const now = new Date(); + + // Query parallele per ottimizzare + const [ + offersCount, + requestsCount, + recentRides, + myActiveRides, + pendingRequestsCount, + unreadMessagesCount + ] = await Promise.all([ + // Conta offerte attive + Ride.countDocuments({ + idapp, + type: 'offer', + status: 'active', + departureDate: { $gte: now } + }), + + // Conta richieste attive + Ride.countDocuments({ + idapp, + type: 'request', + status: 'active', + departureDate: { $gte: now } + }), + + // Ultimi viaggi pubblicati (non propri) + Ride.find({ + idapp, + userId: { $ne: userId }, + status: 'active', + departureDate: { $gte: now } + }) + .sort({ createdAt: -1 }) + .limit(5) + .populate('userId', 'name surname profile') + .lean(), + + // I miei viaggi attivi + Ride.find({ + idapp, + userId: userId, + status: 'active', + departureDate: { $gte: now } + }) + .sort({ departureDate: 1 }) + .limit(3) + .lean(), + + // Richieste pendenti ricevute (per i miei viaggi) + RideRequest.countDocuments({ + idapp, + driverUserId: userId, + status: 'pending' + }), + + // Messaggi non letti + Chat.countDocuments({ + idapp, + participants: userId, + isDeleted: false, + [`deletedBy.${userId}`]: { $ne: true }, + 'messages': { + $elemMatch: { + senderId: { $ne: userId }, + readBy: { $ne: userId } + } + } + }) + ]); + + // Calcola "matches" - viaggi compatibili con le mie richieste + let matchesCount = 0; + const myRequests = await Ride.find({ + idapp, + userId: userId, + type: 'request', + status: 'active', + departureDate: { $gte: now } + }).select('departure destination departureDate').lean(); + + if (myRequests.length > 0) { + // Per ogni mia richiesta, cerca offerte compatibili + for (const request of myRequests) { + const compatibleOffers = await Ride.countDocuments({ + idapp, + userId: { $ne: userId }, + type: 'offer', + status: 'active', + departureDate: { + $gte: new Date(request.departureDate.getTime() - 2 * 60 * 60 * 1000), // -2h + $lte: new Date(request.departureDate.getTime() + 2 * 60 * 60 * 1000) // +2h + }, + // Potresti aggiungere filtri geografici qui + 'departure.city': request.departure?.city, + 'destination.city': request.destination?.city + }); + matchesCount += compatibleOffers; + } + } + + return res.status(200).json({ + success: true, + data: { + stats: { + offers: offersCount, + requests: requestsCount, + matches: matchesCount + }, + recentRides: recentRides, + myActiveRides: myActiveRides, + pendingRequests: pendingRequestsCount, + unreadMessages: unreadMessagesCount + } + }); + + } catch (error) { + console.error('❌ Errore getWidgetData:', error); + return res.status(500).json({ + success: false, + message: 'Errore nel caricamento dei dati widget', + error: error.message + }); + } +}; + +/** + * 📊 GET /api/viaggi/stats/quick + * Statistiche rapide per badge/notifiche + */ +exports.getQuickStats = async (req, res) => { + try { + const { idapp } = req.query; + const userId = req.user._id; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è richiesto' + }); + } + + const [pendingRequests, unreadMessages, activeRides] = await Promise.all([ + RideRequest.countDocuments({ + idapp, + driverUserId: userId, + status: 'pending' + }), + + Chat.countDocuments({ + idapp, + participants: userId, + isDeleted: false, + [`deletedBy.${userId}`]: { $ne: true }, + 'messages': { + $elemMatch: { + senderId: { $ne: userId }, + readBy: { $ne: userId } + } + } + }), + + Ride.countDocuments({ + idapp, + userId: userId, + status: 'active', + departureDate: { $gte: new Date() } + }) + ]); + + return res.status(200).json({ + success: true, + data: { + pendingRequests, + unreadMessages, + activeRides, + totalNotifications: pendingRequests + unreadMessages + } + }); + + } catch (error) { + console.error('❌ Errore getQuickStats:', error); + return res.status(500).json({ + success: false, + message: 'Errore nel caricamento delle statistiche rapide', + error: error.message + }); + } +}; + +module.exports = exports; \ No newline at end of file diff --git a/src/helpers/recurrenceHelper.js b/src/helpers/recurrenceHelper.js new file mode 100644 index 0000000..beef849 --- /dev/null +++ b/src/helpers/recurrenceHelper.js @@ -0,0 +1,77 @@ +// Helper per calcolare le date dalle ricorrenze +const getRecurrenceDates = (ride, startRange, endRange) => { + const { recurrence, departureDate } = ride; + + if (!recurrence || recurrence.type === 'once') { + return [new Date(departureDate)]; + } + + const dates = []; + const start = new Date(startRange || recurrence.startDate || departureDate); + const end = new Date(endRange || recurrence.endDate || new Date(start.getTime() + 365 * 24 * 60 * 60 * 1000)); // Default 1 anno + + const excludedDatesSet = new Set( + (recurrence.excludedDates || []).map(d => new Date(d).toISOString().split('T')[0]) + ); + + switch (recurrence.type) { + case 'weekly': + if (!recurrence.daysOfWeek || recurrence.daysOfWeek.length === 0) break; + + let current = new Date(start); + while (current <= end) { + const dayOfWeek = current.getDay(); + if (recurrence.daysOfWeek.includes(dayOfWeek)) { + const dateStr = current.toISOString().split('T')[0]; + if (!excludedDatesSet.has(dateStr)) { + dates.push(new Date(current)); + } + } + current.setDate(current.getDate() + 1); + } + break; + + case 'custom_days': + if (!recurrence.daysOfWeek || recurrence.daysOfWeek.length === 0) break; + + let curr = new Date(start); + while (curr <= end) { + const dayOfWeek = curr.getDay(); + if (recurrence.daysOfWeek.includes(dayOfWeek)) { + const dateStr = curr.toISOString().split('T')[0]; + if (!excludedDatesSet.has(dateStr)) { + dates.push(new Date(curr)); + } + } + curr.setDate(curr.getDate() + 1); + } + break; + + case 'custom_dates': + if (!recurrence.customDates || recurrence.customDates.length === 0) break; + + recurrence.customDates.forEach(date => { + const d = new Date(date); + if (d >= start && d <= end) { + const dateStr = d.toISOString().split('T')[0]; + if (!excludedDatesSet.has(dateStr)) { + dates.push(d); + } + } + }); + break; + } + + return dates.length > 0 ? dates : [new Date(departureDate)]; +}; + +const isRideActiveOnDate = (ride, targetDate) => { + const dates = getRecurrenceDates(ride, targetDate, targetDate); + const targetStr = new Date(targetDate).toISOString().split('T')[0]; + return dates.some(d => d.toISOString().split('T')[0] === targetStr); +}; + +module.exports = { + getRecurrenceDates, + isRideActiveOnDate +}; \ No newline at end of file diff --git a/src/models/user.js b/src/models/user.js index 0e5a304..da219da 100755 --- a/src/models/user.js +++ b/src/models/user.js @@ -32,6 +32,8 @@ const i18n = require('i18n'); const shared_consts = require('../tools/shared_nodejs'); +const { notificationPreferencesSchema } = require('../controllers/viaggi/trasportiNotificationsController'); + mongoose.Promise = global.Promise; mongoose.level = 'F'; @@ -847,6 +849,10 @@ const UserSchema = new mongoose.Schema( }, }, }, + notificationPreferences: { + type: notificationPreferencesSchema, + default: () => ({}), + }, updatedAt: { type: Date, default: Date.now }, }, { @@ -7072,7 +7078,9 @@ UserSchema.statics.addNewSite = async function (idappPass, body) { } if (arrSite && arrSite.length === 1 && numutenti < 2) { - const MyTelegramBot = require('../telegram/telegrambot'); + //const MyTelegramBot = require('../telegram/telegrambot'); + const MyTelegramBot = require('../telegram'); + // Nessun Sito Installato e Nessun Utente installato ! let myuser = new User(); diff --git a/src/models/Chat.js b/src/models/viaggi/Chat.js similarity index 100% rename from src/models/Chat.js rename to src/models/viaggi/Chat.js diff --git a/src/models/Feedback.js b/src/models/viaggi/Feedback.js similarity index 99% rename from src/models/Feedback.js rename to src/models/viaggi/Feedback.js index b35c78a..2995684 100644 --- a/src/models/Feedback.js +++ b/src/models/viaggi/Feedback.js @@ -339,7 +339,7 @@ FeedbackSchema.statics.getRatingDistribution = async function (idapp, userId, ro // Hook post-save per aggiornare rating utente FeedbackSchema.post('save', async function (doc) { try { - const { User } = require('./User'); + const { User } = require('../User'); const stats = await mongoose.model('Feedback').getStatsForUser(doc.idapp, doc.toUserId); diff --git a/src/models/Ride.js b/src/models/viaggi/Ride.js similarity index 96% rename from src/models/Ride.js rename to src/models/viaggi/Ride.js index b43a0fb..f41052e 100644 --- a/src/models/Ride.js +++ b/src/models/viaggi/Ride.js @@ -134,7 +134,6 @@ const VehicleSchema = new Schema( { type: { type: String, - enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'], default: 'auto', }, brand: { @@ -309,7 +308,7 @@ const RideSchema = new Schema( required: true, }, waypoints: [WaypointSchema], - dateTime: { + departureDate: { type: Date, required: true, index: true, @@ -422,8 +421,8 @@ RideSchema.index({ 'departure.city': 1, 'destination.city': 1 }); RideSchema.index({ 'departure.coordinates': '2dsphere' }); RideSchema.index({ 'destination.coordinates': '2dsphere' }); RideSchema.index({ 'waypoints.location.city': 1 }); -RideSchema.index({ dateTime: 1, status: 1 }); -RideSchema.index({ idapp: 1, status: 1, dateTime: 1 }); +RideSchema.index({ departureDate: 1, status: 1 }); +RideSchema.index({ idapp: 1, status: 1, departureDate: 1 }); // Virtual per verificare se il viaggio è pieno RideSchema.virtual('isFull').get(function () { @@ -490,7 +489,7 @@ RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinatio const query = { idapp, status: { $in: ['active', 'full'] }, - dateTime: { $gte: new Date() }, + departureDate: { $gte: new Date() }, }; if (departureCity) { @@ -507,12 +506,12 @@ RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinatio startOfDay.setHours(0, 0, 0, 0); const endOfDay = new Date(options.date); endOfDay.setHours(23, 59, 59, 999); - query.dateTime = { $gte: startOfDay, $lte: endOfDay }; + query.departureDate = { $gte: startOfDay, $lte: endOfDay }; } return this.find(query) .populate('userId', 'username name surname profile.driverProfile.averageRating') - .sort({ dateTime: 1 }); + .sort({ departureDate: 1 }); }; // Ricerca viaggi che passano per una città intermedia @@ -521,7 +520,7 @@ RideSchema.statics.findPassingThrough = function (idapp, cityName, options = {}) const query = { idapp, status: { $in: ['active'] }, - dateTime: { $gte: new Date() }, + departureDate: { $gte: new Date() }, $or: [{ 'departure.city': cityRegex }, { 'destination.city': cityRegex }, { 'waypoints.location.city': cityRegex }], }; @@ -531,7 +530,7 @@ RideSchema.statics.findPassingThrough = function (idapp, cityName, options = {}) return this.find(query) .populate('userId', 'username name surname profile.driverProfile.averageRating') - .sort({ dateTime: 1 }); + .sort({ departureDate: 1 }); }; const Ride = mongoose.model('Ride', RideSchema); diff --git a/src/models/RideRequest.js b/src/models/viaggi/RideRequest.js similarity index 99% rename from src/models/RideRequest.js rename to src/models/viaggi/RideRequest.js index 28fdde4..d4a53a6 100644 --- a/src/models/RideRequest.js +++ b/src/models/viaggi/RideRequest.js @@ -250,7 +250,7 @@ RideRequestSchema.statics.getPendingForDriver = function(idapp, driverId) { status: 'pending' }) .populate('passengerId', 'username name surname email') - .populate('rideId', 'departure destination dateTime') + .populate('rideId', 'departure destination departureDate') .sort({ createdAt: -1 }); }; diff --git a/src/models/viaggi/UserSettings.js b/src/models/viaggi/UserSettings.js new file mode 100644 index 0000000..30f77b8 --- /dev/null +++ b/src/models/viaggi/UserSettings.js @@ -0,0 +1,310 @@ +// ============================================================ +// 🔧 USER SETTINGS MODEL - Trasporti Solidali +// ============================================================ +// File: server/models/viaggi/UserSettings.js + +const mongoose = require('mongoose'); + +const userSettingsSchema = new mongoose.Schema({ + // ID App e Utente + idapp: { + type: String, + required: true, + index: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: 'User', + index: true + }, + + // ============================================================ + // 🔔 NOTIFICHE + // ============================================================ + notifications: { + // Notifiche Email + email: { + newMessage: { type: Boolean, default: true }, + rideRequest: { type: Boolean, default: true }, + rideConfirmation: { type: Boolean, default: true }, + rideCancellation: { type: Boolean, default: true }, + rideReminder: { type: Boolean, default: true }, + feedbackReceived: { type: Boolean, default: true }, + newsletter: { type: Boolean, default: false } + }, + // Notifiche Push + push: { + newMessage: { type: Boolean, default: true }, + rideRequest: { type: Boolean, default: true }, + rideConfirmation: { type: Boolean, default: true }, + rideCancellation: { type: Boolean, default: true }, + rideReminder: { type: Boolean, default: true }, + feedbackReceived: { type: Boolean, default: true } + }, + // Notifiche In-App + inApp: { + newMessage: { type: Boolean, default: true }, + rideRequest: { type: Boolean, default: true }, + rideConfirmation: { type: Boolean, default: true }, + rideCancellation: { type: Boolean, default: true } + } + }, + + // ============================================================ + // 🔒 PRIVACY + // ============================================================ + privacy: { + // Visibilità profilo + profileVisibility: { + type: String, + enum: ['public', 'members', 'private'], + default: 'members' + }, + // Mostra informazioni di contatto + showPhone: { type: Boolean, default: false }, + showEmail: { type: Boolean, default: false }, + // Mostra statistiche profilo + showStats: { type: Boolean, default: true }, + // Mostra feedback ricevuti + showFeedbacks: { type: Boolean, default: true }, + // Condividi posizione durante viaggio + shareLocation: { type: Boolean, default: true }, + // Chi può contattarmi + whoCanContact: { + type: String, + enum: ['everyone', 'verified', 'afterBooking'], + default: 'verified' + } + }, + + // ============================================================ + // 🚗 PREFERENZE VIAGGI + // ============================================================ + ridePreferences: { + // Preferenze come conducente + driver: { + // Accetta prenotazioni istantanee + instantBooking: { type: Boolean, default: false }, + // Richiede verifica documento passeggeri + requireVerification: { type: Boolean, default: false }, + // Conversazione durante il viaggio + chattiness: { + type: String, + enum: ['silent', 'moderate', 'chatty', 'any'], + default: 'any' + }, + // Musica + music: { + type: String, + enum: ['no', 'soft', 'any'], + default: 'any' + }, + // Fumatori + smoking: { + type: String, + enum: ['no', 'outside', 'yes'], + default: 'no' + }, + // Animali + pets: { + type: String, + enum: ['no', 'small', 'yes'], + default: 'no' + }, + // Bagagli extra + luggage: { + type: String, + enum: ['small', 'medium', 'large'], + default: 'medium' + } + }, + // Preferenze come passeggero + passenger: { + // Conversazione + chattiness: { + type: String, + enum: ['silent', 'moderate', 'chatty', 'any'], + default: 'any' + }, + // Musica + music: { + type: String, + enum: ['no', 'soft', 'any'], + default: 'any' + }, + // Fumatori + smokingTolerance: { + type: String, + enum: ['no', 'outside', 'yes'], + default: 'no' + }, + // Viaggio con animali + comfortableWithPets: { type: Boolean, default: true } + } + }, + + // ============================================================ + // 🔍 RICERCA & FILTRI PREDEFINITI + // ============================================================ + searchPreferences: { + // Raggio di ricerca predefinito (km) + defaultRadius: { type: Number, default: 50 }, + // Ordine risultati + defaultSortBy: { + type: String, + enum: ['date', 'price', 'distance', 'rating'], + default: 'date' + }, + // Solo viaggi verificati + verifiedOnly: { type: Boolean, default: false }, + // Solo con recensioni positive + minRating: { type: Number, min: 0, max: 5, default: 0 } + }, + + // ============================================================ + // 💳 PAGAMENTI & DONAZIONI + // ============================================================ + payment: { + // Metodo di pagamento predefinito + defaultMethod: { + type: String, + enum: ['cash', 'card', 'app', 'none'], + default: 'cash' + }, + // Contributo suggerito automatico + autoSuggestContribution: { type: Boolean, default: true }, + // Accetta pagamenti anticipati + acceptAdvancePayment: { type: Boolean, default: false } + }, + + // ============================================================ + // 📱 INTERFACCIA + // ============================================================ + interface: { + // Tema + theme: { + type: String, + enum: ['light', 'dark', 'auto'], + default: 'auto' + }, + // Lingua + language: { + type: String, + enum: ['it', 'en', 'de', 'fr', 'es'], + default: 'it' + }, + // Mostra tutorial + showTutorials: { type: Boolean, default: true }, + // Vista mappa predefinita + defaultMapView: { type: Boolean, default: false } + }, + + // ============================================================ + // 🔐 SICUREZZA + // ============================================================ + security: { + // Richiedi verifica telefono per prenotazioni + requirePhoneVerification: { type: Boolean, default: true }, + // Autenticazione a due fattori + twoFactorAuth: { type: Boolean, default: false }, + // Logout automatico dopo inattività (minuti) + autoLogout: { type: Number, default: 30 }, + // Richiedi conferma prima di cancellare viaggio + confirmBeforeCancel: { type: Boolean, default: true } + } + +}, { + timestamps: true +}); + +// ============================================================ +// 📊 INDICI +// ============================================================ +userSettingsSchema.index({ idapp: 1, userId: 1 }, { unique: true }); + +// ============================================================ +// 🎯 METODI STATICI +// ============================================================ + +/** + * Ottieni o crea impostazioni utente con valori predefiniti + */ +userSettingsSchema.statics.getOrCreateSettings = async function(idapp, userId) { + let settings = await this.findOne({ idapp, userId }); + + if (!settings) { + settings = await this.create({ + idapp, + userId, + // I valori predefiniti sono già definiti nello schema + }); + } + + return settings; +}; + +/** + * Aggiorna impostazioni parziali + */ +userSettingsSchema.statics.updateSettings = async function(idapp, userId, updates) { + const settings = await this.getOrCreateSettings(idapp, userId); + + // Merge delle impostazioni + Object.keys(updates).forEach(section => { + if (settings[section] && typeof updates[section] === 'object') { + settings[section] = { + ...settings[section], + ...updates[section] + }; + } else { + settings[section] = updates[section]; + } + }); + + await settings.save(); + return settings; +}; + +// ============================================================ +// 🎯 METODI ISTANZA +// ============================================================ + +/** + * Verifica se una notifica è abilitata + */ +userSettingsSchema.methods.isNotificationEnabled = function(type, channel) { + if (!this.notifications[channel]) return false; + return this.notifications[channel][type] !== false; +}; + +/** + * Ottieni preferenze compatibilità viaggio + */ +userSettingsSchema.methods.getCompatibilityPreferences = function(asRole = 'passenger') { + if (asRole === 'driver') { + return this.ridePreferences.driver; + } + return this.ridePreferences.passenger; +}; + +/** + * Esporta impostazioni per frontend + */ +userSettingsSchema.methods.toClientJSON = function() { + return { + notifications: this.notifications, + privacy: this.privacy, + ridePreferences: this.ridePreferences, + searchPreferences: this.searchPreferences, + payment: this.payment, + interface: this.interface, + security: { + requirePhoneVerification: this.security.requirePhoneVerification, + twoFactorAuth: this.security.twoFactorAuth, + confirmBeforeCancel: this.security.confirmBeforeCancel + } + }; +}; + +module.exports = mongoose.model('TrasportiUserSettings', userSettingsSchema); \ No newline at end of file diff --git a/src/populate/migration-categories.js b/src/populate/migration-categories.js index a208da8..2d5c623 100644 --- a/src/populate/migration-categories.js +++ b/src/populate/migration-categories.js @@ -463,6 +463,8 @@ async function aggiornaCategorieESottoCategorie() { async function runMigration() { try { + const { User } = require('../models/user'); + const idapp = 0; // TUTTI console.log('🚀 Controllo Versioni Tabelle (runMigration)'); @@ -471,6 +473,10 @@ async function runMigration() { idapp, shared_consts.JOB_TO_EXECUTE.MIGRATION_SECTORS_DIC25 ); + const isMigratione30Dic2025Telegram = await Version.isJobExecuted( + idapp, + shared_consts.JOB_TO_EXECUTE.MIGRATION_TELEGRAM_30DIC25 + ); const vers_server_str = await tools.getVersServer(); @@ -522,6 +528,18 @@ async function runMigration() { console.log('\n✅ Migrazione DIC 2025 completata con successo!'); } + if (isMigratione30Dic2025Telegram) { + await User.updateMany({ 'profile.teleg_id': { $exists: true, $ne: 0 } }, [ + { + $set: { + 'notificationPreferences.telegram.enabled': true, + 'notificationPreferences.telegram.chatId': '$profile.teleg_id', + }, + }, + ]); + await Version.setJobExecuted(idapp, shared_consts.JOB_TO_EXECUTE.MIGRATION_TELEGRAM_30DIC25); + } + await Version.setLastVersionRun(idapp, version_server); } catch (error) { console.error('❌ Errore durante la migrazione:', error); @@ -535,5 +553,5 @@ module.exports = { subSkillMapping, sectorGoodMapping, sectorBachecaMapping, - aggiornaCategorieESottoCategorie, + aggiornaCategorieESottoCategorie, }; diff --git a/src/router/api_router.js b/src/router/api_router.js index 47f5e2c..37420e4 100644 --- a/src/router/api_router.js +++ b/src/router/api_router.js @@ -32,9 +32,16 @@ const { MyElem } = require('../models/myelem'); const axios = require('axios'); +const settingsRoutes = require('../routes/viaggi/settingsRoutes'); +router.use('/viaggi/settings', settingsRoutes); + +const widgetRoutes = require('../routes/viaggi/widgetRoutes'); +router.use('/viaggi/widget', widgetRoutes); + const viaggiRoutes = require('../routes/viaggiRoutes'); router.use('/viaggi', viaggiRoutes); + // Importa le routes video const videoRoutes = require('../routes/videoRoutes'); diff --git a/src/routes/viaggi/settingsRoutes.js b/src/routes/viaggi/settingsRoutes.js new file mode 100644 index 0000000..d5bacad --- /dev/null +++ b/src/routes/viaggi/settingsRoutes.js @@ -0,0 +1,100 @@ +// ============================================================ +// 🔧 SETTINGS ROUTES - Trasporti Solidali +// ============================================================ +// File: server/routes/viaggi/settingsRoutes.js + +const express = require('express'); +const router = express.Router(); +const settingsController = require('../../controllers/viaggi/settingsController'); +const { authenticate } = require('../../middleware/authenticate'); + +// ============================================================ +// 🔐 TUTTE LE ROUTES RICHIEDONO AUTENTICAZIONE +// ============================================================ +router.use(authenticate); + +// ============================================================ +// 📄 IMPOSTAZIONI GENERALI +// ============================================================ + +/** + * GET /api/viaggi/settings + * Ottieni tutte le impostazioni dell'utente + */ +router.get('/', settingsController.getSettings); + +/** + * PUT /api/viaggi/settings + * Aggiorna le impostazioni (completo) + */ +router.put('/', settingsController.updateSettings); + +/** + * POST /api/viaggi/settings/reset + * Reset impostazioni ai valori predefiniti + */ +router.post('/reset', settingsController.resetSettings); + +// ============================================================ +// 📝 AGGIORNAMENTI PARZIALI (per sezione) +// ============================================================ + +const notifController = require('../../controllers/viaggi/trasportiNotificationsController'); + +// Preferenze +router.get('/notifications/preferences', authenticate, notifController.getNotificationPreferences); +router.put('/notifications/preferences', authenticate, notifController.updateNotificationPreferences); + +// Telegram +router.post('/notifications/telegram/code', authenticate, notifController.generateTelegramCode); +router.post('/notifications/telegram/connect', authenticate, notifController.connectTelegram); +router.delete('/notifications/telegram/disconnect', authenticate, notifController.disconnectTelegram); + +// Push +router.post('/notifications/push/subscribe', authenticate, notifController.subscribePushNotifications); +router.delete('/notifications/push/unsubscribe', authenticate, notifController.unsubscribePushNotifications); + +// Test +router.post('/notifications/test', authenticate, notifController.sendTestNotification); + +/** + * PATCH /api/viaggi/settings/notifications + * Aggiorna solo le notifiche + */ +router.patch('/notifications', settingsController.updateNotifications); + +/** + * PATCH /api/viaggi/settings/privacy + * Aggiorna solo la privacy + */ +router.patch('/privacy', settingsController.updatePrivacy); + +/** + * PATCH /api/viaggi/settings/ride-preferences + * Aggiorna solo le preferenze viaggi + */ +router.patch('/ride-preferences', settingsController.updateRidePreferences); + +/** + * PATCH /api/viaggi/settings/interface + * Aggiorna solo l'interfaccia + */ +router.patch('/interface', settingsController.updateInterface); + +// ============================================================ +// 📊 EXPORT / IMPORT +// ============================================================ + +/** + * GET /api/viaggi/settings/export + * Esporta tutte le impostazioni + */ +router.get('/export', settingsController.exportSettings); + +/** + * POST /api/viaggi/settings/import + * Importa impostazioni da backup + */ +router.post('/import', settingsController.importSettings); + +module.exports = router; diff --git a/src/routes/viaggi/widgetRoutes.js b/src/routes/viaggi/widgetRoutes.js new file mode 100644 index 0000000..27597d8 --- /dev/null +++ b/src/routes/viaggi/widgetRoutes.js @@ -0,0 +1,32 @@ +// ============================================================ +// 📊 WIDGET & STATS ROUTES - Trasporti Solidali +// ============================================================ +// File: server/routes/viaggi/widgetRoutes.js + +const express = require('express'); +const router = express.Router(); +const widgetController = require('../../controllers/viaggi/widgetController'); +const { authenticate } = require('../../middleware/authenticate'); + +// ============================================================ +// 🔐 TUTTE LE ROUTES RICHIEDONO AUTENTICAZIONE +// ============================================================ +router.use(authenticate); + +// ============================================================ +// 📊 WIDGET DATA +// ============================================================ + +/** + * GET /api/viaggi/widget/data + * Ottieni tutti i dati per il widget dashboard + */ +router.get('/data', widgetController.getWidgetData); + +/** + * GET /api/viaggi/widget/stats + * Statistiche rapide per badge/notifiche + */ +router.get('/stats', widgetController.getQuickStats); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/viaggiRoutes.js b/src/routes/viaggiRoutes.js index 3905408..d632be9 100644 --- a/src/routes/viaggiRoutes.js +++ b/src/routes/viaggiRoutes.js @@ -20,7 +20,6 @@ const geoRoutes = require('./geoRoutes'); // 👈 Importa geoRoutes router.use('/geo', geoRoutes); // 👈 Monta come sub-router - // Middleware di autenticazione (usa il tuo esistente) const { authenticate } = require('../middleware/authenticate'); @@ -42,6 +41,7 @@ router.post('/rides', authenticate, rideController.createRide); */ router.get('/rides', rideController.getRides); + /** * @route GET /api/viaggi/rides/search * @desc Ricerca viaggi avanzata @@ -71,6 +71,18 @@ router.get('/rides/my', authenticate, rideController.getMyRides); */ //router.get('/rides/match', authenticate, rideController.findMatches); +router.get('/rides/cancelled', authenticate, rideController.getCancelledRides); +router.get('/rides/statscomm', authenticate, rideController.getCommunityStatsComm); +router.get('/rides/community', authenticate, rideController.getCommunityRides); +router.get('/rides/calendar', authenticate, rideController.getCalendarRides); + +/** + * @route POST /api/viaggi/rides/:rideId/request + * @desc Richiedi posto su un viaggio + * @access Private + */ +router.post('/rides/:rideId/request', authenticate, rideRequestController.createRequestFromRide); + /** * @route GET /api/viaggi/rides/:id * @desc Dettaglio singolo viaggio @@ -99,6 +111,8 @@ router.delete('/rides/:id', authenticate, rideController.deleteRide); */ router.post('/rides/:id/complete', authenticate, rideController.completeRide); +router.post('/rides/:rideId/favorite', authenticate, rideController.toggleFavoriteRide); + // ============================================================ // 📊 WIDGET & STATS // ============================================================ @@ -110,13 +124,6 @@ router.post('/rides/:id/complete', authenticate, rideController.completeRide); */ router.get('/widget/data', authenticate, rideController.getWidgetData); -/** - * @route GET /api/viaggi/stats/summary - * @desc Stats rapide per header widget (offerte, richieste, match) - * @access Public - */ -router.get('/stats/summary', authenticate, rideController.getStatsSummary); - /** * @route GET /api/viaggi/cities/suggestions * @desc Suggerimenti città per autocomplete (basato su viaggi esistenti) @@ -434,8 +441,8 @@ router.get('/driver/user/:userId', async (req, res) => { const idapp = req.query.idapp; const { User } = require('../models/user'); - const Ride = require('../models/Ride'); - const Feedback = require('../models/Feedback'); + const Ride = require('../models/viaggi/Ride'); + const Feedback = require('../models/viaggi/Feedback'); // Dati utente const user = await User.findById(userId).select( @@ -463,8 +470,8 @@ router.get('/driver/user/:userId', async (req, res) => { type: 'offer', status: { $in: ['active', 'completed'] }, }) - .select('departure destination dateTime status') - .sort({ dateTime: -1 }) + .select('departure destination departureDate status') + .sort({ departureDate: -1 }) .limit(5); // Statistiche feedback @@ -1037,4 +1044,9 @@ router.delete('/upload/vehicle-photo', authenticate, async (req, res) => { } }); +// ============================================ +// EXPORT ROUTES +// ============================================ + + module.exports = router; diff --git a/src/server/wsShellHandler.js b/src/server/wsShellHandler.js deleted file mode 100644 index 9f8df24..0000000 --- a/src/server/wsShellHandler.js +++ /dev/null @@ -1,57 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const pty = require('node-pty'); -const { User } = require('../models/user'); -const { sendMessage } = require('../telegram/api'); - -function setupShellWebSocket(ws) { - console.log('🔌 Client WebSocket Shell connesso'); - let scriptProcess = null; - let buffer = ''; - - ws.on('message', async (message) => { - try { - const parsed = JSON.parse(message); - const { type, user_id, scriptName, data } = parsed; - - if (type === 'start_script' && (await User.isAdminById(user_id))) { - if (scriptProcess) scriptProcess.kill(); - - const scriptPath = path.join(__dirname, '..', '..', scriptName); - if (!fs.existsSync(scriptPath)) { - return ws.send(JSON.stringify({ type: 'error', data: 'Script non trovato' })); - } - - scriptProcess = pty.spawn('bash', [scriptPath], { - name: 'xterm-color', - cols: 80, - rows: 40, - cwd: process.cwd(), - env: process.env, - }); - - scriptProcess.on('data', (chunk) => { - buffer += chunk; - ws.send(JSON.stringify({ type: 'output', data: chunk })); - if (buffer.length > 4096) buffer = buffer.slice(-2048); - }); - - scriptProcess.on('exit', (code) => { - const msg = code === 0 ? '✅ Script completato' : `❌ Uscito con codice ${code}`; - ws.send(JSON.stringify({ type: 'close', data: msg })); - }); - } else if (type === 'input' && scriptProcess) { - scriptProcess.write(data + '\n'); - } - } catch (err) { - console.error('❌ Errore WS Shell:', err.message); - } - }); - - ws.on('close', () => { - if (scriptProcess) scriptProcess.kill(); - console.log('🔌 WS Shell chiuso'); - }); -} - -module.exports = { setupShellWebSocket }; diff --git a/src/telegram/api.js b/src/telegram/api.js deleted file mode 100644 index 0df7008..0000000 --- a/src/telegram/api.js +++ /dev/null @@ -1,33 +0,0 @@ -const axios = require('axios'); -const { API_URL, TIMEOUT } = require('./config'); - -async function callTelegram(method, params) { - try { - const { data } = await axios.post(`${API_URL}/${method}`, params, { timeout: TIMEOUT }); - if (!data.ok) throw new Error(`Telegram error: ${data.description}`); - return data.result; - } catch (err) { - console.error('❌ Telegram API error:', err.message); - return null; - } -} - -async function sendMessage(chatId, text, options = {}) { - return callTelegram('sendMessage', { - chat_id: chatId, - text, - parse_mode: options.parse_mode || 'HTML', - disable_web_page_preview: true, - }); -} - -async function sendPhoto(chatId, photo, caption = '', options = {}) { - return callTelegram('sendPhoto', { - chat_id: chatId, - photo, - caption, - parse_mode: 'HTML', - }); -} - -module.exports = { sendMessage, sendPhoto }; diff --git a/src/telegram/config.js b/src/telegram/config.js deleted file mode 100644 index ca73e6c..0000000 --- a/src/telegram/config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - TOKEN: process.env.TELEGRAM_BOT_TOKEN, - API_URL: `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`, - ADMIN_GROUP_IDS: process.env.TELEGRAM_ADMIN_GROUPS - ? process.env.TELEGRAM_ADMIN_GROUPS.split(',') - : [], - TIMEOUT: 5000, -}; diff --git a/src/telegram/constants.js b/src/telegram/constants.js deleted file mode 100644 index 62b4034..0000000 --- a/src/telegram/constants.js +++ /dev/null @@ -1,18 +0,0 @@ -// Ruoli, fasi logiche e costanti admin (adatta gli ID ai tuoi reali) -module.exports = { - ADMIN_USER_SERVER: process.env.ADMIN_USER_SERVER || 'server_admin', - ADMIN_IDTELEGRAM_SERVER: process.env.ADMIN_IDTELEGRAM_SERVER || '', - phase: { - REGISTRATION: 'REGISTRATION', - REGISTRATION_CONFIRMED: 'REGISTRATION_CONFIRMED', - RESET_PWD: 'RESET_PWD', - NOTIFICATION: 'NOTIFICATION', - GENERIC: 'GENERIC', - }, - roles: { - ADMIN: 'ADMIN', - MANAGER: 'MANAGER', - FACILITATORE: 'FACILITATORE', - EDITOR: 'EDITOR', - }, -}; diff --git a/src/telegram/handlers/adminHandler.js b/src/telegram/handlers/adminHandler.js deleted file mode 100644 index 77eda61..0000000 --- a/src/telegram/handlers/adminHandler.js +++ /dev/null @@ -1,11 +0,0 @@ -const { sendMessage } = require('../api'); -const { ADMIN_GROUP_IDS } = require('../config'); -const { safeExec } = require('../helpers'); - -const sendToAdmins = safeExec(async (message) => { - for (const id of ADMIN_GROUP_IDS) { - await sendMessage(id, message); - } -}); - -module.exports = { sendToAdmins }; diff --git a/src/telegram/handlers/callbackHandler.js b/src/telegram/handlers/callbackHandler.js deleted file mode 100644 index d64753b..0000000 --- a/src/telegram/handlers/callbackHandler.js +++ /dev/null @@ -1,101 +0,0 @@ -// telegram/handlers/callbackHandler.js -const tools = require('../../tools/general'); -const shared_consts = require('../../tools/shared_nodejs'); -const { User } = require('../../models/user'); -const { Circuit } = require('../../models/circuit'); -const { handleRegistration } = require('./registrationHandler'); -const { handleFriends } = require('./friendsHandler'); -const { handleCircuit } = require('./circuitHandler'); -const { handleZoom } = require('./zoomHandler'); -const { handlePassword } = require('./passwordHandler'); - -async function handleCallback(bot, cl, callbackQuery) { - const idapp = cl.idapp; - let notifyText = ''; // testo di notifica Telegram (answerCallbackQuery) - try { - // parsing payload dal tuo formato originale (action|username|userDest|groupId|circuitId|groupname) - let data = { - action: '', - username: '', - userDest: '', - groupId: '', - circuitId: '', - groupname: '', - }; - - const raw = callbackQuery?.data || ''; - if (raw) { - const arr = raw.split(tools.SEP); - data = { - action: arr[0] || '', - username: arr[1] || '', - userDest: arr[2] || '', - groupId: arr[3] || '', - circuitId: arr[4] || '', - groupname: arr[5] || '', - }; - } - - // normalizza username reali (come nel sorgente) - data.username = await User.getRealUsernameByUsername(idapp, data.username); - data.userDest = data.userDest ? await User.getRealUsernameByUsername(idapp, data.userDest) : ''; - - const msg = callbackQuery.message; - const opts = { chat_id: msg.chat.id, message_id: msg.message_id }; - - // contest utente corrente - await cl.setInit?.(msg); // se presente nel tuo codice - const rec = cl.getRecInMem?.(msg); - const username_action = rec?.user ? rec.user.username : ''; - - // carica user e userDest compatti (come nel tuo codice) - const user = data.username ? await User.getUserShortDataByUsername(idapp, data.username) : null; - const userDest = data.userDest ? await User.getUserShortDataByUsername(idapp, data.userDest) : null; - - // routing per ambito - const act = data.action || ''; - - // 1) REGISTRAZIONE e varianti - if (act.includes(shared_consts.CallFunz.REGISTRATION)) { - notifyText = await handleRegistration({ bot, cl, idapp, msg, data, user, userDest, username_action }); - } - // 2) AMICIZIA / HANDSHAKE - else if ( - act.includes(shared_consts.CallFunz.RICHIESTA_AMICIZIA) || - act.includes(shared_consts.CallFunz.RICHIESTA_HANDSHAKE) - ) { - notifyText = await handleFriends({ bot, cl, idapp, msg, data, user, userDest, username_action }); - } - // 3) CIRCUITI (aggiunta/rimozione) - else if ( - act.includes(shared_consts.CallFunz.ADDUSERTOCIRCUIT) || - act.includes(shared_consts.CallFunz.REMUSERFROMCIRCUIT) - ) { - notifyText = await handleCircuit({ bot, cl, idapp, msg, data, user, userDest, username_action }); - } - // 4) ZOOM (registrazione/presenze) - else if (act.includes(shared_consts.CallFunz.REGISTRATION_TOZOOM) || act.includes('ZOOM')) { - notifyText = await handleZoom({ bot, cl, idapp, msg, data, user, userDest, username_action }); - } - // 5) RESET PASSWORD - else if (act.includes(shared_consts.CallFunz.RESET_PWD)) { - notifyText = await handlePassword({ bot, cl, idapp, msg, data, user, userDest, username_action }); - } else if (act.includes(shared_consts.CallFunz.RICHIESTA_GRUPPO)) { - notifyText = await handleGroup({ bot, cl, idapp, msg, data, user, userDest, username_action }); - } - // default - else { - notifyText = 'Operazione completata'; - await cl.sendMsg(msg.chat.id, `⚙️ Azione non riconosciuta: ${act}`); - } - - await bot.answerCallbackQuery(callbackQuery.id, { text: notifyText || 'OK' }); - } catch (err) { - console.error('❌ callbackHandler error:', err.message); - try { - await bot.answerCallbackQuery(callbackQuery.id, { text: 'Errore', show_alert: true }); - } catch (_) {} - } -} - -module.exports = { handleCallback }; diff --git a/src/telegram/handlers/circuitHandler.js b/src/telegram/handlers/circuitHandler.js deleted file mode 100644 index f5a2c48..0000000 --- a/src/telegram/handlers/circuitHandler.js +++ /dev/null @@ -1,54 +0,0 @@ -// telegram/handlers/circuitHandler.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_' }; - -async function handleCircuit({ bot, cl, idapp, msg, data, user, userDest, username_action }) { - let notifyText = ''; - - // Aggiunta al circuito - if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.ADDUSERTOCIRCUIT) { - const cmd = shared_consts.CIRCUITCMD.ADDUSERTOCIRCUIT; - const req = tools.getReqByPar(idapp, username_action); - - // se viene da gruppo usa ifCircuitAlreadyInGroup, altrimenti ifAlreadyInCircuit (come nel tuo codice) - const already = data.groupname - ? await User.ifCircuitAlreadyInGroup(idapp, data.groupname, data.circuitId) - : await User.ifAlreadyInCircuit(idapp, data.username, data.circuitId); - - if (!already) { - await User.setCircuitCmd(idapp, data.username, data.circuitId, cmd, 1, username_action, { groupname: data.groupname }); - await cl.sendMsg(msg.chat.id, `✅ ${data.username} aggiunto al circuito ${data.circuitId}`); - notifyText = 'Circuito OK'; - } else { - await cl.sendMsg(msg.chat.id, `ℹ️ ${data.username} è già nel circuito ${data.circuitId}`); - notifyText = 'Già presente'; - } - return notifyText; - } - - // Rimozione dal circuito - if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REMUSERFROMCIRCUIT) { - const cmd = shared_consts.CIRCUITCMD.REMOVEUSERFROMCIRCUIT; - await User.setCircuitCmd(idapp, data.username, data.circuitId, cmd, 0, username_action, { groupname: data.groupname }); - await cl.sendMsg(msg.chat.id, `🗑️ ${data.username} rimosso dal circuito ${data.circuitId}`); - notifyText = 'Rimosso'; - return notifyText; - } - - // NO / annulla - if ( - data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.ADDUSERTOCIRCUIT || - data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REMUSERFROMCIRCUIT - ) { - await cl.sendMsg(msg.chat.id, '❌ Operazione circuito annullata.'); - notifyText = 'Annullata'; - return notifyText; - } - - return 'OK'; -} - -module.exports = { handleCircuit }; diff --git a/src/telegram/handlers/directHandler.js b/src/telegram/handlers/directHandler.js deleted file mode 100644 index e17bb6b..0000000 --- a/src/telegram/handlers/directHandler.js +++ /dev/null @@ -1,24 +0,0 @@ -const { sendMessage, sendPhoto } = require('../api'); -const { safeExec } = require('../helpers'); - -const sendMsgTelegram = safeExec(async (user, text) => { - if (!user || !user.telegram_id) return null; - return sendMessage(user.telegram_id, text); -}); - -const sendMsgTelegramByIdTelegram = safeExec(async (telegramId, text) => { - if (!telegramId) return null; - return sendMessage(telegramId, text); -}); - -const sendPhotoTelegram = safeExec(async (chatIdOrUser, photoUrl, caption = '') => { - const chatId = typeof chatIdOrUser === 'object' ? chatIdOrUser?.telegram_id : chatIdOrUser; - if (!chatId || !photoUrl) return null; - return sendPhoto(chatId, photoUrl, caption); -}); - -module.exports = { - sendMsgTelegram, - sendMsgTelegramByIdTelegram, - sendPhotoTelegram, -}; diff --git a/src/telegram/handlers/errorHandler.js b/src/telegram/handlers/errorHandler.js deleted file mode 100644 index a946ba1..0000000 --- a/src/telegram/handlers/errorHandler.js +++ /dev/null @@ -1,12 +0,0 @@ -const { sendMessage } = require('../api'); -const { ADMIN_GROUP_IDS } = require('../config'); -const { safeExec } = require('../helpers'); - -const reportError = safeExec(async (context, err) => { - const msg = `🚨 Errore in ${context}\n
${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', }, };