From b78e3ce544ce1bda99c2a7b8c90c39202853066e Mon Sep 17 00:00:00 2001 From: Surya Paolo Date: Mon, 22 Dec 2025 23:39:47 +0100 Subject: [PATCH] - Trasporti- Passo 2 --- .DS_Store | Bin 14340 -> 14340 bytes src/controllers/assetController.js | 10 +- src/controllers/chatController.js | 82 ++- src/controllers/feedbackController.js | 2 +- src/controllers/posterController.js | 10 +- src/controllers/rideController.js | 2 +- src/controllers/rideRequestController.js | 155 ++++- src/data/asset.json | 4 +- src/data/poster.json | 18 +- src/middleware/upload.js | 2 +- src/models/Chat.js | 236 ++++---- src/models/Message.js | 19 +- src/models/Ride.js | 724 ++++++++++++----------- src/models/user.js | 7 +- src/routes/trasportiRoutes.js | 328 +++++++++- src/services/posterRenderer.js | 2 +- 16 files changed, 1096 insertions(+), 505 deletions(-) diff --git a/.DS_Store b/.DS_Store index 8deddaec5388c5a05c03eb34cdf680e7c70ff592..1e57e80b8a58b40ed4d8b04e8f9d1ff111213994 100644 GIT binary patch delta 46 zcmZoEXepTB&se!JU^hRb{bn8kX%0q)%^DI7Oq?mj$vH{+`8kZ6r%6SzZsu0_DGmT& Ckq*HC delta 39 vcmZoEXepTB&secBU^hRb-DVyEX%0rl%^DI7Oq&l&d9Y4=AibGe;iotN2v`jZ diff --git a/src/controllers/assetController.js b/src/controllers/assetController.js index b2a632b..138948a 100644 --- a/src/controllers/assetController.js +++ b/src/controllers/assetController.js @@ -4,7 +4,7 @@ const sharp = require('sharp'); const path = require('path'); const fs = require('fs').promises; -const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'; +const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload'; const assetController = { // POST /assets/upload @@ -50,9 +50,9 @@ const assetController = { sourceType: 'upload', file: { path: file.path, - url: `/uploads/${file.filename}`, + url: `/upload/${file.filename}`, thumbnailPath: thumbPath, - thumbnailUrl: `/uploads/thumbs/${thumbName}`, + thumbnailUrl: `/upload/thumbs/${thumbName}`, originalName: file.originalname, mimeType: file.mimetype, size: file.size, @@ -106,7 +106,7 @@ const assetController = { sourceType: 'upload', file: { path: file.path, - url: `/uploads/${file.filename}`, + url: `/upload/${file.filename}`, originalName: file.originalname, mimeType: file.mimetype, size: file.size, @@ -199,7 +199,7 @@ const assetController = { sourceType: 'ai', file: { path: filePath, - url: `/uploads/ai-generated/${fileName}`, + url: `/upload/ai-generated/${fileName}`, mimeType: 'image/jpeg', size: fileSize, dimensions diff --git a/src/controllers/chatController.js b/src/controllers/chatController.js index 9cbff7c..e7a72a1 100644 --- a/src/controllers/chatController.js +++ b/src/controllers/chatController.js @@ -1,6 +1,6 @@ const Chat = require('../models/Chat'); const Message = require('../models/Message'); -const { User } = require('../models/User'); +const { User } = require('../models/user'); /** * @desc Ottieni tutte le chat dell'utente @@ -10,7 +10,8 @@ const { User } = require('../models/User'); const getMyChats = async (req, res) => { try { const userId = req.user._id; - const { idapp, page = 1, limit = 20 } = req.query; + const idapp = req.user.idapp; + const { page = 1, limit = 20 } = req.query; if (!idapp) { return res.status(400).json({ @@ -83,7 +84,8 @@ const getMyChats = async (req, res) => { const getOrCreateDirectChat = async (req, res) => { try { const userId = req.user._id; - const { idapp, otherUserId, rideId } = req.body; + const idapp = req.user.idapp; + const { otherUserId, rideId } = req.body; if (!idapp || !otherUserId) { return res.status(400).json({ @@ -199,7 +201,8 @@ const getChatById = async (req, res) => { const getChatMessages = async (req, res) => { try { const { id } = req.params; - const { idapp, before, after, limit = 50 } = req.query; + const idapp = req.user.idapp; + const { before, after, limit = 50 } = req.query; const userId = req.user._id; // Verifica accesso alla chat @@ -209,7 +212,7 @@ const getChatMessages = async (req, res) => { success: false, message: 'Chat non trovata' }); - } + } if (!chat.hasParticipant(userId)) { return res.status(403).json({ @@ -258,7 +261,8 @@ const getChatMessages = async (req, res) => { const sendMessage = async (req, res) => { try { const { id } = req.params; - const { idapp, text, type = 'text', metadata, replyTo } = req.body; + const idapp = req.user.idapp; + const { text, type = 'text', metadata, replyTo } = req.body; const userId = req.user._id; if (!idapp || !text) { @@ -499,7 +503,7 @@ const toggleMuteChat = async (req, res) => { const getUnreadCount = async (req, res) => { try { const userId = req.user._id; - const { idapp } = req.query; + const idapp = req.user.idapp; if (!idapp) { return res.status(400).json({ @@ -597,6 +601,67 @@ const deleteMessage = async (req, res) => { } }; +/** + * @desc Elimina una chat (soft delete) + * @route DELETE /api/trasporti/chats/:id + * @access Private + */ +const deleteChat = 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' + }); + } + + // Verifica che l'utente sia partecipante + if (!chat.hasParticipant(userId)) { + return res.status(403).json({ + success: false, + message: 'Non sei autorizzato a eliminare questa chat' + }); + } + + // Soft delete: segna come non attiva per questo utente + // Se vuoi hard delete, usa: await chat.deleteOne(); + + // Opzione 1: Soft delete (chat rimane ma nascosta per questo utente) + if (!chat.deletedBy) { + chat.deletedBy = []; + } + + if (!chat.deletedBy.includes(userId)) { + chat.deletedBy.push(userId); + } + + // Se tutti i partecipanti hanno eliminato, marca come non attiva + if (chat.deletedBy.length === chat.participants.length) { + chat.isActive = false; + } + + await chat.save(); + + res.json({ + success: true, + message: 'Chat eliminata' + }); + + } catch (error) { + console.error('Errore eliminazione chat:', error); + res.status(500).json({ + success: false, + message: 'Errore nell\'eliminazione della chat', + error: error.message + }); + } +}; + module.exports = { getMyChats, getOrCreateDirectChat, @@ -607,5 +672,6 @@ module.exports = { toggleBlockChat, toggleMuteChat, getUnreadCount, - deleteMessage + deleteMessage, + deleteChat, }; \ No newline at end of file diff --git a/src/controllers/feedbackController.js b/src/controllers/feedbackController.js index e2f9b74..55cba6f 100644 --- a/src/controllers/feedbackController.js +++ b/src/controllers/feedbackController.js @@ -1,7 +1,7 @@ const Feedback = require('../models/Feedback'); const Ride = require('../models/Ride'); const RideRequest = require('../models/RideRequest'); -const { User } = require('../models/User'); +const { User } = require('../models/user'); /** * @desc Crea un feedback per un viaggio diff --git a/src/controllers/posterController.js b/src/controllers/posterController.js index 29e5873..ed1c82b 100644 --- a/src/controllers/posterController.js +++ b/src/controllers/posterController.js @@ -6,7 +6,7 @@ const imageGenerator = require('../services/imageGenerator'); const path = require('path'); const fs = require('fs').promises; -const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'; +const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload'; const posterController = { // POST /posters @@ -396,7 +396,7 @@ const posterController = { // Aggiorna asset nel poster const assetData = { sourceType: 'ai', - url: `/uploads/ai-generated/${fileName}`, + url: `/upload/ai-generated/${fileName}`, mimeType: 'image/jpeg', aiParams: { prompt, @@ -572,7 +572,7 @@ const posterController = { assets: { backgroundImage: { sourceType: 'ai', - url: `/uploads/ai-generated/${fileName}`, + url: `/upload/ai-generated/${fileName}`, mimeType: 'image/jpeg', aiParams: { prompt: aiPrompt, @@ -629,12 +629,12 @@ const posterController = { poster.setRenderOutput({ png: { path: result.pngPath, - url: `/uploads/posters/final/${path.basename(result.pngPath)}`, + url: `/upload/posters/final/${path.basename(result.pngPath)}`, size: result.pngSize }, jpg: { path: result.jpgPath, - url: `/uploads/posters/final/${path.basename(result.jpgPath)}`, + url: `/upload/posters/final/${path.basename(result.jpgPath)}`, size: result.jpgSize, quality: 95 }, diff --git a/src/controllers/rideController.js b/src/controllers/rideController.js index 85f17aa..b315188 100644 --- a/src/controllers/rideController.js +++ b/src/controllers/rideController.js @@ -1,5 +1,5 @@ const Ride = require('../models/Ride'); -const User = require('../models/User'); +const User = require('../models/user'); const RideRequest = require('../models/RideRequest'); /** diff --git a/src/controllers/rideRequestController.js b/src/controllers/rideRequestController.js index e354b29..c4278b3 100644 --- a/src/controllers/rideRequestController.js +++ b/src/controllers/rideRequestController.js @@ -602,6 +602,157 @@ const getRequestById = async (req, res) => { } }; +/** + * @desc Ottieni richieste ricevute (io come conducente) + * @route GET /api/trasporti/requests/received + * @access Private + */ +const getReceivedRequests = async (req, res) => { + try { + const driverId = req.user._id; + const { status, page = 1, limit = 20 } = req.query; + + // Build query + const query = { driverId }; + if (status) { + query.status = status; + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + + // Fetch requests + const requests = await RideRequest.find(query) + .populate('passengerId', 'name surname profile.img') // <-- FIX: passengerId invece di userId + .populate('rideId', 'departure destination departureDate availableSeats') + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)) + .lean(); + + // Get counts by status + const counts = await RideRequest.aggregate([ + { $match: { driverId } }, + { + $group: { + _id: '$status', + count: { $sum: 1 } + } + } + ]); + + // Format counts + const statusCounts = { + pending: 0, + accepted: 0, + rejected: 0, + cancelled: 0, + expired: 0, + completed: 0 + }; + + counts.forEach(c => { + if (statusCounts.hasOwnProperty(c._id)) { + statusCounts[c._id] = c.count; + } + }); + + // Total count + const total = await RideRequest.countDocuments(query); + + res.json({ + success: true, + data: { + requests, + counts: statusCounts, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / parseInt(limit)) + } + } + }); + + } catch (error) { + console.error('Error fetching received requests:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero delle richieste ricevute', + error: error.message + }); + } +}; +/** + * @desc Ottieni richieste inviate (io come passeggero) + * @route GET /api/trasporti/requests/sent + * @access Private + */ +const getSentRequests = async (req, res) => { + try { + const userId = req.user._id; + const { status, page = 1, limit = 20 } = req.query; + + // Build query - FIX: usa passengerId invece di userId + const query = { passengerId: userId }; + if (status) { + query.status = status; + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + + // Fetch requests + const requests = await RideRequest.find(query) + .populate('driverId', 'name surname profile.img') + .populate('rideId', 'departure destination departureDate availableSeats currentPassengers') + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)) + .lean(); + + // Enrich with ride info + const enrichedRequests = requests.map(request => { + const req = { ...request }; + + // Add ride info if rideId is populated + if (req.rideId) { + req.rideInfo = { + departure: req.rideId.departure?.city || req.rideId.departure, + destination: req.rideId.destination?.city || req.rideId.destination, + departureDate: req.rideId.departureDate, + availableSeats: req.rideId.availableSeats, + currentPassengers: req.rideId.currentPassengers || 0 + }; + } + + return req; + }); + + // Total count + const total = await RideRequest.countDocuments(query); + + res.json({ + success: true, + data: { + requests: enrichedRequests, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / parseInt(limit)) + } + } + }); + + } catch (error) { + console.error('Error fetching sent requests:', error); + res.status(500).json({ + success: false, + message: 'Errore nel recupero delle richieste inviate', + error: error.message + }); + } +}; + module.exports = { createRequest, getRequestsForRide, @@ -611,6 +762,6 @@ module.exports = { rejectRequest, cancelRequest, getRequestById, - getReceivedRequests: getPendingRequests, - getSentRequests: getMyRequests, + getReceivedRequests, + getSentRequests, }; diff --git a/src/data/asset.json b/src/data/asset.json index 8389cca..05a8a9c 100644 --- a/src/data/asset.json +++ b/src/data/asset.json @@ -5,9 +5,9 @@ "sourceType": "ai", "file": { - "path": "/uploads/assets/backgrounds/forest_autumn_001.jpg", + "path": "/upload/assets/backgrounds/forest_autumn_001.jpg", "url": "/api/assets/asset_bg_001/file", - "thumbnailPath": "/uploads/assets/backgrounds/thumbs/forest_autumn_001_thumb.jpg", + "thumbnailPath": "/upload/assets/backgrounds/thumbs/forest_autumn_001_thumb.jpg", "thumbnailUrl": "/api/assets/asset_bg_001/thumbnail", "originalName": null, "mimeType": "image/jpeg", diff --git a/src/data/poster.json b/src/data/poster.json index 21294d2..1edacbe 100644 --- a/src/data/poster.json +++ b/src/data/poster.json @@ -21,8 +21,8 @@ "backgroundImage": { "id": "asset_bg_001", "sourceType": "ai", - "url": "/uploads/posters/poster_sagra_2025_bg.jpg", - "thumbnailUrl": "/uploads/posters/thumbs/poster_sagra_2025_bg_thumb.jpg", + "url": "/upload/posters/poster_sagra_2025_bg.jpg", + "thumbnailUrl": "/upload/posters/thumbs/poster_sagra_2025_bg_thumb.jpg", "mimeType": "image/jpeg", "size": 2458000, "dimensions": { "width": 2480, "height": 3508 }, @@ -41,8 +41,8 @@ "mainImage": { "id": "asset_main_001", "sourceType": "upload", - "url": "/uploads/assets/porcini_basket_hero.jpg", - "thumbnailUrl": "/uploads/assets/thumbs/porcini_basket_hero_thumb.jpg", + "url": "/upload/assets/porcini_basket_hero.jpg", + "thumbnailUrl": "/upload/assets/thumbs/porcini_basket_hero_thumb.jpg", "originalName": "IMG_20241015_porcini.jpg", "mimeType": "image/jpeg", "size": 1845000, @@ -54,7 +54,7 @@ "id": "asset_logo_001", "slotId": "logo_slot_1", "sourceType": "upload", - "url": "/uploads/logos/comune_borgomontano.png", + "url": "/upload/logos/comune_borgomontano.png", "originalName": "logo_comune.png", "mimeType": "image/png", "size": 45000 @@ -63,7 +63,7 @@ "id": "asset_logo_002", "slotId": "logo_slot_2", "sourceType": "upload", - "url": "/uploads/logos/proloco_borgomontano.png", + "url": "/upload/logos/proloco_borgomontano.png", "originalName": "logo_proloco.png", "mimeType": "image/png", "size": 38000 @@ -72,7 +72,7 @@ "id": "asset_logo_003", "slotId": "logo_slot_3", "sourceType": "ai", - "url": "/uploads/logos/ai_generated_mushroom_logo.png", + "url": "/upload/logos/ai_generated_mushroom_logo.png", "mimeType": "image/png", "size": 52000, "aiParams": { @@ -100,12 +100,12 @@ "renderOutput": { "png": { - "path": "/uploads/posters/final/poster_sagra_2025_final.png", + "path": "/upload/posters/final/poster_sagra_2025_final.png", "size": 8945000, "url": "/api/posters/poster_sagra_funghi_2025_001/download/png" }, "jpg": { - "path": "/uploads/posters/final/poster_sagra_2025_final.jpg", + "path": "/upload/posters/final/poster_sagra_2025_final.jpg", "quality": 95, "size": 2145000, "url": "/api/posters/poster_sagra_funghi_2025_001/download/jpg" diff --git a/src/middleware/upload.js b/src/middleware/upload.js index 5cd6d83..8866455 100644 --- a/src/middleware/upload.js +++ b/src/middleware/upload.js @@ -2,7 +2,7 @@ const multer = require('multer'); const path = require('path'); const crypto = require('crypto'); -const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'; +const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload'; const storage = multer.diskStorage({ destination: (req, file, cb) => { diff --git a/src/models/Chat.js b/src/models/Chat.js index 9f0dff6..613a82b 100644 --- a/src/models/Chat.js +++ b/src/models/Chat.js @@ -1,86 +1,94 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; -const LastMessageSchema = new Schema({ - text: { +const LastMessageSchema = new Schema( + { + text: { + type: String, + trim: true, + }, + senderId: { + type: Schema.Types.ObjectId, + ref: 'User', + }, + timestamp: { + type: Date, + default: Date.now, + }, 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 }); + { _id: false } +); -const ChatSchema = new Schema({ - idapp: { - type: String, - required: true, - index: true +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, + }, }, - 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 }, } -}, { - timestamps: true, - toJSON: { virtuals: true }, - toObject: { virtuals: true } -}); +); // Indici ChatSchema.index({ participants: 1 }); @@ -88,71 +96,89 @@ ChatSchema.index({ idapp: 1, participants: 1 }); ChatSchema.index({ idapp: 1, updatedAt: -1 }); // Virtual per contare messaggi non letti totali -ChatSchema.virtual('totalUnread').get(function() { +ChatSchema.virtual('totalUnread').get(function () { if (!this.unreadCount) return 0; let total = 0; - this.unreadCount.forEach(count => { + this.unreadCount.forEach((count) => { total += count; }); return total; }); // Metodo per ottenere unread count per un utente specifico -ChatSchema.methods.getUnreadForUser = function(userId) { +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()) { +// ✅ FIX: incrementUnread (assicura conversione corretta) +ChatSchema.methods.incrementUnread = function (excludeUserId) { + const excludeIdStr = excludeUserId.toString(); + + this.participants.forEach((participantId) => { + // Gestisci sia ObjectId che oggetti popolati + const id = participantId._id + ? participantId._id.toString() + : participantId.toString(); + + if (id !== excludeIdStr) { 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) { +ChatSchema.methods.markAsRead = function (userId) { this.unreadCount.set(userId.toString(), 0); return this.save(); }; // Metodo per aggiornare ultimo messaggio -ChatSchema.methods.updateLastMessage = function(message) { +ChatSchema.methods.updateLastMessage = function (message) { this.lastMessage = { text: message.text, senderId: message.senderId, timestamp: message.createdAt || new Date(), - type: message.type || 'text' + 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() - ); +// ✅ FIX: Gestisce sia ObjectId che oggetti User popolati +ChatSchema.methods.hasParticipant = function (userId) { + const userIdStr = userId.toString(); + + return this.participants.some((p) => { + // Se p è un oggetto popolato (ha _id), usa p._id + // Altrimenti p è già un ObjectId + const participantId = p._id ? p._id.toString() : p.toString(); + return participantId === userIdStr; + }); }; // Metodo per verificare se la chat è bloccata per un utente -ChatSchema.methods.isBlockedFor = function(userId) { - return this.blockedBy.some( - id => id.toString() === userId.toString() - ); +// ✅ FIX: Metodo isBlockedFor (stesso problema) +ChatSchema.methods.isBlockedFor = function (userId) { + const userIdStr = userId.toString(); + + return this.blockedBy.some((id) => { + const blockedId = id._id ? id._id.toString() : id.toString(); + return blockedId === userIdStr; + }); }; + // Metodo statico per trovare o creare una chat diretta -ChatSchema.statics.findOrCreateDirect = async function(idapp, userId1, userId2, rideId = null) { +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 } + participants: { $all: [userId1, userId2], $size: 2 }, }); if (!chat) { @@ -161,7 +187,7 @@ ChatSchema.statics.findOrCreateDirect = async function(idapp, userId1, userId2, type: 'direct', participants: [userId1, userId2], rideId, - unreadCount: new Map() + unreadCount: new Map(), }); await chat.save(); } else if (rideId && !chat.rideId) { @@ -174,31 +200,31 @@ ChatSchema.statics.findOrCreateDirect = async function(idapp, userId1, userId2, }; // Metodo statico per ottenere tutte le chat di un utente -ChatSchema.statics.getChatsForUser = function(idapp, userId) { +ChatSchema.statics.getChatsForUser = function (idapp, userId) { return this.find({ idapp, participants: userId, isActive: true, - blockedBy: { $ne: userId } + blockedBy: { $ne: userId }, }) - .populate('participants', 'username name surname profile.avatar') - .populate('rideId', 'departure destination dateTime') - .sort({ updatedAt: -1 }); + .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) { +ChatSchema.statics.createRideGroupChat = async function (idapp, rideId, title, participantIds) { const chat = new this({ idapp, type: 'group', rideId, title, participants: participantIds, - unreadCount: new Map() + unreadCount: new Map(), }); return chat.save(); }; const Chat = mongoose.model('Chat', ChatSchema); -module.exports = Chat; \ No newline at end of file +module.exports = Chat; diff --git a/src/models/Message.js b/src/models/Message.js index 2568e95..1feb85e 100644 --- a/src/models/Message.js +++ b/src/models/Message.js @@ -168,28 +168,31 @@ MessageSchema.methods.editText = function(newText) { }; // 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; +// Message.js (model) + +MessageSchema.statics.getByChat = async function(idapp, chatId, options = {}) { + const { limit = 50, before, after } = options; const query = { idapp, chatId, isDeleted: false }; - + + // Filtra per timestamp if (before) { query.createdAt = { $lt: new Date(before) }; } if (after) { query.createdAt = { $gt: new Date(after) }; } - + + // ✅ Sempre in ordine decrescente (dal più recente al più vecchio) return this.find(query) - .populate('senderId', 'username name surname profile.avatar') + .populate('senderId', 'username name surname profile.img') .populate('replyTo', 'text senderId') - .populate('metadata.rideId', 'departure destination dateTime') - .sort({ createdAt: -1 }) - .limit(limit); + .sort({ createdAt: -1 }) // -1 = più recente prima + .limit(limit) }; // Metodo statico per creare messaggio di sistema diff --git a/src/models/Ride.js b/src/models/Ride.js index 0a5b0f2..9d5b1df 100644 --- a/src/models/Ride.js +++ b/src/models/Ride.js @@ -2,377 +2,417 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; // Schema per le coordinate geografiche -const CoordinatesSchema = new Schema({ - lat: { - type: Number, - required: true +const CoordinatesSchema = new Schema( + { + lat: { + type: Number, + required: true, + }, + lng: { + type: Number, + required: true, + }, }, - lng: { - type: Number, - required: true - } -}, { _id: false }); + { _id: false } +); // Schema per una località (partenza, destinazione, waypoint) -const LocationSchema = new Schema({ - city: { - type: String, - required: true, - trim: true +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, + }, }, - 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 }); + { _id: false } +); // Schema per i waypoint (tappe intermedie) const WaypointSchema = new Schema({ location: { type: LocationSchema, - required: true + required: true, }, order: { type: Number, - required: true + required: true, }, estimatedArrival: { - type: Date + type: Date, }, stopDuration: { type: Number, // minuti di sosta - default: 0 - } + default: 0, + }, }); // Schema per la ricorrenza del viaggio -const RecurrenceSchema = new Schema({ - type: { - type: String, - enum: ['once', 'weekly', 'custom_days', 'custom_dates'], - default: 'once' +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, + }, + ], }, - 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 }); + { _id: false } +); // Schema per i passeggeri -const PassengersSchema = new Schema({ - available: { - type: Number, - required: true, - min: 0 +const PassengersSchema = new Schema( + { + available: { + type: Number, + required: true, + min: 0, + }, + max: { + type: Number, + required: true, + min: 1, + }, }, - max: { - type: Number, - required: true, - min: 1 - } -}, { _id: false }); + { _id: false } +); // Schema per il veicolo -const VehicleSchema = new Schema({ - type: { - type: String, - enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'], - default: 'auto' +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, + }, + photos: [ + { + type: String, + trim: true, + }, + ], + features: [ + { + type: String, + enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'], + }, + ], }, - 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 }); + { _id: false } +); // Schema per le preferenze di viaggio -const RidePreferencesSchema = new Schema({ - smoking: { - type: String, - enum: ['yes', 'no', 'outside_only'], - default: 'no' +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, + }, }, - 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 }); + { _id: false } +); // Schema per il contributo/pagamento const ContributionItemSchema = new Schema({ contribTypeId: { type: Schema.Types.ObjectId, ref: 'Contribtype', - required: true + required: true, }, price: { type: Number, - min: 0 + 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 } }); +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' }); @@ -382,41 +422,41 @@ 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() { +RideSchema.virtual('isFull').get(function () { if (this.type === 'request') return false; + // ⚠️ CONTROLLO: verifica che passengers esista + if (!this.passengers || typeof this.passengers.available === 'undefined') return false; return this.passengers.available <= 0; }); // Virtual per calcolare posti occupati -RideSchema.virtual('bookedSeats').get(function() { +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() { +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)); + 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) { +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()) + 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') { +RideSchema.methods.updateAvailableSeats = function () { + // ⚠️ CONTROLLO: verifica che sia un'offerta e che passengers esista + if (this.type === 'offer' && this.passengers) { const booked = this.bookedSeats; this.passengers.available = this.passengers.max - booked; if (this.passengers.available <= 0) { @@ -429,9 +469,9 @@ RideSchema.methods.updateAvailableSeats = function() { }; // Pre-save hook -RideSchema.pre('save', function(next) { - // Aggiorna posti disponibili se necessario - if (this.type === 'offer' && this.isModified('confirmedPassengers')) { +RideSchema.pre('save', function (next) { + // ⚠️ CONTROLLO: Aggiorna posti disponibili solo se è un'offerta e passengers esiste + if (this.type === 'offer' && this.passengers && 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) { @@ -442,11 +482,11 @@ RideSchema.pre('save', function(next) { }); // Metodi statici per ricerche comuni -RideSchema.statics.findActiveByCity = function(idapp, departureCity, destinationCity, options = {}) { +RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinationCity, options = {}) { const query = { idapp, status: { $in: ['active', 'full'] }, - dateTime: { $gte: new Date() } + dateTime: { $gte: new Date() }, }; if (departureCity) { @@ -472,17 +512,13 @@ RideSchema.statics.findActiveByCity = function(idapp, departureCity, destination }; // Ricerca viaggi che passano per una città intermedia -RideSchema.statics.findPassingThrough = function(idapp, cityName, options = {}) { +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 } - ] + $or: [{ 'departure.city': cityRegex }, { 'destination.city': cityRegex }, { 'waypoints.location.city': cityRegex }], }; if (options.type) { diff --git a/src/models/user.js b/src/models/user.js index f8296d1..70c1726 100755 --- a/src/models/user.js +++ b/src/models/user.js @@ -643,6 +643,12 @@ const UserSchema = new mongoose.Schema( enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'], }, ], + photos: [ + { + type: String, + trim: true, + }, + ], isDefault: { type: Boolean, default: false, @@ -7336,7 +7342,6 @@ 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 = { diff --git a/src/routes/trasportiRoutes.js b/src/routes/trasportiRoutes.js index 36a8ae4..3837591 100644 --- a/src/routes/trasportiRoutes.js +++ b/src/routes/trasportiRoutes.js @@ -1,7 +1,13 @@ const express = require('express'); const router = express.Router(); +const path = require('path'); -const { Contribtype } = require('../models/Contribtype'); // Adatta al tuo path +const multer = require('multer'); +const fs = require('fs'); + +const tools = require('../tools/general'); + +const { Contribtype } = require('../models/contribtype'); // Adatta al tuo path // Import Controllers const rideController = require('../controllers/rideController'); @@ -113,7 +119,6 @@ router.get('/stats/summary', authenticate, rideController.getStatsSummary); */ router.get('/cities/suggestions', rideController.getCitySuggestions); - /** * @route GET /api/trasporti/cities/recents * @desc città recenti per autocomplete @@ -258,6 +263,13 @@ router.put('/chats/:id/mute', authenticate, chatController.toggleMuteChat); */ router.delete('/chats/:chatId/messages/:messageId', authenticate, chatController.deleteMessage); +/** + * @route DELETE /api/trasporti/chats/:id + * @desc Elimina chat (soft delete) + * @access Private (solo partecipanti) + */ +router.delete('/chats/:id', authenticate, chatController.deleteChat); + // ============================================================ // ⭐ FEEDBACK - Recensioni // ============================================================ @@ -390,16 +402,16 @@ router.get('/geo/suggest-waypoints', geocodingController.suggestWaypoints); // ============================================================ /** - * @route GET /api/trasporti/driver/:userId + * @route GET /api/trasporti/driver/user/:userId * @desc Profilo pubblico del conducente * @access Public */ -router.get('/driver/:userId', async (req, res) => { +router.get('/driver/user/:userId', async (req, res) => { try { const { userId } = req.params; const { idapp } = req.query; - const { User } = require('../models/User'); + const { User } = require('../models/user'); const Ride = require('../models/Ride'); const Feedback = require('../models/Feedback'); @@ -489,6 +501,43 @@ router.get('/driver/:userId', async (req, res) => { } }); +/** + * @route GET /api/trasporti/driver/vehicles + * @desc Ottieni veicoli dell'utente corrente + * @access Private + */ +router.get('/driver/vehicles', authenticate, async (req, res) => { + try { + const { idapp } = req.query; + const userId = req.user._id; // Assumo che ci sia un middleware di autenticazione + + if (!userId) { + return res.status(401).json({ + success: false, + message: 'Autenticazione richiesta', + }); + } + + const { User } = require('../models/user'); + + const user = await User.findById(userId).select('profile.driverProfile.vehicles'); + + const vehicles = user?.profile?.driverProfile?.vehicles || []; + + res.status(200).json({ + success: true, + data: vehicles, + }); + } catch (error) { + console.error('Errore recupero veicoli:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il recupero dei veicoli', + error: error.message, + }); + } +}); + /** * @route PUT /api/trasporti/driver/profile * @desc Aggiorna profilo conducente @@ -499,7 +548,7 @@ router.put('/driver/profile', authenticate, async (req, res) => { const userId = req.user._id; const { idapp, driverProfile, preferences } = req.body; - const { User } = require('../models/User'); + const { User } = require('../models/user'); const updateData = {}; @@ -543,9 +592,9 @@ router.put('/driver/profile', authenticate, async (req, res) => { router.post('/driver/vehicles', authenticate, async (req, res) => { try { const userId = req.user._id; - const { vehicle } = req.body; + const vehicle = req.body; - const { User } = require('../models/User'); + const { User } = require('../models/user'); const user = await User.findByIdAndUpdate( userId, @@ -580,9 +629,9 @@ router.put('/driver/vehicles/:vehicleId', authenticate, async (req, res) => { try { const userId = req.user._id; const { vehicleId } = req.params; - const { vehicle } = req.body; + const vehicle = req.body; - const { User } = require('../models/User'); + const { User } = require('../models/user'); const user = await User.findOneAndUpdate( { @@ -617,6 +666,54 @@ router.put('/driver/vehicles/:vehicleId', authenticate, async (req, res) => { } }); +/** + * @route GET /api/trasporti/driver/vehicles/:vehicleId + * @desc Ottieni dettagli di un veicolo specifico + * @access Private + */ +router.get('/driver/vehicles/:vehicleId', authenticate, async (req, res) => { + try { + const userId = req.user._id; + const { vehicleId } = req.params; + + const { User } = require('../models/user'); + + const user = await User.findOne({ + _id: userId, + 'profile.driverProfile.vehicles._id': vehicleId, + }).select('profile.driverProfile.vehicles'); + + if (!user) { + return res.status(404).json({ + success: false, + message: 'Veicolo non trovato', + }); + } + + // Trova il veicolo specifico nell'array + const vehicle = user.profile.driverProfile.vehicles.find((v) => v._id.toString() === vehicleId); + + if (!vehicle) { + return res.status(404).json({ + success: false, + message: 'Veicolo non trovato', + }); + } + + res.status(200).json({ + success: true, + data: vehicle, + }); + } catch (error) { + console.error('Errore recupero veicolo:', error); + res.status(500).json({ + success: false, + message: 'Errore durante il recupero del veicolo', + error: error.message, + }); + } +}); + /** * @route DELETE /api/trasporti/driver/vehicles/:vehicleId * @desc Rimuovi veicolo @@ -627,7 +724,7 @@ router.delete('/driver/vehicles/:vehicleId', authenticate, async (req, res) => { const userId = req.user._id; const { vehicleId } = req.params; - const { User } = require('../models/User'); + const { User } = require('../models/user'); await User.findByIdAndUpdate(userId, { $pull: { 'profile.driverProfile.vehicles': { _id: vehicleId } }, @@ -657,7 +754,7 @@ router.post('/driver/vehicles/:vehicleId/default', authenticate, async (req, res const userId = req.user._id; const { vehicleId } = req.params; - const { User } = require('../models/User'); + const { User } = require('../models/user'); // Prima rimuovi isDefault da tutti await User.updateOne({ _id: userId }, { $set: { 'profile.driverProfile.vehicles.$[].isDefault': false } }); @@ -711,4 +808,211 @@ router.get('/contrib-types', async (req, res) => { } }); +const vehiclePhotoStorage = multer.diskStorage({ + destination: (req, file, cb) => { + const webServerDir = tools.getdirByIdApp(req.user.idapp) + '/upload/vehicles'; + if (!fs.existsSync(webServerDir)) { + fs.mkdirSync(webServerDir, { recursive: true }); + } + console.log('📁 Destinazione foto veicolo:', webServerDir); + cb(null, webServerDir); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + const ext = path.extname(file.originalname); + const filename = 'vehicle-' + uniqueSuffix + ext; + console.log('📝 Nome file generato:', filename); + cb(null, filename); + }, +}); + +const uploadVehiclePhoto = multer({ + storage: vehiclePhotoStorage, + limits: { + fileSize: 5 * 1024 * 1024, // 5MB + }, + fileFilter: (req, file, cb) => { + console.log('🔍 File ricevuto:', { + fieldname: file.fieldname, + originalname: file.originalname, + mimetype: file.mimetype, + }); + + const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']; + if (allowedMimes.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error('Solo immagini sono ammesse'), false); + } + }, +}); + +/** + * @route POST /api/trasporti/upload/vehicle-photos + * @desc Upload multiple foto veicolo (max 5) + * @access Private + */ +router.post( + '/upload/vehicle-photos', + authenticate, + (req, res, next) => { + console.log('🚀 Request ricevuta per upload multiple foto veicolo'); + next(); + }, + uploadVehiclePhoto.array('photos', 5), + (err, req, res, next) => { + if (err) { + console.error('❌ Errore multer:', err); + return res.status(400).json({ + success: false, + message: err.message || 'Errore durante upload', + }); + } + next(); + }, + async (req, res) => { + try { + console.log('✅ Files ricevuti:', req.files); + + if (!req.files || req.files.length === 0) { + return res.status(400).json({ + success: false, + message: 'Nessuna foto caricata', + }); + } + + const photoUrls = req.files.map((file) => `/upload/vehicles/${file.filename}`); + + res.status(200).json({ + success: true, + message: `${req.files.length} foto caricate con successo`, + data: { + urls: photoUrls, + count: req.files.length, + }, + }); + } catch (error) { + console.error('❌ Errore upload foto veicolo:', error); + + // Elimina tutti i file in caso di errore + if (req.files && req.files.length > 0) { + req.files.forEach((file) => { + if (file.path) { + fs.unlinkSync(file.path); + } + }); + } + + res.status(500).json({ + success: false, + message: "Errore durante l'upload delle foto", + error: error.message, + }); + } + } +); + +/** + * @route POST /api/trasporti/upload/vehicle-photo + * @desc Upload foto veicolo + * @access Private + */ +router.post( + '/upload/vehicle-photo', + authenticate, + (req, res, next) => { + console.log('🚀 Request ricevuta per upload foto veicolo'); + console.log('📋 Headers:', req.headers); + console.log('📦 Body type:', typeof req.body); + next(); + }, + uploadVehiclePhoto.single('photo'), + (err, req, res, next) => { + // Gestione errori multer + if (err) { + console.error('❌ Errore multer:', err); + return res.status(400).json({ + success: false, + message: err.message || 'Errore durante upload', + }); + } + next(); + }, + async (req, res) => { + try { + console.log('✅ File ricevuto:', req.file); + console.log('📄 Body:', req.body); + + if (!req.file) { + return res.status(400).json({ + success: false, + message: 'Nessuna foto caricata - req.file è undefined', + }); + } + + const photoUrl = `/upload/vehicles/${req.file.filename}`; + + res.status(200).json({ + success: true, + message: 'Foto caricata con successo', + data: { + url: photoUrl, + filename: req.file.filename, + }, + }); + } catch (error) { + console.error('❌ Errore upload foto veicolo:', error); + + if (req.file && req.file.path) { + fs.unlinkSync(req.file.path); + } + + res.status(500).json({ + success: false, + message: "Errore durante l'upload della foto", + error: error.message, + }); + } + } +); + +/** + * @route DELETE /api/trasporti/upload/vehicle-photo + * @desc Elimina foto veicolo + * @access Private + */ +router.delete('/upload/vehicle-photo', authenticate, async (req, res) => { + try { + const { photoUrl } = req.body; + + if (!photoUrl) { + return res.status(400).json({ + success: false, + message: 'URL foto non fornito', + }); + } + + // Estrai il filename dall'URL + const filename = photoUrl.split('/').pop(); + const filepath = path.join(vehiclesDir, filename); + + // Verifica che il file esista + if (fs.existsSync(filepath)) { + fs.unlinkSync(filepath); + } + + res.status(200).json({ + success: true, + message: 'Foto eliminata con successo', + }); + } catch (error) { + console.error('❌ Errore eliminazione foto:', error); + res.status(500).json({ + success: false, + message: "Errore durante l'eliminazione della foto", + error: error.message, + }); + } +}); + module.exports = router; diff --git a/src/services/posterRenderer.js b/src/services/posterRenderer.js index 5b22831..fd73dc5 100644 --- a/src/services/posterRenderer.js +++ b/src/services/posterRenderer.js @@ -709,7 +709,7 @@ class PosterRenderer { } // Path locale - if (url.startsWith('/uploads') || url.startsWith('./uploads')) { + if (url.startsWith('/upload') || url.startsWith('./upload')) { const localPath = url.startsWith('/') ? path.join(process.cwd(), url) : url;