diff --git a/src/controllers/chatController.js b/src/controllers/chatController.js new file mode 100644 index 0000000..9cbff7c --- /dev/null +++ b/src/controllers/chatController.js @@ -0,0 +1,611 @@ +const Chat = require('../models/Chat'); +const Message = require('../models/Message'); +const { User } = require('../models/User'); + +/** + * @desc Ottieni tutte le chat dell'utente + * @route GET /api/trasporti/chats + * @access Private + */ +const getMyChats = async (req, res) => { + try { + const userId = req.user._id; + const { idapp, page = 1, limit = 20 } = req.query; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio' + }); + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + + const chats = await Chat.find({ + idapp, + participants: userId, + isActive: true, + blockedBy: { $ne: userId } + }) + .populate('participants', 'username name surname profile.img') + .populate('rideId', 'departure destination dateTime status') + .sort({ updatedAt: -1 }) + .skip(skip) + .limit(parseInt(limit)); + + // Aggiungi conteggio non letti per ogni chat + const chatsWithUnread = chats.map(chat => { + const chatObj = chat.toObject(); + chatObj.unreadCount = chat.getUnreadForUser(userId); + + // Trova l'altro partecipante (per chat dirette) + if (chat.type === 'direct') { + chatObj.otherParticipant = chat.participants.find( + p => p._id.toString() !== userId.toString() + ); + } + + return chatObj; + }); + + const total = await Chat.countDocuments({ + idapp, + participants: userId, + isActive: true + }); + + res.json({ + success: true, + data: chatsWithUnread, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / parseInt(limit)) + } + }); + + } catch (error) { + console.error('Errore recupero chat:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero delle chat', + error: error.message + }); + } +}; + +/** + * @desc Ottieni o crea una chat diretta con un utente + * @route POST /api/trasporti/chats/direct + * @access Private + */ +const getOrCreateDirectChat = async (req, res) => { + try { + const userId = req.user._id; + const { idapp, otherUserId, rideId } = req.body; + + if (!idapp || !otherUserId) { + return res.status(400).json({ + success: false, + message: 'idapp e otherUserId sono obbligatori' + }); + } + + // Verifica che l'altro utente esista + const otherUser = await User.findById(otherUserId); + if (!otherUser) { + return res.status(404).json({ + success: false, + message: 'Utente non trovato' + }); + } + + // Non puoi chattare con te stesso + if (userId.toString() === otherUserId) { + return res.status(400).json({ + success: false, + message: 'Non puoi creare una chat con te stesso' + }); + } + + const chat = await Chat.findOrCreateDirect(idapp, userId, otherUserId, rideId); + + await chat.populate('participants', 'username name surname profile.img'); + await chat.populate('rideId', 'departure destination dateTime'); + + const chatObj = chat.toObject(); + chatObj.otherParticipant = chat.participants.find( + p => p._id.toString() !== userId.toString() + ); + chatObj.unreadCount = chat.getUnreadForUser(userId); + + res.json({ + success: true, + data: chatObj + }); + + } catch (error) { + console.error('Errore creazione chat:', error); + res.status(500).json({ + success: false, + message: 'Errore nella creazione della chat', + error: error.message + }); + } +}; + +/** + * @desc Ottieni una chat per ID + * @route GET /api/trasporti/chats/:id + * @access Private + */ +const getChatById = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user._id; + + const chat = await Chat.findById(id) + .populate('participants', 'username name surname profile.img profile.Cell') + .populate('rideId', 'departure destination dateTime status type'); + + if (!chat) { + return res.status(404).json({ + success: false, + message: 'Chat non trovata' + }); + } + + // Verifica che l'utente sia partecipante + if (!chat.hasParticipant(userId)) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato ad accedere a questa chat' + }); + } + + // Marca come letta + await chat.markAsRead(userId); + + const chatObj = chat.toObject(); + chatObj.unreadCount = 0; // Appena marcata come letta + + if (chat.type === 'direct') { + chatObj.otherParticipant = chat.participants.find( + p => p._id.toString() !== userId.toString() + ); + } + + res.json({ + success: true, + data: chatObj + }); + + } catch (error) { + console.error('Errore recupero chat:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero della chat', + error: error.message + }); + } +}; + +/** + * @desc Ottieni i messaggi di una chat + * @route GET /api/trasporti/chats/:id/messages + * @access Private + */ +const getChatMessages = async (req, res) => { + try { + const { id } = req.params; + const { idapp, before, after, limit = 50 } = req.query; + const userId = req.user._id; + + // Verifica accesso alla chat + const chat = await Chat.findById(id); + if (!chat) { + return res.status(404).json({ + success: false, + message: 'Chat non trovata' + }); + } + + if (!chat.hasParticipant(userId)) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato' + }); + } + + const messages = await Message.getByChat(idapp, id, { + limit: parseInt(limit), + before, + after + }); + + // Marca messaggi come letti + await Promise.all( + messages + .filter(m => m.senderId && m.senderId._id.toString() !== userId.toString()) + .map(m => m.markAsReadBy(userId)) + ); + + // Aggiorna unread count nella chat + await chat.markAsRead(userId); + + res.json({ + success: true, + data: messages.reverse(), // Ordine cronologico + hasMore: messages.length === parseInt(limit) + }); + + } catch (error) { + console.error('Errore recupero messaggi:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero dei messaggi', + error: error.message + }); + } +}; + +/** + * @desc Invia un messaggio + * @route POST /api/trasporti/chats/:id/messages + * @access Private + */ +const sendMessage = async (req, res) => { + try { + const { id } = req.params; + const { idapp, text, type = 'text', metadata, replyTo } = req.body; + const userId = req.user._id; + + if (!idapp || !text) { + return res.status(400).json({ + success: false, + message: 'idapp e text sono obbligatori' + }); + } + + // Verifica accesso alla chat + const chat = await Chat.findById(id); + if (!chat) { + return res.status(404).json({ + success: false, + message: 'Chat non trovata' + }); + } + + if (!chat.hasParticipant(userId)) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato' + }); + } + + // Verifica che la chat non sia bloccata + if (chat.isBlockedFor(userId)) { + return res.status(403).json({ + success: false, + message: 'Non puoi inviare messaggi in questa chat' + }); + } + + // Crea il messaggio + const message = new Message({ + idapp, + chatId: id, + senderId: userId, + text, + type, + metadata: metadata || {}, + replyTo: replyTo || null, + readBy: [{ userId, readAt: new Date() }] // Il mittente l'ha già letto + }); + + await message.save(); + + // Popola per la risposta + await message.populate('senderId', 'username name surname profile.img'); + if (replyTo) { + await message.populate('replyTo', 'text senderId'); + } + + // TODO: Inviare notifica push agli altri partecipanti + + res.status(201).json({ + success: true, + data: message + }); + + } catch (error) { + console.error('Errore invio messaggio:', error); + res.status(500).json({ + success: false, + message: 'Errore nell\'invio del messaggio', + error: error.message + }); + } +}; + +/** + * @desc Marca tutti i messaggi di una chat come letti + * @route PUT /api/trasporti/chats/:id/read + * @access Private + */ +const markChatAsRead = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user._id; + + const chat = await Chat.findById(id); + if (!chat) { + return res.status(404).json({ + success: false, + message: 'Chat non trovata' + }); + } + + if (!chat.hasParticipant(userId)) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato' + }); + } + + await chat.markAsRead(userId); + + // Marca tutti i messaggi come letti + await Message.updateMany( + { + chatId: id, + senderId: { $ne: userId }, + 'readBy.userId': { $ne: userId } + }, + { + $push: { readBy: { userId, readAt: new Date() } } + } + ); + + res.json({ + success: true, + message: 'Chat marcata come letta' + }); + + } catch (error) { + console.error('Errore marca come letto:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +/** + * @desc Blocca/sblocca una chat + * @route PUT /api/trasporti/chats/:id/block + * @access Private + */ +const toggleBlockChat = async (req, res) => { + try { + const { id } = req.params; + const { block } = req.body; + const userId = req.user._id; + + const chat = await Chat.findById(id); + if (!chat) { + return res.status(404).json({ + success: false, + message: 'Chat non trovata' + }); + } + + if (!chat.hasParticipant(userId)) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato' + }); + } + + if (block) { + if (!chat.blockedBy.includes(userId)) { + chat.blockedBy.push(userId); + } + } else { + chat.blockedBy = chat.blockedBy.filter( + id => id.toString() !== userId.toString() + ); + } + + await chat.save(); + + res.json({ + success: true, + message: block ? 'Chat bloccata' : 'Chat sbloccata', + data: { blocked: block } + }); + + } catch (error) { + console.error('Errore blocco chat:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +/** + * @desc Muta/smuta notifiche di una chat + * @route PUT /api/trasporti/chats/:id/mute + * @access Private + */ +const toggleMuteChat = async (req, res) => { + try { + const { id } = req.params; + const { mute } = req.body; + const userId = req.user._id; + + const chat = await Chat.findById(id); + if (!chat) { + return res.status(404).json({ + success: false, + message: 'Chat non trovata' + }); + } + + if (!chat.hasParticipant(userId)) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato' + }); + } + + if (mute) { + if (!chat.mutedBy.includes(userId)) { + chat.mutedBy.push(userId); + } + } else { + chat.mutedBy = chat.mutedBy.filter( + id => id.toString() !== userId.toString() + ); + } + + await chat.save(); + + res.json({ + success: true, + message: mute ? 'Notifiche disattivate' : 'Notifiche attivate', + data: { muted: mute } + }); + + } catch (error) { + console.error('Errore mute chat:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +/** + * @desc Conta messaggi non letti totali + * @route GET /api/trasporti/chats/unread/count + * @access Private + */ +const getUnreadCount = async (req, res) => { + try { + const userId = req.user._id; + const { idapp } = req.query; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio' + }); + } + + const chats = await Chat.find({ + idapp, + participants: userId, + isActive: true + }); + + let totalUnread = 0; + const chatUnreads = []; + + for (const chat of chats) { + const unread = chat.getUnreadForUser(userId); + totalUnread += unread; + if (unread > 0) { + chatUnreads.push({ + chatId: chat._id, + unread + }); + } + } + + res.json({ + success: true, + data: { + total: totalUnread, + chats: chatUnreads + } + }); + + } catch (error) { + console.error('Errore conteggio non letti:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +/** + * @desc Elimina un messaggio (soft delete) + * @route DELETE /api/trasporti/chats/:chatId/messages/:messageId + * @access Private + */ +const deleteMessage = async (req, res) => { + try { + const { chatId, messageId } = req.params; + const userId = req.user._id; + + const message = await Message.findById(messageId); + + if (!message) { + return res.status(404).json({ + success: false, + message: 'Messaggio non trovato' + }); + } + + if (message.chatId.toString() !== chatId) { + return res.status(400).json({ + success: false, + message: 'Messaggio non appartiene a questa chat' + }); + } + + // Solo il mittente può eliminare + if (message.senderId.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato a eliminare questo messaggio' + }); + } + + await message.softDelete(); + + res.json({ + success: true, + message: 'Messaggio eliminato' + }); + + } catch (error) { + console.error('Errore eliminazione messaggio:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +module.exports = { + getMyChats, + getOrCreateDirectChat, + getChatById, + getChatMessages, + sendMessage, + markChatAsRead, + toggleBlockChat, + toggleMuteChat, + getUnreadCount, + deleteMessage +}; \ No newline at end of file diff --git a/src/controllers/feedbackController.js b/src/controllers/feedbackController.js new file mode 100644 index 0000000..e2f9b74 --- /dev/null +++ b/src/controllers/feedbackController.js @@ -0,0 +1,926 @@ +const Feedback = require('../models/Feedback'); +const Ride = require('../models/Ride'); +const RideRequest = require('../models/RideRequest'); +const { User } = require('../models/User'); + +/** + * @desc Crea un feedback per un viaggio + * @route POST /api/trasporti/feedback + * @access Private + */ +const createFeedback = async (req, res) => { + try { + const { + idapp, + rideId, + rideRequestId, + toUserId, + role, + rating, + categories, + comment, + pros, + cons, + tags, + isPublic + } = req.body; + + const fromUserId = req.user._id; + + // Validazione base + if (!idapp || !rideId || !toUserId || !role || !rating) { + return res.status(400).json({ + success: false, + message: 'Campi obbligatori: idapp, rideId, toUserId, role, rating' + }); + } + + // Verifica rating valido + if (rating < 1 || rating > 5) { + return res.status(400).json({ + success: false, + message: 'Il rating deve essere tra 1 e 5' + }); + } + + // Verifica che il ride esista e sia completato + const ride = await Ride.findById(rideId); + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato' + }); + } + + // Verifica che l'utente abbia partecipato al viaggio + const wasDriver = ride.userId.toString() === fromUserId.toString(); + const wasPassenger = ride.confirmedPassengers.some( + p => p.userId.toString() === fromUserId.toString() + ); + + if (!wasDriver && !wasPassenger) { + return res.status(403).json({ + success: false, + message: 'Non hai partecipato a questo viaggio' + }); + } + + // Verifica che non stia valutando se stesso + if (fromUserId.toString() === toUserId.toString()) { + return res.status(400).json({ + success: false, + message: 'Non puoi valutare te stesso' + }); + } + + // Verifica che non esista già un feedback + const existingFeedback = await Feedback.findOne({ + rideId, + fromUserId, + toUserId + }); + + if (existingFeedback) { + return res.status(400).json({ + success: false, + message: 'Hai già lasciato un feedback per questo utente in questo viaggio' + }); + } + + // Crea il feedback + const feedbackData = { + idapp, + rideId, + fromUserId, + toUserId, + role, + rating + }; + + if (rideRequestId) feedbackData.rideRequestId = rideRequestId; + if (categories) feedbackData.categories = categories; + if (comment) feedbackData.comment = comment; + if (pros) feedbackData.pros = pros; + if (cons) feedbackData.cons = cons; + if (tags) feedbackData.tags = tags; + if (isPublic !== undefined) feedbackData.isPublic = isPublic; + + // Verifica automatica se il viaggio è completato + if (ride.status === 'completed') { + feedbackData.isVerified = true; + } + + const feedback = new Feedback(feedbackData); + await feedback.save(); + + // Aggiorna la media rating dell'utente destinatario + await updateUserRating(idapp, toUserId); + + // Aggiorna flag nella richiesta se presente + if (rideRequestId) { + await RideRequest.findByIdAndUpdate(rideRequestId, { + feedbackGiven: true + }); + } + + await feedback.populate('fromUserId', 'username name surname profile.img'); + await feedback.populate('toUserId', 'username name surname'); + + res.status(201).json({ + success: true, + message: 'Feedback inviato con successo!', + data: feedback + }); + + } catch (error) { + console.error('Errore creazione feedback:', error); + res.status(500).json({ + success: false, + message: 'Errore nella creazione del feedback', + error: error.message + }); + } +}; + +/** + * @desc Ottieni i feedback ricevuti da un utente + * @route GET /api/trasporti/feedback/user/:userId + * @access Public + */ +const getUserFeedback = async (req, res) => { + try { + const { userId } = req.params; + const { idapp, role, page = 1, limit = 10 } = req.query; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio' + }); + } + + const query = { + idapp, + toUserId: userId, + isPublic: true + }; + + if (role) { + query.role = role; + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + + const [feedbacks, total, stats] = await Promise.all([ + Feedback.find(query) + .populate('fromUserId', 'username name surname profile.img') + .populate('rideId', 'departure destination dateTime') + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)), + Feedback.countDocuments(query), + getStatsForUser(idapp, userId) + ]); + + res.json({ + success: true, + data: feedbacks, + stats, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / parseInt(limit)) + } + }); + + } catch (error) { + console.error('Errore recupero feedbacks:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero dei feedback', + error: error.message + }); + } +}; + +/** + * @desc Ottieni statistiche feedback per un utente + * @route GET /api/trasporti/feedback/user/:userId/stats + * @access Public + */ +const getUserFeedbackStats = async (req, res) => { + try { + const { userId } = req.params; + const { idapp } = req.query; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio' + }); + } + + const [stats, distribution] = await Promise.all([ + getStatsForUser(idapp, userId), + getRatingDistribution(idapp, userId) + ]); + + res.json({ + success: true, + data: { + ...stats, + distribution + } + }); + + } catch (error) { + console.error('Errore recupero stats:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero delle statistiche', + error: error.message + }); + } +}; + +/** + * @desc Ottieni i feedback per un viaggio + * @route GET /api/trasporti/feedback/ride/:rideId + * @access Public (con info limitate) / Private (info complete) + */ +const getRideFeedback = async (req, res) => { + try { + const { rideId } = req.params; + const { idapp } = req.query; + const userId = req.user?._id; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio' + }); + } + + // Verifica che il ride esista + const ride = await Ride.findById(rideId); + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato' + }); + } + + // Query base per feedback pubblici + const query = { + idapp, + rideId, + isPublic: true + }; + + const feedbacks = await Feedback.find(query) + .populate('fromUserId', 'username name surname profile.img') + .populate('toUserId', 'username name surname profile.img') + .sort({ createdAt: -1 }); + + // Se l'utente è autenticato e ha partecipato, mostra info aggiuntive + let pendingFeedbacks = []; + let myFeedbacks = []; + + if (userId) { + const wasDriver = ride.userId.toString() === userId.toString(); + const wasPassenger = ride.confirmedPassengers.some( + p => p.userId.toString() === userId.toString() + ); + + if (wasDriver || wasPassenger) { + myFeedbacks = feedbacks.filter( + f => f.fromUserId._id.toString() === userId.toString() + ); + + if (wasDriver) { + // Il conducente deve dare feedback ai passeggeri + const feedbackGivenTo = myFeedbacks.map(f => f.toUserId._id.toString()); + pendingFeedbacks = ride.confirmedPassengers + .filter(p => !feedbackGivenTo.includes(p.userId.toString())) + .map(p => ({ userId: p.userId, role: 'passenger' })); + } else { + // Il passeggero deve dare feedback al conducente + const hasGivenToDriver = myFeedbacks.some( + f => f.toUserId._id.toString() === ride.userId.toString() + ); + if (!hasGivenToDriver) { + pendingFeedbacks.push({ userId: ride.userId, role: 'driver' }); + } + } + } + } + + res.json({ + success: true, + data: { + feedbacks, + pendingFeedbacks, + myFeedbacks + } + }); + + } catch (error) { + console.error('Errore recupero feedbacks viaggio:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +/** + * @desc Verifica se l'utente può lasciare un feedback + * @route GET /api/trasporti/feedback/can-leave/:rideId/:toUserId + * @access Private + * @note NUOVA FUNZIONE - Era mancante! + */ +const canLeaveFeedback = async (req, res) => { + try { + const { rideId, toUserId } = req.params; + const { idapp } = req.query; + const fromUserId = req.user._id; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio' + }); + } + + // Verifica che il ride esista + const ride = await Ride.findById(rideId); + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato' + }); + } + + // Verifica che l'utente abbia partecipato al viaggio + const wasDriver = ride.userId.toString() === fromUserId.toString(); + const wasPassenger = ride.confirmedPassengers.some( + p => p.userId.toString() === fromUserId.toString() + ); + + if (!wasDriver && !wasPassenger) { + return res.json({ + success: true, + data: { + canLeave: false, + reason: 'Non hai partecipato a questo viaggio' + } + }); + } + + // Verifica che non stia valutando se stesso + if (fromUserId.toString() === toUserId.toString()) { + return res.json({ + success: true, + data: { + canLeave: false, + reason: 'Non puoi valutare te stesso' + } + }); + } + + // Verifica che toUserId abbia partecipato al viaggio + const toUserWasDriver = ride.userId.toString() === toUserId.toString(); + const toUserWasPassenger = ride.confirmedPassengers.some( + p => p.userId.toString() === toUserId.toString() + ); + + if (!toUserWasDriver && !toUserWasPassenger) { + return res.json({ + success: true, + data: { + canLeave: false, + reason: 'L\'utente destinatario non ha partecipato a questo viaggio' + } + }); + } + + // Verifica che il viaggio sia completato + if (ride.status !== 'completed') { + return res.json({ + success: true, + data: { + canLeave: false, + reason: 'Il viaggio non è ancora stato completato', + rideStatus: ride.status + } + }); + } + + // Verifica che non esista già un feedback + const existingFeedback = await Feedback.findOne({ + rideId, + fromUserId, + toUserId + }); + + if (existingFeedback) { + return res.json({ + success: true, + data: { + canLeave: false, + reason: 'Hai già lasciato un feedback per questo utente in questo viaggio', + existingFeedbackId: existingFeedback._id + } + }); + } + + // Determina il ruolo del destinatario + const toUserRole = toUserWasDriver ? 'driver' : 'passenger'; + + // Recupera info utente destinatario + const toUser = await User.findById(toUserId) + .select('username name surname profile.img'); + + res.json({ + success: true, + data: { + canLeave: true, + toUser: { + _id: toUser._id, + username: toUser.username, + name: toUser.name, + surname: toUser.surname, + img: toUser.profile?.img + }, + toUserRole, + ride: { + _id: ride._id, + departure: ride.departure, + destination: ride.destination, + dateTime: ride.dateTime + } + } + }); + + } catch (error) { + console.error('Errore verifica canLeaveFeedback:', error); + res.status(500).json({ + success: false, + message: 'Errore nella verifica', + error: error.message + }); + } +}; + +/** + * @desc Rispondi a un feedback ricevuto + * @route POST /api/trasporti/feedback/:id/response + * @access Private + */ +const respondToFeedback = async (req, res) => { + try { + const { id } = req.params; + const { text } = req.body; + const userId = req.user._id; + + if (!text) { + return res.status(400).json({ + success: false, + message: 'Il testo della risposta è obbligatorio' + }); + } + + const feedback = await Feedback.findById(id); + + if (!feedback) { + return res.status(404).json({ + success: false, + message: 'Feedback non trovato' + }); + } + + // Solo chi ha ricevuto il feedback può rispondere + if (feedback.toUserId.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato a rispondere a questo feedback' + }); + } + + // Verifica che non abbia già risposto + if (feedback.response && feedback.response.text) { + return res.status(400).json({ + success: false, + message: 'Hai già risposto a questo feedback' + }); + } + + // Aggiungi la risposta + feedback.response = { + text, + createdAt: new Date() + }; + await feedback.save(); + + res.json({ + success: true, + message: 'Risposta aggiunta', + data: feedback + }); + + } catch (error) { + console.error('Errore risposta feedback:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +/** + * @desc Segna un feedback come utile + * @route POST /api/trasporti/feedback/:id/helpful + * @access Private + */ +const markAsHelpful = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user._id; + + const feedback = await Feedback.findById(id); + + if (!feedback) { + return res.status(404).json({ + success: false, + message: 'Feedback non trovato' + }); + } + + // Inizializza helpful se non esiste + if (!feedback.helpful) { + feedback.helpful = { count: 0, users: [] }; + } + + // Verifica se l'utente ha già segnato come utile + const userIdStr = userId.toString(); + const alreadyMarked = feedback.helpful.users.some( + u => u.toString() === userIdStr + ); + + if (alreadyMarked) { + // Rimuovi il voto + feedback.helpful.users = feedback.helpful.users.filter( + u => u.toString() !== userIdStr + ); + feedback.helpful.count = Math.max(0, feedback.helpful.count - 1); + } else { + // Aggiungi il voto + feedback.helpful.users.push(userId); + feedback.helpful.count += 1; + } + + await feedback.save(); + + res.json({ + success: true, + message: alreadyMarked ? 'Voto rimosso' : 'Feedback segnato come utile', + data: { + helpfulCount: feedback.helpful.count, + isHelpful: !alreadyMarked + } + }); + + } catch (error) { + console.error('Errore mark helpful:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +/** + * @desc Segnala un feedback inappropriato + * @route POST /api/trasporti/feedback/:id/report + * @access Private + */ +const reportFeedback = async (req, res) => { + try { + const { id } = req.params; + const { reason } = req.body; + const userId = req.user._id; + + if (!reason) { + return res.status(400).json({ + success: false, + message: 'La motivazione è obbligatoria' + }); + } + + const feedback = await Feedback.findById(id); + + if (!feedback) { + return res.status(404).json({ + success: false, + message: 'Feedback non trovato' + }); + } + + // Inizializza reports se non esiste + if (!feedback.reports) { + feedback.reports = []; + } + + // Verifica se l'utente ha già segnalato + const alreadyReported = feedback.reports.some( + r => r.userId.toString() === userId.toString() + ); + + if (alreadyReported) { + return res.status(400).json({ + success: false, + message: 'Hai già segnalato questo feedback' + }); + } + + // Aggiungi la segnalazione + feedback.reports.push({ + userId, + reason, + createdAt: new Date() + }); + + // Se ci sono troppe segnalazioni, nascondi automaticamente + if (feedback.reports.length >= 3) { + feedback.isPublic = false; + feedback.hiddenReason = 'Nascosto automaticamente per multiple segnalazioni'; + } + + await feedback.save(); + + res.json({ + success: true, + message: 'Feedback segnalato. Lo esamineremo al più presto.' + }); + + } catch (error) { + console.error('Errore segnalazione:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +/** + * @desc Ottieni i miei feedback dati + * @route GET /api/trasporti/feedback/my/given + * @access Private + */ +const getMyGivenFeedback = async (req, res) => { + try { + const userId = req.user._id; + const { idapp, page = 1, limit = 20 } = req.query; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio' + }); + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + + const [feedbacks, total] = await Promise.all([ + Feedback.find({ idapp, fromUserId: userId }) + .populate('toUserId', 'username name surname profile.img') + .populate('rideId', 'departure destination dateTime') + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)), + Feedback.countDocuments({ idapp, fromUserId: userId }) + ]); + + res.json({ + success: true, + data: feedbacks, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / parseInt(limit)) + } + }); + + } catch (error) { + console.error('Errore recupero feedback dati:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + +/** + * @desc Ottieni i miei feedback ricevuti + * @route GET /api/trasporti/feedback/my/received + * @access Private + */ +const getMyReceivedFeedback = async (req, res) => { + try { + const userId = req.user._id; + const { idapp, page = 1, limit = 20 } = req.query; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio' + }); + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + + const [feedbacks, total, stats] = await Promise.all([ + Feedback.find({ idapp, toUserId: userId }) + .populate('fromUserId', 'username name surname profile.img') + .populate('rideId', 'departure destination dateTime') + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)), + Feedback.countDocuments({ idapp, toUserId: userId }), + getStatsForUser(idapp, userId) + ]); + + res.json({ + success: true, + data: feedbacks, + stats, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / parseInt(limit)) + } + }); + + } catch (error) { + console.error('Errore recupero feedback ricevuti:', error); + res.status(500).json({ + success: false, + message: 'Errore', + error: error.message + }); + } +}; + + +// ============================================================ +// 🔧 HELPER FUNCTIONS +// ============================================================ + +/** + * Calcola le statistiche feedback per un utente + */ +async function getStatsForUser(idapp, userId) { + try { + const result = await Feedback.aggregate([ + { + $match: { + idapp, + toUserId: typeof userId === 'string' + ? require('mongoose').Types.ObjectId(userId) + : userId + } + }, + { + $group: { + _id: null, + averageRating: { $avg: '$rating' }, + totalFeedback: { $sum: 1 }, + asDriver: { + $sum: { $cond: [{ $eq: ['$role', 'driver'] }, 1, 0] } + }, + asPassenger: { + $sum: { $cond: [{ $eq: ['$role', 'passenger'] }, 1, 0] } + } + } + } + ]); + + if (result.length === 0) { + return { + averageRating: 0, + totalFeedback: 0, + asDriver: 0, + asPassenger: 0 + }; + } + + return { + averageRating: Math.round(result[0].averageRating * 10) / 10, + totalFeedback: result[0].totalFeedback, + asDriver: result[0].asDriver, + asPassenger: result[0].asPassenger + }; + } catch (error) { + console.error('Errore calcolo stats:', error); + return { + averageRating: 0, + totalFeedback: 0, + asDriver: 0, + asPassenger: 0 + }; + } +} + +/** + * Calcola la distribuzione dei rating per un utente + */ +async function getRatingDistribution(idapp, userId) { + try { + const result = await Feedback.aggregate([ + { + $match: { + idapp, + toUserId: typeof userId === 'string' + ? require('mongoose').Types.ObjectId(userId) + : userId + } + }, + { + $group: { + _id: '$rating', + count: { $sum: 1 } + } + }, + { $sort: { _id: -1 } } + ]); + + // Crea distribuzione completa 1-5 + const distribution = {}; + for (let i = 1; i <= 5; i++) { + distribution[i] = 0; + } + + result.forEach(r => { + distribution[r._id] = r.count; + }); + + return distribution; + } catch (error) { + console.error('Errore calcolo distribuzione:', error); + return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; + } +} + +/** + * Aggiorna la media rating nel profilo utente + */ +async function updateUserRating(idapp, userId) { + try { + const stats = await getStatsForUser(idapp, userId); + + await User.findByIdAndUpdate(userId, { + 'profile.driverProfile.averageRating': stats.averageRating, + 'profile.driverProfile.totalFeedback': stats.totalFeedback + }); + } catch (error) { + console.error('Errore aggiornamento rating utente:', error); + } +} + + +// ============================================================ +// 📤 EXPORTS +// ============================================================ + +module.exports = { + // Funzioni principali (nomi corretti per le routes) + createFeedback, + getUserFeedback, // GET /feedback/user/:userId + getUserFeedbackStats, // GET /feedback/user/:userId/stats + getRideFeedback, // GET /feedback/ride/:rideId + canLeaveFeedback, // GET /feedback/can-leave/:rideId/:toUserId ← NUOVA! + respondToFeedback, // POST /feedback/:id/response + reportFeedback, // POST /feedback/:id/report + markAsHelpful, // POST /feedback/:id/helpful + getMyGivenFeedback, // GET /feedback/my/given + getMyReceivedFeedback, // GET /feedback/my/received + + // Alias per compatibilità (vecchi nomi) + getFeedbacksForUser: getUserFeedback, + getFeedbacksForRide: getRideFeedback, + getMyGivenFeedbacks: getMyGivenFeedback, + getMyReceivedFeedbacks: getMyReceivedFeedback, + + // Helper functions (esportate per uso in altri moduli) + getStatsForUser, + getRatingDistribution, + updateUserRating +}; \ No newline at end of file diff --git a/src/controllers/geocodingController.js b/src/controllers/geocodingController.js new file mode 100644 index 0000000..02247ea --- /dev/null +++ b/src/controllers/geocodingController.js @@ -0,0 +1,522 @@ +/** + * Controller per Geocoding usando servizi Open Source + * - Nominatim (OpenStreetMap) per geocoding/reverse + * - OSRM per routing + * - Photon per autocomplete + */ + +const https = require('https'); +const http = require('http'); + +// Configurazione servizi +const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org'; +const PHOTON_BASE = 'https://photon.komoot.io'; +const OSRM_BASE = 'https://router.project-osrm.org'; + +// User-Agent richiesto da Nominatim +const USER_AGENT = 'FreePlanetApp/1.0'; + +/** + * Helper per fare richieste HTTP/HTTPS + */ +const makeRequest = (url) => { + return new Promise((resolve, reject) => { + const client = url.startsWith('https') ? https : http; + + const req = client.get(url, { + headers: { + 'User-Agent': USER_AGENT, + 'Accept': 'application/json' + } + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (e) { + reject(new Error('Errore parsing risposta')); + } + }); + }); + + req.on('error', reject); + req.setTimeout(10000, () => { + req.destroy(); + reject(new Error('Timeout richiesta')); + }); + }); +}; + +/** + * @desc Autocomplete città (Photon API) + * @route GET /api/trasporti/geo/autocomplete + */ +const autocomplete = async (req, res) => { + try { + const { q, limit = 5, lang = 'it' } = req.query; + + if (!q || q.length < 2) { + return res.status(400).json({ + success: false, + message: 'Query deve essere almeno 2 caratteri' + }); + } + + // Photon API - gratuito e veloce + const url = `${PHOTON_BASE}/api/?q=${encodeURIComponent(q)}&limit=${limit}&lang=${lang}&osm_tag=place:city&osm_tag=place:town&osm_tag=place:village`; + + const data = await makeRequest(url); + + // Formatta risultati + const results = data.features.map(feature => ({ + city: feature.properties.name, + province: feature.properties.county || feature.properties.state, + region: feature.properties.state, + country: feature.properties.country, + postalCode: feature.properties.postcode, + coordinates: { + lat: feature.geometry.coordinates[1], + lng: feature.geometry.coordinates[0] + }, + displayName: [ + feature.properties.name, + feature.properties.county, + feature.properties.state, + feature.properties.country + ].filter(Boolean).join(', '), + type: feature.properties.osm_value || 'place' + })); + + res.status(200).json({ + success: true, + data: results + }); + + } catch (error) { + console.error('Errore autocomplete:', error); + res.status(500).json({ + success: false, + message: 'Errore durante la ricerca', + error: error.message + }); + } +}; + +/** + * @desc Geocoding - indirizzo a coordinate (Nominatim) + * @route GET /api/trasporti/geo/geocode + */ +const geocode = async (req, res) => { + try { + const { address, city, country = 'Italy' } = req.query; + + const searchQuery = [address, city, country].filter(Boolean).join(', '); + + if (!searchQuery) { + return res.status(400).json({ + success: false, + message: 'Fornisci un indirizzo o città da cercare' + }); + } + + const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5&addressdetails=1`; + + const data = await makeRequest(url); + + if (!data || data.length === 0) { + return res.status(404).json({ + success: false, + message: 'Nessun risultato trovato' + }); + } + + const results = data.map(item => ({ + displayName: item.display_name, + city: item.address.city || item.address.town || item.address.village || item.address.municipality, + address: item.address.road ? `${item.address.road}${item.address.house_number ? ' ' + item.address.house_number : ''}` : null, + province: item.address.county || item.address.province, + region: item.address.state, + country: item.address.country, + postalCode: item.address.postcode, + coordinates: { + lat: parseFloat(item.lat), + lng: parseFloat(item.lon) + }, + type: item.type, + importance: item.importance + })); + + res.status(200).json({ + success: true, + data: results + }); + + } catch (error) { + console.error('Errore geocoding:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il geocoding', + error: error.message + }); + } +}; + +/** + * @desc Reverse geocoding - coordinate a indirizzo (Nominatim) + * @route GET /api/trasporti/geo/reverse + */ +const reverseGeocode = async (req, res) => { + try { + const { lat, lng } = req.query; + + if (!lat || !lng) { + return res.status(400).json({ + success: false, + message: 'Coordinate lat e lng richieste' + }); + } + + const url = `${NOMINATIM_BASE}/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`; + + const data = await makeRequest(url); + + if (!data || data.error) { + return res.status(404).json({ + success: false, + message: 'Nessun risultato trovato' + }); + } + + const result = { + displayName: data.display_name, + city: data.address.city || data.address.town || data.address.village || data.address.municipality, + address: data.address.road ? `${data.address.road}${data.address.house_number ? ' ' + data.address.house_number : ''}` : null, + province: data.address.county || data.address.province, + region: data.address.state, + country: data.address.country, + postalCode: data.address.postcode, + coordinates: { + lat: parseFloat(lat), + lng: parseFloat(lng) + } + }; + + res.status(200).json({ + success: true, + data: result + }); + + } catch (error) { + console.error('Errore reverse geocoding:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il reverse geocoding', + error: error.message + }); + } +}; + +/** + * @desc Calcola percorso tra due punti (OSRM) + * @route GET /api/trasporti/geo/route + */ +const getRoute = async (req, res) => { + try { + const { + startLat, startLng, + endLat, endLng, + waypoints // formato: "lat1,lng1;lat2,lng2;..." + } = req.query; + + if (!startLat || !startLng || !endLat || !endLng) { + return res.status(400).json({ + success: false, + message: 'Coordinate di partenza e arrivo richieste' + }); + } + + // Costruisci stringa coordinate + let coordinates = `${startLng},${startLat}`; + + if (waypoints) { + const waypointsList = waypoints.split(';'); + waypointsList.forEach(wp => { + const [lat, lng] = wp.split(','); + coordinates += `;${lng},${lat}`; + }); + } + + coordinates += `;${endLng},${endLat}`; + + const url = `${OSRM_BASE}/route/v1/driving/${coordinates}?overview=full&geometries=polyline&steps=true`; + + const data = await makeRequest(url); + + if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) { + return res.status(404).json({ + success: false, + message: 'Impossibile calcolare il percorso' + }); + } + + const route = data.routes[0]; + + // Estrai città attraversate (dalle istruzioni) + const citiesAlongRoute = []; + if (route.legs) { + route.legs.forEach(leg => { + if (leg.steps) { + leg.steps.forEach(step => { + if (step.name && step.name.length > 0) { + // Qui potresti fare reverse geocoding per ottenere città + // Per ora usiamo i nomi delle strade principali + } + }); + } + }); + } + + const result = { + distance: Math.round(route.distance / 1000 * 10) / 10, // km + duration: Math.round(route.duration / 60), // minuti + polyline: route.geometry, // Polyline encoded + legs: route.legs.map(leg => ({ + distance: Math.round(leg.distance / 1000 * 10) / 10, + duration: Math.round(leg.duration / 60), + summary: leg.summary, + steps: leg.steps ? leg.steps.slice(0, 10).map(s => ({ // Limita step + instruction: s.maneuver ? s.maneuver.instruction : '', + name: s.name, + distance: Math.round(s.distance), + duration: Math.round(s.duration / 60) + })) : [] + })) + }; + + res.status(200).json({ + success: true, + data: result + }); + + } catch (error) { + console.error('Errore calcolo percorso:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il calcolo del percorso', + error: error.message + }); + } +}; + +/** + * @desc Suggerisci città intermedie su un percorso + * @route GET /api/trasporti/geo/suggest-waypoints + */ +const suggestWaypoints = async (req, res) => { + try { + const { startLat, startLng, endLat, endLng } = req.query; + + if (!startLat || !startLng || !endLat || !endLng) { + return res.status(400).json({ + success: false, + message: 'Coordinate di partenza e arrivo richieste' + }); + } + + // Prima ottieni il percorso + const routeUrl = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=full&geometries=geojson`; + + const routeData = await makeRequest(routeUrl); + + if (!routeData || routeData.code !== 'Ok') { + return res.status(404).json({ + success: false, + message: 'Impossibile calcolare il percorso' + }); + } + + // Prendi punti lungo il percorso (ogni ~50km circa) + const coordinates = routeData.routes[0].geometry.coordinates; + const totalPoints = coordinates.length; + const step = Math.max(1, Math.floor(totalPoints / 6)); // ~5 punti intermedi + + const sampledPoints = []; + for (let i = step; i < totalPoints - step; i += step) { + sampledPoints.push(coordinates[i]); + } + + // Fai reverse geocoding per ogni punto + const cities = []; + const seenCities = new Set(); + + for (const point of sampledPoints.slice(0, 5)) { // Limita a 5 richieste + try { + const reverseUrl = `${NOMINATIM_BASE}/reverse?format=json&lat=${point[1]}&lon=${point[0]}&addressdetails=1&zoom=10`; + const data = await makeRequest(reverseUrl); + + if (data && data.address) { + const cityName = data.address.city || data.address.town || data.address.village; + if (cityName && !seenCities.has(cityName.toLowerCase())) { + seenCities.add(cityName.toLowerCase()); + cities.push({ + city: cityName, + province: data.address.county || data.address.province, + region: data.address.state, + coordinates: { + lat: point[1], + lng: point[0] + } + }); + } + } + + // Rate limiting - aspetta 1 secondo tra le richieste (requisito Nominatim) + await new Promise(resolve => setTimeout(resolve, 1100)); + } catch (e) { + console.log('Errore reverse per punto:', e.message); + } + } + + res.status(200).json({ + success: true, + data: cities + }); + + } catch (error) { + console.error('Errore suggerimento waypoints:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il suggerimento delle tappe', + error: error.message + }); + } +}; + +/** + * @desc Cerca città italiane (ottimizzato per Italia) + * @route GET /api/trasporti/geo/cities/it + */ +const searchItalianCities = async (req, res) => { + try { + const { q, limit = 10 } = req.query; + + if (!q || q.length < 2) { + return res.status(400).json({ + success: false, + message: 'Query deve essere almeno 2 caratteri' + }); + } + + // Usa Nominatim con filtro Italia + const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(q)}&countrycodes=it&limit=${limit}&addressdetails=1&featuretype=city`; + + const data = await makeRequest(url); + + const results = data + .filter(item => + item.address && + (item.address.city || item.address.town || item.address.village) + ) + .map(item => ({ + city: item.address.city || item.address.town || item.address.village, + province: item.address.county || item.address.province, + region: item.address.state, + postalCode: item.address.postcode, + coordinates: { + lat: parseFloat(item.lat), + lng: parseFloat(item.lon) + }, + displayName: `${item.address.city || item.address.town || item.address.village}, ${item.address.county || item.address.state}` + })); + + // Rimuovi duplicati + const unique = results.filter((v, i, a) => + a.findIndex(t => t.city.toLowerCase() === v.city.toLowerCase()) === i + ); + + res.status(200).json({ + success: true, + data: unique + }); + + } catch (error) { + console.error('Errore ricerca città italiane:', error); + res.status(500).json({ + success: false, + message: 'Errore durante la ricerca', + error: error.message + }); + } +}; + +/** + * @desc Calcola distanza e durata tra due punti + * @route GET /api/trasporti/geo/distance + */ +const getDistance = async (req, res) => { + try { + const { startLat, startLng, endLat, endLng } = req.query; + + if (!startLat || !startLng || !endLat || !endLng) { + return res.status(400).json({ + success: false, + message: 'Tutte le coordinate sono richieste' + }); + } + + const url = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=false`; + + const data = await makeRequest(url); + + if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) { + return res.status(404).json({ + success: false, + message: 'Impossibile calcolare la distanza' + }); + } + + const route = data.routes[0]; + + res.status(200).json({ + success: true, + data: { + distance: Math.round(route.distance / 1000 * 10) / 10, // km + duration: Math.round(route.duration / 60), // minuti + durationFormatted: formatDuration(route.duration) + } + }); + + } catch (error) { + console.error('Errore calcolo distanza:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il calcolo della distanza', + error: error.message + }); + } +}; + +// Helper per formattare durata +const formatDuration = (seconds) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.round((seconds % 3600) / 60); + + if (hours === 0) { + return `${minutes} min`; + } else if (minutes === 0) { + return `${hours} h`; + } else { + return `${hours} h ${minutes} min`; + } +}; + +module.exports = { + autocomplete, + geocode, + reverseGeocode, + getRoute, + suggestWaypoints, + searchItalianCities, + getDistance +}; \ No newline at end of file diff --git a/src/controllers/rideController.js b/src/controllers/rideController.js new file mode 100644 index 0000000..85f17aa --- /dev/null +++ b/src/controllers/rideController.js @@ -0,0 +1,1820 @@ +const Ride = require('../models/Ride'); +const User = require('../models/User'); +const RideRequest = require('../models/RideRequest'); + +/** + * @desc Crea un nuovo viaggio (offerta o richiesta) + * @route POST /api/trasporti/rides + * @access Private + */ +const createRide = async (req, res) => { + try { + const { idapp } = req.body; + const userId = req.user._id; + + const { + type, + departure, + destination, + waypoints, + dateTime, + flexibleTime, + flexibleMinutes, + recurrence, + passengers, + seatsNeeded, + vehicle, + preferences, + contribution, + 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"' + }); + } + + if (!departure || !departure.city || !departure.coordinates) { + return res.status(400).json({ + success: false, + 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' + }); + } + + if (!dateTime) { + return res.status(400).json({ + success: false, + message: 'Data e ora di partenza richieste' + }); + } + + // Validazioni specifiche per offerta + if (type === 'offer') { + if (!passengers || !passengers.max || passengers.max < 1) { + return res.status(400).json({ + success: false, + message: 'Numero massimo passeggeri richiesto per le offerte' + }); + } + } + + // Ordina waypoints se presenti + let orderedWaypoints = []; + if (waypoints && waypoints.length > 0) { + orderedWaypoints = waypoints.map((wp, index) => ({ + ...wp, + order: wp.order !== undefined ? wp.order : index + 1 + })); + } + + // Prepara i dati del viaggio + const rideData = { + idapp, + userId, + type, + departure, + destination, + waypoints: orderedWaypoints, + dateTime: new Date(dateTime), + flexibleTime: flexibleTime || false, + flexibleMinutes: flexibleMinutes || 30, + recurrence: recurrence || { type: 'once' }, + preferences: preferences || {}, + contribution: contribution || { contribTypes: [] }, + notes, + status: 'active' + }; + + // Aggiungi campi specifici per tipo + if (type === 'offer') { + rideData.passengers = { + available: passengers.max, + max: passengers.max + }; + rideData.vehicle = vehicle || {}; + } else { + rideData.seatsNeeded = seatsNeeded || 1; + } + + const ride = new Ride(rideData); + await ride.save(); + + // Aggiorna profilo utente come driver se è un'offerta + if (type === 'offer') { + await User.findByIdAndUpdate(userId, { + 'profile.driverProfile.isDriver': true + }); + } + + // Popola i dati per la risposta + await ride.populate('userId', 'username name surname profile.img profile.driverProfile.averageRating'); + + res.status(201).json({ + success: true, + message: type === 'offer' ? 'Offerta passaggio creata!' : 'Richiesta passaggio creata!', + 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 + }); + } +}; + +/** + * @desc Ottieni lista viaggi con filtri + * @route GET /api/trasporti/rides + * @access Public + */ +const getRides = async (req, res) => { + try { + const { idapp } = req.query; + + const { + type, + departureCity, + destinationCity, + date, + dateFrom, + dateTo, + passingThrough, + minSeats, + preferences, + contribTypes, + status, + page = 1, + limit = 20, + sortBy = 'dateTime', + sortOrder = 'asc' + } = req.query; + + // Costruisci query + const query = { + idapp, + status: status ? status : { $in: ['active', 'full'] }, + dateTime: { $gte: new Date() } + }; + + // Filtro tipo + if (type && ['offer', 'request'].includes(type)) { + query.type = type; + } + + // Filtro città partenza + if (departureCity) { + query['departure.city'] = new RegExp(departureCity, 'i'); + } + + // Filtro città destinazione + if (destinationCity) { + query['destination.city'] = new RegExp(destinationCity, 'i'); + } + + // Filtro data specifica + if (date) { + const startOfDay = new Date(date); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(date); + endOfDay.setHours(23, 59, 59, 999); + query.dateTime = { $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); + } + + // Filtro posti minimi disponibili + if (minSeats) { + query['passengers.available'] = { $gte: parseInt(minSeats) }; + } + + // Filtro preferenze + if (preferences) { + try { + const prefs = JSON.parse(preferences); + if (prefs.smoking !== undefined) { + query['preferences.smoking'] = prefs.smoking; + } + if (prefs.pets) { + query['preferences.pets'] = { $in: ['all', prefs.pets] }; + } + if (prefs.packages !== undefined) { + query['preferences.packages'] = prefs.packages; + } + } catch (e) { + // Ignora errori parsing preferenze + } + } + + // Paginazione + const skip = (parseInt(page) - 1) * parseInt(limit); + const sortOptions = {}; + sortOptions[sortBy] = sortOrder === 'desc' ? -1 : 1; + + // Query per città di passaggio (più complessa) + let rides; + if (passingThrough) { + const cityRegex = new RegExp(passingThrough, 'i'); + query.$or = [ + { 'departure.city': cityRegex }, + { 'destination.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('contribution.contribTypes.contribTypeId') + .sort(sortOptions) + .skip(skip) + .limit(parseInt(limit)); + + // Conta totale per paginazione + const total = await Ride.countDocuments(query); + + res.status(200).json({ + success: true, + data: rides, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + 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 + }); + } +}; + +/** + * @desc Ottieni singolo viaggio per ID + * @route GET /api/trasporti/rides/:id + * @access Public + */ +const getRideById = async (req, res) => { + try { + const { id } = req.params; + const { idapp } = req.query; + + const ride = await Ride.findOne({ _id: id, idapp }) + .populate('userId', 'username name surname profile.img profile.Biografia profile.driverProfile') + .populate('confirmedPassengers.userId', 'username name surname profile.img') + .populate('contribution.contribTypes.contribTypeId'); + + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato' + }); + } + + // Incrementa visualizzazioni + ride.views += 1; + await ride.save(); + + res.status(200).json({ + success: true, + 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 + }); + } +}; + +/** + * @desc Aggiorna un viaggio + * @route PUT /api/trasporti/rides/:id + * @access Private + */ +const updateRide = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user._id; + const { idapp, ...updateData } = req.body; + + // Trova il viaggio + const ride = await Ride.findOne({ _id: id, idapp }); + + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato' + }); + } + + // Verifica proprietario + if (ride.userId.toString() !== userId) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato a modificare questo viaggio' + }); + } + + // Non permettere modifiche se ci sono passeggeri confermati + if (ride.confirmedPassengers && ride.confirmedPassengers.length > 0) { + // Permetti solo alcune modifiche + const allowedFields = ['notes', 'preferences', 'flexibleTime', 'flexibleMinutes']; + const updateKeys = Object.keys(updateData); + 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.' + }); + } + } + + // Campi non modificabili + delete updateData.userId; + delete updateData.confirmedPassengers; + delete updateData.status; + + // Aggiorna waypoints con ordine + if (updateData.waypoints) { + updateData.waypoints = updateData.waypoints.map((wp, index) => ({ + ...wp, + order: wp.order !== undefined ? wp.order : index + 1 + })); + } + + // Aggiorna posti disponibili se cambiano i max + if (updateData.passengers && updateData.passengers.max) { + const bookedSeats = ride.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0); + 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'); + + res.status(200).json({ + success: true, + message: 'Viaggio aggiornato con successo', + 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 + }); + } +}; + +/** + * @desc Cancella un viaggio + * @route DELETE /api/trasporti/rides/:id + * @access Private + */ +const deleteRide = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user._id; + const { idapp, reason } = req.body; + + const ride = await Ride.findOne({ _id: id, idapp }); + + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato' + }); + } + + // Verifica proprietario + if (ride.userId.toString() !== userId) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato a cancellare questo viaggio' + }); + } + + // Se ci sono passeggeri confermati, notifica invece di eliminare + if (ride.confirmedPassengers && ride.confirmedPassengers.length > 0) { + // Aggiorna le richieste associate + await RideRequest.updateMany( + { rideId: id, status: 'accepted' }, + { + status: 'cancelled', + cancelledBy: 'driver', + cancellationReason: reason || 'Viaggio cancellato dal conducente', + cancelledAt: new Date() + } + ); + } + + // Soft delete - cambia stato invece di eliminare + ride.status = 'cancelled'; + ride.cancellationReason = reason; + ride.cancelledAt = new Date(); + await ride.save(); + + res.status(200).json({ + success: true, + 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 + }); + } +}; + +/** + * @desc Ottieni viaggi dell'utente corrente + * @route GET /api/trasporti/rides/my + * @access Private + */ +const getMyRides = async (req, res) => { + try { + const userId = req.user._id; + const { idapp, type, role, status, page = 1, limit = 20 } = req.query; + + let query = { idapp }; + + if (role === 'driver') { + // Viaggi dove sono il conducente + query.userId = userId; + } else if (role === 'passenger') { + // Viaggi dove sono passeggero confermato + query['confirmedPassengers.userId'] = userId; + } else { + // Tutti i miei viaggi (come driver o passenger) + query.$or = [ + { userId }, + { 'confirmedPassengers.userId': userId } + ]; + } + + if (type) { + query.type = type; + } + + if (status) { + query.status = status; + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + + const rides = await Ride.find(query) + .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 }) + .skip(skip) + .limit(parseInt(limit)); + + const total = await Ride.countDocuments(query); + + // 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); + + res.status(200).json({ + success: true, + data: { + all: rides, + upcoming, + past + }, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + 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 + }); + } +}; + +/** + * @desc Cerca viaggi con match intelligente + * @route GET /api/trasporti/rides/search + * @access Public + */ +const searchRides = async (req, res) => { + try { + const { idapp } = req.query; + + const { + from, + to, + date, + seats = 1, + type = 'offer', + radius = 20, // km di raggio per ricerca geografica + 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' + }); + } + + const query = { + idapp, + type, + status: 'active', + dateTime: { $gte: new Date() } + }; + + // Costruisci condizioni di ricerca + const orConditions = []; + + if (from) { + const fromRegex = new RegExp(from, 'i'); + orConditions.push({ 'departure.city': fromRegex }); + orConditions.push({ 'waypoints.location.city': fromRegex }); + } + + if (to) { + const toRegex = new RegExp(to, 'i'); + orConditions.push({ 'destination.city': toRegex }); + 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: [ + { 'destination.city': toRegex }, + { 'waypoints.location.city': toRegex } + ] + } + ]; + } else { + query.$or = orConditions; + } + } + + // Filtro data + if (date) { + const startOfDay = new Date(date); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(date); + endOfDay.setHours(23, 59, 59, 999); + query.dateTime = { $gte: startOfDay, $lte: endOfDay }; + } + + // Filtro posti + if (type === 'offer') { + 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') + .populate('contribution.contribTypes.contribTypeId') + .sort({ dateTime: 1 }) + .skip(skip) + .limit(parseInt(limit)); + + const total = await Ride.countDocuments(query); + + res.status(200).json({ + success: true, + data: rides, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + 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 + }); + } +}; + +/** + * @desc Completa un viaggio + * @route POST /api/trasporti/rides/:id/complete + * @access Private + */ +const completeRide = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user._id; + const { idapp } = req.body; + + const ride = await Ride.findOne({ _id: id, idapp }); + + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato' + }); + } + + if (ride.userId.toString() !== userId) { + return res.status(403).json({ + success: false, + message: 'Solo il conducente può completare il viaggio' + }); + } + + ride.status = 'completed'; + await ride.save(); + + // Aggiorna contatori utente + await User.findByIdAndUpdate(userId, { + $inc: { 'profile.driverProfile.ridesCompletedAsDriver': 1 } + }); + + // Aggiorna contatori passeggeri + 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() } + ); + + res.status(200).json({ + success: true, + message: 'Viaggio completato! Ora puoi lasciare un feedback.', + 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 + }); + } +}; + +/** + * @desc Ottieni statistiche viaggi per homepage widget + * @route GET /api/trasporti/rides/stats + * @access Private + */ +const getRidesStats = async (req, res) => { + try { + const { idapp } = req.query; + const userId = req.user._id; + + const now = new Date(); + + // Statistiche generali + const [ + totalActiveOffers, + totalActiveRequests, + todayRides, + myUpcomingAsDriver, + myUpcomingAsPassenger, + pendingRequests + ] = await Promise.all([ + // Offerte attive + Ride.countDocuments({ idapp, type: 'offer', status: 'active', dateTime: { $gte: now } }), + // Richieste attive + Ride.countDocuments({ idapp, type: 'request', status: 'active', dateTime: { $gte: now } }), + // Viaggi di oggi + Ride.countDocuments({ + idapp, + status: { $in: ['active', 'full'] }, + dateTime: { + $gte: new Date(now.setHours(0, 0, 0, 0)), + $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() } }), + // I miei prossimi come passeggero + Ride.countDocuments({ idapp, 'confirmedPassengers.userId': userId, status: { $in: ['active', 'full'] }, dateTime: { $gte: new Date() } }), + // Richieste in attesa per me + RideRequest.countDocuments({ idapp, driverId: userId, status: 'pending' }) + ]); + + // Ultimi viaggi attivi + const recentRides = await Ride.find({ + idapp, + status: 'active', + dateTime: { $gte: new Date() } + }) + .populate('userId', 'username name profile.img') + .sort({ createdAt: -1 }) + .limit(5); + + res.status(200).json({ + success: true, + data: { + stats: { + totalActiveOffers, + totalActiveRequests, + todayRides, + myUpcomingAsDriver, + myUpcomingAsPassenger, + pendingRequests + }, + 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 + }); + } +}; + +/** + * Get aggregated data for dashboard widgets + * GET /api/trasporti/widget/data + */ +const getWidgetData = async (req, res) => { + try { + const userId = req.user._id; + const now = new Date(); + const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + + // Parallel queries for efficiency + const [ + myActiveRides, + myUpcomingRides, + myPastRidesCount, + myActiveRequests, + pendingRequestsForMyRides, + recentFeedback, + unreadMessages + ] = await Promise.all([ + // Active rides as driver + Ride.countDocuments({ + driverId: userId, + status: 'active', + departureDate: { $gte: now } + }), + + // Upcoming rides as passenger (accepted requests) + RideRequest.countDocuments({ + userId: userId, + status: 'accepted', + 'rideInfo.departureDate': { $gte: now } + }), + + // Past rides in last 30 days + Ride.countDocuments({ + $or: [ + { driverId: userId }, + { 'passengers.userId': userId } + ], + status: 'completed', + departureDate: { $gte: thirtyDaysAgo, $lt: now } + }), + + // My active requests (pending/accepted) + RideRequest.countDocuments({ + userId: userId, + status: { $in: ['pending', 'accepted'] } + }), + + // Pending requests for my rides + RideRequest.countDocuments({ + driverId: userId, + status: 'pending' + }), + + // Recent feedback (last 7 days) + Feedback.find({ + toUserId: userId, + createdAt: { $gte: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) } + }) + .select('rating createdAt') + .sort({ createdAt: -1 }) + .limit(5) + .lean(), + + // Unread messages count + Chat.countDocuments({ + participants: userId, + 'lastMessage.senderId': { $ne: userId }, + [`unreadCount.${userId}`]: { $gt: 0 } + }) + ]); + + // Calculate average recent rating + 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); + const trendingRoutes = await Ride.aggregate([ + { + $match: { + createdAt: { $gte: sevenDaysAgo }, + status: { $in: ['active', 'completed'] } + } + }, + { + $group: { + _id: { + departure: '$departure.city', + destination: '$destination.city' + }, + count: { $sum: 1 } + } + }, + { + $sort: { count: -1 } + }, + { + $limit: 3 + }, + { + $project: { + _id: 0, + departure: '$_id.departure', + destination: '$_id.destination', + count: 1 + } + } + ]); + + // Get user stats + const userStats = await Feedback.aggregate([ + { + $match: { toUserId: userId } + }, + { + $group: { + _id: null, + averageRating: { $avg: '$rating' }, + totalFeedbacks: { $sum: 1 } + } + } + ]); + + const stats = userStats[0] || { averageRating: 0, totalFeedbacks: 0 }; + + // Prepare widget data + const widgetData = { + overview: { + activeRides: myActiveRides, + upcomingTrips: myUpcomingRides, + pastRidesThisMonth: myPastRidesCount, + activeRequests: myActiveRequests + }, + notifications: { + pendingRequests: pendingRequestsForMyRides, + unreadMessages: unreadMessages, + newFeedbacks: recentFeedback.length + }, + userStats: { + averageRating: stats.averageRating ? parseFloat(stats.averageRating.toFixed(1)) : null, + totalFeedbacks: stats.totalFeedbacks, + recentRating: avgRecentRating ? parseFloat(avgRecentRating.toFixed(1)) : null + }, + trending: { + 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 + } + }; + + // Optional: Check if user has a vehicle (if Vehicle model exists) + try { + const Vehicle = require('../models/Vehicle'); + const hasVehicle = await Vehicle.exists({ userId: userId }); + widgetData.quickActions.hasVehicle = !!hasVehicle; + } catch (err) { + // Vehicle model doesn't exist or error - skip + } + + return res.status(200).json({ + success: true, + 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/trasporti/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 + }); + } +}; + +/** + * Get city suggestions for autocomplete + * GET /api/trasporti/cities/suggestions?q=query + */ +const getCitySuggestions = async (req, res) => { + try { + const { q } = req.query; + + // Validate query + if (!q || q.trim().length < 2) { + return res.status(200).json({ + success: true, + data: { + suggestions: [], + message: 'Inserisci almeno 2 caratteri per la ricerca' + } + }); + } + + const searchQuery = q.trim(); + const regex = new RegExp(`^${searchQuery}`, 'i'); // Case insensitive, starts with + + // Get popular cities from completed rides + const popularCities = await Ride.aggregate([ + { + $match: { + status: { $in: ['active', 'completed'] }, + $or: [ + { 'departure.city': regex }, + { 'destination.city': regex } + ] + } + }, + { + $facet: { + departures: [ + { + $match: { 'departure.city': regex } + }, + { + $group: { + _id: { + city: '$departure.city', + region: '$departure.region', + country: '$departure.country' + }, + count: { $sum: 1 } + } + } + ], + destinations: [ + { + $match: { 'destination.city': regex } + }, + { + $group: { + _id: { + city: '$destination.city', + region: '$destination.region', + country: '$destination.country' + }, + count: { $sum: 1 } + } + } + ] + } + }, + { + $project: { + combined: { + $concatArrays: ['$departures', '$destinations'] + } + } + }, + { + $unwind: '$combined' + }, + { + $group: { + _id: { + city: '$combined._id.city', + region: '$combined._id.region', + country: '$combined._id.country' + }, + totalCount: { $sum: '$combined.count' } + } + }, + { + $sort: { totalCount: -1 } + }, + { + $limit: 10 + }, + { + $project: { + _id: 0, + city: '$_id.city', + region: '$_id.region', + country: '$_id.country', + popularity: '$totalCount' + } + } + ]); + + // Italian cities database (fallback/supplement) + const italianCities = [ + // Major cities + { city: 'Roma', region: 'Lazio', country: 'Italia', type: 'major' }, + { city: 'Milano', region: 'Lombardia', country: 'Italia', type: 'major' }, + { city: 'Napoli', region: 'Campania', country: 'Italia', type: 'major' }, + { city: 'Torino', region: 'Piemonte', country: 'Italia', type: 'major' }, + { city: 'Palermo', region: 'Sicilia', country: 'Italia', type: 'major' }, + { city: 'Genova', region: 'Liguria', country: 'Italia', type: 'major' }, + { city: 'Bologna', region: 'Emilia-Romagna', country: 'Italia', type: 'major' }, + { city: 'Firenze', region: 'Toscana', country: 'Italia', type: 'major' }, + { city: 'Bari', region: 'Puglia', country: 'Italia', type: 'major' }, + { city: 'Catania', region: 'Sicilia', country: 'Italia', type: 'major' }, + { city: 'Venezia', region: 'Veneto', country: 'Italia', type: 'major' }, + { city: 'Verona', region: 'Veneto', country: 'Italia', type: 'major' }, + { city: 'Messina', region: 'Sicilia', country: 'Italia', type: 'major' }, + { city: 'Padova', region: 'Veneto', country: 'Italia', type: 'major' }, + { city: 'Trieste', region: 'Friuli-Venezia Giulia', country: 'Italia', type: 'major' }, + { city: 'Brescia', region: 'Lombardia', country: 'Italia', type: 'major' }, + { city: 'Parma', region: 'Emilia-Romagna', country: 'Italia', type: 'major' }, + { 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: '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: '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' }, + { city: 'Como', region: 'Lombardia', country: 'Italia', type: 'city' }, + { city: 'Cremona', region: 'Lombardia', country: 'Italia', type: 'city' }, + { city: 'Ferrara', region: 'Emilia-Romagna', country: 'Italia', type: 'city' }, + { city: 'Forlì', region: 'Emilia-Romagna', country: 'Italia', type: 'city' }, + { city: 'Latina', region: 'Lazio', country: 'Italia', type: 'city' }, + { city: 'Lecce', region: 'Puglia', country: 'Italia', type: 'city' }, + { city: 'Livorno', region: 'Toscana', country: 'Italia', type: 'city' }, + { city: 'Lucca', region: 'Toscana', country: 'Italia', type: 'city' }, + { city: 'Mantova', region: 'Lombardia', country: 'Italia', type: 'city' }, + { city: 'Monza', region: 'Lombardia', country: 'Italia', type: 'city' }, + { city: 'Novara', region: 'Piemonte', country: 'Italia', type: 'city' }, + { city: 'Pavia', region: 'Lombardia', country: 'Italia', type: 'city' }, + { city: 'Pesaro', region: 'Marche', country: 'Italia', type: 'city' }, + { city: 'Pescara', region: 'Abruzzo', country: 'Italia', type: 'city' }, + { city: 'Piacenza', region: 'Emilia-Romagna', country: 'Italia', type: 'city' }, + { city: 'Pisa', region: 'Toscana', country: 'Italia', type: 'city' }, + { city: 'Ravenna', region: 'Emilia-Romagna', country: 'Italia', type: 'city' }, + { city: 'Reggio Emilia', region: 'Emilia-Romagna', country: 'Italia', type: 'city' }, + { city: 'Rimini', region: 'Emilia-Romagna', country: 'Italia', type: 'city' }, + { city: 'Salerno', region: 'Campania', country: 'Italia', type: 'city' }, + { city: 'Sassari', region: 'Sardegna', country: 'Italia', type: 'city' }, + { city: 'Siena', region: 'Toscana', country: 'Italia', type: 'city' }, + { city: 'Siracusa', region: 'Sicilia', country: 'Italia', type: 'city' }, + { city: 'Taranto', region: 'Puglia', country: 'Italia', type: 'city' }, + { city: 'Terni', region: 'Umbria', country: 'Italia', type: 'city' }, + { 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' } + ]; + + // Filter Italian cities by query + const matchingItalianCities = italianCities + .filter(c => c.city.toLowerCase().startsWith(searchQuery.toLowerCase())) + .map(c => ({ + ...c, + popularity: 0, + source: 'database' + })); + + // Merge results - prioritize cities from actual rides + const cityMap = new Map(); + + // Add popular cities from rides first + popularCities.forEach(city => { + const key = `${city.city}-${city.region}`.toLowerCase(); + cityMap.set(key, { + ...city, + source: 'rides', + verified: true + }); + }); + + // Add Italian cities that aren't already in the map + matchingItalianCities.forEach(city => { + const key = `${city.city}-${city.region}`.toLowerCase(); + if (!cityMap.has(key)) { + cityMap.set(key, city); + } + }); + + // Convert to array and sort + let suggestions = Array.from(cityMap.values()); + + // Sort by: verified rides first, then by type (major > capital > city), then alphabetically + suggestions.sort((a, b) => { + // Prioritize cities from actual rides + if (a.source === 'rides' && b.source !== 'rides') return -1; + if (a.source !== 'rides' && b.source === 'rides') return 1; + + // Then by popularity + if (a.popularity !== b.popularity) { + return b.popularity - a.popularity; + } + + // Then by type + const typeOrder = { major: 0, capital: 1, city: 2 }; + const aType = typeOrder[a.type] ?? 3; + const bType = typeOrder[b.type] ?? 3; + if (aType !== bType) return aType - bType; + + // Finally alphabetically + return a.city.localeCompare(b.city, 'it'); + }); + + // Limit to 10 suggestions + suggestions = suggestions.slice(0, 10); + + // Format suggestions + 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 + })); + + return res.status(200).json({ + success: true, + data: { + query: searchQuery, + suggestions: formattedSuggestions, + 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 + }); + } +}; + +/** + * Get recent cities from user's trip history + * GET /api/trasporti/cities/recent + */ +const getRecentCities = async (req, res) => { + try { + const userId = req.user._id; + + // Get last 2 unique departure cities + const recentDepartures = await Ride.aggregate([ + { + $match: { + $or: [ + { driverId: userId }, + { 'passengers.userId': userId } + ], + status: 'completed' + } + }, + { + $sort: { departureDate: -1 } + }, + { + $group: { + _id: { + city: '$departure.city', + region: '$departure.region', + country: '$departure.country' + }, + lat: { $first: '$departure.lat' }, + lng: { $first: '$departure.lng' }, + lastUsed: { $first: '$departureDate' } + } + }, + { + $limit: 2 + }, + { + $project: { + _id: 0, + city: '$_id.city', + region: '$_id.region', + country: '$_id.country', + lat: 1, + lng: 1, + lastUsed: 1, + type: { $literal: 'departure' } + } + } + ]); + + // Get last 2 unique destination cities + const recentDestinations = await Ride.aggregate([ + { + $match: { + $or: [ + { driverId: userId }, + { 'passengers.userId': userId } + ], + status: 'completed' + } + }, + { + $sort: { departureDate: -1 } + }, + { + $group: { + _id: { + city: '$destination.city', + region: '$destination.region', + country: '$destination.country' + }, + lat: { $first: '$destination.lat' }, + lng: { $first: '$destination.lng' }, + lastUsed: { $first: '$departureDate' } + } + }, + { + $limit: 2 + }, + { + $project: { + _id: 0, + city: '$_id.city', + region: '$_id.region', + country: '$_id.country', + lat: 1, + lng: 1, + lastUsed: 1, + type: { $literal: 'destination' } + } + } + ]); + + // Combine and remove duplicates + const allRecent = [...recentDepartures, ...recentDestinations]; + const uniqueCities = new Map(); + + allRecent.forEach(city => { + const key = `${city.city}-${city.region}`; + if (!uniqueCities.has(key)) { + uniqueCities.set(key, city); + } + }); + + const cities = Array.from(uniqueCities.values()) + .sort((a, b) => new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime()) + .slice(0, 2); + + return res.status(200).json({ + success: true, + data: { + 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 + }); + } +}; + +module.exports = { + createRide, + getRides, + getRideById, + updateRide, + deleteRide, + getMyRides, + searchRides, + completeRide, + getRidesStats, + getWidgetData, + getStatsSummary, + getCitySuggestions, + getRecentCities, +}; diff --git a/src/controllers/rideRequestController.js b/src/controllers/rideRequestController.js new file mode 100644 index 0000000..e354b29 --- /dev/null +++ b/src/controllers/rideRequestController.js @@ -0,0 +1,616 @@ +const RideRequest = require('../models/RideRequest'); +const Ride = require('../models/Ride'); +const Chat = require('../models/Chat'); +const Message = require('../models/Message'); + +/** + * @desc Crea una richiesta di passaggio + * @route POST /api/trasporti/requests + * @access Private + */ +const createRequest = async (req, res) => { + try { + const { + idapp, + rideId, + message, + pickupPoint, + dropoffPoint, + useOriginalRoute, + seatsRequested, + hasLuggage, + luggageSize, + hasPackages, + packageDescription, + hasPets, + petType, + petSize, + specialNeeds, + contribution, + } = req.body; + + const passengerId = req.user._id; + + // Validazione + if (!idapp || !rideId) { + return res.status(400).json({ + success: false, + message: 'idapp e rideId sono obbligatori', + }); + } + + // Verifica che il ride esista + const ride = await Ride.findById(rideId); + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato', + }); + } + + // Verifica che non sia il proprio viaggio + if (ride.userId.toString() === passengerId.toString()) { + return res.status(400).json({ + success: false, + message: 'Non puoi richiedere un passaggio per il tuo stesso viaggio', + }); + } + + // Verifica che non ci sia già una richiesta pendente/accettata + 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', + }); + } + + // Verifica disponibilità posti + const seats = seatsRequested || 1; + if (ride.type === 'offer' && ride.passengers.available < seats) { + return res.status(400).json({ + success: false, + message: `Posti insufficienti. Disponibili: ${ride.passengers.available}`, + }); + } + + // Crea la richiesta + const requestData = { + idapp, + rideId, + passengerId, + driverId: ride.userId, + seatsRequested: seats, + useOriginalRoute: useOriginalRoute !== false, + }; + + if (message) requestData.message = message; + if (pickupPoint) requestData.pickupPoint = pickupPoint; + if (dropoffPoint) requestData.dropoffPoint = dropoffPoint; + if (hasLuggage !== undefined) { + requestData.hasLuggage = hasLuggage; + requestData.luggageSize = luggageSize || 'small'; + } + if (hasPackages !== undefined) { + requestData.hasPackages = hasPackages; + requestData.packageDescription = packageDescription; + } + if (hasPets !== undefined) { + requestData.hasPets = hasPets; + requestData.petType = petType; + requestData.petSize = petSize; + } + if (specialNeeds) requestData.specialNeeds = specialNeeds; + if (contribution) requestData.contribution = contribution; + + const rideRequest = new RideRequest(requestData); + await rideRequest.save(); + + // Popola i dati per la risposta + await rideRequest.populate('passengerId', 'username name surname profile.img'); + await rideRequest.populate('rideId', 'departure destination dateTime'); + + // Crea o recupera la chat tra passeggero e conducente + const chat = await Chat.findOrCreateDirect(idapp, passengerId, ride.userId, rideId); + + // Invia messaggio automatico nella chat + if (message) { + const chatMessage = new Message({ + idapp, + chatId: chat._id, + senderId: passengerId, + text: message, + type: 'ride_request', + metadata: { + rideId, + rideRequestId: rideRequest._id, + }, + }); + await chatMessage.save(); + } + + // TODO: Inviare notifica push al conducente + + res.status(201).json({ + success: true, + message: 'Richiesta di passaggio inviata!', + data: rideRequest, + chatId: chat._id, + }); + } catch (error) { + console.error('Errore creazione richiesta:', error); + 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/trasporti/requests/ride/:rideId + * @access Private + */ +const getRequestsForRide = async (req, res) => { + try { + const { rideId } = req.params; + const { idapp, status } = req.query; + const userId = req.user._id; + + // Verifica che l'utente sia il proprietario del ride + const ride = await Ride.findById(rideId); + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio non trovato', + }); + } + + if (ride.userId.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato a vedere le richieste di questo viaggio', + }); + } + + const query = { idapp, rideId }; + if (status) { + query.status = status; + } + + const requests = await RideRequest.find(query) + .populate( + 'passengerId', + 'username name surname profile.img profile.Cell profile.driverProfile.averageRating profile.driverProfile.ridesCompletedAsPassenger' + ) + .sort({ createdAt: -1 }); + + res.json({ + success: true, + data: requests, + }); + } catch (error) { + console.error('Errore recupero richieste:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero delle richieste', + error: error.message, + }); + } +}; + +/** + * @desc Ottieni le mie richieste (come passeggero) + * @route GET /api/trasporti/requests/my + * @access Private + */ +const getMyRequests = async (req, res) => { + try { + const userId = req.user._id; + const { idapp, status, page = 1, limit = 20 } = req.query; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio', + }); + } + + const query = { idapp, passengerId: userId }; + if (status) { + query.status = status; + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + + const [requests, total] = await Promise.all([ + RideRequest.find(query) + .populate({ + path: 'rideId', + populate: { + path: 'userId', + select: 'username name surname profile.img profile.Cell profile.driverProfile.averageRating', + }, + }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)), + RideRequest.countDocuments(query), + ]); + + res.json({ + success: true, + data: requests, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / parseInt(limit)), + }, + }); + } catch (error) { + console.error('Errore recupero mie richieste:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero delle tue richieste', + error: error.message, + }); + } +}; + +/** + * @desc Ottieni richieste pendenti (per il conducente) + * @route GET /api/trasporti/requests/pending + * @access Private + */ +const getPendingRequests = async (req, res) => { + try { + const userId = req.user._id; + const { idapp } = req.query; + + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio', + }); + } + + const requests = await RideRequest.find({ + idapp, + driverId: userId, + status: 'pending', + }) + .populate('passengerId', 'username name surname profile.img profile.driverProfile.averageRating') + .populate('rideId', 'departure destination dateTime passengers') + .sort({ createdAt: -1 }); + + res.json({ + success: true, + data: requests, + total: requests.length, + }); + } catch (error) { + console.error('Errore recupero richieste pendenti:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero delle richieste pendenti', + error: error.message, + }); + } +}; + +/** + * @desc Accetta una richiesta di passaggio + * @route PUT /api/trasporti/requests/:id/accept + * @access Private (solo conducente) + */ +const acceptRequest = async (req, res) => { + try { + const { id } = req.params; + const { responseMessage, idapp } = req.body; + const userId = req.user._id; + + const request = await RideRequest.findById(id).populate('rideId'); + + if (!request) { + return res.status(404).json({ + success: false, + message: 'Richiesta non trovata', + }); + } + + // Verifica che sia il conducente + if (request.driverId.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato ad accettare questa richiesta', + }); + } + + // Verifica che sia ancora pendente + if (request.status !== 'pending') { + return res.status(400).json({ + success: false, + message: `La richiesta è già stata ${request.status}`, + }); + } + + // Verifica disponibilità posti + const ride = request.rideId; + if (ride.passengers.available < request.seatsRequested) { + return res.status(400).json({ + success: false, + message: 'Posti non più disponibili', + }); + } + + // Accetta la richiesta + request.status = 'accepted'; + request.responseMessage = responseMessage || ''; + request.respondedAt = new Date(); + await request.save(); + + // Aggiorna il ride con il passeggero + ride.confirmedPassengers.push({ + userId: request.passengerId, + seats: request.seatsRequested, + pickupPoint: request.pickupPoint || ride.departure, + dropoffPoint: request.dropoffPoint || ride.destination, + confirmedAt: new Date(), + }); + await ride.updateAvailableSeats(); + + // Invia messaggio nella chat + const chat = await Chat.findOrCreateDirect(idapp, userId, request.passengerId, ride._id); + const chatMessage = new Message({ + idapp, + chatId: chat._id, + senderId: userId, + text: responseMessage || '✅ Richiesta accettata! Ci vediamo al punto di partenza.', + type: 'ride_accepted', + metadata: { + rideId: ride._id, + rideRequestId: request._id, + }, + }); + await chatMessage.save(); + + // TODO: Inviare notifica push al passeggero + + await request.populate('passengerId', 'username name surname profile.img'); + await request.populate('rideId', 'departure destination dateTime'); + + res.json({ + success: true, + message: 'Richiesta accettata!', + data: request, + }); + } catch (error) { + console.error('Errore accettazione richiesta:', error); + res.status(500).json({ + success: false, + message: "Errore nell'accettazione della richiesta", + error: error.message, + }); + } +}; + +/** + * @desc Rifiuta una richiesta di passaggio + * @route PUT /api/trasporti/requests/:id/reject + * @access Private (solo conducente) + */ +const rejectRequest = async (req, res) => { + try { + const { id } = req.params; + const { responseMessage, idapp } = req.body; + const userId = req.user._id; + + const request = await RideRequest.findById(id); + + if (!request) { + return res.status(404).json({ + success: false, + message: 'Richiesta non trovata', + }); + } + + // Verifica che sia il conducente + if (request.driverId.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato', + }); + } + + // Verifica che sia ancora pendente + if (request.status !== 'pending') { + return res.status(400).json({ + success: false, + message: `La richiesta è già stata ${request.status}`, + }); + } + + // Rifiuta la richiesta + request.status = 'rejected'; + request.responseMessage = responseMessage || ''; + request.respondedAt = new Date(); + await request.save(); + + // Invia messaggio nella chat + const chat = await Chat.findOrCreateDirect(idapp, userId, request.passengerId, request.rideId); + const chatMessage = new Message({ + idapp, + chatId: chat._id, + senderId: userId, + text: responseMessage || '❌ Mi dispiace, non posso accettare questa richiesta.', + type: 'ride_rejected', + metadata: { + rideId: request.rideId, + rideRequestId: request._id, + }, + }); + await chatMessage.save(); + + // TODO: Inviare notifica push al passeggero + + res.json({ + success: true, + message: 'Richiesta rifiutata', + data: request, + }); + } catch (error) { + console.error('Errore rifiuto richiesta:', error); + res.status(500).json({ + success: false, + message: 'Errore nel rifiuto della richiesta', + error: error.message, + }); + } +}; + +/** + * @desc Cancella una richiesta (dal passeggero) + * @route PUT /api/trasporti/requests/:id/cancel + * @access Private + */ +const cancelRequest = async (req, res) => { + try { + const { id } = req.params; + const { reason } = req.body; + const userId = req.user._id; + + const request = await RideRequest.findById(id); + + if (!request) { + return res.status(404).json({ + success: false, + message: 'Richiesta non trovata', + }); + } + + // Verifica che sia il passeggero o il conducente + const isPassenger = request.passengerId.toString() === userId.toString(); + const isDriver = request.driverId.toString() === userId.toString(); + + if (!isPassenger && !isDriver) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato', + }); + } + + // Verifica che possa essere cancellata + if (!['pending', 'accepted'].includes(request.status)) { + return res.status(400).json({ + success: false, + message: 'Questa richiesta non può essere cancellata', + }); + } + + // Se era accettata, rimuovi il passeggero dal ride + if (request.status === 'accepted') { + const ride = await Ride.findById(request.rideId); + if (ride) { + ride.confirmedPassengers = ride.confirmedPassengers.filter( + (p) => p.userId.toString() !== request.passengerId.toString() + ); + await ride.updateAvailableSeats(); + } + } + + request.status = 'cancelled'; + request.cancelledBy = isPassenger ? 'passenger' : 'driver'; + request.cancellationReason = reason || ''; + request.cancelledAt = new Date(); + await request.save(); + + // TODO: Inviare notifica all'altra parte + + res.json({ + success: true, + message: 'Richiesta cancellata', + data: request, + }); + } catch (error) { + console.error('Errore cancellazione richiesta:', error); + res.status(500).json({ + success: false, + message: 'Errore nella cancellazione', + error: error.message, + }); + } +}; + +/** + * @desc Ottieni una singola richiesta + * @route GET /api/trasporti/requests/:id + * @access Private + */ +const getRequestById = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user._id; + + const request = await RideRequest.findById(id) + .populate('passengerId', 'username name surname profile.img profile.Cell profile.driverProfile') + .populate('driverId', 'username name surname profile.img profile.Cell profile.driverProfile') + .populate({ + path: 'rideId', + populate: { + path: 'userId', + select: 'username name surname profile.img', + }, + }) + .populate('contribution.contribTypeId', 'label icon color'); + + if (!request) { + return res.status(404).json({ + success: false, + message: 'Richiesta non trovata', + }); + } + + // Verifica che l'utente sia coinvolto + const isPassenger = request.passengerId._id.toString() === userId.toString(); + const isDriver = request.driverId._id.toString() === userId.toString(); + + if (!isPassenger && !isDriver) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato a vedere questa richiesta', + }); + } + + res.json({ + success: true, + data: request, + }); + } catch (error) { + console.error('Errore recupero richiesta:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero della richiesta', + error: error.message, + }); + } +}; + +module.exports = { + createRequest, + getRequestsForRide, + getMyRequests, + getPendingRequests, + acceptRequest, + rejectRequest, + cancelRequest, + getRequestById, + getReceivedRequests: getPendingRequests, + getSentRequests: getMyRequests, +}; diff --git a/src/models/Chat.js b/src/models/Chat.js new file mode 100644 index 0000000..9f0dff6 --- /dev/null +++ b/src/models/Chat.js @@ -0,0 +1,204 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const LastMessageSchema = new Schema({ + text: { + type: String, + trim: true + }, + senderId: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + timestamp: { + type: Date, + default: Date.now + }, + type: { + type: String, + enum: ['text', 'ride_share', 'location', 'image', 'system'], + default: 'text' + } +}, { _id: false }); + +const ChatSchema = new Schema({ + idapp: { + type: String, + required: true, + index: true + }, + participants: [{ + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }], + rideId: { + type: Schema.Types.ObjectId, + ref: 'Ride', + index: true + // Opzionale: chat collegata a un viaggio specifico + }, + rideRequestId: { + type: Schema.Types.ObjectId, + ref: 'RideRequest' + }, + type: { + type: String, + enum: ['direct', 'ride', 'group'], + default: 'direct' + }, + title: { + type: String, + trim: true + // Solo per chat di gruppo + }, + lastMessage: { + type: LastMessageSchema + }, + unreadCount: { + type: Map, + of: Number, + default: new Map() + // { odIdUtente: numeroMessaggiNonLetti } + }, + isActive: { + type: Boolean, + default: true + }, + mutedBy: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }], + blockedBy: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }], + metadata: { + type: Schema.Types.Mixed + } +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}); + +// Indici +ChatSchema.index({ participants: 1 }); +ChatSchema.index({ idapp: 1, participants: 1 }); +ChatSchema.index({ idapp: 1, updatedAt: -1 }); + +// Virtual per contare messaggi non letti totali +ChatSchema.virtual('totalUnread').get(function() { + if (!this.unreadCount) return 0; + let total = 0; + this.unreadCount.forEach(count => { + total += count; + }); + return total; +}); + +// Metodo per ottenere unread count per un utente specifico +ChatSchema.methods.getUnreadForUser = function(userId) { + if (!this.unreadCount) return 0; + return this.unreadCount.get(userId.toString()) || 0; +}; + +// Metodo per incrementare unread count +ChatSchema.methods.incrementUnread = function(excludeUserId) { + this.participants.forEach(participantId => { + const id = participantId.toString(); + if (id !== excludeUserId.toString()) { + const current = this.unreadCount.get(id) || 0; + this.unreadCount.set(id, current + 1); + } + }); + return this.save(); +}; + +// Metodo per resettare unread count per un utente +ChatSchema.methods.markAsRead = function(userId) { + this.unreadCount.set(userId.toString(), 0); + return this.save(); +}; + +// Metodo per aggiornare ultimo messaggio +ChatSchema.methods.updateLastMessage = function(message) { + this.lastMessage = { + text: message.text, + senderId: message.senderId, + timestamp: message.createdAt || new Date(), + type: message.type || 'text' + }; + return this.save(); +}; + +// Metodo per verificare se un utente è partecipante +ChatSchema.methods.hasParticipant = function(userId) { + return this.participants.some( + p => p.toString() === userId.toString() + ); +}; + +// Metodo per verificare se la chat è bloccata per un utente +ChatSchema.methods.isBlockedFor = function(userId) { + return this.blockedBy.some( + id => id.toString() === userId.toString() + ); +}; + +// Metodo statico per trovare o creare una chat diretta +ChatSchema.statics.findOrCreateDirect = async function(idapp, userId1, userId2, rideId = null) { + // Cerca chat esistente tra i due utenti + let chat = await this.findOne({ + idapp, + type: 'direct', + participants: { $all: [userId1, userId2], $size: 2 } + }); + + if (!chat) { + chat = new this({ + idapp, + type: 'direct', + participants: [userId1, userId2], + rideId, + unreadCount: new Map() + }); + await chat.save(); + } else if (rideId && !chat.rideId) { + // Aggiorna con rideId se fornito + chat.rideId = rideId; + await chat.save(); + } + + return chat; +}; + +// Metodo statico per ottenere tutte le chat di un utente +ChatSchema.statics.getChatsForUser = function(idapp, userId) { + return this.find({ + idapp, + participants: userId, + isActive: true, + blockedBy: { $ne: userId } + }) + .populate('participants', 'username name surname profile.avatar') + .populate('rideId', 'departure destination dateTime') + .sort({ updatedAt: -1 }); +}; + +// Metodo statico per creare chat di gruppo per un viaggio +ChatSchema.statics.createRideGroupChat = async function(idapp, rideId, title, participantIds) { + const chat = new this({ + idapp, + type: 'group', + rideId, + title, + participants: participantIds, + unreadCount: new Map() + }); + return chat.save(); +}; + +const Chat = mongoose.model('Chat', ChatSchema); + +module.exports = Chat; \ No newline at end of file diff --git a/src/models/Feedback.js b/src/models/Feedback.js new file mode 100644 index 0000000..b35c78a --- /dev/null +++ b/src/models/Feedback.js @@ -0,0 +1,357 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const FeedbackCategoriesSchema = new Schema( + { + punctuality: { + type: Number, + min: 1, + max: 5, + }, + cleanliness: { + type: Number, + min: 1, + max: 5, + }, + communication: { + type: Number, + min: 1, + max: 5, + }, + driving: { + type: Number, + min: 1, + max: 5, + // Solo per feedback a conducenti + }, + respect: { + type: Number, + min: 1, + max: 5, + }, + reliability: { + type: Number, + min: 1, + max: 5, + }, + }, + { _id: false } +); + +const FeedbackSchema = new Schema( + { + idapp: { + type: String, + required: true, + index: true, + }, + rideId: { + type: Schema.Types.ObjectId, + ref: 'Ride', + required: true, + index: true, + }, + rideRequestId: { + type: Schema.Types.ObjectId, + ref: 'RideRequest', + }, + fromUserId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true, + }, + toUserId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true, + }, + role: { + type: String, + enum: ['driver', 'passenger'], + required: true, + // Il ruolo dell'utente che RICEVE il feedback + // 'driver' = sto valutando il conducente + // 'passenger' = sto valutando il passeggero + }, + rating: { + type: Number, + required: true, + min: 1, + max: 5, + }, + categories: { + type: FeedbackCategoriesSchema, + }, + comment: { + type: String, + trim: true, + maxlength: 1000, + }, + pros: [ + { + type: String, + trim: true, + }, + ], + cons: [ + { + type: String, + trim: true, + }, + ], + tags: [ + { + type: String, + enum: [ + 'puntuale', + 'gentile', + 'auto_pulita', + 'guida_sicura', + 'buona_conversazione', + 'silenzioso', + 'flessibile', + 'rispettoso', + 'affidabile', + 'consigliato', + // Tag negativi + 'in_ritardo', + 'scortese', + 'guida_pericolosa', + 'auto_sporca', + 'non_rispettoso', + ], + }, + ], + isPublic: { + type: Boolean, + default: true, + }, + isVerified: { + type: Boolean, + default: false, + // Feedback verificato (viaggio effettivamente completato) + }, + response: { + text: { + type: String, + trim: true, + maxlength: 500, + }, + respondedAt: { + type: Date, + }, + }, + helpful: { + count: { + type: Number, + default: 0, + }, + users: [ + { + type: Schema.Types.ObjectId, + ref: 'User', + }, + ], + }, + reported: { + isReported: { + type: Boolean, + default: false, + }, + reason: String, + reportedBy: { + type: Schema.Types.ObjectId, + ref: 'User', + }, + reportedAt: Date, + }, + }, + { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true }, + } +); + +// Indici composti +FeedbackSchema.index({ toUserId: 1, role: 1 }); +FeedbackSchema.index({ rideId: 1, fromUserId: 1 }); +FeedbackSchema.index({ idapp: 1, toUserId: 1 }); + +// Vincolo: un utente può lasciare un solo feedback per viaggio verso un altro utente +FeedbackSchema.index({ rideId: 1, fromUserId: 1, toUserId: 1 }, { unique: true }); + +// Virtual per calcolare media categorie +FeedbackSchema.virtual('categoryAverage').get(function () { + if (!this.categories) return null; + const cats = this.categories.toObject ? this.categories.toObject() : this.categories; + const values = Object.values(cats).filter((v) => typeof v === 'number'); + if (values.length === 0) return null; + return values.reduce((a, b) => a + b, 0) / values.length; +}); + +// Metodo per aggiungere risposta +FeedbackSchema.methods.addResponse = function (text) { + this.response = { + text, + respondedAt: new Date(), + }; + return this.save(); +}; + +// Metodo per segnare come utile +FeedbackSchema.methods.markAsHelpful = function (userId) { + if (!this.helpful.users.includes(userId)) { + this.helpful.users.push(userId); + this.helpful.count = this.helpful.users.length; + return this.save(); + } + return Promise.resolve(this); +}; + +// Metodo per segnalare feedback +FeedbackSchema.methods.report = function (userId, reason) { + this.reported = { + isReported: true, + reason, + reportedBy: userId, + reportedAt: new Date(), + }; + return this.save(); +}; + +// Metodo statico per ottenere feedback di un utente +FeedbackSchema.statics.getForUser = function (idapp, userId, options = {}) { + const query = { + idapp, + toUserId: userId, + isPublic: true, + }; + + if (options.role) { + query.role = options.role; + } + + return this.find(query) + .populate('fromUserId', 'username name surname profile.avatar') + .populate('rideId', 'departure destination dateTime') + .sort({ createdAt: -1 }) + .limit(options.limit || 20); +}; + +// Metodo statico per calcolare statistiche +FeedbackSchema.statics.getStatsForUser = async function (idapp, userId) { + const stats = await this.aggregate([ + { + $match: { + idapp, + toUserId: new mongoose.Types.ObjectId(userId), + }, + }, + { + $group: { + _id: '$role', + averageRating: { $avg: '$rating' }, + totalFeedbacks: { $sum: 1 }, + avgPunctuality: { $avg: '$categories.punctuality' }, + avgCleanliness: { $avg: '$categories.cleanliness' }, + avgCommunication: { $avg: '$categories.communication' }, + avgDriving: { $avg: '$categories.driving' }, + avgRespect: { $avg: '$categories.respect' }, + avgReliability: { $avg: '$categories.reliability' }, + }, + }, + ]); + + // Trasforma in oggetto più leggibile + const result = { + asDriver: null, + asPassenger: null, + overall: { + averageRating: 0, + totalFeedbacks: 0, + }, + }; + + stats.forEach((stat) => { + const data = { + averageRating: Math.round(stat.averageRating * 10) / 10, + totalFeedbacks: stat.totalFeedbacks, + categories: { + punctuality: stat.avgPunctuality ? Math.round(stat.avgPunctuality * 10) / 10 : null, + cleanliness: stat.avgCleanliness ? Math.round(stat.avgCleanliness * 10) / 10 : null, + communication: stat.avgCommunication ? Math.round(stat.avgCommunication * 10) / 10 : null, + driving: stat.avgDriving ? Math.round(stat.avgDriving * 10) / 10 : null, + respect: stat.avgRespect ? Math.round(stat.avgRespect * 10) / 10 : null, + reliability: stat.avgReliability ? Math.round(stat.avgReliability * 10) / 10 : null, + }, + }; + + if (stat._id === 'driver') { + result.asDriver = data; + } else if (stat._id === 'passenger') { + result.asPassenger = data; + } + }); + + // Calcola overall + const allStats = stats.reduce( + (acc, s) => { + acc.total += s.totalFeedbacks; + acc.sum += s.averageRating * s.totalFeedbacks; + return acc; + }, + { total: 0, sum: 0 } + ); + + if (allStats.total > 0) { + result.overall = { + averageRating: Math.round((allStats.sum / allStats.total) * 10) / 10, + totalFeedbacks: allStats.total, + }; + } + + return result; +}; + +// Metodo statico per contare distribuzioni rating +FeedbackSchema.statics.getRatingDistribution = async function (idapp, userId, role = null) { + const match = { + idapp, + toUserId: new mongoose.Types.ObjectId(userId), + }; + if (role) match.role = role; + + return this.aggregate([ + { $match: match }, + { + $group: { + _id: '$rating', + count: { $sum: 1 }, + }, + }, + { $sort: { _id: -1 } }, + ]); +}; + +// Hook post-save per aggiornare rating utente +FeedbackSchema.post('save', async function (doc) { + try { + const { User } = require('./User'); + + const stats = await mongoose.model('Feedback').getStatsForUser(doc.idapp, doc.toUserId); + + await User.findByIdAndUpdate(doc.toUserId, { + 'profile.driverProfile.averageRating': stats.overall.averageRating, + 'profile.driverProfile.totalRatings': stats.overall.totalFeedbacks, + }); + } catch (error) { + console.error('Errore aggiornamento rating utente:', error); + } +}); + +const Feedback = mongoose.model('Feedback', FeedbackSchema); + +module.exports = Feedback; diff --git a/src/models/Message.js b/src/models/Message.js new file mode 100644 index 0000000..2568e95 --- /dev/null +++ b/src/models/Message.js @@ -0,0 +1,238 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const MessageSchema = new Schema({ + idapp: { + type: String, + required: true, + index: true + }, + chatId: { + type: Schema.Types.ObjectId, + ref: 'Chat', + required: true, + index: true + }, + senderId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + text: { + type: String, + trim: true, + maxlength: 2000 + }, + type: { + type: String, + enum: ['text', 'ride_share', 'location', 'image', 'voice', 'system', 'ride_request', 'ride_accepted', 'ride_rejected'], + default: 'text' + }, + metadata: { + // Per messaggi speciali (condivisione viaggio, posizione, ecc.) + rideId: { + type: Schema.Types.ObjectId, + ref: 'Ride' + }, + rideRequestId: { + type: Schema.Types.ObjectId, + ref: 'RideRequest' + }, + location: { + lat: Number, + lng: Number, + address: String + }, + imageUrl: String, + voiceUrl: String, + voiceDuration: Number, + systemAction: String + }, + readBy: [{ + userId: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + readAt: { + type: Date, + default: Date.now + } + }], + replyTo: { + type: Schema.Types.ObjectId, + ref: 'Message' + }, + isEdited: { + type: Boolean, + default: false + }, + editedAt: { + type: Date + }, + isDeleted: { + type: Boolean, + default: false + }, + deletedAt: { + type: Date + }, + reactions: [{ + userId: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + emoji: String, + createdAt: { + type: Date, + default: Date.now + } + }] +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}); + +// Indici per query efficienti +MessageSchema.index({ chatId: 1, createdAt: -1 }); +MessageSchema.index({ senderId: 1, createdAt: -1 }); +MessageSchema.index({ idapp: 1, chatId: 1 }); + +// Virtual per verificare se il messaggio è stato letto da tutti +MessageSchema.virtual('isReadByAll').get(function() { + // Logica da implementare confrontando con partecipanti chat + return false; +}); + +// Metodo per marcare come letto da un utente +MessageSchema.methods.markAsReadBy = function(userId) { + const alreadyRead = this.readBy.some( + r => r.userId.toString() === userId.toString() + ); + + if (!alreadyRead) { + this.readBy.push({ + userId, + readAt: new Date() + }); + return this.save(); + } + return Promise.resolve(this); +}; + +// Metodo per verificare se è stato letto da un utente +MessageSchema.methods.isReadBy = function(userId) { + return this.readBy.some( + r => r.userId.toString() === userId.toString() + ); +}; + +// Metodo per aggiungere reazione +MessageSchema.methods.addReaction = function(userId, emoji) { + // Rimuovi eventuale reazione precedente dello stesso utente + this.reactions = this.reactions.filter( + r => r.userId.toString() !== userId.toString() + ); + + this.reactions.push({ + userId, + emoji, + createdAt: new Date() + }); + + return this.save(); +}; + +// Metodo per rimuovere reazione +MessageSchema.methods.removeReaction = function(userId) { + this.reactions = this.reactions.filter( + r => r.userId.toString() !== userId.toString() + ); + return this.save(); +}; + +// Metodo per soft delete +MessageSchema.methods.softDelete = function() { + this.isDeleted = true; + this.deletedAt = new Date(); + this.text = '[Messaggio eliminato]'; + return this.save(); +}; + +// Metodo per modificare testo +MessageSchema.methods.editText = function(newText) { + this.text = newText; + this.isEdited = true; + this.editedAt = new Date(); + return this.save(); +}; + +// Metodo statico per ottenere messaggi di una chat con paginazione +MessageSchema.statics.getByChat = function(idapp, chatId, options = {}) { + const { limit = 50, before = null, after = null } = options; + + const query = { + idapp, + chatId, + isDeleted: false + }; + + if (before) { + query.createdAt = { $lt: new Date(before) }; + } + if (after) { + query.createdAt = { $gt: new Date(after) }; + } + + return this.find(query) + .populate('senderId', 'username name surname profile.avatar') + .populate('replyTo', 'text senderId') + .populate('metadata.rideId', 'departure destination dateTime') + .sort({ createdAt: -1 }) + .limit(limit); +}; + +// Metodo statico per creare messaggio di sistema +MessageSchema.statics.createSystemMessage = async function(idapp, chatId, text, action = null) { + const message = new this({ + idapp, + chatId, + senderId: null, // Sistema + text, + type: 'system', + metadata: { + systemAction: action + } + }); + return message.save(); +}; + +// Metodo statico per contare messaggi non letti +MessageSchema.statics.countUnreadForUser = async function(idapp, chatId, userId) { + return this.countDocuments({ + idapp, + chatId, + isDeleted: false, + senderId: { $ne: userId }, + 'readBy.userId': { $ne: userId } + }); +}; + +// Hook post-save per aggiornare la chat +MessageSchema.post('save', async function(doc) { + try { + const Chat = mongoose.model('Chat'); + const chat = await Chat.findById(doc.chatId); + + if (chat) { + await chat.updateLastMessage(doc); + await chat.incrementUnread(doc.senderId); + } + } catch (error) { + console.error('Errore aggiornamento chat dopo messaggio:', error); + } +}); + +const Message = mongoose.model('Message', MessageSchema); + +module.exports = Message; \ No newline at end of file diff --git a/src/models/Ride.js b/src/models/Ride.js new file mode 100644 index 0000000..0a5b0f2 --- /dev/null +++ b/src/models/Ride.js @@ -0,0 +1,499 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +// Schema per le coordinate geografiche +const CoordinatesSchema = new Schema({ + lat: { + type: Number, + required: true + }, + lng: { + type: Number, + required: true + } +}, { _id: false }); + +// Schema per una località (partenza, destinazione, waypoint) +const LocationSchema = new Schema({ + city: { + type: String, + required: true, + trim: true + }, + address: { + type: String, + trim: true + }, + province: { + type: String, + trim: true + }, + region: { + type: String, + trim: true + }, + country: { + type: String, + default: 'Italia', + trim: true + }, + postalCode: { + type: String, + trim: true + }, + coordinates: { + type: CoordinatesSchema, + required: true + } +}, { _id: false }); + +// Schema per i waypoint (tappe intermedie) +const WaypointSchema = new Schema({ + location: { + type: LocationSchema, + required: true + }, + order: { + type: Number, + required: true + }, + estimatedArrival: { + type: Date + }, + stopDuration: { + type: Number, // minuti di sosta + default: 0 + } +}); + +// Schema per la ricorrenza del viaggio +const RecurrenceSchema = new Schema({ + type: { + type: String, + enum: ['once', 'weekly', 'custom_days', 'custom_dates'], + default: 'once' + }, + daysOfWeek: [{ + type: Number, + min: 0, + max: 6 + // 0 = Domenica, 1 = Lunedì, ..., 6 = Sabato + }], + customDates: [{ + type: Date + }], + startDate: { + type: Date + }, + endDate: { + type: Date + }, + excludedDates: [{ + type: Date + }] +}, { _id: false }); + +// Schema per i passeggeri +const PassengersSchema = new Schema({ + available: { + type: Number, + required: true, + min: 0 + }, + max: { + type: Number, + required: true, + min: 1 + } +}, { _id: false }); + +// Schema per il veicolo +const VehicleSchema = new Schema({ + type: { + type: String, + enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'], + default: 'auto' + }, + brand: { + type: String, + trim: true + }, + model: { + type: String, + trim: true + }, + color: { + type: String, + trim: true + }, + colorHex: { + type: String, + trim: true + }, + year: { + type: Number + }, + licensePlate: { + type: String, + trim: true + }, + seats: { + type: Number, + min: 1 + }, + features: [{ + type: String, + enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'] + }] +}, { _id: false }); + +// Schema per le preferenze di viaggio +const RidePreferencesSchema = new Schema({ + smoking: { + type: String, + enum: ['yes', 'no', 'outside_only'], + default: 'no' + }, + pets: { + type: String, + enum: ['no', 'small', 'medium', 'large', 'all'], + default: 'no' + }, + luggage: { + type: String, + enum: ['none', 'small', 'medium', 'large'], + default: 'medium' + }, + packages: { + type: Boolean, + default: false + }, + maxPackageSize: { + type: String, + enum: ['small', 'medium', 'large', 'xlarge'], + default: 'medium' + }, + music: { + type: String, + enum: ['no_music', 'quiet', 'moderate', 'loud', 'passenger_choice'], + default: 'moderate' + }, + conversation: { + type: String, + enum: ['quiet', 'moderate', 'chatty'], + default: 'moderate' + }, + foodAllowed: { + type: Boolean, + default: true + }, + childrenFriendly: { + type: Boolean, + default: true + }, + wheelchairAccessible: { + type: Boolean, + default: false + }, + otherPreferences: { + type: String, + trim: true, + maxlength: 500 + } +}, { _id: false }); + +// Schema per il contributo/pagamento +const ContributionItemSchema = new Schema({ + contribTypeId: { + type: Schema.Types.ObjectId, + ref: 'Contribtype', + required: true + }, + price: { + type: Number, + min: 0 + }, + pricePerKm: { + type: Number, + min: 0 + }, + notes: { + type: String, + trim: true + } +}); + +const ContributionSchema = new Schema({ + contribTypes: [ContributionItemSchema], + negotiable: { + type: Boolean, + default: true + }, + freeForStudents: { + type: Boolean, + default: false + }, + freeForElders: { + type: Boolean, + default: false + } +}, { _id: false }); + +// Schema principale del Ride +const RideSchema = new Schema({ + idapp: { + type: String, + required: true, + index: true + }, + userId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + type: { + type: String, + enum: ['offer', 'request'], + required: true, + index: true + // offer = 🟢 Offerta passaggio (sono conducente) + // request = 🔴 Richiesta passaggio (cerco passaggio) + }, + departure: { + type: LocationSchema, + required: true + }, + destination: { + type: LocationSchema, + required: true + }, + waypoints: [WaypointSchema], + dateTime: { + type: Date, + required: true, + index: true + }, + flexibleTime: { + type: Boolean, + default: false + }, + flexibleMinutes: { + type: Number, + default: 30, + min: 0, + max: 180 + }, + recurrence: { + type: RecurrenceSchema, + default: () => ({ type: 'once' }) + }, + passengers: { + type: PassengersSchema, + required: function() { + return this.type === 'offer'; + } + }, + seatsNeeded: { + type: Number, + min: 1, + default: 1, + // Solo per type = 'request' + }, + vehicle: { + type: VehicleSchema, + required: function() { + return this.type === 'offer'; + } + }, + preferences: { + type: RidePreferencesSchema, + default: () => ({}) + }, + contribution: { + type: ContributionSchema, + default: () => ({ contribTypes: [] }) + }, + status: { + type: String, + enum: ['draft', 'active', 'full', 'in_progress', 'completed', 'cancelled', 'expired'], + default: 'active', + index: true + }, + estimatedDistance: { + type: Number, // in km + min: 0 + }, + estimatedDuration: { + type: Number, // in minuti + min: 0 + }, + routePolyline: { + type: String // Polyline encoded per visualizzare il percorso + }, + confirmedPassengers: [{ + userId: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + seats: { + type: Number, + default: 1 + }, + pickupPoint: LocationSchema, + dropoffPoint: LocationSchema, + confirmedAt: { + type: Date, + default: Date.now + } + }], + views: { + type: Number, + default: 0 + }, + isFeatured: { + type: Boolean, + default: false + }, + notes: { + type: String, + trim: true, + maxlength: 1000 + }, + cancellationReason: { + type: String, + trim: true + }, + cancelledAt: { + type: Date + } +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}); + +// Indici per ricerche ottimizzate +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 }); + +// Virtual per verificare se il viaggio è pieno +RideSchema.virtual('isFull').get(function() { + if (this.type === 'request') return false; + return this.passengers.available <= 0; +}); + +// Virtual per calcolare posti occupati +RideSchema.virtual('bookedSeats').get(function() { + if (!this.confirmedPassengers) return 0; + return this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0); +}); + +// Virtual per ottenere tutte le città del percorso +RideSchema.virtual('allCities').get(function() { + const cities = [this.departure.city]; + if (this.waypoints && this.waypoints.length > 0) { + this.waypoints + .sort((a, b) => a.order - b.order) + .forEach(wp => cities.push(wp.location.city)); + } + cities.push(this.destination.city); + return cities; +}); + +// Metodo per verificare se passa per una città +RideSchema.methods.passesThrough = function(cityName) { + const normalizedCity = cityName.toLowerCase().trim(); + return this.allCities.some(city => + city.toLowerCase().trim().includes(normalizedCity) || + normalizedCity.includes(city.toLowerCase().trim()) + ); +}; + +// Metodo per aggiornare posti disponibili +RideSchema.methods.updateAvailableSeats = function() { + if (this.type === 'offer') { + const booked = this.bookedSeats; + this.passengers.available = this.passengers.max - booked; + if (this.passengers.available <= 0) { + this.status = 'full'; + } else if (this.status === 'full') { + this.status = 'active'; + } + } + return this.save(); +}; + +// Pre-save hook +RideSchema.pre('save', function(next) { + // Aggiorna posti disponibili se necessario + if (this.type === 'offer' && this.isModified('confirmedPassengers')) { + const booked = this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0); + this.passengers.available = this.passengers.max - booked; + if (this.passengers.available <= 0) { + this.status = 'full'; + } + } + next(); +}); + +// Metodi statici per ricerche comuni +RideSchema.statics.findActiveByCity = function(idapp, departureCity, destinationCity, options = {}) { + const query = { + idapp, + status: { $in: ['active', 'full'] }, + dateTime: { $gte: new Date() } + }; + + if (departureCity) { + query['departure.city'] = new RegExp(departureCity, 'i'); + } + if (destinationCity) { + query['destination.city'] = new RegExp(destinationCity, 'i'); + } + if (options.type) { + query.type = options.type; + } + if (options.date) { + const startOfDay = new Date(options.date); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(options.date); + endOfDay.setHours(23, 59, 59, 999); + query.dateTime = { $gte: startOfDay, $lte: endOfDay }; + } + + return this.find(query) + .populate('userId', 'username name surname profile.driverProfile.averageRating') + .sort({ dateTime: 1 }); +}; + +// Ricerca viaggi che passano per una città intermedia +RideSchema.statics.findPassingThrough = function(idapp, cityName, options = {}) { + const cityRegex = new RegExp(cityName, 'i'); + const query = { + idapp, + status: { $in: ['active'] }, + dateTime: { $gte: new Date() }, + $or: [ + { 'departure.city': cityRegex }, + { 'destination.city': cityRegex }, + { 'waypoints.location.city': cityRegex } + ] + }; + + if (options.type) { + query.type = options.type; + } + + return this.find(query) + .populate('userId', 'username name surname profile.driverProfile.averageRating') + .sort({ dateTime: 1 }); +}; + +const Ride = mongoose.model('Ride', RideSchema); + +module.exports = Ride; \ No newline at end of file diff --git a/src/models/RideRequest.js b/src/models/RideRequest.js new file mode 100644 index 0000000..28fdde4 --- /dev/null +++ b/src/models/RideRequest.js @@ -0,0 +1,296 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +// Schema per le coordinate +const CoordinatesSchema = new Schema({ + lat: { + type: Number, + required: true + }, + lng: { + type: Number, + required: true + } +}, { _id: false }); + +// Schema per località +const LocationSchema = new Schema({ + city: { + type: String, + required: true, + trim: true + }, + address: { + type: String, + trim: true + }, + province: { + type: String, + trim: true + }, + coordinates: { + type: CoordinatesSchema, + required: true + } +}, { _id: false }); + +const RideRequestSchema = new Schema({ + idapp: { + type: String, + required: true, + index: true + }, + rideId: { + type: Schema.Types.ObjectId, + ref: 'Ride', + required: true, + index: true + }, + passengerId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + driverId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + message: { + type: String, + trim: true, + maxlength: 500 + }, + pickupPoint: { + type: LocationSchema + }, + dropoffPoint: { + type: LocationSchema + }, + useOriginalRoute: { + type: Boolean, + default: true + // true = usa partenza/destinazione originali del ride + }, + seatsRequested: { + type: Number, + required: true, + min: 1, + default: 1 + }, + hasLuggage: { + type: Boolean, + default: false + }, + luggageSize: { + type: String, + enum: ['small', 'medium', 'large'], + default: 'small' + }, + hasPackages: { + type: Boolean, + default: false + }, + packageDescription: { + type: String, + trim: true, + maxlength: 200 + }, + hasPets: { + type: Boolean, + default: false + }, + petType: { + type: String, + trim: true + }, + petSize: { + type: String, + enum: ['small', 'medium', 'large'] + }, + specialNeeds: { + type: String, + trim: true, + maxlength: 300 + }, + status: { + type: String, + enum: ['pending', 'accepted', 'rejected', 'cancelled', 'expired', 'completed'], + default: 'pending', + index: true + }, + responseMessage: { + type: String, + trim: true, + maxlength: 500 + }, + respondedAt: { + type: Date + }, + contribution: { + agreed: { + type: Boolean, + default: false + }, + contribTypeId: { + type: Schema.Types.ObjectId, + ref: 'Contribtype' + }, + amount: { + type: Number, + min: 0 + }, + notes: { + type: String, + trim: true + } + }, + cancelledBy: { + type: String, + enum: ['passenger', 'driver'] + }, + cancellationReason: { + type: String, + trim: true + }, + cancelledAt: { + type: Date + }, + completedAt: { + type: Date + }, + feedbackGiven: { + type: Boolean, + default: false + } +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}); + +// Indici composti per ricerche ottimizzate +RideRequestSchema.index({ rideId: 1, status: 1 }); +RideRequestSchema.index({ passengerId: 1, status: 1 }); +RideRequestSchema.index({ driverId: 1, status: 1 }); +RideRequestSchema.index({ idapp: 1, createdAt: -1 }); + +// Virtual per verificare se la richiesta può essere cancellata +RideRequestSchema.virtual('canCancel').get(function() { + return ['pending', 'accepted'].includes(this.status); +}); + +// Virtual per verificare se è in attesa +RideRequestSchema.virtual('isPending').get(function() { + return this.status === 'pending'; +}); + +// Metodo per accettare la richiesta +RideRequestSchema.methods.accept = async function(responseMessage = '') { + this.status = 'accepted'; + this.responseMessage = responseMessage; + this.respondedAt = new Date(); + + // Aggiorna il ride con il passeggero confermato + const Ride = mongoose.model('Ride'); + const ride = await Ride.findById(this.rideId); + + if (ride) { + ride.confirmedPassengers.push({ + userId: this.passengerId, + seats: this.seatsRequested, + pickupPoint: this.pickupPoint || ride.departure, + dropoffPoint: this.dropoffPoint || ride.destination, + confirmedAt: new Date() + }); + await ride.updateAvailableSeats(); + } + + return this.save(); +}; + +// Metodo per rifiutare la richiesta +RideRequestSchema.methods.reject = function(responseMessage = '') { + this.status = 'rejected'; + this.responseMessage = responseMessage; + this.respondedAt = new Date(); + return this.save(); +}; + +// Metodo per cancellare la richiesta +RideRequestSchema.methods.cancel = async function(cancelledBy, reason = '') { + this.status = 'cancelled'; + this.cancelledBy = cancelledBy; + this.cancellationReason = reason; + this.cancelledAt = new Date(); + + // Se era accettata, rimuovi il passeggero dal ride + if (this.status === 'accepted') { + const Ride = mongoose.model('Ride'); + const ride = await Ride.findById(this.rideId); + + if (ride) { + ride.confirmedPassengers = ride.confirmedPassengers.filter( + p => p.userId.toString() !== this.passengerId.toString() + ); + await ride.updateAvailableSeats(); + } + } + + return this.save(); +}; + +// Metodo statico per ottenere richieste pendenti di un conducente +RideRequestSchema.statics.getPendingForDriver = function(idapp, driverId) { + return this.find({ + idapp, + driverId, + status: 'pending' + }) + .populate('passengerId', 'username name surname email') + .populate('rideId', 'departure destination dateTime') + .sort({ createdAt: -1 }); +}; + +// Metodo statico per ottenere richieste di un passeggero +RideRequestSchema.statics.getByPassenger = function(idapp, passengerId, status = null) { + const query = { idapp, passengerId }; + if (status) { + query.status = status; + } + return this.find(query) + .populate('rideId') + .populate('driverId', 'username name surname') + .sort({ createdAt: -1 }); +}; + +// Pre-save hook per validazioni +RideRequestSchema.pre('save', async function(next) { + if (this.isNew) { + // Verifica che il ride esista e abbia posti disponibili + const Ride = mongoose.model('Ride'); + const ride = await Ride.findById(this.rideId); + + if (!ride) { + throw new Error('Viaggio non trovato'); + } + + if (ride.type === 'offer' && ride.passengers.available < this.seatsRequested) { + throw new Error('Posti non sufficienti per questo viaggio'); + } + + if (ride.userId.toString() === this.passengerId.toString()) { + throw new Error('Non puoi richiedere un passaggio per il tuo stesso viaggio'); + } + + // Imposta il driverId dal ride + this.driverId = ride.userId; + } + next(); +}); + +const RideRequest = mongoose.model('RideRequest', RideRequestSchema); + +module.exports = RideRequest; \ No newline at end of file diff --git a/src/models/contribtype.js b/src/models/contribtype.js index c7b3676..c80a33a 100755 --- a/src/models/contribtype.js +++ b/src/models/contribtype.js @@ -56,11 +56,11 @@ ContribtypeSchema.statics.findAllIdApp = async function (idapp) { return await Contribtype.find(myfind).lean(); }; -const Contribtype = mongoose.model('Contribtype', ContribtypeSchema); +const Contribtype = mongoose.models.Contribtype || mongoose.model('Contribtype', ContribtypeSchema); Contribtype.createIndexes() .then(() => { }) .catch((err) => { throw err; }); -module.exports = { Contribtype }; +module.exports = { Contribtype }; \ No newline at end of file diff --git a/src/models/site.js b/src/models/site.js index cb29b80..70a5b11 100755 --- a/src/models/site.js +++ b/src/models/site.js @@ -175,6 +175,7 @@ const SiteSchema = new Schema({ enableEcommerce: { type: Boolean, default: false }, enableAI: { type: Boolean, default: false }, enablePoster: { type: Boolean, default: false }, + enableTrasporti: { type: Boolean, default: false }, enableGroups: { type: Boolean, default: false }, enableCircuits: { type: Boolean, default: false }, enableGoods: { type: Boolean, default: false }, diff --git a/src/models/user.js b/src/models/user.js index 8d6924e..f8296d1 100755 --- a/src/models/user.js +++ b/src/models/user.js @@ -1,5 +1,7 @@ const bcrypt = require('bcryptjs'); const mongoose = require('mongoose').set('debug', false); +const Schema = mongoose.Schema; + const validator = require('validator'); const jwt = require('jsonwebtoken'); const _ = require('lodash'); @@ -285,6 +287,10 @@ const UserSchema = new mongoose.Schema( cell: { type: String, }, + cellVerified: { + type: Boolean, + default: false, + }, country_pay: { type: String, }, @@ -584,6 +590,264 @@ const UserSchema = new mongoose.Schema( ], version: { type: Number }, insert_circuito_ita: { type: Boolean }, + + // ============ DRIVER PROFILE ============ + driverProfile: { + isDriver: { + type: Boolean, + default: false, + }, + bio: { + type: String, + trim: true, + maxlength: 500, + }, + vehicles: [ + { + type: { + type: String, + enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'], + default: 'auto', + }, + brand: { + type: String, + trim: true, + }, + model: { + type: String, + trim: true, + }, + color: { + type: String, + trim: true, + }, + colorHex: { + type: String, + trim: true, + }, + year: { + type: Number, + }, + seats: { + type: Number, + min: 1, + max: 50, + }, + licensePlate: { + type: String, + trim: true, + }, + features: [ + { + type: String, + enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'], + }, + ], + isDefault: { + type: Boolean, + default: false, + }, + isVerified: { + type: Boolean, + default: false, + }, + }, + ], + ridesCompletedAsDriver: { + type: Number, + default: 0, + }, + ridesCompletedAsPassenger: { + type: Number, + default: 0, + }, + averageRating: { + type: Number, + default: 0, + min: 0, + max: 5, + }, + totalRatings: { + type: Number, + default: 0, + }, + verifiedDriver: { + type: Boolean, + default: false, + }, + licenseVerified: { + type: Boolean, + default: false, + }, + licenseNumber: { + type: String, + trim: true, + }, + licenseExpiry: { + type: Date, + }, + memberSince: { + type: Date, + default: Date.now, + }, + responseRate: { + type: Number, + default: 100, + min: 0, + max: 100, + }, + responseTime: { + type: String, + enum: ['within_hour', 'within_day', 'within_days'], + default: 'within_day', + }, + totalKmShared: { + type: Number, + default: 0, + }, + co2Saved: { + type: Number, + default: 0, + // kg di CO2 risparmiati + }, + badges: [ + { + name: { + type: String, + }, + earnedAt: { + type: Date, + default: Date.now, + }, + }, + ], + level: { + type: Number, + default: 1, + min: 1, + }, + points: { + type: Number, + default: 0, + }, + }, + + // ============ PREFERENCES ============ + preferences: { + // Preferenze di viaggio + smoking: { + type: String, + enum: ['yes', 'no', 'outside_only'], + default: 'no', + }, + pets: { + type: String, + enum: ['no', 'small', 'medium', 'large', 'all'], + default: 'small', + }, + music: { + type: String, + enum: ['no_music', 'quiet', 'moderate', 'loud', 'passenger_choice'], + default: 'moderate', + }, + conversation: { + type: String, + enum: ['quiet', 'moderate', 'chatty'], + default: 'moderate', + }, + + // Notifiche + notifications: { + rideRequests: { + type: Boolean, + default: true, + }, + rideAccepted: { + type: Boolean, + default: true, + }, + rideReminders: { + type: Boolean, + default: true, + }, + messages: { + type: Boolean, + default: true, + }, + marketing: { + type: Boolean, + default: false, + }, + pushEnabled: { + type: Boolean, + default: true, + }, + emailEnabled: { + type: Boolean, + default: true, + }, + }, + + // Privacy + privacy: { + showEmail: { + type: Boolean, + default: false, + }, + showPhone: { + type: Boolean, + default: false, + }, + showLastName: { + type: Boolean, + default: true, + }, + showRides: { + type: Boolean, + default: true, + }, + }, + + // Località preferite + favoriteLocations: [ + { + name: { + type: String, + trim: true, + }, + city: { + type: String, + trim: true, + }, + address: { + type: String, + trim: true, + }, + coordinates: { + lat: Number, + lng: Number, + }, + type: { + type: String, + enum: ['home', 'work', 'other'], + default: 'other', + }, + }, + ], + + // Lingue parlate + languages: [ + { + type: String, + }, + ], + + // Metodo di pagamento preferito + preferredContribType: { + type: Schema.Types.ObjectId, + ref: 'Contribtype', + }, + }, }, updatedAt: { type: Date, default: Date.now }, }, @@ -7016,8 +7280,6 @@ UserSchema.statics.getUsersList = function (idapp) { }).lean(); }; -const User = mongoose.model('User', UserSchema); - class Hero { constructor(name, level) { this.name = name; @@ -7074,6 +7336,9 @@ const FuncUsers = { UserSchema.index({ 'tokens.token': 1, 'tokens.access': 1, idapp: 1, deleted: 1, updatedAt: 1 }); + +const User = mongoose.models.User || mongoose.model('User', UserSchema); + module.exports = { User, Hero, diff --git a/src/router/api_router.js b/src/router/api_router.js index fb46050..fe7f488 100644 --- a/src/router/api_router.js +++ b/src/router/api_router.js @@ -33,6 +33,10 @@ const { MyElem } = require('../models/myelem'); const axios = require('axios'); +const trasportiRoutes = require('../routes/trasportiRoutes'); +router.use('/trasporti', trasportiRoutes); + + // Importa le routes video const videoRoutes = require('../routes/videoRoutes'); diff --git a/src/routes/trasportiRoutes.js b/src/routes/trasportiRoutes.js new file mode 100644 index 0000000..36a8ae4 --- /dev/null +++ b/src/routes/trasportiRoutes.js @@ -0,0 +1,714 @@ +const express = require('express'); +const router = express.Router(); + +const { Contribtype } = require('../models/Contribtype'); // Adatta al tuo path + +// Import Controllers +const rideController = require('../controllers/rideController'); +const rideRequestController = require('../controllers/rideRequestController'); +const chatController = require('../controllers/chatController'); +const feedbackController = require('../controllers/feedbackController'); +const geocodingController = require('../controllers/geocodingController'); + +// Middleware di autenticazione (usa il tuo esistente) +const { authenticate } = require('../middleware/authenticate'); + +// ============================================================ +// 🚗 RIDES - Gestione Viaggi +// ============================================================ + +/** + * @route POST /api/trasporti/rides + * @desc Crea nuovo viaggio (offerta o richiesta) + * @access Private + */ +router.post('/rides', authenticate, rideController.createRide); + +/** + * @route GET /api/trasporti/rides + * @desc Ottieni lista viaggi con filtri + * @access Public + */ +router.get('/rides', rideController.getRides); + +/** + * @route GET /api/trasporti/rides/search + * @desc Ricerca viaggi avanzata + * @access Public + */ +router.get('/rides/search', rideController.searchRides); + +/** + * @route GET /api/trasporti/rides/stats + * @desc Statistiche per widget homepage + * @access Private + */ +router.get('/rides/stats', authenticate, rideController.getRidesStats); + +/** + * @route GET /api/trasporti/rides/my + * @desc I miei viaggi (come driver e passenger) + * @access Private + */ +router.get('/rides/my', authenticate, rideController.getMyRides); + +/** + * @route GET /api/trasporti/rides/match + * @desc Match automatico offerta/richiesta + * @access Private + * @note ⚠️ IMPORTANTE: Questa route DEVE stare PRIMA di /rides/:id + */ +//router.get('/rides/match', authenticate, rideController.findMatches); + +/** + * @route GET /api/trasporti/rides/:id + * @desc Dettaglio singolo viaggio + * @access Public + */ +router.get('/rides/:id', rideController.getRideById); + +/** + * @route PUT /api/trasporti/rides/:id + * @desc Aggiorna viaggio + * @access Private (solo owner) + */ +router.put('/rides/:id', authenticate, rideController.updateRide); + +/** + * @route DELETE /api/trasporti/rides/:id + * @desc Cancella viaggio + * @access Private (solo owner) + */ +router.delete('/rides/:id', authenticate, rideController.deleteRide); + +/** + * @route POST /api/trasporti/rides/:id/complete + * @desc Completa un viaggio + * @access Private (solo driver) + */ +router.post('/rides/:id/complete', authenticate, rideController.completeRide); + +// ============================================================ +// 📊 WIDGET & STATS +// ============================================================ + +/** + * @route GET /api/trasporti/widget/data + * @desc Dati completi per widget homepage + * @access Private + */ +router.get('/widget/data', authenticate, rideController.getWidgetData); + +/** + * @route GET /api/trasporti/stats/summary + * @desc Stats rapide per header widget (offerte, richieste, match) + * @access Public + */ +router.get('/stats/summary', authenticate, rideController.getStatsSummary); + +/** + * @route GET /api/trasporti/cities/suggestions + * @desc Suggerimenti città per autocomplete (basato su viaggi esistenti) + * @access Public + */ +router.get('/cities/suggestions', rideController.getCitySuggestions); + + +/** + * @route GET /api/trasporti/cities/recents + * @desc città recenti per autocomplete + * @access Public + */ +router.get('/cities/recent', authenticate, rideController.getRecentCities); + +// ============================================================ +// 📩 REQUESTS - Richieste Passaggio +// ============================================================ + +/** + * @route POST /api/trasporti/requests + * @desc Crea richiesta passaggio per un viaggio + * @access Private + */ +router.post('/requests', authenticate, rideRequestController.createRequest); + +/** + * @route GET /api/trasporti/requests/received + * @desc Richieste ricevute (sono conducente) + * @access Private + */ +router.get('/requests/received', authenticate, rideRequestController.getReceivedRequests); + +/** + * @route GET /api/trasporti/requests/sent + * @desc Richieste inviate (sono passeggero) + * @access Private + */ +router.get('/requests/sent', authenticate, rideRequestController.getSentRequests); + +/** + * @route GET /api/trasporti/requests/ride/:rideId + * @desc Richieste per un viaggio specifico + * @access Private (solo owner del viaggio) + */ +router.get('/requests/ride/:rideId', authenticate, rideRequestController.getRequestsForRide); + +/** + * @route GET /api/trasporti/requests/:id + * @desc Dettaglio singola richiesta + * @access Private (driver o passenger) + */ +router.get('/requests/:id', authenticate, rideRequestController.getRequestById); + +/** + * @route POST /api/trasporti/requests/:id/accept + * @desc Accetta richiesta passaggio + * @access Private (solo driver) + */ +router.post('/requests/:id/accept', authenticate, rideRequestController.acceptRequest); + +/** + * @route POST /api/trasporti/requests/:id/reject + * @desc Rifiuta richiesta passaggio + * @access Private (solo driver) + */ +router.post('/requests/:id/reject', authenticate, rideRequestController.rejectRequest); + +/** + * @route POST /api/trasporti/requests/:id/cancel + * @desc Cancella richiesta/prenotazione + * @access Private (driver o passenger) + */ +router.post('/requests/:id/cancel', authenticate, rideRequestController.cancelRequest); + +// ============================================================ +// 💬 CHAT - Messaggistica +// ============================================================ + +/** + * @route GET /api/trasporti/chats + * @desc Lista tutte le mie chat + * @access Private + */ +router.get('/chats', authenticate, chatController.getMyChats); + +/** + * @route GET /api/trasporti/chats/unread/count + * @desc Conta messaggi non letti totali + * @access Private + */ +router.get('/chats/unread/count', authenticate, chatController.getUnreadCount); + +/** + * @route POST /api/trasporti/chats/direct + * @desc Ottieni o crea chat diretta con utente + * @access Private + */ +router.post('/chats/direct', authenticate, chatController.getOrCreateDirectChat); + +/** + * @route GET /api/trasporti/chats/:id + * @desc Dettaglio chat + * @access Private (solo partecipanti) + */ +router.get('/chats/:id', authenticate, chatController.getChatById); + +/** + * @route GET /api/trasporti/chats/:id/messages + * @desc Messaggi di una chat + * @access Private (solo partecipanti) + */ +router.get('/chats/:id/messages', authenticate, chatController.getChatMessages); + +/** + * @route POST /api/trasporti/chats/:id/messages + * @desc Invia messaggio + * @access Private (solo partecipanti) + */ +router.post('/chats/:id/messages', authenticate, chatController.sendMessage); + +/** + * @route PUT /api/trasporti/chats/:id/read + * @desc Segna chat come letta + * @access Private (solo partecipanti) + * @fix Corretto: markAsRead → markChatAsRead + */ +router.put('/chats/:id/read', authenticate, chatController.markChatAsRead); + +/** + * @route PUT /api/trasporti/chats/:id/block + * @desc Blocca/sblocca chat + * @access Private (solo partecipanti) + */ +router.put('/chats/:id/block', authenticate, chatController.toggleBlockChat); + +/** + * @route PUT /api/trasporti/chats/:id/mute + * @desc Muta/smuta notifiche di una chat + * @access Private (solo partecipanti) + * @fix Aggiunta route mancante + */ +router.put('/chats/:id/mute', authenticate, chatController.toggleMuteChat); + +/** + * @route DELETE /api/trasporti/chats/:chatId/messages/:messageId + * @desc Elimina messaggio + * @access Private (solo mittente) + * @fix Corretto: /messages/:id → /chats/:chatId/messages/:messageId + */ +router.delete('/chats/:chatId/messages/:messageId', authenticate, chatController.deleteMessage); + +// ============================================================ +// ⭐ FEEDBACK - Recensioni +// ============================================================ + +/** + * @route POST /api/trasporti/feedback + * @desc Crea feedback per un viaggio + * @access Private + */ +router.post('/feedback', authenticate, feedbackController.createFeedback); + +/** + * @route GET /api/trasporti/feedback/my/received + * @desc I miei feedback ricevuti + * @access Private + */ +router.get('/feedback/my/received', authenticate, feedbackController.getMyReceivedFeedbacks); + +/** + * @route GET /api/trasporti/feedback/my/given + * @desc I miei feedback lasciati + * @access Private + */ +router.get('/feedback/my/given', authenticate, feedbackController.getMyGivenFeedbacks); + +/** + * @route GET /api/trasporti/feedback/user/:userId + * @desc Feedback di un utente + * @access Public + */ +router.get('/feedback/user/:userId', feedbackController.getFeedbacksForUser); + +/** + * @route GET /api/trasporti/feedback/user/:userId/stats + * @desc Statistiche feedback utente + * @access Public + */ +router.get('/feedback/user/:userId/stats', feedbackController.getUserFeedbackStats); + +/** + * @route GET /api/trasporti/feedback/ride/:rideId + * @desc Feedback per un viaggio + * @access Public + */ +router.get('/feedback/ride/:rideId', feedbackController.getRideFeedback); + +/** + * @route GET /api/trasporti/feedback/can-leave/:rideId/:toUserId + * @desc Verifica se posso lasciare feedback + * @access Private + */ +router.get('/feedback/can-leave/:rideId/:toUserId', authenticate, feedbackController.canLeaveFeedback); + +/** + * @route POST /api/trasporti/feedback/:id/response + * @desc Rispondi a un feedback + * @access Private (solo destinatario) + */ +router.post('/feedback/:id/response', authenticate, feedbackController.respondToFeedback); + +/** + * @route POST /api/trasporti/feedback/:id/report + * @desc Segnala feedback + * @access Private + */ +router.post('/feedback/:id/report', authenticate, feedbackController.reportFeedback); + +/** + * @route POST /api/trasporti/feedback/:id/helpful + * @desc Segna feedback come utile + * @access Private + */ +router.post('/feedback/:id/helpful', authenticate, feedbackController.markAsHelpful); + +// ============================================================ +// 🗺️ GEO - Geocoding & Mappe (Open Source) +// ============================================================ + +/** + * @route GET /api/trasporti/geo/autocomplete + * @desc Autocomplete città (Photon) + * @access Public + */ +router.get('/geo/autocomplete', geocodingController.autocomplete); + +/** + * @route GET /api/trasporti/geo/cities/it + * @desc Cerca città italiane + * @access Public + */ +router.get('/geo/cities/it', geocodingController.searchItalianCities); + +/** + * @route GET /api/trasporti/geo/geocode + * @desc Indirizzo → Coordinate + * @access Public + */ +router.get('/geo/geocode', geocodingController.geocode); + +/** + * @route GET /api/trasporti/geo/reverse + * @desc Coordinate → Indirizzo + * @access Public + */ +router.get('/geo/reverse', geocodingController.reverseGeocode); + +/** + * @route GET /api/trasporti/geo/route + * @desc Calcola percorso tra punti + * @access Public + */ +router.get('/geo/route', geocodingController.getRoute); + +/** + * @route GET /api/trasporti/geo/distance + * @desc Calcola distanza e durata + * @access Public + */ +router.get('/geo/distance', geocodingController.getDistance); + +/** + * @route GET /api/trasporti/geo/suggest-waypoints + * @desc Suggerisci città intermedie sul percorso + * @access Public + */ +router.get('/geo/suggest-waypoints', geocodingController.suggestWaypoints); + +// ============================================================ +// 🔧 UTILITY & DRIVER PROFILE +// ============================================================ + +/** + * @route GET /api/trasporti/driver/:userId + * @desc Profilo pubblico del conducente + * @access Public + */ +router.get('/driver/:userId', async (req, res) => { + try { + const { userId } = req.params; + const { idapp } = req.query; + + const { User } = require('../models/User'); + const Ride = require('../models/Ride'); + const Feedback = require('../models/Feedback'); + + // Dati utente + const user = await User.findById(userId).select( + 'username name surname profile.img profile.Biografia profile.driverProfile profile.preferences.languages' + ); + + if (!user) { + return res.status(404).json({ + success: false, + message: 'Utente non trovato', + }); + } + + // Statistiche viaggi + const [ridesAsDriver, ridesAsPassenger, completedRides] = await Promise.all([ + Ride.countDocuments({ idapp, userId, type: 'offer' }), + Ride.countDocuments({ idapp, 'confirmedPassengers.userId': userId }), + Ride.countDocuments({ idapp, userId, status: 'completed' }), + ]); + + // Ultimi viaggi come driver + const recentRides = await Ride.find({ + idapp, + userId, + type: 'offer', + status: { $in: ['active', 'completed'] }, + }) + .select('departure destination dateTime status') + .sort({ dateTime: -1 }) + .limit(5); + + // Statistiche feedback + let feedbackStats = { averageRating: 0, totalFeedback: 0 }; + try { + feedbackStats = await Feedback.getStatsForUser(idapp, userId); + } catch (e) { + console.log('Feedback stats non disponibili'); + } + + // Ultimi feedback ricevuti + let recentFeedback = []; + try { + recentFeedback = await Feedback.find({ + idapp, + toUserId: userId, + isPublic: true, + }) + .populate('fromUserId', 'username name profile.img') + .sort({ createdAt: -1 }) + .limit(3); + } catch (e) { + console.log('Recent feedback non disponibili'); + } + + res.status(200).json({ + success: true, + data: { + user: { + _id: user._id, + username: user.username, + name: user.name, + surname: user.surname, + img: user.profile?.img, + bio: user.profile?.Biografia, + driverProfile: user.profile?.driverProfile, + languages: user.profile?.preferences?.languages, + }, + stats: { + ridesAsDriver, + ridesAsPassenger, + completedRides, + ...feedbackStats, + }, + recentRides, + recentFeedback, + }, + }); + } catch (error) { + console.error('Errore recupero profilo driver:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il recupero del profilo', + error: error.message, + }); + } +}); + +/** + * @route PUT /api/trasporti/driver/profile + * @desc Aggiorna profilo conducente + * @access Private + */ +router.put('/driver/profile', authenticate, async (req, res) => { + try { + const userId = req.user._id; + const { idapp, driverProfile, preferences } = req.body; + + const { User } = require('../models/User'); + + const updateData = {}; + + if (driverProfile) { + // Merge con dati esistenti + Object.keys(driverProfile).forEach((key) => { + updateData[`profile.driverProfile.${key}`] = driverProfile[key]; + }); + } + + if (preferences) { + Object.keys(preferences).forEach((key) => { + updateData[`profile.preferences.${key}`] = preferences[key]; + }); + } + + const user = await User.findByIdAndUpdate(userId, { $set: updateData }, { new: true }).select( + 'profile.driverProfile profile.preferences' + ); + + res.status(200).json({ + success: true, + message: 'Profilo aggiornato', + data: user.profile, + }); + } catch (error) { + console.error('Errore aggiornamento profilo:', error); + res.status(500).json({ + success: false, + message: "Errore durante l'aggiornamento", + error: error.message, + }); + } +}); + +/** + * @route POST /api/trasporti/driver/vehicles + * @desc Aggiungi veicolo + * @access Private + */ +router.post('/driver/vehicles', authenticate, async (req, res) => { + try { + const userId = req.user._id; + const { vehicle } = req.body; + + const { User } = require('../models/User'); + + const user = await User.findByIdAndUpdate( + userId, + { + $push: { 'profile.driverProfile.vehicles': vehicle }, + $set: { 'profile.driverProfile.isDriver': true }, + }, + { new: true } + ).select('profile.driverProfile.vehicles'); + + res.status(201).json({ + success: true, + message: 'Veicolo aggiunto', + data: user.profile.driverProfile.vehicles, + }); + } catch (error) { + console.error('Errore aggiunta veicolo:', error); + res.status(500).json({ + success: false, + message: "Errore durante l'aggiunta del veicolo", + error: error.message, + }); + } +}); + +/** + * @route PUT /api/trasporti/driver/vehicles/:vehicleId + * @desc Aggiorna veicolo + * @access Private + */ +router.put('/driver/vehicles/:vehicleId', authenticate, async (req, res) => { + try { + const userId = req.user._id; + const { vehicleId } = req.params; + const { vehicle } = req.body; + + const { User } = require('../models/User'); + + const user = await User.findOneAndUpdate( + { + _id: userId, + 'profile.driverProfile.vehicles._id': vehicleId, + }, + { + $set: { 'profile.driverProfile.vehicles.$': { ...vehicle, _id: vehicleId } }, + }, + { new: true } + ).select('profile.driverProfile.vehicles'); + + if (!user) { + return res.status(404).json({ + success: false, + message: 'Veicolo non trovato', + }); + } + + res.status(200).json({ + success: true, + message: 'Veicolo aggiornato', + data: user.profile.driverProfile.vehicles, + }); + } catch (error) { + console.error('Errore aggiornamento veicolo:', error); + res.status(500).json({ + success: false, + message: "Errore durante l'aggiornamento", + error: error.message, + }); + } +}); + +/** + * @route DELETE /api/trasporti/driver/vehicles/:vehicleId + * @desc Rimuovi veicolo + * @access Private + */ +router.delete('/driver/vehicles/:vehicleId', authenticate, async (req, res) => { + try { + const userId = req.user._id; + const { vehicleId } = req.params; + + const { User } = require('../models/User'); + + await User.findByIdAndUpdate(userId, { + $pull: { 'profile.driverProfile.vehicles': { _id: vehicleId } }, + }); + + res.status(200).json({ + success: true, + message: 'Veicolo rimosso', + }); + } catch (error) { + console.error('Errore rimozione veicolo:', error); + res.status(500).json({ + success: false, + message: 'Errore durante la rimozione', + error: error.message, + }); + } +}); + +/** + * @route POST /api/trasporti/driver/vehicles/:vehicleId/default + * @desc Imposta veicolo come predefinito + * @access Private + */ +router.post('/driver/vehicles/:vehicleId/default', authenticate, async (req, res) => { + try { + const userId = req.user._id; + const { vehicleId } = req.params; + + const { User } = require('../models/User'); + + // Prima rimuovi isDefault da tutti + await User.updateOne({ _id: userId }, { $set: { 'profile.driverProfile.vehicles.$[].isDefault': false } }); + + // Poi imposta quello selezionato + await User.updateOne( + { _id: userId, 'profile.driverProfile.vehicles._id': vehicleId }, + { $set: { 'profile.driverProfile.vehicles.$.isDefault': true } } + ); + + res.status(200).json({ + success: true, + message: 'Veicolo predefinito impostato', + }); + } catch (error) { + console.error('Errore impostazione default:', error); + res.status(500).json({ + success: false, + message: "Errore durante l'operazione", + error: error.message, + }); + } +}); + +// ============================================================ +// 📊 CONTRIB TYPES - Tipi di Contributo +// ============================================================ + +/** + * @route GET /api/trasporti/contrib-types + * @desc Lista tipi di contributo disponibili + * @access Public + */ +router.get('/contrib-types', async (req, res) => { + try { + const { idapp } = req.query; + + const contribTypes = await Contribtype.find({ idapp }); + + res.status(200).json({ + success: true, + data: contribTypes, + }); + } catch (error) { + console.error('Errore recupero contrib types:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il recupero', + error: error.message, + }); + } +}); + +module.exports = router; diff --git a/src/server/router/index_router_NONUSATO.js b/src/server/router/index_router_NONUSATO.js deleted file mode 100644 index 1451bee..0000000 --- a/src/server/router/index_router_NONUSATO.js +++ /dev/null @@ -1,2542 +0,0 @@ -const express = require('express'); -const router = express.Router(), - fs = require('fs'), - path = require('path'); - -const telegrambot = require('../telegrambot') - -const i18n = require('i18n'); - -const sharp = require('sharp'); - -const { - authenticate, - authenticate_noerror, - authenticate_noerror_WithUser, - authenticate_noerror_WithUserLean, -} = require('../middleware/authenticate'); - -const { ObjectId } = require('mongodb'); - -// const {ListaIngresso} = require('../models/listaingresso'); -const { Graduatoria } = require('../models/graduatoria'); - -const mongoose = require('mongoose').set('debug', false); -const { CfgServer } = require('../models/cfgserver'); - -// const uuidv4 = require('uuid/v4'); // I chose v4 ‒ you can select others - -// const ftp = require('../ftp/FTPClient'); -const formidable = require('formidable'); -const folder = path.join(__dirname, 'upload'); - -const sanitizeHtml = require('sanitize-html'); - -if (!fs.existsSync(folder)) { - fs.mkdirSync(folder); -} - -const _ = require('lodash'); - -const { User } = require('../models/user'); -const { MyGroup } = require('../models/mygroup'); -const { Circuit } = require('../models/circuit'); -const { Booking } = require('../models/booking'); -const { Operator } = require('../models/operator'); -const { Where } = require('../models/where'); -const { MyEvent } = require('../models/myevent'); -const { Contribtype } = require('../models/contribtype'); -const { PaymentType } = require('../models/paymenttype'); -const { Discipline } = require('../models/discipline'); -const { MyElem } = require('../models/myelem'); -const { Cron } = require('../models/cron'); -const { Skill } = require('../models/skill'); -const { Good } = require('../models/good'); -const { StatusSkill } = require('../models/statusSkill'); -const { Province } = require('../models/province'); -const { Sector } = require('../models/sector'); -const { SectorGood } = require('../models/sectorgood'); -const { CatGrp } = require('../models/catgrp'); -const Site = require('../models/site'); -const { Level } = require('../models/level'); -const { AdType } = require('../models/adtype'); -const { AdTypeGood } = require('../models/adtypegood'); -const { Newstosent } = require('../models/newstosent'); -const { MyPage } = require('../models/mypage'); -const { CalZoom } = require('../models/calzoom'); -const { Gallery } = require('../models/gallery'); -const { Settings } = require('../models/settings'); -const { SendMsg } = require('../models/sendmsg'); -const { SendNotif } = require('../models/sendnotif'); -const { Permission } = require('../models/permission'); -const Producer = require('../models/producer'); -const Cart = require('../models/cart'); -const OrdersCart = require('../models/orderscart'); -const Storehouse = require('../models/storehouse'); -const Provider = require('../models/provider'); -const CatProd = require('../models/catprod'); -const CatAI = require('../models/catai'); -const SubCatProd = require('../models/subcatprod'); -const Gasordine = require('../models/gasordine'); -const Product = require('../models/product'); -const Author = require('../models/author'); -const Collana = require('../models/collana'); -const { Catalog } = require('../models/catalog'); -const { RaccoltaCataloghi } = require('../models/raccoltacataloghi'); -const Publisher = require('../models/publisher'); -const Scontistica = require('../models/scontistica'); -const Department = require('../models/department'); -const { Category } = require('../models/category'); -const Group = require('../models/group'); - -const T_WEB_StatiProdotto = require('../models/t_web_statiprodotto'); -const T_WEB_Tipologie = require('../models/t_web_tipologie'); -const T_WEB_TipiFormato = require('../models/t_web_tipiformato'); - -const tools = require('../tools/general'); - -const server_constants = require('../tools/server_constants'); -const actions = require('./api/actions'); - -const shared_consts = require('../tools/shared_nodejs'); - -const globalTables = require('../tools/globalTables'); - -const UserCost = { - FIELDS_UPDATE_TELEGRAM_BOT: [ - 'profile.teleg_id', - 'profile.manage_telegram', - 'profile.admin_telegram', - 'deleted', - 'reported', - ], - - FIELDS_REQUISITI: [ - 'verified_email', - 'profile.teleg_id', - 'profile.saw_and_accepted', - 'profile.revolut', - 'profile.payeer_id', - 'profile.advcash_id', - 'profile.link_payment', - 'profile.email_paypal', - 'profile.paymenttypes', - ], -}; - -router.post(process.env.LINKVERIF_REG, (req, res) => { - const body = _.pick(req.body, ['idapp', 'idlink']); - const idapp = body.idapp; - const idlink = body.idlink; - - // Cerco l'idlink se è ancora da Verificare - - User.findByLinkreg(idapp, idlink) - .then((user) => { - if (!user) { - //console.log("NON TROVATO!"); - return res.status(404).send(); - } else { - console.log('user', user); - if (user.verified_email) { - res.send({ - code: server_constants.RIS_CODE_EMAIL_ALREADY_VERIFIED, - msg: tools.getres__("L'Email è già stata Verificata", res), - }); - } else { - user.verified_email = true; - user.lasttimeonline = new Date(); - user.save().then(() => { - //console.log("TROVATOOOOOO!"); - res.send({ - code: server_constants.RIS_CODE_EMAIL_VERIFIED, - msg: tools.getres__('EMAIL', res) + ' ' + tools.getres__('VERIF', res), - }); - }); - } - } - }) - .catch((e) => { - console.log(process.env.LINKVERIF_REG, e.message); - res.status(400).send(); - }); -}); - -router.post(process.env.ADD_NEW_SITE, async (req, res) => { - try { - const body = req.body; - const idapp = body.idappSent; - const name = body.name; - const email = body.email.toLowerCase().trim(); - - console.log('Add New Site: idapp = ', idapp, 'email=', email, 'name=', name); - - const ris = await User.addNewSite(idapp, body); - - if (ris) { - res.send(ris); - } else { - res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: 'Errore' }); - } - } catch (e) { - console.log(process.env.ADD_NEW_SITE, e.message); - res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: e }); - } -}); - -// Faccio richiesta di una Nuova Password -router.post(process.env.LINK_REQUEST_NEWPASSWORD, async (req, res) => { - try { - const body = _.pick(req.body, ['idapp', 'email', 'codetocheck']); - const idapp = body.idapp; - const email = body.email.toLowerCase().trim(); - const codetocheck = body.codetocheck ? body.codetocheck.trim() : ''; - - console.log('Request Reset Pwd:', email, ' idapp=', idapp); - - // Check if too many requests - if (await User.tooManyReqPassword(idapp, email, true)) { - let text = 'Troppe richieste di Password: ' + email; - telegrambot.sendMsgTelegramToTheManagers(idapp, text); - console.log(process.env.LINK_REQUEST_NEWPASSWORD, text, email); - res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: text }); - return false; - } - - console.log('POST ' + process.env.LINK_REQUEST_NEWPASSWORD + ' idapp= ' + idapp + ' email = ' + email); - - const reqpwd = await User.createNewRequestPwd(idapp, email, codetocheck); - if (reqpwd && reqpwd.ris) { - res.send({ code: server_constants.RIS_CODE_OK, msg: '', link: reqpwd.link }); - } else { - return res.status(200).send({ code: server_constants.RIS_CODE_EMAIL_NOT_EXIST, msg: '' }); - } - } catch (e) { - console.log(process.env.LINK_REQUEST_NEWPASSWORD, e.message); - res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: e }); - } -}); - -// Invio la Nuova Password richiesta dal reset! -// Ritorna il token per poter effettuare le chiamate... -router.post(process.env.LINK_UPDATE_PWD, async (req, res) => { - try { - const body = _.pick(req.body, ['idapp', 'email', 'tokenforgot', 'tokenforgot_code', 'password']); - const idapp = body.idapp; - const email = body.email.toLowerCase().trim(); - const tokenforgot = body.tokenforgot; - const tokenforgot_code = body.tokenforgot_code; - const password = body.password; - const msg = 'Richiesta Nuova Password: idapp= ' + idapp + ' email = ' + email; - - console.log(msg); - - // telegrambot.sendMsgTelegramToTheManagers(body.idapp, msg); - - let user = null; - - user = await User.findByLinkTokenforgot(idapp, email, tokenforgot) - .then((user) => { - return user; - }) - .catch((e) => { - console.log(process.env.LINK_UPDATE_PWD, e.message); - res.status(400).send(); - }); - - if (!user) { - user = await User.findByLinkTokenforgotCode(idapp, email, tokenforgot_code) - .then((user) => { - return user; - }) - .catch((e) => { - console.log(process.env.LINK_UPDATE_PWD, e.message); - res.status(400).send(); - }); - } - - if (!user) { - return res.send({ code: server_constants.RIS_CODE_TOKEN_RESETPASSWORD_NOT_FOUND }); - } else { - // aggiorna la nuova password - user.password = password; - user.lasttimeonline = new Date(); - - // Crea token - user.generateAuthToken(req).then((ris) => { - user.tokenforgot = ''; // Svuota il tokenforgot perché non ti servirà più... - user.tokenforgot_code = ''; // Svuota il tokenforgot perché non ti servirà più... - - // Salva lo User - user.save().then(() => { - res - .header('x-auth', ris.token) - .header('x-refrtok', ris.refreshToken) - .send({ code: server_constants.RIS_CODE_OK }); // Ritorna il token di ritorno - }); - }); - } - } catch (e) { - console.error('Error: ', e); - } -}); - -router.post('/testServer', authenticate_noerror, async (req, res) => { - try { - const test = req.body.test; - let ris = { test }; - - if (req.user) { - await tools.sendNotificationToUser(req.user._id, 'Test Server', 'Test Server OK', '/', '', 'server', []); - } - - return res.send(ris); - } catch (e) { - console.error('testServer', e.message); - return res.status(400).send(e); - } -}); - - -router.get('/test1', authenticate_noerror, async (req, res) => { - try { - const test = req.query.test; - let ris = { test }; - - if (req.user) { - await tools.sendNotificationToUser(req.user._id, 'Test Server', 'Test Server OK', '/', '', 'server', []); - } - - return res.send(ris); - } catch (e) { - console.error('testServer', e.message); - return res.status(400).send(e); - } -}); - -router.post('/settable', authenticate, async (req, res) => { - try { - const params = req.body; - const mytable = globalTables.getTableByTableName(params.table); - - let mydata = req.body.data; - let extrarec = {}; - if (mydata && mydata.hasOwnProperty('extrarec')) { - extrarec = mydata['extrarec']; - delete mydata['extrarec']; - } - - if (mydata === undefined) { - console.error('MYDATA VUOTO !'); - return res.status(400).send('Mydata VUOTO'); - } - - const fieldsvalue = { ALL: 1 }; - - mydata.idapp = req.user?.idapp; - const idapp = mydata.idapp; - - if (req.user && req.user.username) { - User.setOnLine(req.user.idapp, req.user.username); - } - - let consentito = false; - - if ( - User.isAdmin(req.user.perm) || - User.isManager(req.user.perm) || - User.isEditor(req.user.perm) || - User.isCommerciale(req.user.perm) || - User.isFacilitatore(req.user.perm) - ) { - consentito = true; - } - - if ( - !User.isAdmin(req.user.perm) && - !User.isManager(req.user.perm) && - !User.isEditor(req.user.perm) && - !User.isCommerciale(req.user.perm) && - !User.isGrafico(req.user.perm) && - !User.isFacilitatore(req.user.perm) && - (await !tools.ModificheConsentite(req, params.table, fieldsvalue, mydata ? mydata._id : '')) - ) { - // If without permissions, exit - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - - if (shared_consts.TABLES_USER_ID.includes(params.table)) { - if (!mydata.userId) mydata.userId = req.user._id; - } - - if (shared_consts.TABLES_CREATEDBY.includes(params.table)) { - if (!mydata.createdBy) mydata.createdBy = req.user.username; - } - - if (shared_consts.TABLES_UPDATE_LASTMODIFIED.includes(params.table)) { - mydata.date_updated = new Date(); - } - - if (shared_consts.TABLES_PERM_NEWREC.includes(params.table)) { - if (!consentito) { - mydata.verifyrec = false; - } - } - - if (params.table === shared_consts.TAB_MYGROUPS) { - if (shared_consts.MYGROUPS_KEY_TO_CRYPTED in mydata) { - if (mydata[shared_consts.MYGROUPS_KEY_TO_CRYPTED]) { - mydata[shared_consts.MYGROUPS_KEY_TO_CRYPTED + shared_consts.SUFFIX_CRYPTED] = tools.cryptdata( - mydata[shared_consts.MYGROUPS_KEY_TO_CRYPTED] - ); - } - } - } - - if (shared_consts.TABLES_USER_INCLUDE_MY.includes(params.table)) { - if (!mydata.admins || mydata.admins.length <= 0) { - // Aggiungi solo se non esistono Admin: - mydata.admins = []; - const indfind = mydata.admins.findIndex((rec) => rec.username === req.user.username); - - if (indfind < 0) { - mydata.admins.push({ username: req.user.username }); - } - } - } - - delete mydata['__v']; - delete mydata['__proto__']; - - const isNotNew = mydata['_id'] !== undefined && mydata['_id'] !== 0 && mydata['_id'] !== ''; - - let mytablerec = null; - - mytablerec = new mytable(mydata); - - // console.log('mytablerec', mytablerec); - - const mytablestrutt = globalTables.getTableByTableName(params.table); - - if (isNotNew) { - mytablerec.isNew = false; - } - - if ( - shared_consts.TABLES_ID_NUMBER.includes(params.table) || - shared_consts.TABLES_ID_STRING.includes(params.table) - ) { - } else if (params.table === 'hours') { - } else { - if ( - (mydata['_id'] === undefined || mydata['_id'] === '' || (mytablerec.isNew && mydata['_id'] === 0)) && - (mytablerec._id === undefined || mytablerec._id === '0' || mytablerec._id === 0) - ) { - mytablerec._id = new ObjectId(); - mydata._id = new ObjectId(); - mytablerec.isNew = true; - } - } - - const isnewrec = mytablerec.isNew; - - if (params.table === shared_consts.TAB_MYGROUPS && isnewrec) { - // Controlla se esiste già con lo stesso nome - let alreadyexist = await MyGroup.findOne({ idapp, groupname: mydata.groupname }); - if (alreadyexist) { - return res.send({ code: server_constants.RIS_CODE_REC_ALREADY_EXIST_CODE }); - } - alreadyexist = await MyGroup.findOne({ idapp, title: mydata.title }); - if (alreadyexist) { - return res.send({ code: server_constants.RIS_CODE_REC_ALREADY_EXIST_NAME }); - } - } else if (params.table === shared_consts.TAB_MYCIRCUITS && isnewrec) { - // Controlla se esiste già con lo stesso nome - let alreadyexist = await Circuit.findOne({ idapp, name: mydata.name }); - if (alreadyexist) { - return res.send({ code: server_constants.RIS_CODE_REC_ALREADY_EXIST_CODE }); - } - alreadyexist = await Circuit.findOne({ idapp, path: mydata.path }); - if (alreadyexist) { - return res.send({ code: server_constants.RIS_CODE_REC_ALREADY_EXIST_NAME }); - } - /*alreadyexist = await Circuit.findOne({idapp, symbol: mydata.symbol}); - if (alreadyexist) { - return res.send({code: server_constants.RIS_CODE_REC_ALREADY_EXIST_SYMBOL}); - } - - */ - } - - if (shared_consts.TABLES_UPDATE_LASTMODIFIED.includes(params.table)) { - mytablerec.date_updated = new Date(); - mydata.date_updated = new Date(); - } - - // console.log('mydata',mydata); - // return await mytablerec.save(). - // then(async (rec) => { - - const myPromise = new Promise((resolve, reject) => { - resolve(mytablerec._id && mytable.findById(mytablerec._id)); - }); - - // Controlla se esiste già questo record: - if (shared_consts.TABLES_FIELDS_DESCR_AND_CITY_AND_USER.includes(params.table)) { - if (mytablerec.isNew) { - const trovatoDuplicato = await mytable - .findOne({ - idapp: mytablerec.idapp, - descr: mytablerec.descr, - idCity: mytablerec.idCity, - userId: mytablerec.userId, - }) - .lean(); - if (trovatoDuplicato) { - // trovatoDuplicato - return res.status(200).send({ code: server_constants.RIS_CODE_REC_DUPLICATED_DESCR_CITY_USER, msg: '' }); - } - } - } - - return await myPromise - .then(async (doupdate) => { - if (false) { - let plainObject = mytablerec.toObject(); - console.log(plainObject); - } - - if (doupdate) return mytable.updateOne({ _id: mytablerec._id }, mydata, { new: true }); - else return mytablerec.save(); - }) - .then(async (risult) => { - let rec = null; - if (risult && risult.acknowledged) { - rec = await mytable.findById(mytablerec._id).lean(); - } else { - rec = risult; - } - - if (shared_consts.TABLES_GETCOMPLETEREC.includes(params.table)) { - return await mytablestrutt.getCompleteRecord(rec.idapp, rec._id); - } else { - return rec; - } - - // tools.mylog('rec', rec); - }) - .then(async (myrec) => { - let setnotif = false; - let typedir = 0; - let typeid = 0; - let groupnameDest = ''; - let circuitnameDest = ''; - - if (isnewrec) { - // New Record created - - if (shared_consts.TABLES_ADV_NOTIFICATION.includes(params.table)) { - typedir = shared_consts.TypeNotifs.TYPEDIR_BACHECA; - if (params.table === shared_consts.TABLES_MYGOODS) typeid = shared_consts.TypeNotifs.ID_BACHECA_NEW_GOOD; - else if (params.table === shared_consts.TABLES_MYSKILLS) - typeid = shared_consts.TypeNotifs.ID_BACHECA_NEW_SERVICE; - else if (params.table === shared_consts.TABLES_MYHOSPS) - typeid = shared_consts.TypeNotifs.ID_BACHECA_NEW_HOSP; - setnotif = true; - } - - if (shared_consts.TABLES_EVENTS_NOTIFICATION.includes(params.table)) { - typedir = shared_consts.TypeNotifs.TYPEDIR_EVENTS; - typeid = shared_consts.TypeNotifs.ID_EVENTS_NEW_REC; - setnotif = true; - } - - if (shared_consts.TABLES_GROUPS_NOTIFICATION.includes(params.table)) { - typedir = shared_consts.TypeNotifs.TYPEDIR_GROUPS; - typeid = shared_consts.TypeNotifs.ID_GROUP_NEW_REC; - groupnameDest = myrec ? myrec.groupname : ''; - setnotif = true; - } - /*if (shared_consts.TABLES_CIRCUITS_NOTIFICATION.includes(params.table)) { - typedir = shared_consts.TypeNotifs.TYPEDIR_CIRCUITS; - typeid = shared_consts.TypeNotifs.ID_CIRCUIT_NEW_REC; - circuitnameDest = myrec ? myrec.name : ''; - setnotif = (myrec.visibility === 0); // Not send a notification to others if the Circuit is HIDDEN or PRIVATE - }*/ - } - - if (setnotif) { - const myreq = { ...req }; - const myres = { ...res }; - SendNotif.createNewNotification( - myreq, - myres, - { groupnameDest, circuitnameDest }, - params.table, - myrec, - typedir, - typeid - ); - } - - if (params.table === 'circuits') { - await Circuit.updateData(myrec.idapp, myrec.name); - } - - if (params.table === shared_consts.TAB_MYGROUPS && isnewrec) { - // nuovo Record: - // aggiungi il creatore al gruppo stesso - return await User.setGroupsCmd( - mydata.idapp, - req.user.username, - myrec.groupname, - shared_consts.GROUPSCMD.SETGROUP, - true, - req.user.username - ).then((ris) => { - return res.send({ rec: myrec, ris }); - }); - } else if (params.table === shared_consts.TAB_MYCIRCUITS && isnewrec) { - // nuovo Circuito: - await User.setCircuitCmd( - mydata.idapp, - req.user.username, - myrec.name, - shared_consts.CIRCUITCMD.CREATE, - true, - req.user.username, - extrarec - ).then((ris) => { - return res.send({ rec: myrec, ris }); - }); - - // aggiungi il creatore al Circuito stesso - return await User.setCircuitCmd( - mydata.idapp, - req.user.username, - myrec.name, - shared_consts.CIRCUITCMD.SET, - true, - req.user.username, - extrarec - ).then((ris) => { - return res.send({ rec: myrec, ris }); - }); - } - - return res.send({ rec: myrec, ris: null }); - }) - .catch(async (e) => { - console.error('settable', e.message); - if (e.code === 11000) { - const id = mytablerec._id; - delete mytablerec._doc['_id']; - const myfields = mytablerec._doc; - if (!myfields.userId) { - myfields.userId = req.user._id.toString(); - } - return await mytablestrutt - .findByIdAndUpdate(id, { $set: myfields }) - .then(async (rec) => { - return res.send({ rec }); - }) - .catch((err) => { - tools.mylog('error: ', err.message); - return res.status(400).send(err); - }); - } else { - console.log(e.message); - } - }); - } catch (e) { - console.error('settable', e.message); - return res.status(400).send(e); - } -}); - -router.post('/setsubrec', authenticate, (req, res) => { - const params = req.body; - const mytable = globalTables.getTableByTableName(params.table); - const mydata = req.body.data; - - mydata.idapp = req.user.idapp; - - let mytablerec = new mytable(mydata); - // console.log('mytablerec', mytablerec); - - const mytablestrutt = globalTables.getTableByTableName(params.table); - - const rec = mytablestrutt - .createNewSubRecord(mydata.idapp, req) - .then((rec) => { - // tools.mylog('rec', rec); - return res.send(rec); - }) - .catch((e) => {}); - - return res.send(rec); - - return mytablerec - .save() - .then((rec) => { - // tools.mylog('rec', rec); - return res.send(rec); - }) - .catch((e) => { - if (e.code === 11000) { - const id = mytablerec._id; - delete mytablerec._doc['_id']; - const myfields = mytablerec._doc; - if (!myfields.userId) { - myfields.userId = req.user._id.toString(); - } - return mytablestrutt - .findByIdAndUpdate(id, { $set: myfields }) - .then(async (rec) => { - return res.send(rec); - }) - .catch((err) => { - tools.mylog('error: ', err.message); - return res.status(400).send(err); - }); - } else { - console.log(e.message); - } - }); -}); - -router.post('/getobj', authenticate_noerror, async (req, res) => { - try { - let cmd = req.body.cmd; - let idapp = req.user ? req.user.idapp : sanitizeHtml(req.body.idapp); // Cambiato from params.idapp a req.body.idapp - let ris = null; - - if (cmd === 'lista_editori') { - ris = await User.find( - { - idapp, - perm: { $bitsAnySet: 0b10000 }, - }, - { username: 1, name: 1, surname: 1 } - ) - .sort({ username: 1 }) - .lean(); - } else if (cmd === 'lista_referenti') { - ris = await User.find( - { - idapp, - perm: { $bitsAnySet: 0b110010000 }, - }, - { username: 1, name: 1, surname: 1 } - ) - .sort({ username: 1 }) - .lean(); - } - - // Invia la risposta - res.status(200).send({ code: server_constants.RIS_CODE_OK, data: ris }); - } catch (e) { - console.error(`ERROR getobj`, e.message); - res.status(200).send({ code: server_constants.RIS_CODE_OK, data: [] }); - } -}); - -router.post('/gettable', authenticate_noerror, (req, res) => { - let params = req.body; - - params.table = sanitizeHtml(params.table); - - if (!shared_consts.TABLES_ENABLE_GETTABLE_FOR_NOT_LOGGED.includes(params.table) && !req.user) { - return res.status(server_constants.RIS_CODE_HTTP_FORBIDDEN_PERMESSI).send({msgerr: 'Non hai i permessi per vedere questa pagina.'}); - } - - let idapp = req.user ? req.user.idapp : sanitizeHtml(params.idapp); - const mytable = globalTables.getTableByTableName(params.table); - //console.log('mytable', mytable); - if (!mytable) { - console.log(`Table ${params.table} not found`); - return res.status(400).send({}); - } - - try { - if (req.user && req.user.username) { - User.setOnLine(req.user.idapp, req.user.username); - } - - return mytable - .executeQueryTable(idapp, params, req.user) - .then((ris) => { - // console.log('ris=', ris); - return res.send(ris); - }) - .catch((e) => { - console.error('gettable: ' + e.message); - res.status(400).send(e); - }); - } catch (e) { - console.error(`ERROR gettable ${params.table}: `, e.message, 'params', params); - res.status(500).send(e); - } -}); - -router.post('/getexp', authenticate, (req, res) => { - const params = req.body; - let idapp = req.user.idapp; - const myUser = globalTables.getTableByTableName('users'); - // console.log('mytable', mytable); - if (!myUser || params.filtersearch2 !== 'fdsgas1') { - return res.status(400).send({}); - } - - if (!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm)) { - // If without permissions, exit - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - - try { - if (params.table === 'exp') { - return myUser - .find( - { - idapp, - $or: [{ deleted: { $exists: false } }, { deleted: { $exists: true, $eq: false } }], - }, - { - username: 1, - name: 1, - surname: 1, - email: 1, - reported: 1, - date_report: 1, - username_who_report: 1, - 'profile.teleg_id': 1, - verified_by_aportador: 1, - 'profile.username_telegram': 1, - 'profile.firstname_telegram': 1, - 'profile.lastname_telegram': 1, - } - ) - .then((ris) => { - return res.send(ris); - }) - .catch((e) => { - console.error('getexp: ' + e.message); - res.status(400).send(e); - }); - } - } catch (e) { - console.error(`ERROR getexp ${params.table}: `, e.message, 'params', params); - res.status(500).send(e); - } -}); - -router.post('/pickup', authenticate_noerror, (req, res) => { - const params = req.body; - let idapp = req.body.idapp; - let mytable = globalTables.getTableByTableName(params.table); - // console.log('mytable', mytable); - if (!mytable) { - console.log(`Table ${params.table} not found`); - return res.status(400).send({}); - } - - return mytable - .executeQueryPickup(idapp, params) - .then((ris) => { - return res.send(ris); - }) - .catch((e) => { - console.log(e.message); - res.status(400).send(e); - }); -}); - -router.post('/getpage', async (req, res) => { - const params = req.body; - const idapp = req.body.idapp; - const mypath = params.path; - - let found = await MyPage.findOne({ idapp, path: mypath }) - .then((ris) => { - if (ris && ris._doc) return res.send({ mypage: ris._doc }); - else return null; - }) - .catch((e) => { - console.log(e.message); - res.status(400).send(e); - }); - - if (!found) { - // trova quelli con il : - let regexp = new RegExp(`:`, 'ig'); - const searchpagesSpec = await MyPage.find({ idapp, path: { $regex: regexp } }); - - if (searchpagesSpec) { - let arrsubpath = mypath.split('/'); - for (let i = 0; i < searchpagesSpec.length; i++) { - let arrsubstr = searchpagesSpec[i].path.split('/'); - - if (arrsubpath.length === arrsubpath.length) { - let mypathbuild = ''; - - for (let j = 0; j < arrsubstr.length; j++) { - if (arrsubstr[j].includes(':')) { - mypathbuild += arrsubpath[j] + '/'; - } else { - mypathbuild += arrsubstr[j] + '/'; - } - } - - if (mypath + '/' === mypathbuild) { - return res.send({ mypage: searchpagesSpec[i] }); - } - } - } - } - - return await MyPage.findOne({ idapp, path: mypath }).then((ris) => { - return res.send({ mypage: ris }); - }); - } - return found; -}); - -router.post('/savepage', authenticate, async (req, res) => { - const params = req.body; - const idapp = req.body.idapp; - const mypage = params.page; - - try { - if (!mypage?._id) { - // creazione nuovo record - return await MyPage.create({ idapp, ...mypage }) - .then((ris) => { - if (ris) { - return res.send({ code: server_constants.RIS_CODE_OK, mypage: ris }); - } - return res.send({ code: server_constants.RIS_CODE_ERR, msg: '' }); - }) - .catch((e) => { - console.log(e.message); - res.status(400).send(e); - }); - } else { - // update record - return await MyPage.findOneAndUpdate({ idapp, _id: mypage._id }, mypage, { upsert: true, new: true }) - .then((ris) => { - if (ris) { - return res.send({ code: server_constants.RIS_CODE_OK, mypage: ris }); - } - return res.send({ code: server_constants.RIS_CODE_ERR, msg: '' }); - }) - .catch((e) => { - console.log(e.message); - res.status(400).send(e); - }); - } - } catch (e) { - console.error('Error', e); - } -}); - -async function exportPage(idapp, pageId) { - try { - const myexp = { - mypages: [], - myelems: [], - }; - - // Trova il record di Page da duplicare - const pageToExp = await MyPage.find({ _id: pageId, idapp }).lean(); - if (!pageToExp) { - console.error('Page not found.'); - return; - } - - myexp.mypages = [...pageToExp]; - - // Trova tutti gli elementi associati a Page da duplicare - const elemsToExp = await MyElem.find({ idapp, idPage: pageId }).lean(); - - myexp.myelems = [...elemsToExp]; - - const jsonString = JSON.stringify(myexp); - - if (jsonString) { - console.log('Esportazione completata con successo.'); - - return jsonString; - } - - return ''; - } catch (error) { - console.error("Errore durante l'esportazione:", error); - return ''; - } -} - -async function upsertRecord(table, record, appId, newIdPage = null) { - let newId = null; - - const existingRecord = await table.findOne({ idapp: appId, _id: record._id }); // Assumendo che `record` ha un ID - if (existingRecord) { - if (newIdPage && record.idPage) { - record.idPage = newIdPage; - } - const modif = await table.updateOne({ _id: record._id }, { $set: { ...record, idapp: appId } }); - wasModified = modif.modifiedCount > 0; - } else { - // Se sono sulla tabella mypages - if (table.modelName === 'MyPage') { - // Controlla se esiste già la pagina in questione - const existingRecPage = await table.findOne({ idapp: appId, path: record.path }); - if (existingRecPage) { - // Esiste già ! quindi aggiorno questo record, e cancello tutti gli elementi precedenti ! - delete record._id; - const modif = await table.updateOne({ _id: existingRecPage._id }, { $set: { ...record, idapp: appId } }); - - const tableElems = globalTables.getTableByTableName('myelems'); - if (tableElems) risdel = await tableElems.deleteMany({ idapp: appId, idPage: existingRecPage._id }); - - newId = existingRecPage._id.toString(); - return { ImportedRecords: false, newId }; // Torna il numero di record importati (1 in questo caso) - } - } - // Se non esiste, allora la creo, con un id Nuovo ! - - const existingRecSameID = await table.findOne({ _id: record._id }); // Assumendo che `record` ha un ID - if (existingRecSameID) { - // Se - newId = new ObjectId(); - record._id = newId; - } - if (newIdPage && record.idPage) { - record.idPage = newIdPage; - } - const newrec = { - ...record, - idapp: appId, - }; - - const ris = await table.create(newrec); - wasModified = !!ris; - } - - return { ImportedRecords: wasModified, newId }; // Torna il numero di record importati (1 in questo caso) -} - -async function importPage(req, idapp, jsonString) { - try { - console.log('INIZIO importPage'); - - // Parsing dei dati JSON - const myexp = JSON.parse(jsonString); - - // Assicurati che i dati siano ben strutturati - if (!myexp) { - console.error("Dati non validi per l'importazione."); - return; - } - - let totalImportedRecords = 0; - let newIdPage = null; - - const importCounts = []; // Array per memorizzare i conteggi di importazione - - // Cicla prima sulle pagine - for (const key in myexp) { - if (myexp.hasOwnProperty(key)) { - let ImportedRecordstemp = 0; - const tableName = key; - - // Verifica se la tabella esiste - if (tableName) { - const table = globalTables.getTableByTableName(tableName); - - if (tableName === 'mypages') { - if ( - User.isAdmin(req.user.perm) || - User.isEditor(req.user.perm) || - User.isCommerciale(req.user.perm) || - User.isCollaboratore(req.user.perm) - ) { - for (const page of myexp.mypages) { - const { ImportedRecords, newId } = await upsertRecord(table, page, idapp); - if (!newIdPage && newId) { - newIdPage = newId; - } - ImportedRecordstemp += ImportedRecords ? 1 : 0; - } - } - } - totalImportedRecords += ImportedRecordstemp; - } - if (ImportedRecordstemp > 0) importCounts.push({ tableName, count: ImportedRecordstemp }); - } - } - - // Ciclo su ogni proprietà di myexp - for (const key in myexp) { - if (myexp.hasOwnProperty(key)) { - let ImportedRecordstemp = 0; - const tableName = key; - - // Verifica se la tabella esiste - if (tableName) { - const table = globalTables.getTableByTableName(tableName); - - if (tableName === 'myelems') { - if (User.isEditor(req.user.perm) || User.isCommerciale(req.user.perm)) { - for (const elem of myexp.myelems) { - const { ImportedRecords, newId } = await upsertRecord(table, elem, idapp, newIdPage); - ImportedRecordstemp += ImportedRecords ? 1 : 0; - } - } - } else if (tableName === 'myusers') { - if (User.isManager(req.user.perm)) { - for (const user of myexp.myusers) { - const { ImportedRecords, newId } = await upsertRecord(table, user, idapp); - ImportedRecordstemp += ImportedRecords ? 1 : 0; - } - } - } else if (table && tableName !== 'mypages') { - // Tutte le altre tabelle - if (User.isManager(req.user.perm)) { - for (const rec of myexp[key]) { - const { ImportedRecords, newId } = await upsertRecord(table, rec, idapp); - ImportedRecordstemp += ImportedRecords ? 1 : 0; - } - } - } - - totalImportedRecords += ImportedRecordstemp; - if (ImportedRecordstemp > 0) importCounts.push({ tableName, count: ImportedRecordstemp }); - } - } - } - - if (totalImportedRecords) { - console.log(`Importazione completata con successo. Totale record importati: ${totalImportedRecords}`); - } - - return { imported: totalImportedRecords, importCounts }; - } catch (error) { - console.error("Errore durante l'importazione:", error); - } -} - -router.post('/duppage', authenticate, async (req, res) => { - const params = req.body; - const idapp = req.body.idapp; - const mypath = params.path; - const newpath = params.newpath; - - try { - let found = await MyPage.findOne({ idapp, path: mypath }) - .then(async (ris) => { - const result = await globalTables.duplicatePage(ris._id, newpath); - if (result && result.newpage) { - return res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - } else { - return res.send({ code: server_constants.RIS_CODE_ERR, msg: '' }); - } - }) - .catch((e) => { - console.log(e.message); - res.status(400).send(e); - }); - } catch (e) { - console.error('Error', e); - } -}); - -router.post('/exppage', authenticate, async (req, res) => { - const params = req.body; - const idapp = req.body.idapp; - const mypath = params.path; - - try { - let found = await MyPage.findOne({ idapp, path: mypath }) - .then(async (ris) => { - const resultJSon = await exportPage(idapp, ris._id); - if (resultJSon) { - return res.send({ code: server_constants.RIS_CODE_OK, json: resultJSon }); - } else { - return res.send({ code: server_constants.RIS_CODE_ERR, msg: '' }); - } - }) - .catch((e) => { - console.log(e.message); - res.status(400).send(e); - }); - } catch (e) { - console.error('Error', e); - } -}); -router.post('/imppage', authenticate, async (req, res) => { - const params = req.body; - const idapp = req.body.idapp; - const jsonString = params.jsonString; - - try { - const result = await importPage(req, idapp, jsonString); - if (result) { - return res.send({ code: server_constants.RIS_CODE_OK, ris: result }); - } else { - return res.send({ code: server_constants.RIS_CODE_ERR, ris: '' }); - } - } catch (e) { - console.log(e.message); - res.status(400).send(e); - } -}); - -router.patch('/setlang', authenticate, async (req, res) => { - const username = req.body.data.username; - const idapp = req.user.idapp; - const mydata = req.body.data; - - const lang = mydata.lang; - - const fieldsvalue = { - lang, - }; - - if (!!lang) { - const rec = await User.findByUsername(idapp, username, false); - let ris = null; - if (!!rec) ris = await User.findByIdAndUpdate(rec.id, { $set: fieldsvalue }); - - if (!!ris) { - return res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - } - - res.status(400).send(); - } -}); - -function toObjId(id) { - try { - return Types.ObjectId.isValid(id) ? new Types.ObjectId(id) : id; - } catch { - return id; - } -} - -async function updateElemInsideColumn({ idapp, id, mydata }) { - const { fieldsvalue = {} } = mydata || {}; - - // 1) id della colonna parent (dove si trova l'elemento da aggiornare) - let idElemParent = null; - if (fieldsvalue && fieldsvalue.idElemParent) { - idElemParent = fieldsvalue.idElemParent; - } - - // 2) recupera il documento parent "top-level" (es. section) che contiene la colonna - const myelemParent = await MyElem.findParentElem(idapp, idElemParent); - if (!myelemParent) { - return { ok: false, msg: 'Parent non trovato' }; - } - - // 3) costruisci il $set campo-per-campo - // path target: rows.$[].columns.$[col].elems.$[el]. - const setOps = {}; - for (const [key, value] of Object.entries(fieldsvalue)) { - if (key === '_id') continue; // non tocchiamo l'_id - setOps[`rows.$[].columns.$[col].elems.$[el].${key}`] = value; - } - - // Se non c’è nulla da settare, esci pulito - if (Object.keys(setOps).length === 0) { - return { ok: true, msg: 'Nulla da aggiornare' }; - } - - // 4) esegui l’update - const filter = { _id: toObjId(myelemParent._id) }; - const update = { $set: setOps }; - const options = { - arrayFilters: [{ 'col._id': toObjId(idElemParent) }, { 'el._id': toObjId(id) }], - }; - - const result = await MyElem.updateOne(filter, update, options); - - return { - ok: result?.matchedCount > 0, - modified: result?.modifiedCount || 0, - result, - }; -} - -router.patch('/chval', authenticate, async (req, res) => { - // const idapp = req.body.idapp; - const id = req.body.data.id; - const idapp = req.user.idapp; - const mydata = req.body.data; - - try { - const mytable = globalTables.getTableByTableName(mydata.table); - let fieldsvalue = mydata.fieldsvalue; - const unset = mydata.unset; - - const { Account } = require('../models/account'); - - // tools.mylogshow('PATCH CHVAL: ', id, fieldsvalue); - - // If I change my record... - if ( - !User.isAdmin(req.user.perm) && - !User.isManager(req.user.perm) && - !User.isEditor(req.user.perm) && - !User.isCommerciale(req.user.perm) && - !User.isFacilitatore(req.user.perm) && - (await !tools.ModificheConsentite(req, mydata.table, fieldsvalue, id)) && - !(mydata.table === 'accounts' && (await Account.canEditAccountAdmins(req.user.username, mydata.id))) - ) { - // If without permissions, exit - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - - const camporequisiti = UserCost.FIELDS_REQUISITI.includes(Object.keys(fieldsvalue)[0]); - - let allData = {}; - let username = ''; - if (mydata.table === 'users') { - if (camporequisiti) { - allData = {}; - allData.myuser = await User.getUserById(idapp, id); - username = allData.myuser.username; - if (!!allData.myuser) allData.precDataUser = await User.getInfoUser(idapp, allData.myuser.username); - else allData.precDataUser = null; - // allData.useraportador = await ListaIngresso.getUserByInvitante_Username(idapp, allData.myuser.aportador_solidario); - // allData.precDataAportador = await getInfoUser(idapp, allData.myuser.aportador_solidario); - } - } - - let index = 0; - - let recoldnave = null; - - let myuser = null; - let mydatamsg = {}; - - let flotta = null; - let strflotta = ''; - - if (shared_consts.TABLES_UPDATE_LASTMODIFIED.includes(mydata.table)) { - fieldsvalue.date_updated = new Date(); - } - - const numobj = tools.getNumObj(fieldsvalue); - if (numobj === 1 && fieldsvalue['_id']) { - const myrec = await mytable.findById(id); - const myidDel = myrec['_id']; - myrec['_id'] = fieldsvalue['_id']; - return await mytable.insertMany(myrec).then((ris) => { - if (ris) { - return mytable.deleteMany({ _id: myidDel }).then((risdel) => { - return res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - }); - } else { - return res.status(404).send(); - } - }); - } - - const updatebot = UserCost.FIELDS_UPDATE_TELEGRAM_BOT.includes(Object.keys(fieldsvalue)[0]); - - tools.refreshAllTablesInMem(idapp, mydata.table, updatebot, username); - - if (mydata.table === shared_consts.TAB_SETTINGS) { - if (shared_consts.KEY_TO_CRYPTED.includes(fieldsvalue.key)) { - fieldsvalue.crypted = true; - fieldsvalue.value_str = tools.cryptdata(fieldsvalue.value_str); - } - } - - if (mydata.table === shared_consts.TAB_SITES) { - if (shared_consts.SITES_KEY_TO_CRYPTED in fieldsvalue) { - fieldsvalue[shared_consts.SITES_KEY_TO_CRYPTED] = tools.cryptdata( - fieldsvalue[shared_consts.SITES_KEY_TO_CRYPTED] - ); - } - } - - let precRec = null; - - if (mydata.table === 'accounts') { - precRec = await mytable.findById(id); - } - - if (mydata.table === 'arrvariazioni') { - let chiave = null; - let valore = null; - - for (const [key, value] of Object.entries(fieldsvalue)) { - chiave = key; - valore = value; - } - - if (chiave) { - // Costruiamo la stringa di assegnazione dinamica - fieldsvalue = { [`arrvariazioni.0.${chiave}`]: valore }; - } - } - if (mydata.table === 'myelems' && mydata.fieldsvalue.idElemParent) { - // se è un myelem, allora cerca l'id anche sugli elementi contenuti in elems, (sotto rows e columns) - // quindi devo aggiornare quell'elemento in elems - - const risult = await updateElemInsideColumn({ idapp, id, mydata }); - - if (risult.ok) { - res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - } - - /*return await mytable.findByIdAndUpdate(id, { $set: fieldsvalue }, { new: true }).then(async (rec) => { - res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - });*/ - } else { - return await mytable - .findByIdAndUpdate(id, { $set: fieldsvalue }, { new: true }) - .then(async (rec) => { - // tools.mylogshow(' REC TO MODIFY: ', rec); - if (!rec) { - return res.status(404).send(); - } else { - let addmsg = ''; - - if (mydata.notifBot) { - // Send Notification to the BOT - await telegrambot.sendMsgTelegram(idapp, mydata.notifBot.un, mydata.notifBot.txt); - if (!!addmsg) await telegrambot.sendMsgTelegram(idapp, mydata.notifBot.un, addmsg); - let addtext = '[Msg Inviato a ' + mydata.notifBot.un + ']:' + '\n' + mydata.notifBot.txt; - telegrambot.sendMsgTelegramToTheManagers(idapp, addtext, true); - - if (!!flotta) tools.writeFlottaLog(idapp, addtext, flotta.riga, flotta.col_prima); - } - - if (mydata.table === 'accounts') { - let msg = ''; - if (rec.circuitId) circuit = await Circuit.getCircuitByCircuitId(rec.circuitId); - - let dest = rec.groupname ? rec.groupname : rec.username; - let valprec = 0; - - if ('saldo' in fieldsvalue) { - valprec = precRec && precRec.saldo ? precRec.saldo : 0; - msg = i18n.__( - 'SALDO_VARIATO', - circuit.name, - req.user.username, - dest, - valprec, - fieldsvalue.saldo, - circuit.symbol - ); - } else if ('fidoConcesso' in fieldsvalue) { - valprec = precRec && precRec.fidoConcesso ? precRec.fidoConcesso : 0; - msg = i18n.__( - 'FIDOCONCESSO_VARIATO', - circuit.name, - req.user.username, - dest, - valprec, - fieldsvalue.fidoConcesso, - circuit.symbol - ); - } else if ('qta_maxConcessa' in fieldsvalue) { - valprec = precRec && precRec.qta_maxConcessa ? precRec.qta_maxConcessa : 0; - msg = i18n.__( - 'QTAMAX_VARIATO', - circuit.name, - req.user.username, - dest, - valprec, - fieldsvalue.qta_maxConcessa, - circuit.symbol - ); - } - - if (msg) { - telegrambot.sendMsgTelegramToTheManagers(idapp, msg); - telegrambot.sendMsgTelegramToTheAdminsOfCircuit(idapp, circuit.path, msg); - } - } - - if (mydata.table === 'users') { - if ('profile.resid_province' in fieldsvalue) { - const card = fieldsvalue.hasOwnProperty('profile.resid_card') ? fieldsvalue['profile.resid_card'] : ''; - // Controlla se esiste il Circuito di questa provincia, se non esiste lo crea! - await Circuit.createCircuitIfNotExist(req, idapp, fieldsvalue['profile.resid_province'], card); - } - - if (camporequisiti) { - await User.checkIfSbloccatiRequisiti(idapp, allData, id); - } - - if ('aportador_solidario' in fieldsvalue) { - let ind_order_ingr = mydata.ind_order_ingr; - // SERVE SE CI METTO LE MINUSCOLE/MAIUSCOLE SBAGLIATE in invitante_username! - const myuserfound = await User.findByUsername(idapp, fieldsvalue.aportador_solidario, false); - if (!!myuserfound) { - if (!!myuserfound._id && !myuserfound.deleted) { - const aportador = await User.getUsernameById(idapp, myuserfound._id); - fieldsvalue.aportador_solidario = aportador; - //Aggiorna record ! - await mytable.findByIdAndUpdate(id, { $set: fieldsvalue }); - } - } else { - res.send({ - code: server_constants.RIS_CODE_ERR, - msg: 'Non aggiornato', - }); - res.status(400).send(); - return false; - } - } else if ('deleted' in fieldsvalue) { - let msg = ''; - if (fieldsvalue.deleted) msg = 'cancellato (nascosto)'; - else msg = 'Ripristinato'; - - await telegrambot.sendMsgTelegramToTheManagers( - idapp, - `L\'utente ` + - tools.getNomeCognomeEUserNameByUser(rec) + - ` è stato ${msg} da ` + - tools.getNomeCognomeEUserNameByUser(req.user) - ); - } - } - - if (await tools.ModificheConsentite(req, mydata.table, fieldsvalue)) { - let msg = ''; - if (mydata.table === 'users') { - if ('aportador_solidario' in fieldsvalue) { - const nomecognomenuovo = await User.getNameSurnameByUsername(idapp, fieldsvalue.aportador_solidario); - const nomecognomeas = await User.getNameSurnameByUsername(idapp, rec.aportador_solidario); - msg = - `Variato l'invitante di ` + - tools.getNomeCognomeEUserNameByUser(rec) + - '\nmodificato da ' + - tools.getNomeCognomeEUserNameByUser(req.user) + - ' \n' + - 'Prima: ' + - nomecognomeas + - ' (' + - rec.aportador_solidario + - ')\n' + - 'Dopo: ' + - nomecognomenuovo + - ' (' + - fieldsvalue.aportador_solidario + - ') ]'; - - // Metti l'iniziale - if (!(await User.AportadorOrig(id))) { - await mytable.findByIdAndUpdate( - id, - { $set: { aportador_iniziale: fieldsvalue.aportador_solidario } }, - { new: false } - ); - } - } - } - - if (msg !== '') telegrambot.sendMsgTelegramToTheManagers(idapp, msg); - } - - res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - } - }) - .catch((e) => { - tools.mylogserr('Error patch USER: ', e.message); - res.status(400).send(); - }); - } - } catch (e) { - tools.mylogserr('Error chval: ', e.message); - res.status(400).send(); - } -}); - -router.patch('/askfunz', authenticate, async (req, res) => { - // const idapp = req.body.idapp; - const id = req.body.data.id; - // const ind_order = req.body.data.ind_order; - // const username = req.body.data.username; - const idapp = req.user.idapp; - const mydata = req.body.data; - - let entra = false; - if (!entra) { - // If I change my record... - if ( - !User.isAdmin(req.user.perm) && - !User.isManager(req.user.perm) && - !User.isFacilitatore(req.user.perm) && - req.user._id.toString() !== id - ) { - // If without permissions, exit - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - } - - if (mydata.myfunc === shared_consts.CallFunz.DAMMI_PRIMO_UTENTE_LIBERO) { - const userfree = await Graduatoria.getFirstUserGradFree(idapp); - - if (!!userfree) return res.send({ code: server_constants.RIS_CODE_OK, out: userfree }); - /*} else if (mydata.myfunc === shared_consts.CallFunz.GET_VALBYTABLE) { - const mytable = globalTables.getTableByTableName(mydata.table); - const coltoshow = { - [mydata.coltoshow]: 1 - }; - - const ris = await mytable.findOne({ _id: id }, coltoshow); - - return ris; - } else if (mydata.myfunc === shared_consts.CallFunz.SET_VALBYTABLE) { - const mytable = globalTables.getTableByTableName(mydata.table); - const value = mydata.value; - const coltoset = { - [mydata.coltoshow]: value - }; - - const ris = await mytable.findOneAndUpdate({ _id: id }, { $set: coltoset }, { new: false }); - if (!!ris) - return res.send({ code: server_constants.RIS_CODE_OK });*/ - } - - return res.send({ code: server_constants.RIS_CODE_ERR }); -}); - -router.patch('/callfunz', authenticate, async (req, res) => { - // const idapp = req.body.idapp; - const id = req.body.data.id; - // const ind_order = req.body.data.ind_order; - const username = req.body.data.username; - const idapp = req.user.idapp; - const mydata = req.body.data; - - // const telegrambot = require('../telegram/telegrambot'); - - try { - let entra = false; - if ( - mydata.myfunc === shared_consts.CallFunz.AGGIUNGI_NUOVO_IMBARCO || - mydata.myfunc === shared_consts.CallFunz.CANCELLA_IMBARCO - ) { - entra = true; - } - if (!entra) { - // If I change my record... - if ( - !User.isAdmin(req.user.perm) && - !User.isManager(req.user.perm) && - !User.isFacilitatore(req.user.perm) && - req.user._id.toString() !== id - ) { - // If without permissions, exit - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - } - - let myuser = await User.findOne({ idapp, username }); - - let fieldsvalue = {}; - - if (mydata.myfunc === shared_consts.CallFunz.ZOOM_GIA_PARTECIPATO) { - if (!!myuser.username) { - let FormDaMostrare = telegrambot.getFormDaMostrare(idapp, mydata.myfunc, myuser); - - await telegrambot.sendMsgTelegramToTheManagers( - idapp, - `L\'utente ${myuser.name} ${myuser.surname} (${myuser.username}) ha detto di aver già visto lo Zoom di Benvenuto`, - false, - FormDaMostrare - ); - - const ris = await User.findByIdAndUpdate(myuser.id, { $set: { 'profile.ask_zoom_partecipato': true } }); - if (ris) return res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - } - } - - return res.send({ code: server_constants.RIS_CODE_ERR }); - } catch (e) { - console.log(e.message); - res.status(400).send(); - } -}); - -router.get('/copyfromapptoapp/:idapporig/:idappdest', async (req, res) => { - const idapporig = req.params.idapporig; - const idappdest = req.params.idappdest; - const idcode = req.params.code; - if (!idapporig || !idappdest || idcode !== 'ASD3429Kjgà#@cvX') res.status(400).send(); - - const mytablesstr = ['settings', 'users', 'templemail', 'destnewsletter']; - - try { - let numrectot = 0; - for (const table of mytablesstr) { - const mytable = globalTables.getTableByTableName(table); - - tools.mylogshow('copyfromapptoapp: ', table, mytable); - - await mytable.DuplicateAllRecords(idapporig, idappdest).then((numrec) => { - // tools.mylogshow(' REC TO MODIFY: ', rec); - numrectot += numrec; - }); - } - - res.send({ code: server_constants.RIS_CODE_OK, msg: '', numrectot }); - } catch (e) { - tools.mylogserr('Error copyfromapptoapp: ', e); - res.status(400).send(); - } -}); - -router.delete('/delrec/:table/:id', authenticate, async (req, res) => { - const id = req.params.id; - // const idapp = req.user.idapp; - const tablename = req.params.table; - // let notifBot = false; - // const idapp = req.body.idapp; - - console.log('delete RECORD: id', id, 'table', tablename); - - const mytable = globalTables.getTableByTableName(tablename); - - const fields = { ALL: 1 }; - - if ( - !User.isAdmin(req.user.perm) && - !User.isManager(req.user.perm) && - tablename !== 'extralist' && - (await !tools.ModificheConsentite(req, tablename, fields, id, req.user)) - ) { - // If without permissions, exit - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - - let cancellato = false; - - //++Tools: Notify... - tools.NotifyIfDelRecord(tablename); - - // if (!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm)) { - if (true) { - if (tablename === 'users') { - let fieldsvalue = { - deleted: true, - date_deleted: new Date(), - }; - const utente = await mytable.findById(id); - - if (utente) { - idapp = utente.idapp; - await mytable.findByIdAndUpdate(id, { $set: fieldsvalue }); - - // ... - let text = - `L\'utente ${utente.username} (${utente.name} ${utente.surname}) si è cancellato dal sito ` + - tools.getNomeAppByIdApp(utente.idapp) + - ` Deleted = true`; - telegrambot.sendMsgTelegramToTheManagers(idapp, text); - } - - if (!User.isAdmin(req.user.perm)) { - cancellato = true; - return res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - } - } - } - - if (tablename === 'catalogs') { - // Devo cancellare anche la pagina associato al Catalogo! - - const myrec = await mytable.findOne({ _id: id }).lean(); - if (myrec.idPageAssigned) { - await MyPage.deleteOne({ _id: myrec.idPageAssigned }).then((rec) => { - if (!rec) { - console.log('Errore cancellazione pagina associata al catalogo'); - } - }); - } - } - - let ris = null; - - if (!cancellato) { - // ELIMINA VERAMENTE IL RECORD !!! - ris = await mytable - .deleteOne({ _id: id }) - .then((rec) => { - if (!rec) { - // res.status(404).send(); - return false; - } - - if (tablename === shared_consts.TAB_MYGROUPS) { - // Se è un gruppo, allora cancella anche tutti i suoi riferimenti - User.removeAllUsersFromMyGroups(rec.idapp, rec.groupname); - } else if (tablename === shared_consts.TAB_MYCIRCUITS) { - // Se è un gruppo, allora cancella anche tutti i suoi riferimenti - User.removeAllUsersFromMyCircuits(rec.idapp, rec.name); - } else if (tablename === shared_consts.TAB_MYPAGES) { - // Cancella tutti gli elementi di quella pagina - MyElem.deleteAllFromThisPage(rec._id); - } - - tools.refreshAllTablesInMem(rec.idapp, tablename, true, rec.username); - - cancellato = true; - - tools.mylog('DELETED ', rec._id); - - return true; - }) - .catch((e) => { - console.log(e.message); - res.status(400).send(); - }); - } - - if (cancellato) { - // Do extra things after deleted - //let ris2 = await actions.doOtherlasThingsAfterDeleted(tablename, myrec, notifBot, req); - if (!!ris) { - return res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - } - } - - res.send({ code: server_constants.RIS_CODE_ERR, msg: '' }); - return ris; -}); - -router.post('/duprec/:table/:id', authenticate, async (req, res) => { - const id = req.params.id; - const tablename = req.params.table; - // const idapp = req.body.idapp; - - console.log('id', id, 'table', tablename); - - const mytable = globalTables.getTableByTableName(tablename); - - if (!req.user) { - return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - - /* if (!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm)) { - // If without permissions, exit - return res.status(404). - send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } */ - - return await mytable.findById(id).then(async (mydata) => { - const datadup = tools.CloneRecordToNew(mydata, mytable.modelName); - const mynewrec = new mytable(datadup); - - return await mynewrec - .save() - .then(async (rec) => { - if (!rec) { - return res.status(404).send(); - } - - tools.mylog('DUPLICATED ', rec); - - // Do extra things after deleted - return await actions.doOtherThingsAfterDuplicated(tablename, rec).then(({ myrec }) => { - // ... - mytable.findById(myrec._id).then((record) => { - return res.send({ code: server_constants.RIS_CODE_OK, record, msg: '' }); - }); - }); - }) - .catch((e) => { - console.error(e.message); - res.status(400).send(); - }); - }); -}); - -router.get('/loadsite/:userId/:idapp', authenticate_noerror_WithUserLean, async (req, res) => { - try { - await load(req, res, '0'); - } catch (e) { - console.error('loadsite error', e); - res.status(500).send({ error: 'Impossibile caricare il sito' }); - } -}); - -// Funzione di test per misurare le performance di MongoDB -async function testMongoPerformance(ind, iterations = 20) { - let logString = ''; - const log = (msg) => { - logString += msg + '\n'; - }; - - log(`Avvio del test ${ind} di performance MongoDB con ${iterations} iterazioni...`); - - const timings = []; - - for (let i = 0; i < iterations; i++) { - const start = process.hrtime(); - try { - // Esegui una query semplice; sostituisci "User" con il tuo modello se necessario - if (ind === 1) { - await User.findOne({}).lean(); - } else { - const token = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiJQUk9WQU1TR0AxQSIsInNtYXJ0IjoiNjIwODAwYWRjMTI5ZDFlYmE3NjBiZWNiIiwiYWNjZXNzIjoiYXV0aCIsInVuIjoic3VyeWExOTc3IiwiaWF0IjoxNzQxODcyMzEwLCJleHAiOjE3NDE4Nzk1MTB9.SXJLmsS6EZVhaU7sUWYMnaqGpiiy8RfE9K43xTdxNuU'; - - await User.findByToken(token, 'auth', true, true); - } - } catch (err) { - log(`Errore nell'iterazione ${i + 1}: ${err.message}`); - } - const diff = process.hrtime(start); - const timeInSeconds = diff[0] + diff[1] / 1e9; - timings.push(timeInSeconds); - log(`Iterazione ${i + 1}: ${timeInSeconds.toFixed(3)} sec`); - } - - const totalTime = timings.reduce((acc, t) => acc + t, 0); - const averageTime = totalTime / timings.length; - const minTime = Math.min(...timings); - const maxTime = Math.max(...timings); - - log(`--- Risultati del test ${ind} ---`); - log(`Tempo totale: ${totalTime.toFixed(3)} sec`); - log(`Tempo medio: ${averageTime.toFixed(3)} sec`); - log(`Tempo minimo: ${minTime.toFixed(3)} sec`); - log(`Tempo massimo: ${maxTime.toFixed(3)} sec`); - - return { totalTime, averageTime, minTime, maxTime, timings, log: logString }; -} - -// Supponendo di usare Express e di avere già definito "router" -router.get('/testpao', async (req, res) => { - try { - let ind = req.query.ind; - let numval = req.query.numval; - const result = await testMongoPerformance(ind, numval); - res.status(200).json({ log: result.log }); - } catch (error) { - console.error('Errore nel test di performance:', error); - res.status(500).json({ error: error.message }); - } -}); - -router.get('/loadsite/:userId/:idapp/:vers', authenticate_noerror_WithUserLean, async (req, res) => { - try { - let versionstr = req.params.vers; - - let version = tools.getVersionint(versionstr); - - return await load(req, res, version); - } catch (error) { - console.error('Errore durante il caricamento del sito:', error); - res.status(500).json({ error: 'Errore interno del server' }); - } -}); - -async function measurePromises(promises) { - const keys = Object.keys(promises); - const timings = {}; // memorizza il tempo per ogni promise - const startTotal = process.hrtime(); // tempo iniziale totale - - // Avvolgo ogni promise per misurare il tempo - const wrappedPromises = keys.map((key) => { - const promise = promises[key]; - return (async () => { - const start = process.hrtime(); // inizio timer per questa promise - const result = await promise; - const diff = process.hrtime(start); - // Calcola i secondi (precisione in nanosecondi) - const seconds = diff[0] + diff[1] / 1e9; - timings[key] = seconds; - return result; - })(); - }); - - // Attendo tutte le promise in parallelo - const results = await Promise.all(wrappedPromises); - const diffTotal = process.hrtime(startTotal); - const totalTime = diffTotal[0] + diffTotal[1] / 1e9; - - // Ricostruisco l'oggetto data con i risultati - const data = keys.reduce((acc, key, index) => { - acc[key] = results[index]; - return acc; - }, {}); - - // Ordina le chiamate per tempo decrescente e prende le 10 più lente - const slowCalls = Object.entries(timings) - .sort(([, timeA], [, timeB]) => timeB - timeA) - .slice(0, 10) - .map(([key, time]) => ({ key, time })); - - return { data, totalTime, slowCalls }; -} - -async function load(req, res, version = '0') { - try { - // console.log(' ... 1) richiesta LOAD'); - - // Estrazione e validazione degli input - const userId = req.user ? req.user._id.toString() : req.params.userId || '0'; - const idapp = req.params.idapp; - /*const status = req.code === server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED - ? server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED - : 200;*/ - - let status = req.code; - if (status === server_constants.RIS_CODE_OK) { - status = 200; - } - - const token = req.header('x-auth'); - // Se non ho il token, vado cmq stato a 200 - if (!token && status === server_constants.RIS_CODE_HTTP_INVALID_TOKEN) { - status = 200; - } - - // Determina se l'utente ha determinati permessi - const gestoredelSito = - req.user && (User.isAdmin(req.user.perm) || User.isManager(req.user.perm) || User.isEditor(req.user.perm)) - ? '1' - : '0'; - - const socioresidente = req.user && req.user.profile ? req.user.profile.socioresidente : false; - - // Costruzione dell'oggetto delle promesse - const promises = { - bookedevent: - userId !== '0' ? Booking.findAllByUserIdAndIdApp(userId, idapp, gestoredelSito) : Promise.resolve([]), - eventlist: MyEvent.findAllIdApp(socioresidente, idapp), - operators: Operator.findAllIdApp(idapp), - wheres: Where.findAllIdApp(idapp), - contribtype: Contribtype.findAllIdApp(idapp), - settings: Settings.findAllIdApp(idapp, false, false), - permissions: Permission.findAllIdApp(), - disciplines: Discipline.findAllIdApp(idapp), - newstosent: gestoredelSito ? Newstosent.findAllIdApp(idapp) : Promise.resolve([]), - mailinglist: Promise.resolve([]), - mypage: version > 91 ? MyPage.findOnlyStruttRec(idapp) : MyPage.findAllIdApp(idapp), - gallery: gestoredelSito ? Gallery.findAllIdApp(idapp) : Promise.resolve([]), - paymenttype: PaymentType.findAllIdApp(idapp), - calcstat: req.user ? User.calculateStat(idapp, req.user.username) : Promise.resolve(null), - calzoom: CalZoom.findAllIdApp(idapp), - producers: Producer.findAllIdApp(idapp), - cart: req.user ? Cart.getCartByUserId(userId, idapp) : Promise.resolve(null), - storehouses: Storehouse.findAllIdApp(idapp), - departments: Department.findAllIdApp(idapp), - orderscart: req.user - ? User.isManager(req.user.perm) - ? OrdersCart.getOrdersCartByUserId('ALL', idapp, 0, false) - : OrdersCart.getOrdersCartByUserId(userId, idapp, 0, false) - : Promise.resolve(null), - groups: Group.findAllIdApp(idapp), - resps: User.getusersRespList(idapp), - workers: User.getusersWorkersList(idapp), - internalpages: MyPage.findInternalPages(idapp), - // Campi aggiuntivi per versioni >= 91 - levels: version >= 91 ? Level.findAllIdApp(idapp) : Promise.resolve([]), - skills: version >= 91 ? Skill.findAllIdApp(idapp) : Promise.resolve([]), - sectors: version >= 91 ? Sector.findAllIdApp(idapp) : Promise.resolve([]), - statusSkills: version >= 91 ? StatusSkill.findAllIdApp(idapp) : Promise.resolve([]), - provinces: version >= 91 ? Province.findAllIdApp(idapp) : Promise.resolve([]), - catgrps: version >= 91 ? CatGrp.findAllIdApp(idapp) : Promise.resolve([]), - adtypes: version >= 91 ? AdType.findAllIdApp(idapp) : Promise.resolve([]), - adtypegoods: version >= 91 ? AdTypeGood.findAllIdApp(idapp) : Promise.resolve([]), - sectorgoods: version >= 91 ? SectorGood.findAllIdApp(idapp) : Promise.resolve([]), - goods: version >= 91 ? Good.findAllIdApp(idapp) : Promise.resolve([]), - site: version >= 91 ? Site.findAllIdApp(idapp) : Promise.resolve([]), - mygroups: version >= 91 ? MyGroup.findAllGroups(idapp) : Promise.resolve([]), - listcircuits: version >= 91 ? Circuit.findAllIdApp(idapp) : Promise.resolve([]), - myelems: version >= 91 ? MyElem.findAllIdApp(idapp) : Promise.resolve([]), - categories: version >= 91 ? Category.findAllIdApp(idapp) : Promise.resolve([]), - providers: version >= 91 ? Provider.findAllIdApp(idapp) : Promise.resolve([]), - scontisticas: version >= 91 ? Scontistica.findAllIdApp(idapp) : Promise.resolve([]), - gasordines: version >= 91 ? Gasordine.findAllIdApp(idapp) : Promise.resolve([]), - /*products: version >= 91 - ? Product.findAllIdApp(idapp, undefined, undefined, req.user ? User.isManager(req.user.perm) : false) - : Promise.resolve([]),*/ - products: Promise.resolve([]), - catprods: version >= 91 ? Product.getArrCatProds(idapp, shared_consts.PROD.BOTTEGA) : Promise.resolve([]), - subcatprods: version >= 91 ? SubCatProd.findAllIdApp(idapp) : Promise.resolve([]), - catprods_gas: version >= 91 ? Product.getArrCatProds(idapp, shared_consts.PROD.GAS) : Promise.resolve([]), - catAI: version >= 91 ? CatAI.findAllIdApp(idapp) : Promise.resolve([]), - authors: version >= 91 ? Author.findAllIdApp(idapp) : Promise.resolve([]), - publishers: version >= 91 ? Publisher.getEditoriWithTitleCount(idapp) : Promise.resolve([]), - myschedas: version >= 91 ? MyElem.findallSchedeTemplate(idapp) : Promise.resolve([]), - collane: version >= 91 ? Collana.getCollaneWithTitleCount(idapp) : Promise.resolve([]), - catalogs: version >= 91 ? Catalog.findAllIdApp(idapp) : Promise.resolve([]), - catprtotali: version >= 91 ? CatProd.getCatProdWithTitleCount(idapp) : Promise.resolve([]), - stati_prodotto: version >= 91 ? T_WEB_StatiProdotto.findAllIdApp() : Promise.resolve([]), - tipologie: version >= 91 ? T_WEB_Tipologie.findAllIdApp() : Promise.resolve([]), - tipoformato: version >= 91 ? T_WEB_TipiFormato.findAllIdApp() : Promise.resolve([]), - crons: version >= 91 ? Cron.findAllIdApp() : Promise.resolve([]), - raccoltacataloghis: version >= 91 ? RaccoltaCataloghi.findAllIdApp(idapp) : Promise.resolve([]), - myuserextra: req.user ? User.addExtraInfo(idapp, req.user, version) : Promise.resolve(null), - statuscode2: version >= 91 ? req.statuscode2 : Promise.resolve([]), - }; - - // Esecuzione parallela di tutte le promesse - /*const keys = Object.keys(promises); - const results = await Promise.all(Object.values(promises)); - const data = keys.reduce((acc, key, index) => { - acc[key] = results[index]; - return acc; - }, {}); - */ - - const { data, totalTime, slowCalls } = await measurePromises(promises); - // console.log('Risultati delle promise:', data); - // console.log('Tempo di esecuzione:', totalTime, 'secondi'); - //console.log('Le 10 chiamate più lente:', slowCalls); - - // Aggiornamento delle informazioni dell'utente, se presente - let myuser = req.user; - if (myuser && data.myuserextra) { - myuser = data.myuserextra; - myuser.password = ''; - myuser.calcstat = data.calcstat; - } - - // Costruzione dell'oggetto di risposta in base alla versione - let responseData; - if (version < 91) { - responseData = { - bookedevent: data.bookedevent, - eventlist: data.eventlist, - operators: data.operators, - wheres: data.wheres, - contribtype: data.contribtype, - settings: data.settings, - permissions: data.permissions, - disciplines: data.disciplines, - newstosent: data.newstosent, - mailinglist: data.mailinglist, - mypage: data.mypage, - gallery: data.gallery, - paymenttypes: data.paymenttype, - calzoom: data.calzoom, - producers: data.producers, - cart: data.cart, - storehouses: data.storehouses, - departments: data.departments, - orders: data.orderscart, - groups: data.groups, - resps: data.resps, - workers: data.workers, - myuser, - internalpages: data.internalpages, - }; - } else { - responseData = { - bookedevent: data.bookedevent, - eventlist: data.eventlist, - operators: data.operators, - wheres: data.wheres, - contribtype: data.contribtype, - settings: data.settings, - permissions: data.permissions, - disciplines: data.disciplines, - newstosent: data.newstosent, - mailinglist: data.mailinglist, - mypage: data.mypage, - gallery: data.gallery, - paymenttypes: data.paymenttype, - calzoom: data.calzoom, - producers: data.producers, - cart: data.cart, - storehouses: data.storehouses, - departments: data.departments, - orders: data.orderscart, - groups: data.groups, - resps: data.resps, - workers: data.workers, - myuser, - internalpages: data.internalpages, - levels: data.levels, - skills: data.skills, - sectors: data.sectors, - statusSkills: data.statusSkills, - provinces: data.provinces, - catgrps: data.catgrps, - adtypes: data.adtypes, - adtypegoods: data.adtypegoods, - sectorgoods: data.sectorgoods, - goods: data.goods, - site: data.site, - mygroups: data.mygroups, - listcircuits: data.listcircuits, - myelems: data.myelems, - categories: data.categories, - providers: data.providers, - scontisticas: data.scontisticas, - gasordines: data.gasordines, - products: data.products, - catprods: data.catprods, - subcatprods: data.subcatprods, - catprods_gas: data.catprods_gas, - catAI: data.catAI, - code: req.code, - authors: data.authors, - publishers: data.publishers, - myschedas: data.myschedas, - collane: data.collane, - catalogs: data.catalogs, - catprtotali: data.catprtotali, - stati_prodotto: data.stati_prodotto, - tipologie: data.tipologie, - tipoformato: data.tipoformato, - crons: data.crons, - raccoltacataloghis: data.raccoltacataloghis, - statuscode2: data.statuscode2, - }; - } - - // console.log(' ... 2) load dati caricati ...'); - res.status(status).send(responseData); - } catch (e) { - console.error('Errore in load:', e); - res.status(400).send({ error: e.message }); - } -} - -router.get('/checkupdates', authenticate_noerror, async (req, res) => { - try { - const idapp = req.query.idapp; - - // console.log("POST " + process.env.LINK_CHECK_UPDATES + " userId=" + userId); - if (!req.user) { - if (req.code === 1) return res.status(200).send(); - else return res.status(req.code).send(); - } - - await CfgServer.find({ idapp }) - .then(async (arrcfgrec) => { - if (arrcfgrec.length === 0) { - if (User.isAdmin(req.user.perm)) { - // crea un nuovo record - const mycfgServer = new CfgServer(); - mycfgServer.idapp = idapp; - mycfgServer.chiave = 'vers'; - mycfgServer.userId = 'ALL'; - mycfgServer.valore = await tools.getVersServer(); - - mycfgServer.save(); - - arrcfgrec = await CfgServer.find({ idapp }); - } else { - return res.status(404).send(); - } - } - - // ++Add to Log Stat .... - - let last_msgs = null; - let last_notifs = null; - let last_notifcoins = null; - let usersList = null; - let last_notifcoins_inattesa = null; - // const sall = '0'; - - // msgs = SendMsg.findAllByUserIdAndIdApp(userId, req.user.username, req.user.idapp); - if (req.user) { - const userId = req.user._id; - if (!ObjectId.isValid(userId)) { - return res.status(404).send(); - } - - last_msgs = SendMsg.findLastGroupByUserIdAndIdApp(userId, req.user.username, idapp); - last_notifs = SendNotif.findLastNotifsByUserIdAndIdApp(req.user.username, idapp, 40); - // Se sono il Gestore, le ricevo tutte quante: - if (User.isAdmin(req.user.perm)) { - last_notifcoins_inattesa = SendNotif.findAllNotifCoinsAllIdAndIdApp(idapp); - } else { - last_notifcoins_inattesa = SendNotif.findLastNotifCoinsByUserIdAndIdApp( - req.user.username, - idapp, - 200, - true - ); - } - last_notifcoins = SendNotif.findLastNotifCoinsByUserIdAndIdApp(req.user.username, idapp, 1, false); - - if (req.user) { - // If User is Admin, then send user Lists - if (User.isAdmin(req.user.perm) || User.isEditor(req.user.perm) || User.isManager(req.user.perm)) { - // Send UsersList - usersList = User.getUsersList(idapp); - // usersList = null; - } - } - } - - return Promise.all([usersList, last_msgs, last_notifs, last_notifcoins, last_notifcoins_inattesa]).then( - (arrdata) => { - // console.table(arrdata); - return res.send({ - CfgServer: arrcfgrec, - usersList: arrdata[0], - last_msgs: arrdata[1], - last_notifs: arrdata[2], - last_notifcoins: [...arrdata[4], ...arrdata[3]], - }); - } - ); - }) - .catch((e) => { - console.log(e.message); - res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: e }); - }); - } catch (e) { - console.error('error:', e); - } -}); - -router.post('/upload_from_other_server/:dir', authenticate, (req, res) => { - // const dir = req.params.dir; - // const idapp = req.user.idapp; - /* - const form = new formidable.IncomingForm(); - - form.parse(req); - - const client = new ftp(process.env.FTPSERVER_HOST, process.env.FTPSERVER_PORT, process.env.FTPSERVER_USER + idapp + '@associazioneshen.it', process.env.FTPSERVER_PWD + idapp, false, 134217728); - - // SSL_OP_NO_TLSv1_2 = 134217728 - - // console.log('client', client); - - form.uploadDir = folder + '/' + dir; - try { - - form.on('fileBegin', async function (name, file){ - file.path = folder + '/' + file.name; - }); - - form.on('file', async function (name, file){ - try { - // Create directory remote - - if (!!dir) - await client.createDir(dir); - - const miofile = (dir) ? dir + ` / ` + file.name : file.name; - console.log('Upload...'); - const ret = await client.upload(file.path, miofile, 755); - console.log('Uploaded ' + file.name, 'status:', ret); - if (!ret) - res.status(400).send(); - else { - // Delete file from local directory - fs.unlinkSync(file.path); - res.end(); - } - }catch (e) { - console.log('error', e); - res.status(400).send(); - } - }); - - form.on('aborted', () => { - console.error('Request aborted by the user'); - res.status(400).send(); - }); - - form.on('error', (err) => { - console.error('Error Uploading', err); - res.status(400).send(); - }); - - } catch (e) { - console.log('Error', e) - } - */ -}); - -// Funzione principale che gestisce l'upload -async function uploadFile(req, res, version, options = {}) { - try { - const quality = options.quality || 'original'; - const dirParam = req.params.dir || ''; - const dir = tools.invertescapeslash(dirParam); - const idapp = req.user?.idapp; - - if (!idapp) { - return res.status(400).send('ID applicazione mancante'); - } - - // Determina la cartella base - const dirmain = version > 0 && tools.sulServer() ? '' : server_constants.DIR_PUBLIC_LOCALE; - const baseUploadFolder = tools.getdirByIdApp(idapp) + dirmain + server_constants.DIR_UPLOAD; - - // Directory di upload specifica - const uploadDir = path.join(baseUploadFolder, dir); - - // Crea la cartella se non esiste - await tools.mkdirpath(uploadDir); - - // Configura formidable - const form = new formidable.IncomingForm({ uploadDir }); - - // Parsing in modalità Promise - const files = await new Promise((resolve, reject) => { - form.parse(req, (err, fields, files) => { - if (err) return reject(err); - resolve(files); - }); - }); - - // Gestione file - for (const key in files) { - if (!Object.prototype.hasOwnProperty.call(files, key)) continue; - - const file = files[key][0]; - console.log('File ricevuto:', file.originalFilename); - - const oldFile = file.filepath || file.path; - //const newFilePath = path.join(uploadDir, `${file.newFilename}_${file.originalFilename}`); - const newFilePath = path.join(uploadDir, `${file.originalFilename}`); - - //@@ATTENZIONE ! HO RIMESSO COM'ERA PRIMA ! MA NON MI CONVINCE ! - // RICONTROLLARE SE DEVO METTERLGI UN SUFFISSO PRIMA... (newFilePath) - - // Sposta e rinomina - await tools.move(oldFile, newFilePath); - - // Ridimensionamento opzionale - await handleImageResizing(newFilePath, quality); - - console.log(`File processato e salvato in: ${newFilePath}`); - } - - return res.status(200).send('Upload completato con successo'); - } catch (err) { - console.error('Errore durante uploadFile:', err); - if (!res.headersSent) { - return res.status(500).send("Errore durante l'upload"); - } - } -} - -// Funzione per muovere il file nella destinazione finale -async function moveFile(fromfile, tofile, res) { - console.log('Moving file from ' + fromfile + ' to ' + tofile); - try { - if (false && !tools.sulServer()) { - console.log('Copying file (locally):', fromfile, 'to', tofile); - await tools.execScriptNoOutput("sudo cp -R '" + fromfile + "' '" + tofile + "'"); - res.end(); - return; - } - await tools.move(fromfile, tofile); - } catch (error) { - console.log('Error moving file:', error); - res.status(400).send(); - } -} - -// Funzione per gestire il ridimensionamento dell'immagine -async function handleImageResizing(filePath, quality) { - const resizedImgPath = - tools.extractFilePath(filePath) + '/' + server_constants.PREFIX_IMG + tools.extractFileName(filePath); - const resizedSmallPath = - tools.extractFilePath(filePath) + '/' + server_constants.PREFIX_IMG_SMALL + tools.extractFileName(filePath); - - try { - // Ridimensionamento per la qualità "small" - - if (quality === 'small' || quality === 'both') { - await resizeImage(filePath, resizedSmallPath, 64, 64); - } - - // Ridimensionamento per la qualità "medium" - if (quality === 'medium' || quality === 'both') { - await resizeImage(filePath, resizedImgPath, 512, 512); - } - - // Se la qualità è "original", non fare il ridimensionamento - if (quality === 'original') { - console.log('Keeping original size for image: ' + filePath); - return; - } - } catch (err) { - console.error('Error resizing image: ', err); - } -} - -// Funzione per ridimensionare l'immagine -async function resizeImage(inputPath, outputPath, width, height) { - try { - await sharp(inputPath).resize(width, height, { fit: sharp.fit.contain }).withMetadata().toFile(outputPath); - console.log('Image resized to ' + width + 'x' + height + ' and saved as ' + outputPath); - } catch (err) { - console.error('Error resizing image:', err); - } -} -router.post('/upload/:dir', authenticate, (req, res) => { - return uploadFile(req, res, 0, req.body.options); -}); - -router.post('/uploadnew/:vers/:dir/', authenticate_noerror_WithUser, (req, res) => { - let versionstr = req.params.vers; - let version = tools.getVersionint(versionstr); - - try { - return uploadFile(req, res, version, req.body.options); - } catch (e) { - console.log('error', e); - res.status(400).send(); - } -}); - -router.delete('/delfile/:vers', authenticate, async (req, res) => { - let versionstr = req.params.vers; - let version = tools.getVersionint(versionstr); - await deleteFile(req, res, version); -}); - -router.delete('/delfile', authenticate, async (req, res) => { - await deleteFile(req, res, 0); -}); - -async function deleteFile(req, res, version) { - const relativefile = req.query.filename; - const idapp = req.user.idapp; - - if (!relativefile || relativefile.endsWith('/')) { - return res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - } - - try { - let dirmain = ''; - if (version > 0) { - if (!tools.sulServer()) { - dirmain = server_constants.DIR_PUBLIC_LOCALE; - } - } - - try { - console.log('Delete file ' + relativefile); - // ++ Move in the folder application ! - let fullpathfile = tools.getdirByIdApp(idapp) + dirmain + '/' + relativefile.replace(/^\//, ''); - - await tools.delete(fullpathfile, true, (err) => { - if (err) console.log('err', err); - if (err === undefined || err.errno === -2) return res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - }); - } catch (e) { - console.log('error', e?.message); - res.status(400).send(); - } - } catch (e) { - console.log('Error', e?.message); - } -} - -module.exports = router; diff --git a/src/services/posterRenderer.js b/src/services/posterRenderer.js index 7533ea2..5b22831 100644 --- a/src/services/posterRenderer.js +++ b/src/services/posterRenderer.js @@ -21,6 +21,7 @@ const registerFonts = async () => { { file: 'PlayfairDisplay-Regular.ttf', family: 'Playfair Display', weight: '400' } ]; + for (const font of fontMappings) { const fontPath = path.join(FONTS_DIR, font.file); try {