- Trasporti- Passo 2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Ride = require('../models/Ride');
|
||||
const User = require('../models/User');
|
||||
const User = require('../models/user');
|
||||
const RideRequest = require('../models/RideRequest');
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
module.exports = Chat;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user