Compare commits
6 Commits
ChatBox
...
feat/trasp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85141df8a4 | ||
|
|
cb965eaa27 | ||
|
|
b78e3ce544 | ||
|
|
2e7801b4ba | ||
|
|
afeedf27a5 | ||
|
|
80c929436c |
@@ -43,4 +43,5 @@ MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
|||||||
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||||
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
|
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
|
||||||
SERVER_A_URL="http://51.77.156.69:3000"
|
SERVER_A_URL="http://51.77.156.69:3000"
|
||||||
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
||||||
|
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||||
@@ -39,3 +39,4 @@ AUTH_NEW_SITES=123123123
|
|||||||
SCRIPTS_DIR=admin_scripts
|
SCRIPTS_DIR=admin_scripts
|
||||||
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
|
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
|
||||||
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
|
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
|
||||||
|
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||||
@@ -44,4 +44,5 @@ OLLAMA_DEFAULT_MODEL=llama3.2:3b
|
|||||||
GROK_API="xai-PcNM5obgPaETtmnfDWPZk235D75ZgxENU2QmeqPfMQCHh9dwCDVeRRe0oVVA2YOpiUDh1uJieZsMasja"
|
GROK_API="xai-PcNM5obgPaETtmnfDWPZk235D75ZgxENU2QmeqPfMQCHh9dwCDVeRRe0oVVA2YOpiUDh1uJieZsMasja"
|
||||||
REPLICATE_API_TOKEN="r8_AVhM6igwvoOnUA65cHVZdhEDfTqBVk94WTB0u"
|
REPLICATE_API_TOKEN="r8_AVhM6igwvoOnUA65cHVZdhEDfTqBVk94WTB0u"
|
||||||
FAL_KEY="7d251c88-21b5-4b55-8b3e-4bafd910f99f:b81c0a36a25b052f26eb8ac226c7efff"
|
FAL_KEY="7d251c88-21b5-4b55-8b3e-4bafd910f99f:b81c0a36a25b052f26eb8ac226c7efff"
|
||||||
HF_TOKEN="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"
|
HF_TOKEN="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"
|
||||||
|
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||||
@@ -41,4 +41,5 @@ MIAB_HOST=box.lamiaposta.org
|
|||||||
MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
||||||
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||||
SERVER_A_URL="http://51.77.156.69:3000"
|
SERVER_A_URL="http://51.77.156.69:3000"
|
||||||
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
||||||
|
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||||
@@ -38,4 +38,5 @@ SCRIPTS_DIR=admin_scripts
|
|||||||
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
|
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
|
||||||
MIAB_HOST=box.lamiaposta.org
|
MIAB_HOST=box.lamiaposta.org
|
||||||
MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
||||||
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||||
|
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||||
17
logtrans.txt
17
logtrans.txt
@@ -589,4 +589,19 @@ Gio 18/12 ORE 16:52: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977
|
|||||||
|
|
||||||
Saldi:
|
Saldi:
|
||||||
surya1977: -27.50 RIS]
|
surya1977: -27.50 RIS]
|
||||||
amandadi: 105.50 RIS]
|
amandadi: 105.50 RIS]
|
||||||
|
Gio 18/12 ORE 18:30: [<b>Circuito RIS Bologna</b>]: Inviate Monete da surya1977 a ElenaEspx 1 RIS [causale: prova 1]
|
||||||
|
|
||||||
|
Saldi:
|
||||||
|
surya1977: 34.90 RIS]
|
||||||
|
ElenaEspx: 39.05 RIS]
|
||||||
|
Gio 18/12 ORE 18:42: [<b>Circuito RIS Pordenone</b>]: Inviate Monete da surya1977 a GruppoYurta 3 RIS [causale: ECCOLO]
|
||||||
|
|
||||||
|
Saldi:
|
||||||
|
surya1977: -3.00 RIS]
|
||||||
|
GruppoYurta: -1.00 RIS]
|
||||||
|
Gio 18/12 ORE 18:53: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 50 RIS [causale: asdasdasda]
|
||||||
|
|
||||||
|
Saldi:
|
||||||
|
surya1977: -77.50 RIS]
|
||||||
|
amandadi: 155.50 RIS]
|
||||||
@@ -55,7 +55,11 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send response with tokens
|
// Send response with tokens
|
||||||
res.header('x-auth', result.token).header('x-refrtok', result.refreshToken).header('x-browser-random', result.browser_random).send(result.user);
|
res
|
||||||
|
.header('x-auth', result.token)
|
||||||
|
.header('x-refrtok', result.refreshToken)
|
||||||
|
.header('x-browser-random', result.browser_random)
|
||||||
|
.send(result.user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in registration:', error.message);
|
console.error('Error in registration:', error.message);
|
||||||
res.status(400).send({
|
res.status(400).send({
|
||||||
@@ -103,11 +107,15 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send response with tokens
|
// Send response with tokens
|
||||||
res.header('x-auth', result.token).header('x-refrtok', result.refreshToken).header('x-browser-random', result.browser_random).send({
|
res
|
||||||
usertosend: result.user,
|
.header('x-auth', result.token)
|
||||||
code: server_constants.RIS_CODE_OK,
|
.header('x-refrtok', result.refreshToken)
|
||||||
subsExistonDb: result.subsExistonDb,
|
.header('x-browser-random', result.browser_random)
|
||||||
});
|
.send({
|
||||||
|
usertosend: result.user,
|
||||||
|
code: server_constants.RIS_CODE_OK,
|
||||||
|
subsExistonDb: result.subsExistonDb,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in login:', error.message);
|
console.error('Error in login:', error.message);
|
||||||
res.status(400).send({
|
res.status(400).send({
|
||||||
@@ -487,6 +495,7 @@ class UserController {
|
|||||||
const { User } = require('../models/user');
|
const { User } = require('../models/user');
|
||||||
return User.isCollaboratore(user.perm);
|
return User.isCollaboratore(user.perm);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = UserController;
|
module.exports = UserController;
|
||||||
|
|||||||
588
src/controllers/VideoController.js
Normal file
588
src/controllers/VideoController.js
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
class VideoController {
|
||||||
|
constructor(baseUploadPath = 'uploads/videos') {
|
||||||
|
this.basePath = path.resolve(baseUploadPath);
|
||||||
|
this._ensureDirectory(this.basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ PRIVATE METHODS ============
|
||||||
|
|
||||||
|
_ensureDirectory(dir) {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isVideoFile(filename) {
|
||||||
|
return /\.(mp4|webm|ogg|mov|avi|mkv)$/i.test(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getFileInfo(filePath, relativePath = '') {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
const filename = path.basename(filePath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: uuidv4(),
|
||||||
|
filename,
|
||||||
|
folder: relativePath,
|
||||||
|
path: `/videos/${relativePath ? relativePath + '/' : ''}${filename}`,
|
||||||
|
size: stat.size,
|
||||||
|
createdAt: stat.birthtime.toISOString(),
|
||||||
|
modifiedAt: stat.mtime.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_scanFolders(dir, relativePath = '') {
|
||||||
|
const folders = [];
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) return folders;
|
||||||
|
|
||||||
|
const items = fs.readdirSync(dir);
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const fullPath = path.join(dir, item);
|
||||||
|
const relPath = relativePath ? `${relativePath}/${item}` : item;
|
||||||
|
|
||||||
|
if (fs.statSync(fullPath).isDirectory()) {
|
||||||
|
folders.push({
|
||||||
|
name: item,
|
||||||
|
path: relPath,
|
||||||
|
level: relPath.split('/').length,
|
||||||
|
});
|
||||||
|
// Ricorsione per sottocartelle
|
||||||
|
folders.push(...this._scanFolders(fullPath, relPath));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ FOLDER METHODS ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ottiene tutte le cartelle
|
||||||
|
*/
|
||||||
|
getFolders = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const folders = this._scanFolders(this.basePath);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { folders },
|
||||||
|
message: 'Cartelle recuperate con successo',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea una nuova cartella
|
||||||
|
*/
|
||||||
|
createFolder = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { folderName, parentPath = '' } = req.body;
|
||||||
|
|
||||||
|
if (!folderName || !folderName.trim()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Nome cartella richiesto',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitizza il nome cartella
|
||||||
|
const sanitizedName = folderName.replace(/[<>:"/\\|?*]/g, '_').trim();
|
||||||
|
|
||||||
|
const basePath = parentPath ? path.join(this.basePath, parentPath) : this.basePath;
|
||||||
|
|
||||||
|
const newFolderPath = path.join(basePath, sanitizedName);
|
||||||
|
|
||||||
|
if (fs.existsSync(newFolderPath)) {
|
||||||
|
return res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
error: 'La cartella esiste già',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(newFolderPath, { recursive: true });
|
||||||
|
|
||||||
|
const folderData = {
|
||||||
|
name: sanitizedName,
|
||||||
|
path: parentPath ? `${parentPath}/${sanitizedName}` : sanitizedName,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: { folder: folderData },
|
||||||
|
message: 'Cartella creata con successo',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rinomina una cartella
|
||||||
|
*/
|
||||||
|
renameFolder = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { folderPath } = req.params;
|
||||||
|
const { newName } = req.body;
|
||||||
|
|
||||||
|
if (!newName || !newName.trim()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Nuovo nome richiesto',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, '_').trim();
|
||||||
|
const oldPath = path.join(this.basePath, folderPath);
|
||||||
|
const parentDir = path.dirname(oldPath);
|
||||||
|
const newPath = path.join(parentDir, sanitizedName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(oldPath)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Cartella non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(newPath)) {
|
||||||
|
return res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Una cartella con questo nome esiste già',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(oldPath, newPath);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Cartella rinominata con successo',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina una cartella
|
||||||
|
*/
|
||||||
|
deleteFolder = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { folderPath } = req.params;
|
||||||
|
const fullPath = path.join(this.basePath, folderPath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Cartella non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che sia una directory
|
||||||
|
if (!fs.statSync(fullPath).isDirectory()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Il percorso non è una cartella',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Cartella eliminata con successo',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ VIDEO METHODS ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ottiene i video di una cartella
|
||||||
|
*/
|
||||||
|
getVideos = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const folder = req.query.folder || '';
|
||||||
|
const targetPath = folder ? path.join(this.basePath, folder) : this.basePath;
|
||||||
|
|
||||||
|
if (!fs.existsSync(targetPath)) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
videos: [],
|
||||||
|
folders: [],
|
||||||
|
currentPath: folder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = fs.readdirSync(targetPath);
|
||||||
|
const videos = [];
|
||||||
|
const subfolders = [];
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const itemPath = path.join(targetPath, item);
|
||||||
|
const stat = fs.statSync(itemPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
subfolders.push({
|
||||||
|
name: item,
|
||||||
|
path: folder ? `${folder}/${item}` : item,
|
||||||
|
});
|
||||||
|
} else if (stat.isFile() && this._isVideoFile(item)) {
|
||||||
|
videos.push(this._getFileInfo(itemPath, folder));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ordina per data di creazione (più recenti prima)
|
||||||
|
videos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
videos,
|
||||||
|
folders: subfolders,
|
||||||
|
currentPath: folder,
|
||||||
|
totalVideos: videos.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload singolo video
|
||||||
|
*/
|
||||||
|
uploadVideo = async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Nessun file caricato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Legge da query parameter
|
||||||
|
const folder = req.query.folder || 'default';
|
||||||
|
|
||||||
|
const videoInfo = {
|
||||||
|
id: uuidv4(),
|
||||||
|
originalName: req.file.originalname,
|
||||||
|
filename: req.file.filename,
|
||||||
|
folder: folder,
|
||||||
|
path: `/videos/${folder}/${req.file.filename}`,
|
||||||
|
size: req.file.size,
|
||||||
|
mimetype: req.file.mimetype,
|
||||||
|
uploadedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: { video: videoInfo },
|
||||||
|
message: 'Video caricato con successo',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload multiplo video
|
||||||
|
*/
|
||||||
|
uploadVideos = async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.files || req.files.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Nessun file caricato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Legge da query parameter
|
||||||
|
const folder = req.query.folder || 'default';
|
||||||
|
|
||||||
|
const videos = req.files.map((file) => ({
|
||||||
|
id: uuidv4(),
|
||||||
|
originalName: file.originalname,
|
||||||
|
filename: file.filename,
|
||||||
|
folder: folder,
|
||||||
|
path: `/videos/${folder}/${file.filename}`,
|
||||||
|
size: file.size,
|
||||||
|
mimetype: file.mimetype,
|
||||||
|
uploadedAt: new Date().toISOString(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: { videos },
|
||||||
|
message: `${videos.length} video caricati con successo`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ottiene info di un singolo video
|
||||||
|
*/
|
||||||
|
getVideo = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { folder, filename } = req.params;
|
||||||
|
const videoPath = path.join(this.basePath, folder, filename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(videoPath)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Video non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoInfo = this._getFileInfo(videoPath, folder);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { video: videoInfo },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rinomina un video
|
||||||
|
*/
|
||||||
|
renameVideo = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { folder, filename } = req.params;
|
||||||
|
const { newFilename } = req.body;
|
||||||
|
|
||||||
|
if (!newFilename || !newFilename.trim()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Nuovo nome file richiesto',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldPath = path.join(this.basePath, folder, filename);
|
||||||
|
const newPath = path.join(this.basePath, folder, newFilename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(oldPath)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Video non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(newPath)) {
|
||||||
|
return res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Un file con questo nome esiste già',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(oldPath, newPath);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Video rinominato con successo',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sposta un video in un'altra cartella
|
||||||
|
*/
|
||||||
|
moveVideo = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { folder, filename } = req.params;
|
||||||
|
const { destinationFolder } = req.body;
|
||||||
|
|
||||||
|
const sourcePath = path.join(this.basePath, folder, filename);
|
||||||
|
const destDir = path.join(this.basePath, destinationFolder);
|
||||||
|
const destPath = path.join(destDir, filename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(sourcePath)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Video non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._ensureDirectory(destDir);
|
||||||
|
|
||||||
|
if (fs.existsSync(destPath)) {
|
||||||
|
return res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Un file con questo nome esiste già nella destinazione',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(sourcePath, destPath);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Video spostato con successo',
|
||||||
|
data: {
|
||||||
|
newPath: `/videos/${destinationFolder}/${filename}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina un video
|
||||||
|
*/
|
||||||
|
deleteVideo = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { folder, filename } = req.params;
|
||||||
|
const videoPath = path.join(this.basePath, folder, filename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(videoPath)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Video non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.unlinkSync(videoPath);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Video eliminato con successo',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream video (per player)
|
||||||
|
*/
|
||||||
|
streamVideo = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { folder, filename } = req.params;
|
||||||
|
const videoPath = path.join(this.basePath, folder, filename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(videoPath)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Video non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(videoPath);
|
||||||
|
const fileSize = stat.size;
|
||||||
|
const range = req.headers.range;
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
const parts = range.replace(/bytes=/, '').split('-');
|
||||||
|
const start = parseInt(parts[0], 10);
|
||||||
|
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
||||||
|
const chunkSize = end - start + 1;
|
||||||
|
|
||||||
|
const file = fs.createReadStream(videoPath, { start, end });
|
||||||
|
const headers = {
|
||||||
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Length': chunkSize,
|
||||||
|
'Content-Type': 'video/mp4',
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(206, headers);
|
||||||
|
file.pipe(res);
|
||||||
|
} else {
|
||||||
|
const headers = {
|
||||||
|
'Content-Length': fileSize,
|
||||||
|
'Content-Type': 'video/mp4',
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200, headers);
|
||||||
|
fs.createReadStream(videoPath).pipe(res);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ ERROR HANDLER MIDDLEWARE ============
|
||||||
|
|
||||||
|
static errorHandler = (error, req, res, next) => {
|
||||||
|
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||||
|
return res.status(413).json({
|
||||||
|
success: false,
|
||||||
|
error: 'File troppo grande. Dimensione massima: 500MB',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 'LIMIT_FILE_COUNT') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Troppi file. Massimo 10 file per upload',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message.includes('Tipo file non supportato')) {
|
||||||
|
return res.status(415).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Errore interno del server',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = VideoController;
|
||||||
@@ -4,7 +4,7 @@ const sharp = require('sharp');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
|
|
||||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload';
|
||||||
|
|
||||||
const assetController = {
|
const assetController = {
|
||||||
// POST /assets/upload
|
// POST /assets/upload
|
||||||
@@ -50,9 +50,9 @@ const assetController = {
|
|||||||
sourceType: 'upload',
|
sourceType: 'upload',
|
||||||
file: {
|
file: {
|
||||||
path: file.path,
|
path: file.path,
|
||||||
url: `/uploads/${file.filename}`,
|
url: `/upload/${file.filename}`,
|
||||||
thumbnailPath: thumbPath,
|
thumbnailPath: thumbPath,
|
||||||
thumbnailUrl: `/uploads/thumbs/${thumbName}`,
|
thumbnailUrl: `/upload/thumbs/${thumbName}`,
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
@@ -106,7 +106,7 @@ const assetController = {
|
|||||||
sourceType: 'upload',
|
sourceType: 'upload',
|
||||||
file: {
|
file: {
|
||||||
path: file.path,
|
path: file.path,
|
||||||
url: `/uploads/${file.filename}`,
|
url: `/upload/${file.filename}`,
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
@@ -199,7 +199,7 @@ const assetController = {
|
|||||||
sourceType: 'ai',
|
sourceType: 'ai',
|
||||||
file: {
|
file: {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
url: `/uploads/ai-generated/${fileName}`,
|
url: `/upload/ai-generated/${fileName}`,
|
||||||
mimeType: 'image/jpeg',
|
mimeType: 'image/jpeg',
|
||||||
size: fileSize,
|
size: fileSize,
|
||||||
dimensions
|
dimensions
|
||||||
|
|||||||
789
src/controllers/chatController.js
Normal file
789
src/controllers/chatController.js
Normal file
@@ -0,0 +1,789 @@
|
|||||||
|
const Chat = require('../models/Chat');
|
||||||
|
const Message = require('../models/Message');
|
||||||
|
const { User } = require('../models/user');
|
||||||
|
|
||||||
|
// ===== GET USER CHATS =====
|
||||||
|
exports.getUserChats = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const idapp = req.user.idapp;
|
||||||
|
const page = parseInt(req.query.page) || 1;
|
||||||
|
const limit = parseInt(req.query.limit) || 20;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
// ✅ Trova chat dove l'utente è partecipante E non l'ha cancellata
|
||||||
|
const chats = await Chat.find({
|
||||||
|
idapp,
|
||||||
|
participants: userId,
|
||||||
|
isActive: true,
|
||||||
|
deletedBy: { $ne: userId }, // ✅ Escludi chat cancellate
|
||||||
|
})
|
||||||
|
.populate('participants', 'username name surname profile')
|
||||||
|
.populate({
|
||||||
|
path: 'rideId',
|
||||||
|
select: 'departure destination departureDate departureTime status',
|
||||||
|
})
|
||||||
|
.sort({ updatedAt: -1 })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(limit)
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
const enrichedChats = chats.map((chat) => {
|
||||||
|
let unreadCount = 0;
|
||||||
|
|
||||||
|
if (chat.unreadCount) {
|
||||||
|
if (chat.unreadCount instanceof Map) {
|
||||||
|
unreadCount = chat.unreadCount.get(userId.toString()) || 0;
|
||||||
|
} else if (typeof chat.unreadCount === 'object') {
|
||||||
|
// Dopo .lean(), la Map diventa un oggetto plain
|
||||||
|
unreadCount = chat.unreadCount[userId.toString()] || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...chat,
|
||||||
|
unreadCount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: enrichedChats,
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasMore: chats.length === limit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching chats:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel recupero delle chat',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== GET OR CREATE DIRECT CHAT =====
|
||||||
|
exports.getOrCreateDirectChat = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { otherUserId, rideId } = req.body;
|
||||||
|
const idapp = req.user.idapp;
|
||||||
|
|
||||||
|
if (!otherUserId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'otherUserId è richiesto',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che l'altro utente esista
|
||||||
|
const otherUser = await User.findById(otherUserId);
|
||||||
|
if (!otherUser) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Utente non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cerca chat esistente
|
||||||
|
let chat = await Chat.findOne({
|
||||||
|
idapp,
|
||||||
|
type: 'direct',
|
||||||
|
participants: { $all: [userId, otherUserId], $size: 2 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chat) {
|
||||||
|
// Crea nuova chat
|
||||||
|
chat = new Chat({
|
||||||
|
idapp,
|
||||||
|
type: 'direct',
|
||||||
|
participants: [userId, otherUserId],
|
||||||
|
rideId: rideId || null,
|
||||||
|
unreadCount: new Map(),
|
||||||
|
});
|
||||||
|
await chat.save();
|
||||||
|
} else if (rideId && !chat.rideId) {
|
||||||
|
// Aggiungi rideId se non presente
|
||||||
|
chat.rideId = rideId;
|
||||||
|
await chat.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Se la chat era stata cancellata da uno dei due, rimuovilo da deletedBy
|
||||||
|
if (chat.deletedBy && chat.deletedBy.length > 0) {
|
||||||
|
const wasDeleted = chat.deletedBy.some((id) => id.toString() === userId.toString());
|
||||||
|
|
||||||
|
if (wasDeleted) {
|
||||||
|
chat.deletedBy = chat.deletedBy.filter((id) => id.toString() !== userId.toString());
|
||||||
|
await chat.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popola i partecipanti
|
||||||
|
await chat.populate('participants', 'username name surname profile');
|
||||||
|
if (chat.rideId) {
|
||||||
|
await chat.populate('rideId', 'departure destination departureDate');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiungi unread count
|
||||||
|
const chatObj = chat.toObject();
|
||||||
|
chatObj.unreadCount = chat.getUnreadForUser(userId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: chatObj,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting/creating direct chat:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nella creazione della chat',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== GET CHAT BY ID =====
|
||||||
|
exports.getChatById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { chatId } = req.params;
|
||||||
|
|
||||||
|
const chat = await Chat.findById(chatId)
|
||||||
|
.populate('participants', 'username name surname profile')
|
||||||
|
.populate({
|
||||||
|
path: 'rideId',
|
||||||
|
select: 'departure destination departureDate departureTime status',
|
||||||
|
})
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
if (!chat) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Chat non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che l'utente sia partecipante
|
||||||
|
const isParticipant = chat.participants.some((p) => {
|
||||||
|
const pId = p._id ? p._id.toString() : p.toString();
|
||||||
|
return pId === userId.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isParticipant) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non autorizzato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica se l'utente ha cancellato questa chat
|
||||||
|
const wasDeleted = chat.deletedBy?.some((id) => id.toString() === userId.toString());
|
||||||
|
|
||||||
|
if (wasDeleted) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Chat non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiungi unread count per l'utente corrente
|
||||||
|
let unreadCount = 0;
|
||||||
|
if (chat.unreadCount) {
|
||||||
|
if (chat.unreadCount instanceof Map) {
|
||||||
|
unreadCount = chat.unreadCount.get(userId.toString()) || 0;
|
||||||
|
} else if (typeof chat.unreadCount === 'object') {
|
||||||
|
unreadCount = chat.unreadCount[userId.toString()] || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatObj = {
|
||||||
|
...chat,
|
||||||
|
unreadCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: chatObj,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting chat by ID:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel recupero della chat',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== GET CHAT MESSAGES =====
|
||||||
|
exports.getChatMessages = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { chatId } = req.params;
|
||||||
|
const idapp = req.user.idapp;
|
||||||
|
const page = parseInt(req.query.page) || 1;
|
||||||
|
const { before, after, limit = 50 } = req.query;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Verifica chat e partecipazione
|
||||||
|
const chat = await Chat.findById(chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Chat non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.hasParticipant(userId)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non autorizzato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
chatId,
|
||||||
|
idapp,
|
||||||
|
isDeleted: { $ne: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
// clearedBefore
|
||||||
|
let clearedDate = null;
|
||||||
|
if (chat.clearedBefore) {
|
||||||
|
if (chat.clearedBefore instanceof Map) {
|
||||||
|
clearedDate = chat.clearedBefore.get(userId.toString());
|
||||||
|
} else if (typeof chat.clearedBefore === 'object') {
|
||||||
|
clearedDate = chat.clearedBefore[userId.toString()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearedDate) {
|
||||||
|
query.createdAt = { $gt: new Date(clearedDate) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Paginazione: before (messaggi più vecchi)
|
||||||
|
if (before) {
|
||||||
|
query.createdAt = {
|
||||||
|
...query.createdAt,
|
||||||
|
$lt: new Date(before),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Polling: after (messaggi più nuovi)
|
||||||
|
if (after) {
|
||||||
|
query.createdAt = {
|
||||||
|
...query.createdAt,
|
||||||
|
$gt: new Date(after), // Messaggi DOPO questo timestamp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Ordina in base alla direzione
|
||||||
|
const sortOrder = after ? 1 : -1; // after: asc, before: desc
|
||||||
|
|
||||||
|
const messages = await Message.find(query)
|
||||||
|
.sort({ createdAt: sortOrder })
|
||||||
|
.limit(parseInt(limit))
|
||||||
|
.populate('senderId', 'username name surname profile.img profile.avatar')
|
||||||
|
.populate({
|
||||||
|
path: 'replyTo',
|
||||||
|
select: 'text senderId',
|
||||||
|
populate: {
|
||||||
|
path: 'senderId',
|
||||||
|
select: 'username name',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
// ✅ Se usato after, i messaggi sono già in ordine cronologico
|
||||||
|
// Se usato before, invertili
|
||||||
|
if (!after) {
|
||||||
|
messages.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marca i messaggi come letti
|
||||||
|
await chat.markAsRead(userId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: messages.reverse(),
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasMore: messages.length === limit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching messages:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel recupero dei messaggi',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== SEND MESSAGE =====
|
||||||
|
exports.sendMessage = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const idapp = req.user.idapp;
|
||||||
|
const { chatId } = req.params;
|
||||||
|
const { text, type = 'text', metadata } = req.body;
|
||||||
|
|
||||||
|
// Verifica chat
|
||||||
|
const chat = await Chat.findById(chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Chat non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.hasParticipant(userId)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non autorizzato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chat.isBlockedFor(userId)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non puoi inviare messaggi in questa chat',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ IMPORTANTE: Se qualcuno aveva cancellato la chat, rimuovilo da deletedBy
|
||||||
|
// così la chat riappare nella sua lista
|
||||||
|
if (chat.deletedBy && chat.deletedBy.length > 0) {
|
||||||
|
const otherParticipants = chat.participants.filter((p) => p.toString() !== userId.toString());
|
||||||
|
|
||||||
|
let needsSave = false;
|
||||||
|
otherParticipants.forEach((participantId) => {
|
||||||
|
const wasDeleted = chat.deletedBy.some((id) => id.toString() === participantId.toString());
|
||||||
|
if (wasDeleted) {
|
||||||
|
chat.deletedBy = chat.deletedBy.filter((id) => id.toString() !== participantId.toString());
|
||||||
|
needsSave = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (needsSave) {
|
||||||
|
await chat.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea messaggio
|
||||||
|
const message = new Message({
|
||||||
|
idapp,
|
||||||
|
chatId: chat._id,
|
||||||
|
senderId: userId,
|
||||||
|
text,
|
||||||
|
type,
|
||||||
|
metadata,
|
||||||
|
readBy: [userId],
|
||||||
|
});
|
||||||
|
|
||||||
|
await message.save();
|
||||||
|
await message.populate('senderId', 'username name surname profile');
|
||||||
|
|
||||||
|
// Aggiorna chat
|
||||||
|
await chat.updateLastMessage(message);
|
||||||
|
await chat.incrementUnread(userId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: message,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending message:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Errore nell'invio del messaggio",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== MARK MESSAGES AS READ =====
|
||||||
|
exports.markAsRead = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { chatId } = req.params;
|
||||||
|
|
||||||
|
const chat = await Chat.findById(chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Chat non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.hasParticipant(userId)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non autorizzato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marca come letti
|
||||||
|
await chat.markAsRead(userId);
|
||||||
|
|
||||||
|
// Aggiorna anche i singoli messaggi
|
||||||
|
await Message.updateMany(
|
||||||
|
{
|
||||||
|
chatId: chat._id,
|
||||||
|
senderId: { $ne: userId },
|
||||||
|
readBy: { $ne: userId },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$addToSet: { readBy: userId },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Messaggi marcati come letti',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error marking as read:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nella marcatura dei messaggi',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== DELETE CHAT (SOFT DELETE) =====
|
||||||
|
exports.deleteChat = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { chatId } = req.params;
|
||||||
|
|
||||||
|
const chat = await Chat.findById(chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Chat non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.hasParticipant(userId)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non autorizzato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Soft delete: aggiungi userId a deletedBy
|
||||||
|
if (!chat.deletedBy) {
|
||||||
|
chat.deletedBy = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyDeleted = chat.deletedBy.some((id) => id.toString() === userId.toString());
|
||||||
|
|
||||||
|
if (!alreadyDeleted) {
|
||||||
|
chat.deletedBy.push(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Salva il timestamp di quando l'utente ha cancellato
|
||||||
|
// così quando riappare la chat, vedrà solo messaggi nuovi
|
||||||
|
if (!chat.clearedBefore) {
|
||||||
|
chat.clearedBefore = new Map();
|
||||||
|
}
|
||||||
|
chat.clearedBefore.set(userId.toString(), new Date());
|
||||||
|
|
||||||
|
await chat.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Chat eliminata',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting chat:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Errore nell'eliminazione della chat",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== TOGGLE MUTE CHAT =====
|
||||||
|
exports.toggleMuteChat = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { chatId } = req.params;
|
||||||
|
const { mute } = req.body;
|
||||||
|
|
||||||
|
const chat = await Chat.findById(chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Chat non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.hasParticipant(userId)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non autorizzato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.mutedBy) {
|
||||||
|
chat.mutedBy = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mute) {
|
||||||
|
// Aggiungi a mutedBy se non presente
|
||||||
|
const alreadyMuted = chat.mutedBy.some((id) => id.toString() === userId.toString());
|
||||||
|
if (!alreadyMuted) {
|
||||||
|
chat.mutedBy.push(userId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Rimuovi da mutedBy
|
||||||
|
chat.mutedBy = chat.mutedBy.filter((id) => id.toString() !== userId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await chat.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: mute ? 'Chat silenziata' : 'Notifiche attivate',
|
||||||
|
data: { muted: mute },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling mute:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Errore nell'aggiornamento",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== GET UNREAD COUNT =====
|
||||||
|
exports.getUnreadCount = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const idapp = req.user.idapp;
|
||||||
|
|
||||||
|
const chats = await Chat.find({
|
||||||
|
idapp,
|
||||||
|
participants: userId,
|
||||||
|
isActive: true,
|
||||||
|
deletedBy: { $ne: userId },
|
||||||
|
}).lean();
|
||||||
|
|
||||||
|
let totalUnread = 0;
|
||||||
|
chats.forEach((chat) => {
|
||||||
|
const unread = chat.unreadCount?.get(userId.toString()) || 0;
|
||||||
|
totalUnread += unread;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
totalUnread,
|
||||||
|
chatCount: chats.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting unread count:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel conteggio messaggi non letti',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Marca una chat come letta
|
||||||
|
* @route PUT /api/viaggi/chats/:chatId/read
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
exports.markChatAsRead = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { chatId } = req.params;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
const chat = await Chat.findById(chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Chat non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.hasParticipant(userId)) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non sei autorizzato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset unread count
|
||||||
|
if (!chat.unreadCount) {
|
||||||
|
chat.unreadCount = new Map();
|
||||||
|
}
|
||||||
|
chat.unreadCount.set(userId.toString(), 0);
|
||||||
|
chat.markModified('unreadCount');
|
||||||
|
await chat.save();
|
||||||
|
|
||||||
|
// Marca messaggi come letti
|
||||||
|
await Message.updateMany(
|
||||||
|
{
|
||||||
|
chatId,
|
||||||
|
senderId: { $ne: userId },
|
||||||
|
readBy: { $ne: userId },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$addToSet: { readBy: userId },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Chat marcata come letta',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore marca come letto:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Muta/smuta notifiche
|
||||||
|
* @route PUT /api/viaggi/chats/:chatId/mute
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
exports.toggleMuteChat = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { chatId } = req.params;
|
||||||
|
const { mute } = req.body;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
const chat = await Chat.findById(chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Chat non trovata' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.hasParticipant(userId)) {
|
||||||
|
return res.status(403).json({ success: false, message: 'Non sei autorizzato' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.mutedBy) {
|
||||||
|
chat.mutedBy = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMuted = chat.mutedBy.some((mid) => mid.toString() === userId.toString());
|
||||||
|
|
||||||
|
if (mute && !isMuted) {
|
||||||
|
chat.mutedBy.push(userId);
|
||||||
|
} else if (!mute && isMuted) {
|
||||||
|
chat.mutedBy = chat.mutedBy.filter((mid) => mid.toString() !== userId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await chat.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: mute ? 'Notifiche disattivate' : 'Notifiche attivate',
|
||||||
|
data: { muted: mute },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore mute chat:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Errore', error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Elimina un messaggio (soft delete)
|
||||||
|
* @route DELETE /api/viaggi/chats/:chatId/messages/:messageId
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
exports.deleteMessage = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { chatId, messageId } = req.params;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
const message = await Message.findById(messageId);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Messaggio non trovato' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.chatId.toString() !== chatId) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Messaggio non appartiene a questa chat' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.senderId.toString() !== userId.toString()) {
|
||||||
|
return res.status(403).json({ success: false, message: 'Non sei autorizzato' });
|
||||||
|
}
|
||||||
|
|
||||||
|
message.isDeleted = true;
|
||||||
|
message.deletedAt = new Date();
|
||||||
|
message.text = 'Messaggio eliminato';
|
||||||
|
await message.save();
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Messaggio eliminato' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore eliminazione messaggio:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Errore', error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Blocca/sblocca una chat
|
||||||
|
* @route PUT /api/viaggi/chats/:chatId/block
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
exports.toggleBlockChat = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { chatId } = req.params;
|
||||||
|
const { block } = req.body;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
const chat = await Chat.findById(chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Chat non trovata' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.hasParticipant(userId)) {
|
||||||
|
return res.status(403).json({ success: false, message: 'Non sei autorizzato' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat.blockedBy) {
|
||||||
|
chat.blockedBy = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBlocked = chat.blockedBy.some((bid) => bid.toString() === userId.toString());
|
||||||
|
|
||||||
|
if (block && !isBlocked) {
|
||||||
|
chat.blockedBy.push(userId);
|
||||||
|
} else if (!block && isBlocked) {
|
||||||
|
chat.blockedBy = chat.blockedBy.filter((bid) => bid.toString() !== userId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await chat.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: block ? 'Chat bloccata' : 'Chat sbloccata',
|
||||||
|
data: { blocked: block },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore blocco chat:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Errore', error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = exports;
|
||||||
931
src/controllers/feedbackController.js
Normal file
931
src/controllers/feedbackController.js
Normal file
@@ -0,0 +1,931 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Feedback = require('../models/Feedback');
|
||||||
|
const Ride = require('../models/Ride');
|
||||||
|
const RideRequest = require('../models/RideRequest');
|
||||||
|
const { User } = require('../models/user');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 🔧 HELPER FUNCTIONS (definite prima per essere disponibili)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converti userId in ObjectId in modo sicuro
|
||||||
|
*/
|
||||||
|
const toObjectId = (id) => {
|
||||||
|
if (!id) return null;
|
||||||
|
|
||||||
|
if (id instanceof mongoose.Types.ObjectId) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof id === 'object' && id._id) {
|
||||||
|
return new mongoose.Types.ObjectId(id._id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new mongoose.Types.ObjectId(id.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ottieni statistiche feedback per un utente
|
||||||
|
*/
|
||||||
|
const getStatsForUser = async (idapp, userId) => {
|
||||||
|
try {
|
||||||
|
const userObjectId = toObjectId(userId);
|
||||||
|
|
||||||
|
if (!userObjectId) {
|
||||||
|
return {
|
||||||
|
averageRating: 0,
|
||||||
|
totalFeedback: 0,
|
||||||
|
asDriver: 0,
|
||||||
|
asPassenger: 0,
|
||||||
|
distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Feedback.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
idapp,
|
||||||
|
toUserId: userObjectId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: null,
|
||||||
|
averageRating: { $avg: '$rating' },
|
||||||
|
totalFeedback: { $sum: 1 },
|
||||||
|
asDriver: {
|
||||||
|
$sum: { $cond: [{ $eq: ['$role', 'driver'] }, 1, 0] },
|
||||||
|
},
|
||||||
|
asPassenger: {
|
||||||
|
$sum: { $cond: [{ $eq: ['$role', 'passenger'] }, 1, 0] },
|
||||||
|
},
|
||||||
|
rating5: { $sum: { $cond: [{ $eq: ['$rating', 5] }, 1, 0] } },
|
||||||
|
rating4: { $sum: { $cond: [{ $eq: ['$rating', 4] }, 1, 0] } },
|
||||||
|
rating3: { $sum: { $cond: [{ $eq: ['$rating', 3] }, 1, 0] } },
|
||||||
|
rating2: { $sum: { $cond: [{ $eq: ['$rating', 2] }, 1, 0] } },
|
||||||
|
rating1: { $sum: { $cond: [{ $eq: ['$rating', 1] }, 1, 0] } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!result || result.length === 0) {
|
||||||
|
return {
|
||||||
|
averageRating: 0,
|
||||||
|
totalFeedback: 0,
|
||||||
|
asDriver: 0,
|
||||||
|
asPassenger: 0,
|
||||||
|
distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = result[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
averageRating: stats.averageRating
|
||||||
|
? Math.round(stats.averageRating * 10) / 10
|
||||||
|
: 0,
|
||||||
|
totalFeedback: stats.totalFeedback || 0,
|
||||||
|
asDriver: stats.asDriver || 0,
|
||||||
|
asPassenger: stats.asPassenger || 0,
|
||||||
|
distribution: {
|
||||||
|
1: stats.rating1 || 0,
|
||||||
|
2: stats.rating2 || 0,
|
||||||
|
3: stats.rating3 || 0,
|
||||||
|
4: stats.rating4 || 0,
|
||||||
|
5: stats.rating5 || 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore calcolo stats feedback:', error);
|
||||||
|
return {
|
||||||
|
averageRating: 0,
|
||||||
|
totalFeedback: 0,
|
||||||
|
asDriver: 0,
|
||||||
|
asPassenger: 0,
|
||||||
|
distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcola la distribuzione dei rating per un utente
|
||||||
|
*/
|
||||||
|
const getRatingDistribution = async (idapp, userId) => {
|
||||||
|
try {
|
||||||
|
const userObjectId = toObjectId(userId);
|
||||||
|
|
||||||
|
if (!userObjectId) {
|
||||||
|
return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Feedback.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
idapp,
|
||||||
|
toUserId: userObjectId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$rating',
|
||||||
|
count: { $sum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { _id: -1 } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
||||||
|
|
||||||
|
result.forEach((r) => {
|
||||||
|
if (r._id >= 1 && r._id <= 5) {
|
||||||
|
distribution[r._id] = r.count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return distribution;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore calcolo distribuzione:', error);
|
||||||
|
return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggiorna la media rating nel profilo utente
|
||||||
|
*/
|
||||||
|
const updateUserRating = async (idapp, userId) => {
|
||||||
|
try {
|
||||||
|
const stats = await getStatsForUser(idapp, userId);
|
||||||
|
|
||||||
|
await User.findByIdAndUpdate(userId, {
|
||||||
|
$set: {
|
||||||
|
'profile.driverProfile.averageRating': stats.averageRating,
|
||||||
|
'profile.driverProfile.totalFeedback': stats.totalFeedback,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore aggiornamento rating utente:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 📝 CONTROLLER FUNCTIONS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Crea un feedback per un viaggio
|
||||||
|
* @route POST /api/viaggi/feedback
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const createFeedback = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
idapp,
|
||||||
|
rideId,
|
||||||
|
rideRequestId,
|
||||||
|
toUserId,
|
||||||
|
role,
|
||||||
|
rating,
|
||||||
|
categories,
|
||||||
|
comment,
|
||||||
|
pros,
|
||||||
|
cons,
|
||||||
|
tags,
|
||||||
|
isPublic,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const fromUserId = req.user._id;
|
||||||
|
|
||||||
|
// Validazione base
|
||||||
|
if (!idapp || !rideId || !toUserId || !role || !rating) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Campi obbligatori: idapp, rideId, toUserId, role, rating',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica rating valido
|
||||||
|
if (rating < 1 || rating > 5) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Il rating deve essere tra 1 e 5',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che il ride esista
|
||||||
|
const ride = await Ride.findById(rideId);
|
||||||
|
if (!ride) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Viaggio non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che l'utente abbia partecipato al viaggio
|
||||||
|
const wasDriver = ride.userId.toString() === fromUserId.toString();
|
||||||
|
const wasPassenger = ride.confirmedPassengers?.some(
|
||||||
|
(p) => p.userId.toString() === fromUserId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!wasDriver && !wasPassenger) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non hai partecipato a questo viaggio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che non stia valutando se stesso
|
||||||
|
if (fromUserId.toString() === toUserId.toString()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non puoi valutare te stesso',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che non esista già un feedback
|
||||||
|
const existingFeedback = await Feedback.findOne({
|
||||||
|
rideId,
|
||||||
|
fromUserId,
|
||||||
|
toUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingFeedback) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Hai già lasciato un feedback per questo utente in questo viaggio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea il feedback
|
||||||
|
const feedbackData = {
|
||||||
|
idapp,
|
||||||
|
rideId,
|
||||||
|
fromUserId,
|
||||||
|
toUserId,
|
||||||
|
role,
|
||||||
|
rating,
|
||||||
|
isVerified: ride.status === 'completed',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rideRequestId) feedbackData.rideRequestId = rideRequestId;
|
||||||
|
if (categories) feedbackData.categories = categories;
|
||||||
|
if (comment) feedbackData.comment = comment;
|
||||||
|
if (pros) feedbackData.pros = pros;
|
||||||
|
if (cons) feedbackData.cons = cons;
|
||||||
|
if (tags) feedbackData.tags = tags;
|
||||||
|
if (isPublic !== undefined) feedbackData.isPublic = isPublic;
|
||||||
|
|
||||||
|
const feedback = new Feedback(feedbackData);
|
||||||
|
await feedback.save();
|
||||||
|
|
||||||
|
// Aggiorna la media rating dell'utente destinatario
|
||||||
|
await updateUserRating(idapp, toUserId);
|
||||||
|
|
||||||
|
// Aggiorna flag nella richiesta se presente
|
||||||
|
if (rideRequestId) {
|
||||||
|
await RideRequest.findByIdAndUpdate(rideRequestId, {
|
||||||
|
$set: { feedbackGiven: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await feedback.populate('fromUserId', 'username name surname profile.img');
|
||||||
|
await feedback.populate('toUserId', 'username name surname');
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Feedback inviato con successo!',
|
||||||
|
data: feedback,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore creazione feedback:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nella creazione del feedback',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni i feedback ricevuti da un utente
|
||||||
|
* @route GET /api/viaggi/feedback/user/:userId
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
const getUserFeedback = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.params;
|
||||||
|
const { idapp, role, page = 1, limit = 10 } = req.query;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
idapp,
|
||||||
|
toUserId: userId,
|
||||||
|
isPublic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (role) {
|
||||||
|
query.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||||
|
|
||||||
|
const [feedbacks, total, stats] = await Promise.all([
|
||||||
|
Feedback.find(query)
|
||||||
|
.populate('fromUserId', 'username name surname profile.img')
|
||||||
|
.populate('rideId', 'departure destination dateTime')
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(parseInt(limit)),
|
||||||
|
Feedback.countDocuments(query),
|
||||||
|
getStatsForUser(idapp, userId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: feedbacks,
|
||||||
|
stats,
|
||||||
|
pagination: {
|
||||||
|
page: parseInt(page),
|
||||||
|
limit: parseInt(limit),
|
||||||
|
total,
|
||||||
|
pages: Math.ceil(total / parseInt(limit)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore recupero feedbacks:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel recupero dei feedback',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni statistiche feedback per un utente
|
||||||
|
* @route GET /api/viaggi/feedback/user/:userId/stats
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
const getUserFeedbackStats = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.params;
|
||||||
|
const { idapp } = req.query;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await getStatsForUser(idapp, userId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: stats,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore recupero stats:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel recupero delle statistiche',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni i feedback per un viaggio
|
||||||
|
* @route GET /api/viaggi/feedback/ride/:rideId
|
||||||
|
* @access Public/Private
|
||||||
|
*/
|
||||||
|
const getRideFeedback = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { rideId } = req.params;
|
||||||
|
const { idapp } = req.query;
|
||||||
|
const userId = req.user?._id;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ride = await Ride.findById(rideId);
|
||||||
|
if (!ride) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Viaggio non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
idapp,
|
||||||
|
rideId,
|
||||||
|
isPublic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const feedbacks = await Feedback.find(query)
|
||||||
|
.populate('fromUserId', 'username name surname profile.img')
|
||||||
|
.populate('toUserId', 'username name surname profile.img')
|
||||||
|
.sort({ createdAt: -1 });
|
||||||
|
|
||||||
|
let pendingFeedbacks = [];
|
||||||
|
let myFeedbacks = [];
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
const wasDriver = ride.userId.toString() === userId.toString();
|
||||||
|
const wasPassenger = ride.confirmedPassengers?.some(
|
||||||
|
(p) => p.userId.toString() === userId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wasDriver || wasPassenger) {
|
||||||
|
myFeedbacks = feedbacks.filter(
|
||||||
|
(f) => f.fromUserId._id.toString() === userId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wasDriver) {
|
||||||
|
const feedbackGivenTo = myFeedbacks.map((f) => f.toUserId._id.toString());
|
||||||
|
pendingFeedbacks = (ride.confirmedPassengers || [])
|
||||||
|
.filter((p) => !feedbackGivenTo.includes(p.userId.toString()))
|
||||||
|
.map((p) => ({ userId: p.userId, role: 'passenger' }));
|
||||||
|
} else {
|
||||||
|
const hasGivenToDriver = myFeedbacks.some(
|
||||||
|
(f) => f.toUserId._id.toString() === ride.userId.toString()
|
||||||
|
);
|
||||||
|
if (!hasGivenToDriver) {
|
||||||
|
pendingFeedbacks.push({ userId: ride.userId, role: 'driver' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
feedbacks,
|
||||||
|
pendingFeedbacks,
|
||||||
|
myFeedbacks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore recupero feedbacks viaggio:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Verifica se l'utente può lasciare un feedback
|
||||||
|
* @route GET /api/viaggi/feedback/can-leave/:rideId/:toUserId
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const canLeaveFeedback = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { rideId, toUserId } = req.params;
|
||||||
|
const { idapp } = req.query;
|
||||||
|
const fromUserId = req.user._id;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ride = await Ride.findById(rideId);
|
||||||
|
if (!ride) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Viaggio non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasDriver = ride.userId.toString() === fromUserId.toString();
|
||||||
|
const wasPassenger = ride.confirmedPassengers?.some(
|
||||||
|
(p) => p.userId.toString() === fromUserId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!wasDriver && !wasPassenger) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
canLeave: false,
|
||||||
|
reason: 'Non hai partecipato a questo viaggio',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromUserId.toString() === toUserId.toString()) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
canLeave: false,
|
||||||
|
reason: 'Non puoi valutare te stesso',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const toUserWasDriver = ride.userId.toString() === toUserId.toString();
|
||||||
|
const toUserWasPassenger = ride.confirmedPassengers?.some(
|
||||||
|
(p) => p.userId.toString() === toUserId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!toUserWasDriver && !toUserWasPassenger) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
canLeave: false,
|
||||||
|
reason: "L'utente destinatario non ha partecipato a questo viaggio",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ride.status !== 'completed') {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
canLeave: false,
|
||||||
|
reason: 'Il viaggio non è ancora stato completato',
|
||||||
|
rideStatus: ride.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingFeedback = await Feedback.findOne({
|
||||||
|
rideId,
|
||||||
|
fromUserId,
|
||||||
|
toUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingFeedback) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
canLeave: false,
|
||||||
|
reason: 'Hai già lasciato un feedback per questo utente in questo viaggio',
|
||||||
|
existingFeedbackId: existingFeedback._id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const toUserRole = toUserWasDriver ? 'driver' : 'passenger';
|
||||||
|
const toUser = await User.findById(toUserId).select('username name surname profile.img');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
canLeave: true,
|
||||||
|
toUser: toUser ? {
|
||||||
|
_id: toUser._id,
|
||||||
|
username: toUser.username,
|
||||||
|
name: toUser.name,
|
||||||
|
surname: toUser.surname,
|
||||||
|
img: toUser.profile?.img,
|
||||||
|
} : null,
|
||||||
|
toUserRole,
|
||||||
|
ride: {
|
||||||
|
_id: ride._id,
|
||||||
|
departure: ride.departure,
|
||||||
|
destination: ride.destination,
|
||||||
|
dateTime: ride.dateTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore verifica canLeaveFeedback:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nella verifica',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Rispondi a un feedback ricevuto
|
||||||
|
* @route POST /api/viaggi/feedback/:id/response
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const respondToFeedback = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { text } = req.body;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
if (!text || !text.trim()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Il testo della risposta è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback = await Feedback.findById(id);
|
||||||
|
|
||||||
|
if (!feedback) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Feedback non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feedback.toUserId.toString() !== userId.toString()) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non sei autorizzato a rispondere a questo feedback',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feedback.response?.text) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Hai già risposto a questo feedback',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
feedback.response = {
|
||||||
|
text: text.trim(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
await feedback.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Risposta aggiunta',
|
||||||
|
data: feedback,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore risposta feedback:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Segna un feedback come utile
|
||||||
|
* @route POST /api/viaggi/feedback/:id/helpful
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const markAsHelpful = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
const feedback = await Feedback.findById(id);
|
||||||
|
|
||||||
|
if (!feedback) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Feedback non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!feedback.helpful) {
|
||||||
|
feedback.helpful = { count: 0, users: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIdStr = userId.toString();
|
||||||
|
const alreadyMarked = feedback.helpful.users.some(
|
||||||
|
(u) => u.toString() === userIdStr
|
||||||
|
);
|
||||||
|
|
||||||
|
if (alreadyMarked) {
|
||||||
|
feedback.helpful.users = feedback.helpful.users.filter(
|
||||||
|
(u) => u.toString() !== userIdStr
|
||||||
|
);
|
||||||
|
feedback.helpful.count = Math.max(0, feedback.helpful.count - 1);
|
||||||
|
} else {
|
||||||
|
feedback.helpful.users.push(userId);
|
||||||
|
feedback.helpful.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
await feedback.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: alreadyMarked ? 'Voto rimosso' : 'Feedback segnato come utile',
|
||||||
|
data: {
|
||||||
|
helpfulCount: feedback.helpful.count,
|
||||||
|
isHelpful: !alreadyMarked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore mark helpful:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Segnala un feedback inappropriato
|
||||||
|
* @route POST /api/viaggi/feedback/:id/report
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const reportFeedback = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { reason } = req.body;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
if (!reason || !reason.trim()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'La motivazione è obbligatoria',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback = await Feedback.findById(id);
|
||||||
|
|
||||||
|
if (!feedback) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Feedback non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!feedback.reports) {
|
||||||
|
feedback.reports = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyReported = feedback.reports.some(
|
||||||
|
(r) => r.userId.toString() === userId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (alreadyReported) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Hai già segnalato questo feedback',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
feedback.reports.push({
|
||||||
|
userId,
|
||||||
|
reason: reason.trim(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (feedback.reports.length >= 3) {
|
||||||
|
feedback.isPublic = false;
|
||||||
|
feedback.hiddenReason = 'Nascosto automaticamente per multiple segnalazioni';
|
||||||
|
}
|
||||||
|
|
||||||
|
await feedback.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Feedback segnalato. Lo esamineremo al più presto.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore segnalazione:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni i miei feedback dati
|
||||||
|
* @route GET /api/viaggi/feedback/my/given
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const getMyGivenFeedback = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { idapp, page = 1, limit = 20 } = req.query;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||||
|
|
||||||
|
const [feedbacks, total] = await Promise.all([
|
||||||
|
Feedback.find({ idapp, fromUserId: userId })
|
||||||
|
.populate('toUserId', 'username name surname profile.img')
|
||||||
|
.populate('rideId', 'departure destination dateTime')
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(parseInt(limit)),
|
||||||
|
Feedback.countDocuments({ idapp, fromUserId: userId }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: feedbacks,
|
||||||
|
pagination: {
|
||||||
|
page: parseInt(page),
|
||||||
|
limit: parseInt(limit),
|
||||||
|
total,
|
||||||
|
pages: Math.ceil(total / parseInt(limit)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore recupero feedback dati:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni i miei feedback ricevuti
|
||||||
|
* @route GET /api/viaggi/feedback/my/received
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const getMyReceivedFeedback = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { idapp, page = 1, limit = 20 } = req.query;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||||
|
|
||||||
|
const [feedbacks, total, stats] = await Promise.all([
|
||||||
|
Feedback.find({ idapp, toUserId: userId })
|
||||||
|
.populate('fromUserId', 'username name surname profile.img')
|
||||||
|
.populate('rideId', 'departure destination dateTime')
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(parseInt(limit)),
|
||||||
|
Feedback.countDocuments({ idapp, toUserId: userId }),
|
||||||
|
getStatsForUser(idapp, userId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: feedbacks,
|
||||||
|
stats,
|
||||||
|
pagination: {
|
||||||
|
page: parseInt(page),
|
||||||
|
limit: parseInt(limit),
|
||||||
|
total,
|
||||||
|
pages: Math.ceil(total / parseInt(limit)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore recupero feedback ricevuti:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 📤 EXPORTS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// Controller functions
|
||||||
|
createFeedback,
|
||||||
|
getUserFeedback,
|
||||||
|
getUserFeedbackStats,
|
||||||
|
getRideFeedback,
|
||||||
|
canLeaveFeedback,
|
||||||
|
respondToFeedback,
|
||||||
|
reportFeedback,
|
||||||
|
markAsHelpful,
|
||||||
|
getMyGivenFeedback,
|
||||||
|
getMyReceivedFeedback,
|
||||||
|
|
||||||
|
// Alias per compatibilità
|
||||||
|
getFeedbacksForUser: getUserFeedback,
|
||||||
|
getFeedbacksForRide: getRideFeedback,
|
||||||
|
getMyGivenFeedbacks: getMyGivenFeedback,
|
||||||
|
getMyReceivedFeedbacks: getMyReceivedFeedback,
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
getStatsForUser,
|
||||||
|
getRatingDistribution,
|
||||||
|
updateUserRating,
|
||||||
|
};
|
||||||
735
src/controllers/geocodingController.js
Normal file
735
src/controllers/geocodingController.js
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
/**
|
||||||
|
* Controller per Geocoding usando OpenRouteService
|
||||||
|
* Documentazione: https://openrouteservice.org/dev/#/api-docs
|
||||||
|
*/
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
// Configurazione OpenRouteService
|
||||||
|
const ORS_BASE = 'https://api.openrouteservice.org';
|
||||||
|
const ORS_API_KEY = process.env.ORS_API_KEY || 'YOUR_API_KEY_HERE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper per fare richieste HTTPS a OpenRouteService
|
||||||
|
*/
|
||||||
|
const makeRequest = (url, method = 'GET', body = null) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: urlObj.hostname,
|
||||||
|
path: urlObj.pathname + urlObj.search,
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
Authorization: ORS_API_KEY,
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => (data += chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
reject(new Error(parsed.error?.message || `HTTP ${res.statusCode}`));
|
||||||
|
} else {
|
||||||
|
resolve(parsed);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject(new Error('Errore parsing risposta'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.setTimeout(15000, () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('Timeout richiesta'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
req.write(JSON.stringify(body));
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Autocomplete città (ORS Geocode Autocomplete)
|
||||||
|
* @route GET /api/geo/autocomplete
|
||||||
|
*/
|
||||||
|
const autocomplete = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { q, limit = 5, lang = 'it', country = 'IT' } = req.query;
|
||||||
|
|
||||||
|
if (!q || q.length < 2) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Query deve essere almeno 2 caratteri',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
text: q,
|
||||||
|
size: limit,
|
||||||
|
lang,
|
||||||
|
'boundary.country': country,
|
||||||
|
layers: 'locality,county,region', // Solo città/comuni
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `${ORS_BASE}/geocode/autocomplete?${params}`;
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
const results = data.features.map((feature) => ({
|
||||||
|
id: feature.properties.id,
|
||||||
|
city: feature.properties.name,
|
||||||
|
locality: feature.properties.locality,
|
||||||
|
county: feature.properties.county,
|
||||||
|
region: feature.properties.region,
|
||||||
|
country: feature.properties.country,
|
||||||
|
postalCode: feature.properties.postalcode,
|
||||||
|
coordinates: {
|
||||||
|
lat: feature.geometry.coordinates[1],
|
||||||
|
lng: feature.geometry.coordinates[0],
|
||||||
|
},
|
||||||
|
displayName: feature.properties.label,
|
||||||
|
type: feature.properties.layer,
|
||||||
|
confidence: feature.properties.confidence,
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
count: results.length,
|
||||||
|
data: results,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore autocomplete:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante la ricerca',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Geocoding - indirizzo a coordinate (ORS Geocode Search)
|
||||||
|
* @route GET /api/geo/geocode
|
||||||
|
*/
|
||||||
|
const geocode = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { address, city, country = 'IT', limit = 5, lang = 'it' } = req.query;
|
||||||
|
|
||||||
|
const searchQuery = [address, city].filter(Boolean).join(', ');
|
||||||
|
|
||||||
|
if (!searchQuery) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fornisci un indirizzo o città da cercare',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
text: searchQuery,
|
||||||
|
size: limit,
|
||||||
|
lang,
|
||||||
|
'boundary.country': country,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `${ORS_BASE}/geocode/search?${params}`;
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
if (!data.features || data.features.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Nessun risultato trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = data.features.map((feature) => ({
|
||||||
|
id: feature.properties.id,
|
||||||
|
displayName: feature.properties.label,
|
||||||
|
name: feature.properties.name,
|
||||||
|
street: feature.properties.street,
|
||||||
|
houseNumber: feature.properties.housenumber,
|
||||||
|
city: feature.properties.locality || feature.properties.county,
|
||||||
|
county: feature.properties.county,
|
||||||
|
region: feature.properties.region,
|
||||||
|
country: feature.properties.country,
|
||||||
|
postalCode: feature.properties.postalcode,
|
||||||
|
coordinates: {
|
||||||
|
lat: feature.geometry.coordinates[1],
|
||||||
|
lng: feature.geometry.coordinates[0],
|
||||||
|
},
|
||||||
|
type: feature.properties.layer,
|
||||||
|
confidence: feature.properties.confidence,
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
count: results.length,
|
||||||
|
data: results,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore geocoding:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il geocoding',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Reverse geocoding - coordinate a indirizzo (ORS Reverse)
|
||||||
|
* @route GET /api/geo/reverse
|
||||||
|
*/
|
||||||
|
const reverseGeocode = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { lat, lng, lang = 'it' } = req.query;
|
||||||
|
|
||||||
|
if (!lat || !lng) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Coordinate lat e lng richieste',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
'point.lat': lat,
|
||||||
|
'point.lon': lng,
|
||||||
|
lang,
|
||||||
|
size: '1',
|
||||||
|
layers: 'address,street,locality',
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `${ORS_BASE}/geocode/reverse?${params}`;
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
if (!data.features || data.features.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Nessun risultato trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const feature = data.features[0];
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
displayName: feature.properties.label,
|
||||||
|
name: feature.properties.name,
|
||||||
|
street: feature.properties.street,
|
||||||
|
houseNumber: feature.properties.housenumber,
|
||||||
|
city: feature.properties.locality || feature.properties.county,
|
||||||
|
county: feature.properties.county,
|
||||||
|
region: feature.properties.region,
|
||||||
|
country: feature.properties.country,
|
||||||
|
postalCode: feature.properties.postalcode,
|
||||||
|
coordinates: {
|
||||||
|
lat: parseFloat(lat),
|
||||||
|
lng: parseFloat(lng),
|
||||||
|
},
|
||||||
|
distance: feature.properties.distance, // distanza dal punto esatto
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore reverse geocoding:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il reverse geocoding',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Calcola percorso tra due o più punti (ORS Directions)
|
||||||
|
* @route POST /api/geo/route
|
||||||
|
* @body { coordinates: [[lng,lat], [lng,lat], ...], profile: 'driving-car' }
|
||||||
|
*/
|
||||||
|
const getRoute = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
startLat,
|
||||||
|
startLng,
|
||||||
|
endLat,
|
||||||
|
endLng,
|
||||||
|
waypoints, // formato: "lat1,lng1;lat2,lng2;..."
|
||||||
|
profile = 'driving-car', // driving-car, driving-hgv, cycling-regular, foot-walking
|
||||||
|
language = 'it',
|
||||||
|
units = 'km',
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
if (!startLat || !startLng || !endLat || !endLng) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Coordinate di partenza e arrivo richieste',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Costruisci array coordinate [lng, lat] (formato GeoJSON)
|
||||||
|
const coordinates = [[parseFloat(startLng), parseFloat(startLat)]];
|
||||||
|
|
||||||
|
if (waypoints) {
|
||||||
|
const waypointsList = waypoints.split(';');
|
||||||
|
waypointsList.forEach((wp) => {
|
||||||
|
const [lat, lng] = wp.split(',').map(parseFloat);
|
||||||
|
coordinates.push([lng, lat]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
coordinates.push([parseFloat(endLng), parseFloat(endLat)]);
|
||||||
|
|
||||||
|
// Richiesta POST a ORS Directions
|
||||||
|
const url = `${ORS_BASE}/v2/directions/${profile}`;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
coordinates,
|
||||||
|
language,
|
||||||
|
units,
|
||||||
|
geometry: true,
|
||||||
|
instructions: true,
|
||||||
|
maneuvers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await makeRequest(url, 'POST', body);
|
||||||
|
|
||||||
|
if (!data.routes || data.routes.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Impossibile calcolare il percorso',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = data.routes[0];
|
||||||
|
const summary = route.summary;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
distance: Math.round(summary.distance * 10) / 10, // km
|
||||||
|
duration: Math.round(summary.duration / 60), // minuti
|
||||||
|
durationFormatted: formatDuration(summary.duration),
|
||||||
|
bbox: data.bbox, // Bounding box
|
||||||
|
geometry: route.geometry, // Polyline encoded
|
||||||
|
segments: route.segments.map((segment) => ({
|
||||||
|
distance: Math.round(segment.distance * 10) / 10,
|
||||||
|
duration: Math.round(segment.duration / 60),
|
||||||
|
steps: segment.steps.map((step) => ({
|
||||||
|
instruction: step.instruction,
|
||||||
|
name: step.name,
|
||||||
|
distance: Math.round(step.distance * 100) / 100,
|
||||||
|
duration: Math.round(step.duration / 60),
|
||||||
|
type: step.type,
|
||||||
|
maneuver: step.maneuver,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore calcolo percorso:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il calcolo del percorso',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Calcola matrice distanze tra più punti (ORS Matrix)
|
||||||
|
* @route POST /api/geo/matrix
|
||||||
|
*/
|
||||||
|
const getMatrix = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { locations, profile = 'driving-car' } = req.body;
|
||||||
|
|
||||||
|
if (!locations || locations.length < 2) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Almeno 2 location richieste',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formato locations: [[lng, lat], [lng, lat], ...]
|
||||||
|
const url = `${ORS_BASE}/v2/matrix/${profile}`;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
locations,
|
||||||
|
metrics: ['distance', 'duration'],
|
||||||
|
units: 'km',
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await makeRequest(url, 'POST', body);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
distances: data.distances, // Matrice distanze in km
|
||||||
|
durations: data.durations, // Matrice durate in secondi
|
||||||
|
sources: data.sources,
|
||||||
|
destinations: data.destinations,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore calcolo matrice:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il calcolo della matrice',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Suggerisci città intermedie su un percorso
|
||||||
|
* @route GET /api/geo/suggest-waypoints
|
||||||
|
*/
|
||||||
|
const suggestWaypoints = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { startLat, startLng, endLat, endLng, count = 3 } = req.query;
|
||||||
|
|
||||||
|
if (!startLat || !startLng || !endLat || !endLng) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Coordinate di partenza e arrivo richieste',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prima ottieni il percorso
|
||||||
|
const routeUrl = `${ORS_BASE}/v2/directions/driving-car`;
|
||||||
|
const routeBody = {
|
||||||
|
coordinates: [
|
||||||
|
[parseFloat(startLng), parseFloat(startLat)],
|
||||||
|
[parseFloat(endLng), parseFloat(endLat)],
|
||||||
|
],
|
||||||
|
geometry: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeData = await makeRequest(routeUrl, 'POST', routeBody);
|
||||||
|
|
||||||
|
if (!routeData.routes || routeData.routes.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Impossibile calcolare il percorso',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decodifica polyline per ottenere punti
|
||||||
|
const geometry = routeData.routes[0].geometry;
|
||||||
|
const decodedPoints = decodePolyline(geometry);
|
||||||
|
|
||||||
|
// Seleziona punti equidistanti lungo il percorso
|
||||||
|
const totalPoints = decodedPoints.length;
|
||||||
|
const step = Math.floor(totalPoints / (parseInt(count) + 1));
|
||||||
|
|
||||||
|
const sampledPoints = [];
|
||||||
|
for (let i = 1; i <= count; i++) {
|
||||||
|
const index = Math.min(step * i, totalPoints - 1);
|
||||||
|
sampledPoints.push(decodedPoints[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fai reverse geocoding per ogni punto
|
||||||
|
const cities = [];
|
||||||
|
const seenCities = new Set();
|
||||||
|
|
||||||
|
for (const point of sampledPoints) {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
'point.lat': point[1],
|
||||||
|
'point.lon': point[0],
|
||||||
|
lang: 'it',
|
||||||
|
size: '1',
|
||||||
|
layers: 'locality,county',
|
||||||
|
});
|
||||||
|
|
||||||
|
const reverseUrl = `${ORS_BASE}/geocode/reverse?${params}`;
|
||||||
|
const data = await makeRequest(reverseUrl);
|
||||||
|
|
||||||
|
if (data.features && data.features.length > 0) {
|
||||||
|
const feature = data.features[0];
|
||||||
|
const cityName = feature.properties.locality || feature.properties.county;
|
||||||
|
|
||||||
|
if (cityName && !seenCities.has(cityName.toLowerCase())) {
|
||||||
|
seenCities.add(cityName.toLowerCase());
|
||||||
|
cities.push({
|
||||||
|
city: cityName,
|
||||||
|
county: feature.properties.county,
|
||||||
|
region: feature.properties.region,
|
||||||
|
coordinates: {
|
||||||
|
lat: point[1],
|
||||||
|
lng: point[0],
|
||||||
|
},
|
||||||
|
displayName: feature.properties.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Errore reverse per punto:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
count: cities.length,
|
||||||
|
data: cities,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore suggerimento waypoints:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il suggerimento delle tappe',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Cerca città italiane (ottimizzato)
|
||||||
|
* @route GET /api/geo/cities/it
|
||||||
|
*/
|
||||||
|
const searchItalianCities = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { q, limit = 10, region } = req.query;
|
||||||
|
|
||||||
|
if (!q || q.length < 2) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Query deve essere almeno 2 caratteri',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
text: q,
|
||||||
|
size: limit,
|
||||||
|
lang: 'it',
|
||||||
|
'boundary.country': 'IT',
|
||||||
|
layers: 'locality,county',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtro opzionale per regione
|
||||||
|
if (region) {
|
||||||
|
params.append('region', region);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${ORS_BASE}/geocode/search?${params}`;
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
const results = data.features
|
||||||
|
.filter((f) => f.properties.locality || f.properties.county)
|
||||||
|
.map((feature) => ({
|
||||||
|
city: feature.properties.locality || feature.properties.name,
|
||||||
|
county: feature.properties.county,
|
||||||
|
region: feature.properties.region,
|
||||||
|
postalCode: feature.properties.postalcode,
|
||||||
|
coordinates: {
|
||||||
|
lat: feature.geometry.coordinates[1],
|
||||||
|
lng: feature.geometry.coordinates[0],
|
||||||
|
},
|
||||||
|
displayName: `${feature.properties.locality || feature.properties.name}, ${feature.properties.region}`,
|
||||||
|
confidence: feature.properties.confidence,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rimuovi duplicati
|
||||||
|
const unique = results.filter(
|
||||||
|
(v, i, a) => a.findIndex((t) => t.city?.toLowerCase() === v.city?.toLowerCase()) === i
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
count: unique.length,
|
||||||
|
data: unique,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore ricerca città italiane:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante la ricerca',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Calcola distanza e durata tra due punti (semplificato)
|
||||||
|
* @route GET /api/geo/distance
|
||||||
|
*/
|
||||||
|
const getDistance = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { startLat, startLng, endLat, endLng, profile = 'driving-car' } = req.query;
|
||||||
|
|
||||||
|
if (!startLat || !startLng || !endLat || !endLng) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Tutte le coordinate sono richieste',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${ORS_BASE}/v2/directions/${profile}`;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
coordinates: [
|
||||||
|
[parseFloat(startLng), parseFloat(startLat)],
|
||||||
|
[parseFloat(endLng), parseFloat(endLat)],
|
||||||
|
],
|
||||||
|
geometry: false,
|
||||||
|
instructions: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await makeRequest(url, 'POST', body);
|
||||||
|
|
||||||
|
if (!data.routes || data.routes.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Impossibile calcolare la distanza',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = data.routes[0].summary;
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
distance: Math.round(summary.distance * 10) / 10, // km
|
||||||
|
duration: Math.round(summary.duration / 60), // minuti
|
||||||
|
durationFormatted: formatDuration(summary.duration),
|
||||||
|
profile,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore calcolo distanza:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il calcolo della distanza',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni isocrone (aree raggiungibili in X minuti)
|
||||||
|
* @route GET /api/geo/isochrone
|
||||||
|
*/
|
||||||
|
const getIsochrone = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { lat, lng, minutes = 30, profile = 'driving-car' } = req.query;
|
||||||
|
|
||||||
|
if (!lat || !lng) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Coordinate richieste',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${ORS_BASE}/v2/isochrones/${profile}`;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
locations: [[parseFloat(lng), parseFloat(lat)]],
|
||||||
|
range: [parseInt(minutes) * 60], // secondi
|
||||||
|
range_type: 'time',
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await makeRequest(url, 'POST', body);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: data.features,
|
||||||
|
center: { lat: parseFloat(lat), lng: parseFloat(lng) },
|
||||||
|
minutes: parseInt(minutes),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore calcolo isocrone:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il calcolo isocrone',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatta durata in formato leggibile
|
||||||
|
*/
|
||||||
|
const formatDuration = (seconds) => {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.round((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (hours === 0) {
|
||||||
|
return `${minutes} min`;
|
||||||
|
} else if (minutes === 0) {
|
||||||
|
return `${hours} h`;
|
||||||
|
} else {
|
||||||
|
return `${hours} h ${minutes} min`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodifica polyline encoded (formato Google/ORS)
|
||||||
|
*/
|
||||||
|
const decodePolyline = (encoded) => {
|
||||||
|
const points = [];
|
||||||
|
let index = 0;
|
||||||
|
let lat = 0;
|
||||||
|
let lng = 0;
|
||||||
|
|
||||||
|
while (index < encoded.length) {
|
||||||
|
let b;
|
||||||
|
let shift = 0;
|
||||||
|
let result = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
b = encoded.charCodeAt(index++) - 63;
|
||||||
|
result |= (b & 0x1f) << shift;
|
||||||
|
shift += 5;
|
||||||
|
} while (b >= 0x20);
|
||||||
|
|
||||||
|
const dlat = result & 1 ? ~(result >> 1) : result >> 1;
|
||||||
|
lat += dlat;
|
||||||
|
|
||||||
|
shift = 0;
|
||||||
|
result = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
b = encoded.charCodeAt(index++) - 63;
|
||||||
|
result |= (b & 0x1f) << shift;
|
||||||
|
shift += 5;
|
||||||
|
} while (b >= 0x20);
|
||||||
|
|
||||||
|
const dlng = result & 1 ? ~(result >> 1) : result >> 1;
|
||||||
|
lng += dlng;
|
||||||
|
|
||||||
|
points.push([lng / 1e5, lat / 1e5]); // [lng, lat] formato GeoJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
autocomplete,
|
||||||
|
geocode,
|
||||||
|
reverseGeocode,
|
||||||
|
getRoute,
|
||||||
|
getMatrix,
|
||||||
|
suggestWaypoints,
|
||||||
|
searchItalianCities,
|
||||||
|
getDistance,
|
||||||
|
getIsochrone,
|
||||||
|
};
|
||||||
522
src/controllers/geocodingController_OLD.js
Normal file
522
src/controllers/geocodingController_OLD.js
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
/**
|
||||||
|
* Controller per Geocoding usando servizi Open Source
|
||||||
|
* - Nominatim (OpenStreetMap) per geocoding/reverse
|
||||||
|
* - OSRM per routing
|
||||||
|
* - Photon per autocomplete
|
||||||
|
*/
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
// Configurazione servizi
|
||||||
|
const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org';
|
||||||
|
const PHOTON_BASE = 'https://photon.komoot.io';
|
||||||
|
const OSRM_BASE = 'https://router.project-osrm.org';
|
||||||
|
|
||||||
|
// User-Agent richiesto da Nominatim
|
||||||
|
const USER_AGENT = 'FreePlanetApp/1.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper per fare richieste HTTP/HTTPS
|
||||||
|
*/
|
||||||
|
const makeRequest = (url) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = url.startsWith('https') ? https : http;
|
||||||
|
|
||||||
|
const req = client.get(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', chunk => data += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
} catch (e) {
|
||||||
|
reject(new Error('Errore parsing risposta'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.setTimeout(10000, () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('Timeout richiesta'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Autocomplete città (Photon API)
|
||||||
|
* @route GET /api/viaggi/geo/autocomplete
|
||||||
|
*/
|
||||||
|
const autocomplete = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { q, limit = 5, lang = 'it' } = req.query;
|
||||||
|
|
||||||
|
if (!q || q.length < 2) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Query deve essere almeno 2 caratteri'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Photon API - gratuito e veloce
|
||||||
|
const url = `${PHOTON_BASE}/api/?q=${encodeURIComponent(q)}&limit=${limit}&lang=${lang}&osm_tag=place:city&osm_tag=place:town&osm_tag=place:village`;
|
||||||
|
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
// Formatta risultati
|
||||||
|
const results = data.features.map(feature => ({
|
||||||
|
city: feature.properties.name,
|
||||||
|
province: feature.properties.county || feature.properties.state,
|
||||||
|
region: feature.properties.state,
|
||||||
|
country: feature.properties.country,
|
||||||
|
postalCode: feature.properties.postcode,
|
||||||
|
coordinates: {
|
||||||
|
lat: feature.geometry.coordinates[1],
|
||||||
|
lng: feature.geometry.coordinates[0]
|
||||||
|
},
|
||||||
|
displayName: [
|
||||||
|
feature.properties.name,
|
||||||
|
feature.properties.county,
|
||||||
|
feature.properties.state,
|
||||||
|
feature.properties.country
|
||||||
|
].filter(Boolean).join(', '),
|
||||||
|
type: feature.properties.osm_value || 'place'
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: results
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore autocomplete:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante la ricerca',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Geocoding - indirizzo a coordinate (Nominatim)
|
||||||
|
* @route GET /api/viaggi/geo/geocode
|
||||||
|
*/
|
||||||
|
const geocode = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { address, city, country = 'Italy' } = req.query;
|
||||||
|
|
||||||
|
const searchQuery = [address, city, country].filter(Boolean).join(', ');
|
||||||
|
|
||||||
|
if (!searchQuery) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fornisci un indirizzo o città da cercare'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5&addressdetails=1`;
|
||||||
|
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Nessun risultato trovato'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = data.map(item => ({
|
||||||
|
displayName: item.display_name,
|
||||||
|
city: item.address.city || item.address.town || item.address.village || item.address.municipality,
|
||||||
|
address: item.address.road ? `${item.address.road}${item.address.house_number ? ' ' + item.address.house_number : ''}` : null,
|
||||||
|
province: item.address.county || item.address.province,
|
||||||
|
region: item.address.state,
|
||||||
|
country: item.address.country,
|
||||||
|
postalCode: item.address.postcode,
|
||||||
|
coordinates: {
|
||||||
|
lat: parseFloat(item.lat),
|
||||||
|
lng: parseFloat(item.lon)
|
||||||
|
},
|
||||||
|
type: item.type,
|
||||||
|
importance: item.importance
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: results
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore geocoding:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il geocoding',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Reverse geocoding - coordinate a indirizzo (Nominatim)
|
||||||
|
* @route GET /api/viaggi/geo/reverse
|
||||||
|
*/
|
||||||
|
const reverseGeocode = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { lat, lng } = req.query;
|
||||||
|
|
||||||
|
if (!lat || !lng) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Coordinate lat e lng richieste'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${NOMINATIM_BASE}/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`;
|
||||||
|
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
if (!data || data.error) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Nessun risultato trovato'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
displayName: data.display_name,
|
||||||
|
city: data.address.city || data.address.town || data.address.village || data.address.municipality,
|
||||||
|
address: data.address.road ? `${data.address.road}${data.address.house_number ? ' ' + data.address.house_number : ''}` : null,
|
||||||
|
province: data.address.county || data.address.province,
|
||||||
|
region: data.address.state,
|
||||||
|
country: data.address.country,
|
||||||
|
postalCode: data.address.postcode,
|
||||||
|
coordinates: {
|
||||||
|
lat: parseFloat(lat),
|
||||||
|
lng: parseFloat(lng)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore reverse geocoding:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il reverse geocoding',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Calcola percorso tra due punti (OSRM)
|
||||||
|
* @route GET /api/viaggi/geo/route
|
||||||
|
*/
|
||||||
|
const getRoute = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
startLat, startLng,
|
||||||
|
endLat, endLng,
|
||||||
|
waypoints // formato: "lat1,lng1;lat2,lng2;..."
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
if (!startLat || !startLng || !endLat || !endLng) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Coordinate di partenza e arrivo richieste'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Costruisci stringa coordinate
|
||||||
|
let coordinates = `${startLng},${startLat}`;
|
||||||
|
|
||||||
|
if (waypoints) {
|
||||||
|
const waypointsList = waypoints.split(';');
|
||||||
|
waypointsList.forEach(wp => {
|
||||||
|
const [lat, lng] = wp.split(',');
|
||||||
|
coordinates += `;${lng},${lat}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
coordinates += `;${endLng},${endLat}`;
|
||||||
|
|
||||||
|
const url = `${OSRM_BASE}/route/v1/driving/${coordinates}?overview=full&geometries=polyline&steps=true`;
|
||||||
|
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Impossibile calcolare il percorso'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = data.routes[0];
|
||||||
|
|
||||||
|
// Estrai città attraversate (dalle istruzioni)
|
||||||
|
const citiesAlongRoute = [];
|
||||||
|
if (route.legs) {
|
||||||
|
route.legs.forEach(leg => {
|
||||||
|
if (leg.steps) {
|
||||||
|
leg.steps.forEach(step => {
|
||||||
|
if (step.name && step.name.length > 0) {
|
||||||
|
// Qui potresti fare reverse geocoding per ottenere città
|
||||||
|
// Per ora usiamo i nomi delle strade principali
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
distance: Math.round(route.distance / 1000 * 10) / 10, // km
|
||||||
|
duration: Math.round(route.duration / 60), // minuti
|
||||||
|
polyline: route.geometry, // Polyline encoded
|
||||||
|
legs: route.legs.map(leg => ({
|
||||||
|
distance: Math.round(leg.distance / 1000 * 10) / 10,
|
||||||
|
duration: Math.round(leg.duration / 60),
|
||||||
|
summary: leg.summary,
|
||||||
|
steps: leg.steps ? leg.steps.slice(0, 10).map(s => ({ // Limita step
|
||||||
|
instruction: s.maneuver ? s.maneuver.instruction : '',
|
||||||
|
name: s.name,
|
||||||
|
distance: Math.round(s.distance),
|
||||||
|
duration: Math.round(s.duration / 60)
|
||||||
|
})) : []
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore calcolo percorso:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il calcolo del percorso',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Suggerisci città intermedie su un percorso
|
||||||
|
* @route GET /api/viaggi/geo/suggest-waypoints
|
||||||
|
*/
|
||||||
|
const suggestWaypoints = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { startLat, startLng, endLat, endLng } = req.query;
|
||||||
|
|
||||||
|
if (!startLat || !startLng || !endLat || !endLng) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Coordinate di partenza e arrivo richieste'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prima ottieni il percorso
|
||||||
|
const routeUrl = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=full&geometries=geojson`;
|
||||||
|
|
||||||
|
const routeData = await makeRequest(routeUrl);
|
||||||
|
|
||||||
|
if (!routeData || routeData.code !== 'Ok') {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Impossibile calcolare il percorso'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prendi punti lungo il percorso (ogni ~50km circa)
|
||||||
|
const coordinates = routeData.routes[0].geometry.coordinates;
|
||||||
|
const totalPoints = coordinates.length;
|
||||||
|
const step = Math.max(1, Math.floor(totalPoints / 6)); // ~5 punti intermedi
|
||||||
|
|
||||||
|
const sampledPoints = [];
|
||||||
|
for (let i = step; i < totalPoints - step; i += step) {
|
||||||
|
sampledPoints.push(coordinates[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fai reverse geocoding per ogni punto
|
||||||
|
const cities = [];
|
||||||
|
const seenCities = new Set();
|
||||||
|
|
||||||
|
for (const point of sampledPoints.slice(0, 5)) { // Limita a 5 richieste
|
||||||
|
try {
|
||||||
|
const reverseUrl = `${NOMINATIM_BASE}/reverse?format=json&lat=${point[1]}&lon=${point[0]}&addressdetails=1&zoom=10`;
|
||||||
|
const data = await makeRequest(reverseUrl);
|
||||||
|
|
||||||
|
if (data && data.address) {
|
||||||
|
const cityName = data.address.city || data.address.town || data.address.village;
|
||||||
|
if (cityName && !seenCities.has(cityName.toLowerCase())) {
|
||||||
|
seenCities.add(cityName.toLowerCase());
|
||||||
|
cities.push({
|
||||||
|
city: cityName,
|
||||||
|
province: data.address.county || data.address.province,
|
||||||
|
region: data.address.state,
|
||||||
|
coordinates: {
|
||||||
|
lat: point[1],
|
||||||
|
lng: point[0]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting - aspetta 1 secondo tra le richieste (requisito Nominatim)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Errore reverse per punto:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: cities
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore suggerimento waypoints:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il suggerimento delle tappe',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Cerca città italiane (ottimizzato per Italia)
|
||||||
|
* @route GET /api/viaggi/geo/cities/it
|
||||||
|
*/
|
||||||
|
const searchItalianCities = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { q, limit = 10 } = req.query;
|
||||||
|
|
||||||
|
if (!q || q.length < 2) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Query deve essere almeno 2 caratteri'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usa Nominatim con filtro Italia
|
||||||
|
const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(q)}&countrycodes=it&limit=${limit}&addressdetails=1&featuretype=city`;
|
||||||
|
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
const results = data
|
||||||
|
.filter(item =>
|
||||||
|
item.address &&
|
||||||
|
(item.address.city || item.address.town || item.address.village)
|
||||||
|
)
|
||||||
|
.map(item => ({
|
||||||
|
city: item.address.city || item.address.town || item.address.village,
|
||||||
|
province: item.address.county || item.address.province,
|
||||||
|
region: item.address.state,
|
||||||
|
postalCode: item.address.postcode,
|
||||||
|
coordinates: {
|
||||||
|
lat: parseFloat(item.lat),
|
||||||
|
lng: parseFloat(item.lon)
|
||||||
|
},
|
||||||
|
displayName: `${item.address.city || item.address.town || item.address.village}, ${item.address.county || item.address.state}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rimuovi duplicati
|
||||||
|
const unique = results.filter((v, i, a) =>
|
||||||
|
a.findIndex(t => t.city.toLowerCase() === v.city.toLowerCase()) === i
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: unique
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore ricerca città italiane:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante la ricerca',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Calcola distanza e durata tra due punti
|
||||||
|
* @route GET /api/viaggi/geo/distance
|
||||||
|
*/
|
||||||
|
const getDistance = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { startLat, startLng, endLat, endLng } = req.query;
|
||||||
|
|
||||||
|
if (!startLat || !startLng || !endLat || !endLng) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Tutte le coordinate sono richieste'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=false`;
|
||||||
|
|
||||||
|
const data = await makeRequest(url);
|
||||||
|
|
||||||
|
if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Impossibile calcolare la distanza'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = data.routes[0];
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
distance: Math.round(route.distance / 1000 * 10) / 10, // km
|
||||||
|
duration: Math.round(route.duration / 60), // minuti
|
||||||
|
durationFormatted: formatDuration(route.duration)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore calcolo distanza:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore durante il calcolo della distanza',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper per formattare durata
|
||||||
|
const formatDuration = (seconds) => {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.round((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (hours === 0) {
|
||||||
|
return `${minutes} min`;
|
||||||
|
} else if (minutes === 0) {
|
||||||
|
return `${hours} h`;
|
||||||
|
} else {
|
||||||
|
return `${hours} h ${minutes} min`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
autocomplete,
|
||||||
|
geocode,
|
||||||
|
reverseGeocode,
|
||||||
|
getRoute,
|
||||||
|
suggestWaypoints,
|
||||||
|
searchItalianCities,
|
||||||
|
getDistance
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ const imageGenerator = require('../services/imageGenerator');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
|
|
||||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload';
|
||||||
|
|
||||||
const posterController = {
|
const posterController = {
|
||||||
// POST /posters
|
// POST /posters
|
||||||
@@ -396,7 +396,7 @@ const posterController = {
|
|||||||
// Aggiorna asset nel poster
|
// Aggiorna asset nel poster
|
||||||
const assetData = {
|
const assetData = {
|
||||||
sourceType: 'ai',
|
sourceType: 'ai',
|
||||||
url: `/uploads/ai-generated/${fileName}`,
|
url: `/upload/ai-generated/${fileName}`,
|
||||||
mimeType: 'image/jpeg',
|
mimeType: 'image/jpeg',
|
||||||
aiParams: {
|
aiParams: {
|
||||||
prompt,
|
prompt,
|
||||||
@@ -572,7 +572,7 @@ const posterController = {
|
|||||||
assets: {
|
assets: {
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
sourceType: 'ai',
|
sourceType: 'ai',
|
||||||
url: `/uploads/ai-generated/${fileName}`,
|
url: `/upload/ai-generated/${fileName}`,
|
||||||
mimeType: 'image/jpeg',
|
mimeType: 'image/jpeg',
|
||||||
aiParams: {
|
aiParams: {
|
||||||
prompt: aiPrompt,
|
prompt: aiPrompt,
|
||||||
@@ -629,12 +629,12 @@ const posterController = {
|
|||||||
poster.setRenderOutput({
|
poster.setRenderOutput({
|
||||||
png: {
|
png: {
|
||||||
path: result.pngPath,
|
path: result.pngPath,
|
||||||
url: `/uploads/posters/final/${path.basename(result.pngPath)}`,
|
url: `/upload/posters/final/${path.basename(result.pngPath)}`,
|
||||||
size: result.pngSize
|
size: result.pngSize
|
||||||
},
|
},
|
||||||
jpg: {
|
jpg: {
|
||||||
path: result.jpgPath,
|
path: result.jpgPath,
|
||||||
url: `/uploads/posters/final/${path.basename(result.jpgPath)}`,
|
url: `/upload/posters/final/${path.basename(result.jpgPath)}`,
|
||||||
size: result.jpgSize,
|
size: result.jpgSize,
|
||||||
quality: 95
|
quality: 95
|
||||||
},
|
},
|
||||||
|
|||||||
1822
src/controllers/rideController.js
Normal file
1822
src/controllers/rideController.js
Normal file
File diff suppressed because it is too large
Load Diff
952
src/controllers/rideRequestController.js
Normal file
952
src/controllers/rideRequestController.js
Normal file
@@ -0,0 +1,952 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const RideRequest = require('../models/RideRequest');
|
||||||
|
const Ride = require('../models/Ride');
|
||||||
|
const Chat = require('../models/Chat');
|
||||||
|
const Message = require('../models/Message');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper per convertire ID in ObjectId
|
||||||
|
*/
|
||||||
|
const toObjectId = (id) => {
|
||||||
|
if (!id) return null;
|
||||||
|
if (id instanceof mongoose.Types.ObjectId) return id;
|
||||||
|
if (typeof id === 'object' && id._id) {
|
||||||
|
return new mongoose.Types.ObjectId(id._id.toString());
|
||||||
|
}
|
||||||
|
return new mongoose.Types.ObjectId(id.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Crea una richiesta di passaggio
|
||||||
|
* @route POST /api/viaggi/requests
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const createRequest = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
rideId,
|
||||||
|
message,
|
||||||
|
pickupPoint,
|
||||||
|
dropoffPoint,
|
||||||
|
useOriginalRoute,
|
||||||
|
seatsRequested,
|
||||||
|
hasLuggage,
|
||||||
|
luggageSize,
|
||||||
|
hasPackages,
|
||||||
|
packageDescription,
|
||||||
|
hasPets,
|
||||||
|
petType,
|
||||||
|
petSize,
|
||||||
|
specialNeeds,
|
||||||
|
contribution,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const idapp = req.body.idapp || req.query.idapp || req.user?.idapp;
|
||||||
|
const passengerId = req.user._id;
|
||||||
|
|
||||||
|
// Validazione
|
||||||
|
if (!idapp || !rideId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp e rideId sono obbligatori',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che il ride esista
|
||||||
|
const ride = await Ride.findById(rideId);
|
||||||
|
if (!ride) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Viaggio non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che non sia il proprio viaggio
|
||||||
|
if (ride.userId.toString() === passengerId.toString()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non puoi richiedere un passaggio per il tuo stesso viaggio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che non ci sia già una richiesta pendente/accettata
|
||||||
|
const existingRequest = await RideRequest.findOne({
|
||||||
|
rideId,
|
||||||
|
passengerId,
|
||||||
|
status: { $in: ['pending', 'accepted'] },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingRequest) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Hai già una richiesta attiva per questo viaggio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica disponibilità posti
|
||||||
|
const seats = seatsRequested || 1;
|
||||||
|
if (ride.type === 'offer' && ride.passengers?.available < seats) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Posti insufficienti. Disponibili: ${ride.passengers?.available || 0}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea la richiesta
|
||||||
|
const requestData = {
|
||||||
|
idapp,
|
||||||
|
rideId,
|
||||||
|
passengerId,
|
||||||
|
driverId: ride.userId,
|
||||||
|
seatsRequested: seats,
|
||||||
|
useOriginalRoute: useOriginalRoute !== false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message) requestData.message = message;
|
||||||
|
if (pickupPoint) requestData.pickupPoint = pickupPoint;
|
||||||
|
if (dropoffPoint) requestData.dropoffPoint = dropoffPoint;
|
||||||
|
if (hasLuggage !== undefined) {
|
||||||
|
requestData.hasLuggage = hasLuggage;
|
||||||
|
requestData.luggageSize = luggageSize || 'small';
|
||||||
|
}
|
||||||
|
if (hasPackages !== undefined) {
|
||||||
|
requestData.hasPackages = hasPackages;
|
||||||
|
requestData.packageDescription = packageDescription;
|
||||||
|
}
|
||||||
|
if (hasPets !== undefined) {
|
||||||
|
requestData.hasPets = hasPets;
|
||||||
|
requestData.petType = petType;
|
||||||
|
requestData.petSize = petSize;
|
||||||
|
}
|
||||||
|
if (specialNeeds) requestData.specialNeeds = specialNeeds;
|
||||||
|
if (contribution) requestData.contribution = contribution;
|
||||||
|
|
||||||
|
const rideRequest = new RideRequest(requestData);
|
||||||
|
await rideRequest.save();
|
||||||
|
|
||||||
|
// Popola i dati per la risposta
|
||||||
|
await rideRequest.populate('passengerId', 'username name surname profile.img');
|
||||||
|
await rideRequest.populate('rideId', 'departure destination dateTime');
|
||||||
|
|
||||||
|
// Crea o recupera la chat tra passeggero e conducente
|
||||||
|
let chat;
|
||||||
|
let chatId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
chat = await Chat.findOrCreateDirect(idapp, passengerId, ride.userId, rideId);
|
||||||
|
chatId = chat._id;
|
||||||
|
|
||||||
|
// Invia messaggio automatico nella chat
|
||||||
|
if (message) {
|
||||||
|
const chatMessage = new Message({
|
||||||
|
idapp,
|
||||||
|
chatId: chat._id,
|
||||||
|
senderId: passengerId,
|
||||||
|
text: message,
|
||||||
|
type: 'ride_request',
|
||||||
|
metadata: {
|
||||||
|
rideId,
|
||||||
|
rideRequestId: rideRequest._id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await chatMessage.save();
|
||||||
|
|
||||||
|
// Aggiorna lastMessage della chat
|
||||||
|
chat.lastMessage = {
|
||||||
|
text: message.substring(0, 100),
|
||||||
|
senderId: passengerId,
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: 'ride_request',
|
||||||
|
};
|
||||||
|
await chat.save();
|
||||||
|
}
|
||||||
|
} catch (chatError) {
|
||||||
|
console.error('Errore creazione chat:', chatError);
|
||||||
|
// Non bloccare la creazione della richiesta se la chat fallisce
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Inviare notifica push al conducente
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Richiesta di passaggio inviata!',
|
||||||
|
data: rideRequest,
|
||||||
|
chatId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore creazione richiesta:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nella creazione della richiesta',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni le richieste per un viaggio (per il conducente)
|
||||||
|
* @route GET /api/viaggi/requests/ride/:rideId
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const getRequestsForRide = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { rideId } = req.params;
|
||||||
|
const { status } = req.query;
|
||||||
|
const idapp = req.query.idapp || req.user?.idapp;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che l'utente sia il proprietario del ride
|
||||||
|
const ride = await Ride.findById(rideId);
|
||||||
|
if (!ride) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Viaggio non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ride.userId.toString() !== userId.toString()) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non sei autorizzato a vedere le richieste di questo viaggio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = { idapp, rideId };
|
||||||
|
if (status) {
|
||||||
|
query.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requests = await RideRequest.find(query)
|
||||||
|
.populate(
|
||||||
|
'passengerId',
|
||||||
|
'username name surname profile.img profile.Cell profile.driverProfile.averageRating profile.driverProfile.ridesCompletedAsPassenger'
|
||||||
|
)
|
||||||
|
.sort({ createdAt: -1 });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: requests,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore recupero richieste:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel recupero delle richieste',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni le mie richieste (come passeggero)
|
||||||
|
* @route GET /api/viaggi/requests/my
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const getMyRequests = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { status, page = 1, limit = 20 } = req.query;
|
||||||
|
const idapp = req.query.idapp || req.user?.idapp;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = { idapp, passengerId: userId };
|
||||||
|
if (status) {
|
||||||
|
query.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||||
|
|
||||||
|
const [requests, total] = await Promise.all([
|
||||||
|
RideRequest.find(query)
|
||||||
|
.populate({
|
||||||
|
path: 'rideId',
|
||||||
|
populate: {
|
||||||
|
path: 'userId',
|
||||||
|
select: 'username name surname profile.img profile.Cell profile.driverProfile.averageRating',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(parseInt(limit)),
|
||||||
|
RideRequest.countDocuments(query),
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: requests,
|
||||||
|
pagination: {
|
||||||
|
page: parseInt(page),
|
||||||
|
limit: parseInt(limit),
|
||||||
|
total,
|
||||||
|
pages: Math.ceil(total / parseInt(limit)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore recupero mie richieste:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel recupero delle tue richieste',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni richieste pendenti (per il conducente)
|
||||||
|
* @route GET /api/viaggi/requests/pending
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const getPendingRequests = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const idapp = req.query.idapp || req.user?.idapp;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const requests = await RideRequest.find({
|
||||||
|
idapp,
|
||||||
|
driverId: userId,
|
||||||
|
status: 'pending',
|
||||||
|
})
|
||||||
|
.populate('passengerId', 'username name surname profile.img profile.driverProfile.averageRating')
|
||||||
|
.populate('rideId', 'departure destination dateTime passengers')
|
||||||
|
.sort({ createdAt: -1 });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: requests,
|
||||||
|
total: requests.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore recupero richieste pendenti:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel recupero delle richieste pendenti',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Accetta una richiesta di passaggio
|
||||||
|
* @route POST /api/viaggi/requests/:id/accept
|
||||||
|
* @access Private (solo conducente)
|
||||||
|
*/
|
||||||
|
const acceptRequest = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { responseMessage } = req.body;
|
||||||
|
const idapp = req.body.idapp || req.query.idapp || req.user?.idapp;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = await RideRequest.findById(id).populate('rideId');
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Richiesta non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che sia il conducente
|
||||||
|
if (request.driverId.toString() !== userId.toString()) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non sei autorizzato ad accettare questa richiesta',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che sia ancora pendente
|
||||||
|
if (request.status !== 'pending') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `La richiesta è già stata ${request.status}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica disponibilità posti
|
||||||
|
const ride = request.rideId;
|
||||||
|
if (!ride) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Viaggio associato non trovato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ride.passengers?.available || 0) < request.seatsRequested) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Posti non più disponibili',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accetta la richiesta
|
||||||
|
request.status = 'accepted';
|
||||||
|
request.responseMessage = responseMessage || '';
|
||||||
|
request.respondedAt = new Date();
|
||||||
|
await request.save();
|
||||||
|
|
||||||
|
// Aggiorna il ride con il passeggero
|
||||||
|
if (!ride.confirmedPassengers) {
|
||||||
|
ride.confirmedPassengers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ride.confirmedPassengers.push({
|
||||||
|
userId: request.passengerId,
|
||||||
|
seats: request.seatsRequested,
|
||||||
|
pickupPoint: request.pickupPoint || ride.departure,
|
||||||
|
dropoffPoint: request.dropoffPoint || ride.destination,
|
||||||
|
confirmedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aggiorna posti disponibili
|
||||||
|
if (typeof ride.updateAvailableSeats === 'function') {
|
||||||
|
await ride.updateAvailableSeats();
|
||||||
|
} else {
|
||||||
|
// Fallback manuale
|
||||||
|
const totalConfirmed = ride.confirmedPassengers.reduce(
|
||||||
|
(sum, p) => sum + (p.seats || 1),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
if (ride.passengers) {
|
||||||
|
ride.passengers.available = Math.max(0, (ride.passengers.total || 0) - totalConfirmed);
|
||||||
|
}
|
||||||
|
await ride.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invia messaggio nella chat
|
||||||
|
try {
|
||||||
|
const chat = await Chat.findOrCreateDirect(idapp, userId, request.passengerId, ride._id);
|
||||||
|
const chatMessage = new Message({
|
||||||
|
idapp,
|
||||||
|
chatId: chat._id,
|
||||||
|
senderId: userId,
|
||||||
|
text: responseMessage || '✅ Richiesta accettata! Ci vediamo al punto di partenza.',
|
||||||
|
type: 'ride_accepted',
|
||||||
|
metadata: {
|
||||||
|
rideId: ride._id,
|
||||||
|
rideRequestId: request._id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await chatMessage.save();
|
||||||
|
|
||||||
|
// Aggiorna lastMessage
|
||||||
|
chat.lastMessage = {
|
||||||
|
text: (responseMessage || '✅ Richiesta accettata!').substring(0, 100),
|
||||||
|
senderId: userId,
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: 'ride_accepted',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Incrementa unread per il passeggero
|
||||||
|
if (!chat.unreadCount) {
|
||||||
|
chat.unreadCount = new Map();
|
||||||
|
}
|
||||||
|
const passengerIdStr = request.passengerId.toString();
|
||||||
|
const current = chat.unreadCount.get(passengerIdStr) || 0;
|
||||||
|
chat.unreadCount.set(passengerIdStr, current + 1);
|
||||||
|
chat.markModified('unreadCount');
|
||||||
|
|
||||||
|
await chat.save();
|
||||||
|
} catch (chatError) {
|
||||||
|
console.error('Errore invio messaggio chat:', chatError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Inviare notifica push al passeggero
|
||||||
|
|
||||||
|
await request.populate('passengerId', 'username name surname profile.img');
|
||||||
|
await request.populate('rideId', 'departure destination dateTime');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Richiesta accettata!',
|
||||||
|
data: request,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore accettazione richiesta:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Errore nell'accettazione della richiesta",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Rifiuta una richiesta di passaggio
|
||||||
|
* @route POST /api/viaggi/requests/:id/reject
|
||||||
|
* @access Private (solo conducente)
|
||||||
|
*/
|
||||||
|
const rejectRequest = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { responseMessage } = req.body;
|
||||||
|
const idapp = req.body.idapp || req.query.idapp || req.user?.idapp;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
if (!idapp) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'idapp è obbligatorio',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = await RideRequest.findById(id);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Richiesta non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che sia il conducente
|
||||||
|
if (request.driverId.toString() !== userId.toString()) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non sei autorizzato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che sia ancora pendente
|
||||||
|
if (request.status !== 'pending') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `La richiesta è già stata ${request.status}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rifiuta la richiesta
|
||||||
|
request.status = 'rejected';
|
||||||
|
request.responseMessage = responseMessage || '';
|
||||||
|
request.respondedAt = new Date();
|
||||||
|
await request.save();
|
||||||
|
|
||||||
|
// Invia messaggio nella chat
|
||||||
|
try {
|
||||||
|
const chat = await Chat.findOrCreateDirect(idapp, userId, request.passengerId, request.rideId);
|
||||||
|
const chatMessage = new Message({
|
||||||
|
idapp,
|
||||||
|
chatId: chat._id,
|
||||||
|
senderId: userId,
|
||||||
|
text: responseMessage || '❌ Mi dispiace, non posso accettare questa richiesta.',
|
||||||
|
type: 'ride_rejected',
|
||||||
|
metadata: {
|
||||||
|
rideId: request.rideId,
|
||||||
|
rideRequestId: request._id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await chatMessage.save();
|
||||||
|
|
||||||
|
// Aggiorna lastMessage e unread
|
||||||
|
chat.lastMessage = {
|
||||||
|
text: (responseMessage || '❌ Richiesta rifiutata').substring(0, 100),
|
||||||
|
senderId: userId,
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: 'ride_rejected',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!chat.unreadCount) {
|
||||||
|
chat.unreadCount = new Map();
|
||||||
|
}
|
||||||
|
const passengerIdStr = request.passengerId.toString();
|
||||||
|
const current = chat.unreadCount.get(passengerIdStr) || 0;
|
||||||
|
chat.unreadCount.set(passengerIdStr, current + 1);
|
||||||
|
chat.markModified('unreadCount');
|
||||||
|
|
||||||
|
await chat.save();
|
||||||
|
} catch (chatError) {
|
||||||
|
console.error('Errore invio messaggio chat:', chatError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Inviare notifica push al passeggero
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Richiesta rifiutata',
|
||||||
|
data: request,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore rifiuto richiesta:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel rifiuto della richiesta',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Cancella una richiesta (dal passeggero o conducente)
|
||||||
|
* @route POST /api/viaggi/requests/:id/cancel
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const cancelRequest = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { reason } = req.body;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
const request = await RideRequest.findById(id);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Richiesta non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che sia il passeggero o il conducente
|
||||||
|
const isPassenger = request.passengerId.toString() === userId.toString();
|
||||||
|
const isDriver = request.driverId.toString() === userId.toString();
|
||||||
|
|
||||||
|
if (!isPassenger && !isDriver) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non sei autorizzato',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che possa essere cancellata
|
||||||
|
if (!['pending', 'accepted'].includes(request.status)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Questa richiesta non può essere cancellata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se era accettata, rimuovi il passeggero dal ride
|
||||||
|
if (request.status === 'accepted') {
|
||||||
|
const ride = await Ride.findById(request.rideId);
|
||||||
|
if (ride && ride.confirmedPassengers) {
|
||||||
|
ride.confirmedPassengers = ride.confirmedPassengers.filter(
|
||||||
|
(p) => p.userId.toString() !== request.passengerId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof ride.updateAvailableSeats === 'function') {
|
||||||
|
await ride.updateAvailableSeats();
|
||||||
|
} else {
|
||||||
|
const totalConfirmed = ride.confirmedPassengers.reduce(
|
||||||
|
(sum, p) => sum + (p.seats || 1),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
if (ride.passengers) {
|
||||||
|
ride.passengers.available = Math.max(0, (ride.passengers.total || 0) - totalConfirmed);
|
||||||
|
}
|
||||||
|
await ride.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.status = 'cancelled';
|
||||||
|
request.cancelledBy = isPassenger ? 'passenger' : 'driver';
|
||||||
|
request.cancellationReason = reason || '';
|
||||||
|
request.cancelledAt = new Date();
|
||||||
|
await request.save();
|
||||||
|
|
||||||
|
// TODO: Inviare notifica all'altra parte
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Richiesta cancellata',
|
||||||
|
data: request,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore cancellazione richiesta:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nella cancellazione',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni una singola richiesta
|
||||||
|
* @route GET /api/viaggi/requests/:id
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const getRequestById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const userId = req.user._id;
|
||||||
|
|
||||||
|
const request = await RideRequest.findById(id)
|
||||||
|
.populate('passengerId', 'username name surname profile.img profile.Cell profile.driverProfile')
|
||||||
|
.populate('driverId', 'username name surname profile.img profile.Cell profile.driverProfile')
|
||||||
|
.populate({
|
||||||
|
path: 'rideId',
|
||||||
|
populate: {
|
||||||
|
path: 'userId',
|
||||||
|
select: 'username name surname profile.img',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.populate('contribution.contribTypeId', 'label icon color');
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Richiesta non trovata',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che l'utente sia coinvolto
|
||||||
|
const passengerId = request.passengerId?._id || request.passengerId;
|
||||||
|
const driverId = request.driverId?._id || request.driverId;
|
||||||
|
|
||||||
|
const isPassenger = passengerId?.toString() === userId.toString();
|
||||||
|
const isDriver = driverId?.toString() === userId.toString();
|
||||||
|
|
||||||
|
if (!isPassenger && !isDriver) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Non sei autorizzato a vedere questa richiesta',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: request,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore recupero richiesta:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Errore nel recupero della richiesta',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc Ottieni richieste ricevute (io come conducente)
|
||||||
|
* @route GET /api/viaggi/requests/received
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const getReceivedRequests = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const driverId = req.user._id;
|
||||||
|
const { status, page = 1, limit = 20 } = req.query;
|
||||||
|
const idapp = req.query.idapp || req.user?.idapp;
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
const query = { driverId };
|
||||||
|
if (idapp) {
|
||||||
|
query.idapp = idapp;
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
query.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||||
|
|
||||||
|
// Fetch requests
|
||||||
|
const requests = await RideRequest.find(query)
|
||||||
|
.populate('passengerId', 'username name surname profile.img profile.driverProfile.averageRating')
|
||||||
|
.populate('rideId', 'departure destination dateTime passengers')
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(parseInt(limit))
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
// Get counts by status
|
||||||
|
const driverObjectId = toObjectId(driverId);
|
||||||
|
const matchQuery = { driverId: driverObjectId };
|
||||||
|
if (idapp) {
|
||||||
|
matchQuery.idapp = idapp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts = await RideRequest.aggregate([
|
||||||
|
{ $match: matchQuery },
|
||||||
|
{
|
||||||
|
$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 (Object.prototype.hasOwnProperty.call(statusCounts, 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/viaggi/requests/sent
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
const getSentRequests = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user._id;
|
||||||
|
const { status, page = 1, limit = 20 } = req.query;
|
||||||
|
const idapp = req.query.idapp || req.user?.idapp;
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
const query = { passengerId: userId };
|
||||||
|
if (idapp) {
|
||||||
|
query.idapp = idapp;
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
query.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||||
|
|
||||||
|
// Fetch requests
|
||||||
|
const requests = await RideRequest.find(query)
|
||||||
|
.populate('driverId', 'username name surname profile.img profile.driverProfile.averageRating')
|
||||||
|
.populate('rideId', 'departure destination dateTime passengers')
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(parseInt(limit))
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
// Enrich with ride info
|
||||||
|
const enrichedRequests = requests.map((request) => {
|
||||||
|
const enriched = { ...request };
|
||||||
|
|
||||||
|
if (enriched.rideId) {
|
||||||
|
enriched.rideInfo = {
|
||||||
|
departure: enriched.rideId.departure?.city || enriched.rideId.departure,
|
||||||
|
destination: enriched.rideId.destination?.city || enriched.rideId.destination,
|
||||||
|
dateTime: enriched.rideId.dateTime,
|
||||||
|
availableSeats: enriched.rideId.passengers?.available || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return enriched;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get counts by status
|
||||||
|
const passengerObjectId = toObjectId(userId);
|
||||||
|
const matchQuery = { passengerId: passengerObjectId };
|
||||||
|
if (idapp) {
|
||||||
|
matchQuery.idapp = idapp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts = await RideRequest.aggregate([
|
||||||
|
{ $match: matchQuery },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$status',
|
||||||
|
count: { $sum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const statusCounts = {
|
||||||
|
pending: 0,
|
||||||
|
accepted: 0,
|
||||||
|
rejected: 0,
|
||||||
|
cancelled: 0,
|
||||||
|
expired: 0,
|
||||||
|
completed: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
counts.forEach((c) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(statusCounts, c._id)) {
|
||||||
|
statusCounts[c._id] = c.count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Total count
|
||||||
|
const total = await RideRequest.countDocuments(query);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
requests: enrichedRequests,
|
||||||
|
counts: statusCounts,
|
||||||
|
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,
|
||||||
|
getMyRequests,
|
||||||
|
getPendingRequests,
|
||||||
|
acceptRequest,
|
||||||
|
rejectRequest,
|
||||||
|
cancelRequest,
|
||||||
|
getRequestById,
|
||||||
|
getReceivedRequests,
|
||||||
|
getSentRequests,
|
||||||
|
};
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
"sourceType": "ai",
|
"sourceType": "ai",
|
||||||
|
|
||||||
"file": {
|
"file": {
|
||||||
"path": "/uploads/assets/backgrounds/forest_autumn_001.jpg",
|
"path": "/upload/assets/backgrounds/forest_autumn_001.jpg",
|
||||||
"url": "/api/assets/asset_bg_001/file",
|
"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",
|
"thumbnailUrl": "/api/assets/asset_bg_001/thumbnail",
|
||||||
"originalName": null,
|
"originalName": null,
|
||||||
"mimeType": "image/jpeg",
|
"mimeType": "image/jpeg",
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
"backgroundImage": {
|
"backgroundImage": {
|
||||||
"id": "asset_bg_001",
|
"id": "asset_bg_001",
|
||||||
"sourceType": "ai",
|
"sourceType": "ai",
|
||||||
"url": "/uploads/posters/poster_sagra_2025_bg.jpg",
|
"url": "/upload/posters/poster_sagra_2025_bg.jpg",
|
||||||
"thumbnailUrl": "/uploads/posters/thumbs/poster_sagra_2025_bg_thumb.jpg",
|
"thumbnailUrl": "/upload/posters/thumbs/poster_sagra_2025_bg_thumb.jpg",
|
||||||
"mimeType": "image/jpeg",
|
"mimeType": "image/jpeg",
|
||||||
"size": 2458000,
|
"size": 2458000,
|
||||||
"dimensions": { "width": 2480, "height": 3508 },
|
"dimensions": { "width": 2480, "height": 3508 },
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
"mainImage": {
|
"mainImage": {
|
||||||
"id": "asset_main_001",
|
"id": "asset_main_001",
|
||||||
"sourceType": "upload",
|
"sourceType": "upload",
|
||||||
"url": "/uploads/assets/porcini_basket_hero.jpg",
|
"url": "/upload/assets/porcini_basket_hero.jpg",
|
||||||
"thumbnailUrl": "/uploads/assets/thumbs/porcini_basket_hero_thumb.jpg",
|
"thumbnailUrl": "/upload/assets/thumbs/porcini_basket_hero_thumb.jpg",
|
||||||
"originalName": "IMG_20241015_porcini.jpg",
|
"originalName": "IMG_20241015_porcini.jpg",
|
||||||
"mimeType": "image/jpeg",
|
"mimeType": "image/jpeg",
|
||||||
"size": 1845000,
|
"size": 1845000,
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"id": "asset_logo_001",
|
"id": "asset_logo_001",
|
||||||
"slotId": "logo_slot_1",
|
"slotId": "logo_slot_1",
|
||||||
"sourceType": "upload",
|
"sourceType": "upload",
|
||||||
"url": "/uploads/logos/comune_borgomontano.png",
|
"url": "/upload/logos/comune_borgomontano.png",
|
||||||
"originalName": "logo_comune.png",
|
"originalName": "logo_comune.png",
|
||||||
"mimeType": "image/png",
|
"mimeType": "image/png",
|
||||||
"size": 45000
|
"size": 45000
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"id": "asset_logo_002",
|
"id": "asset_logo_002",
|
||||||
"slotId": "logo_slot_2",
|
"slotId": "logo_slot_2",
|
||||||
"sourceType": "upload",
|
"sourceType": "upload",
|
||||||
"url": "/uploads/logos/proloco_borgomontano.png",
|
"url": "/upload/logos/proloco_borgomontano.png",
|
||||||
"originalName": "logo_proloco.png",
|
"originalName": "logo_proloco.png",
|
||||||
"mimeType": "image/png",
|
"mimeType": "image/png",
|
||||||
"size": 38000
|
"size": 38000
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"id": "asset_logo_003",
|
"id": "asset_logo_003",
|
||||||
"slotId": "logo_slot_3",
|
"slotId": "logo_slot_3",
|
||||||
"sourceType": "ai",
|
"sourceType": "ai",
|
||||||
"url": "/uploads/logos/ai_generated_mushroom_logo.png",
|
"url": "/upload/logos/ai_generated_mushroom_logo.png",
|
||||||
"mimeType": "image/png",
|
"mimeType": "image/png",
|
||||||
"size": 52000,
|
"size": 52000,
|
||||||
"aiParams": {
|
"aiParams": {
|
||||||
@@ -100,12 +100,12 @@
|
|||||||
|
|
||||||
"renderOutput": {
|
"renderOutput": {
|
||||||
"png": {
|
"png": {
|
||||||
"path": "/uploads/posters/final/poster_sagra_2025_final.png",
|
"path": "/upload/posters/final/poster_sagra_2025_final.png",
|
||||||
"size": 8945000,
|
"size": 8945000,
|
||||||
"url": "/api/posters/poster_sagra_funghi_2025_001/download/png"
|
"url": "/api/posters/poster_sagra_funghi_2025_001/download/png"
|
||||||
},
|
},
|
||||||
"jpg": {
|
"jpg": {
|
||||||
"path": "/uploads/posters/final/poster_sagra_2025_final.jpg",
|
"path": "/upload/posters/final/poster_sagra_2025_final.jpg",
|
||||||
"quality": 95,
|
"quality": 95,
|
||||||
"size": 2145000,
|
"size": 2145000,
|
||||||
"url": "/api/posters/poster_sagra_funghi_2025_001/download/jpg"
|
"url": "/api/posters/poster_sagra_funghi_2025_001/download/jpg"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const multer = require('multer');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload';
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
|
|||||||
81
src/middleware/uploadMiddleware.js
Normal file
81
src/middleware/uploadMiddleware.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
class UploadMiddleware {
|
||||||
|
constructor(baseUploadPath = 'uploads/videos') {
|
||||||
|
this.baseUploadPath = path.resolve(baseUploadPath);
|
||||||
|
this._ensureDirectory(this.baseUploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureDirectory(dir) {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createStorage() {
|
||||||
|
return multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
// ✅ Legge SOLO da req.query (affidabile con multer)
|
||||||
|
const folder = req.query.folder || 'default';
|
||||||
|
|
||||||
|
console.log('📁 Upload folder:', folder); // Debug
|
||||||
|
|
||||||
|
const uploadPath = path.join(this.baseUploadPath, folder);
|
||||||
|
this._ensureDirectory(uploadPath);
|
||||||
|
cb(null, uploadPath);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const ext = path.extname(file.originalname);
|
||||||
|
const uniqueName = `${uuidv4()}-${Date.now()}${ext}`;
|
||||||
|
cb(null, uniqueName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileFilter(req, file, cb) {
|
||||||
|
const allowedMimes = [
|
||||||
|
'video/mp4',
|
||||||
|
'video/webm',
|
||||||
|
'video/ogg',
|
||||||
|
'video/quicktime',
|
||||||
|
'video/x-msvideo',
|
||||||
|
'video/x-matroska'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allowedMimes.includes(file.mimetype)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error(`Tipo file non supportato: ${file.mimetype}`), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUploader(options = {}) {
|
||||||
|
const config = {
|
||||||
|
storage: this._createStorage(),
|
||||||
|
fileFilter: this._fileFilter.bind(this),
|
||||||
|
limits: {
|
||||||
|
fileSize: options.maxSize || 500 * 1024 * 1024,
|
||||||
|
files: options.maxFiles || 10
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return multer(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
single(fieldName = 'video') {
|
||||||
|
return this.getUploader().single(fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
multiple(fieldName = 'videos', maxCount = 10) {
|
||||||
|
return this.getUploader({ maxFiles: maxCount }).array(fieldName, maxCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBasePath() {
|
||||||
|
return this.baseUploadPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UploadMiddleware;
|
||||||
237
src/models/Chat.js
Normal file
237
src/models/Chat.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
const LastMessageSchema = new Schema(
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
senderId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const ChatSchema = new Schema(
|
||||||
|
{
|
||||||
|
idapp: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rideId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Ride',
|
||||||
|
index: true,
|
||||||
|
// Opzionale: chat collegata a un viaggio specifico
|
||||||
|
},
|
||||||
|
rideRequestId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'RideRequest',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ['direct', 'ride', 'group'],
|
||||||
|
default: 'direct',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
// Solo per chat di gruppo
|
||||||
|
},
|
||||||
|
lastMessage: {
|
||||||
|
type: LastMessageSchema,
|
||||||
|
},
|
||||||
|
unreadCount: {
|
||||||
|
type: Map,
|
||||||
|
of: Number,
|
||||||
|
default: new Map(),
|
||||||
|
// { odIdUtente: numeroMessaggiNonLetti }
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
mutedBy: [
|
||||||
|
{
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
blockedBy: [
|
||||||
|
{
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
deletedBy: [
|
||||||
|
{
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
clearedBefore: {
|
||||||
|
type: Map,
|
||||||
|
of: Date,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: Schema.Types.Mixed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
toJSON: { virtuals: true },
|
||||||
|
toObject: { virtuals: true },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Indici
|
||||||
|
ChatSchema.index({ participants: 1 });
|
||||||
|
ChatSchema.index({ idapp: 1, participants: 1 });
|
||||||
|
ChatSchema.index({ idapp: 1, updatedAt: -1 });
|
||||||
|
|
||||||
|
// Virtual per contare messaggi non letti totali
|
||||||
|
ChatSchema.virtual('totalUnread').get(function () {
|
||||||
|
if (!this.unreadCount) return 0;
|
||||||
|
let total = 0;
|
||||||
|
this.unreadCount.forEach((count) => {
|
||||||
|
total += count;
|
||||||
|
});
|
||||||
|
return total;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metodo per ottenere unread count per un utente specifico
|
||||||
|
ChatSchema.methods.getUnreadForUser = function (userId) {
|
||||||
|
if (!this.unreadCount) return 0;
|
||||||
|
return this.unreadCount.get(userId.toString()) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ 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) {
|
||||||
|
this.unreadCount.set(userId.toString(), 0);
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per aggiornare ultimo messaggio
|
||||||
|
ChatSchema.methods.updateLastMessage = function (message) {
|
||||||
|
this.lastMessage = {
|
||||||
|
text: message.text,
|
||||||
|
senderId: message.senderId,
|
||||||
|
timestamp: message.createdAt || new Date(),
|
||||||
|
type: message.type || 'text',
|
||||||
|
};
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per verificare se un utente è partecipante
|
||||||
|
// ✅ 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
|
||||||
|
// ✅ 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) {
|
||||||
|
// Cerca chat esistente tra i due utenti
|
||||||
|
let chat = await this.findOne({
|
||||||
|
idapp,
|
||||||
|
type: 'direct',
|
||||||
|
participants: { $all: [userId1, userId2], $size: 2 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chat) {
|
||||||
|
chat = new this({
|
||||||
|
idapp,
|
||||||
|
type: 'direct',
|
||||||
|
participants: [userId1, userId2],
|
||||||
|
rideId,
|
||||||
|
unreadCount: new Map(),
|
||||||
|
});
|
||||||
|
await chat.save();
|
||||||
|
} else if (rideId && !chat.rideId) {
|
||||||
|
// Aggiorna con rideId se fornito
|
||||||
|
chat.rideId = rideId;
|
||||||
|
await chat.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per ottenere tutte le chat di un utente
|
||||||
|
ChatSchema.statics.getChatsForUser = function (idapp, userId) {
|
||||||
|
return this.find({
|
||||||
|
idapp,
|
||||||
|
participants: userId,
|
||||||
|
isActive: true,
|
||||||
|
blockedBy: { $ne: userId },
|
||||||
|
})
|
||||||
|
.populate('participants', 'username name surname profile.avatar')
|
||||||
|
.populate('rideId', 'departure destination dateTime')
|
||||||
|
.sort({ updatedAt: -1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per creare chat di gruppo per un viaggio
|
||||||
|
ChatSchema.statics.createRideGroupChat = async function (idapp, rideId, title, participantIds) {
|
||||||
|
const chat = new this({
|
||||||
|
idapp,
|
||||||
|
type: 'group',
|
||||||
|
rideId,
|
||||||
|
title,
|
||||||
|
participants: participantIds,
|
||||||
|
unreadCount: new Map(),
|
||||||
|
});
|
||||||
|
return chat.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
const Chat = mongoose.model('Chat', ChatSchema);
|
||||||
|
|
||||||
|
module.exports = Chat;
|
||||||
357
src/models/Feedback.js
Normal file
357
src/models/Feedback.js
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
const FeedbackCategoriesSchema = new Schema(
|
||||||
|
{
|
||||||
|
punctuality: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
|
cleanliness: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
|
communication: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
|
driving: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
// Solo per feedback a conducenti
|
||||||
|
},
|
||||||
|
respect: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
|
reliability: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const FeedbackSchema = new Schema(
|
||||||
|
{
|
||||||
|
idapp: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
rideId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Ride',
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
rideRequestId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'RideRequest',
|
||||||
|
},
|
||||||
|
fromUserId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
toUserId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: String,
|
||||||
|
enum: ['driver', 'passenger'],
|
||||||
|
required: true,
|
||||||
|
// Il ruolo dell'utente che RICEVE il feedback
|
||||||
|
// 'driver' = sto valutando il conducente
|
||||||
|
// 'passenger' = sto valutando il passeggero
|
||||||
|
},
|
||||||
|
rating: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
type: FeedbackCategoriesSchema,
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 1000,
|
||||||
|
},
|
||||||
|
pros: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
'puntuale',
|
||||||
|
'gentile',
|
||||||
|
'auto_pulita',
|
||||||
|
'guida_sicura',
|
||||||
|
'buona_conversazione',
|
||||||
|
'silenzioso',
|
||||||
|
'flessibile',
|
||||||
|
'rispettoso',
|
||||||
|
'affidabile',
|
||||||
|
'consigliato',
|
||||||
|
// Tag negativi
|
||||||
|
'in_ritardo',
|
||||||
|
'scortese',
|
||||||
|
'guida_pericolosa',
|
||||||
|
'auto_sporca',
|
||||||
|
'non_rispettoso',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isPublic: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
isVerified: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
// Feedback verificato (viaggio effettivamente completato)
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 500,
|
||||||
|
},
|
||||||
|
respondedAt: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
helpful: {
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
reported: {
|
||||||
|
isReported: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
reason: String,
|
||||||
|
reportedBy: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
reportedAt: Date,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
toJSON: { virtuals: true },
|
||||||
|
toObject: { virtuals: true },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Indici composti
|
||||||
|
FeedbackSchema.index({ toUserId: 1, role: 1 });
|
||||||
|
FeedbackSchema.index({ rideId: 1, fromUserId: 1 });
|
||||||
|
FeedbackSchema.index({ idapp: 1, toUserId: 1 });
|
||||||
|
|
||||||
|
// Vincolo: un utente può lasciare un solo feedback per viaggio verso un altro utente
|
||||||
|
FeedbackSchema.index({ rideId: 1, fromUserId: 1, toUserId: 1 }, { unique: true });
|
||||||
|
|
||||||
|
// Virtual per calcolare media categorie
|
||||||
|
FeedbackSchema.virtual('categoryAverage').get(function () {
|
||||||
|
if (!this.categories) return null;
|
||||||
|
const cats = this.categories.toObject ? this.categories.toObject() : this.categories;
|
||||||
|
const values = Object.values(cats).filter((v) => typeof v === 'number');
|
||||||
|
if (values.length === 0) return null;
|
||||||
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metodo per aggiungere risposta
|
||||||
|
FeedbackSchema.methods.addResponse = function (text) {
|
||||||
|
this.response = {
|
||||||
|
text,
|
||||||
|
respondedAt: new Date(),
|
||||||
|
};
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per segnare come utile
|
||||||
|
FeedbackSchema.methods.markAsHelpful = function (userId) {
|
||||||
|
if (!this.helpful.users.includes(userId)) {
|
||||||
|
this.helpful.users.push(userId);
|
||||||
|
this.helpful.count = this.helpful.users.length;
|
||||||
|
return this.save();
|
||||||
|
}
|
||||||
|
return Promise.resolve(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per segnalare feedback
|
||||||
|
FeedbackSchema.methods.report = function (userId, reason) {
|
||||||
|
this.reported = {
|
||||||
|
isReported: true,
|
||||||
|
reason,
|
||||||
|
reportedBy: userId,
|
||||||
|
reportedAt: new Date(),
|
||||||
|
};
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per ottenere feedback di un utente
|
||||||
|
FeedbackSchema.statics.getForUser = function (idapp, userId, options = {}) {
|
||||||
|
const query = {
|
||||||
|
idapp,
|
||||||
|
toUserId: userId,
|
||||||
|
isPublic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.role) {
|
||||||
|
query.role = options.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.find(query)
|
||||||
|
.populate('fromUserId', 'username name surname profile.avatar')
|
||||||
|
.populate('rideId', 'departure destination dateTime')
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.limit(options.limit || 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per calcolare statistiche
|
||||||
|
FeedbackSchema.statics.getStatsForUser = async function (idapp, userId) {
|
||||||
|
const stats = await this.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
idapp,
|
||||||
|
toUserId: new mongoose.Types.ObjectId(userId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$role',
|
||||||
|
averageRating: { $avg: '$rating' },
|
||||||
|
totalFeedbacks: { $sum: 1 },
|
||||||
|
avgPunctuality: { $avg: '$categories.punctuality' },
|
||||||
|
avgCleanliness: { $avg: '$categories.cleanliness' },
|
||||||
|
avgCommunication: { $avg: '$categories.communication' },
|
||||||
|
avgDriving: { $avg: '$categories.driving' },
|
||||||
|
avgRespect: { $avg: '$categories.respect' },
|
||||||
|
avgReliability: { $avg: '$categories.reliability' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Trasforma in oggetto più leggibile
|
||||||
|
const result = {
|
||||||
|
asDriver: null,
|
||||||
|
asPassenger: null,
|
||||||
|
overall: {
|
||||||
|
averageRating: 0,
|
||||||
|
totalFeedbacks: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
stats.forEach((stat) => {
|
||||||
|
const data = {
|
||||||
|
averageRating: Math.round(stat.averageRating * 10) / 10,
|
||||||
|
totalFeedbacks: stat.totalFeedbacks,
|
||||||
|
categories: {
|
||||||
|
punctuality: stat.avgPunctuality ? Math.round(stat.avgPunctuality * 10) / 10 : null,
|
||||||
|
cleanliness: stat.avgCleanliness ? Math.round(stat.avgCleanliness * 10) / 10 : null,
|
||||||
|
communication: stat.avgCommunication ? Math.round(stat.avgCommunication * 10) / 10 : null,
|
||||||
|
driving: stat.avgDriving ? Math.round(stat.avgDriving * 10) / 10 : null,
|
||||||
|
respect: stat.avgRespect ? Math.round(stat.avgRespect * 10) / 10 : null,
|
||||||
|
reliability: stat.avgReliability ? Math.round(stat.avgReliability * 10) / 10 : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stat._id === 'driver') {
|
||||||
|
result.asDriver = data;
|
||||||
|
} else if (stat._id === 'passenger') {
|
||||||
|
result.asPassenger = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calcola overall
|
||||||
|
const allStats = stats.reduce(
|
||||||
|
(acc, s) => {
|
||||||
|
acc.total += s.totalFeedbacks;
|
||||||
|
acc.sum += s.averageRating * s.totalFeedbacks;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ total: 0, sum: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allStats.total > 0) {
|
||||||
|
result.overall = {
|
||||||
|
averageRating: Math.round((allStats.sum / allStats.total) * 10) / 10,
|
||||||
|
totalFeedbacks: allStats.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per contare distribuzioni rating
|
||||||
|
FeedbackSchema.statics.getRatingDistribution = async function (idapp, userId, role = null) {
|
||||||
|
const match = {
|
||||||
|
idapp,
|
||||||
|
toUserId: new mongoose.Types.ObjectId(userId),
|
||||||
|
};
|
||||||
|
if (role) match.role = role;
|
||||||
|
|
||||||
|
return this.aggregate([
|
||||||
|
{ $match: match },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$rating',
|
||||||
|
count: { $sum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { _id: -1 } },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook post-save per aggiornare rating utente
|
||||||
|
FeedbackSchema.post('save', async function (doc) {
|
||||||
|
try {
|
||||||
|
const { User } = require('./User');
|
||||||
|
|
||||||
|
const stats = await mongoose.model('Feedback').getStatsForUser(doc.idapp, doc.toUserId);
|
||||||
|
|
||||||
|
await User.findByIdAndUpdate(doc.toUserId, {
|
||||||
|
'profile.driverProfile.averageRating': stats.overall.averageRating,
|
||||||
|
'profile.driverProfile.totalRatings': stats.overall.totalFeedbacks,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore aggiornamento rating utente:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const Feedback = mongoose.model('Feedback', FeedbackSchema);
|
||||||
|
|
||||||
|
module.exports = Feedback;
|
||||||
241
src/models/Message.js
Normal file
241
src/models/Message.js
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
const MessageSchema = new Schema({
|
||||||
|
idapp: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
chatId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Chat',
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
senderId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 2000
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ['text', 'ride_share', 'location', 'image', 'voice', 'system', 'ride_request', 'ride_accepted', 'ride_rejected'],
|
||||||
|
default: 'text'
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
// Per messaggi speciali (condivisione viaggio, posizione, ecc.)
|
||||||
|
rideId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Ride'
|
||||||
|
},
|
||||||
|
rideRequestId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'RideRequest'
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
lat: Number,
|
||||||
|
lng: Number,
|
||||||
|
address: String
|
||||||
|
},
|
||||||
|
imageUrl: String,
|
||||||
|
voiceUrl: String,
|
||||||
|
voiceDuration: Number,
|
||||||
|
systemAction: String
|
||||||
|
},
|
||||||
|
readBy: [{
|
||||||
|
userId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
readAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
replyTo: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Message'
|
||||||
|
},
|
||||||
|
isEdited: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
editedAt: {
|
||||||
|
type: Date
|
||||||
|
},
|
||||||
|
isDeleted: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
deletedAt: {
|
||||||
|
type: Date
|
||||||
|
},
|
||||||
|
reactions: [{
|
||||||
|
userId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
emoji: String,
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
timestamps: true,
|
||||||
|
toJSON: { virtuals: true },
|
||||||
|
toObject: { virtuals: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Indici per query efficienti
|
||||||
|
MessageSchema.index({ chatId: 1, createdAt: -1 });
|
||||||
|
MessageSchema.index({ senderId: 1, createdAt: -1 });
|
||||||
|
MessageSchema.index({ idapp: 1, chatId: 1 });
|
||||||
|
|
||||||
|
// Virtual per verificare se il messaggio è stato letto da tutti
|
||||||
|
MessageSchema.virtual('isReadByAll').get(function() {
|
||||||
|
// Logica da implementare confrontando con partecipanti chat
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metodo per marcare come letto da un utente
|
||||||
|
MessageSchema.methods.markAsReadBy = function(userId) {
|
||||||
|
const alreadyRead = this.readBy.some(
|
||||||
|
r => r.userId.toString() === userId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!alreadyRead) {
|
||||||
|
this.readBy.push({
|
||||||
|
userId,
|
||||||
|
readAt: new Date()
|
||||||
|
});
|
||||||
|
return this.save();
|
||||||
|
}
|
||||||
|
return Promise.resolve(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per verificare se è stato letto da un utente
|
||||||
|
MessageSchema.methods.isReadBy = function(userId) {
|
||||||
|
return this.readBy.some(
|
||||||
|
r => r.userId.toString() === userId.toString()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per aggiungere reazione
|
||||||
|
MessageSchema.methods.addReaction = function(userId, emoji) {
|
||||||
|
// Rimuovi eventuale reazione precedente dello stesso utente
|
||||||
|
this.reactions = this.reactions.filter(
|
||||||
|
r => r.userId.toString() !== userId.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.reactions.push({
|
||||||
|
userId,
|
||||||
|
emoji,
|
||||||
|
createdAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per rimuovere reazione
|
||||||
|
MessageSchema.methods.removeReaction = function(userId) {
|
||||||
|
this.reactions = this.reactions.filter(
|
||||||
|
r => r.userId.toString() !== userId.toString()
|
||||||
|
);
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per soft delete
|
||||||
|
MessageSchema.methods.softDelete = function() {
|
||||||
|
this.isDeleted = true;
|
||||||
|
this.deletedAt = new Date();
|
||||||
|
this.text = '[Messaggio eliminato]';
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per modificare testo
|
||||||
|
MessageSchema.methods.editText = function(newText) {
|
||||||
|
this.text = newText;
|
||||||
|
this.isEdited = true;
|
||||||
|
this.editedAt = new Date();
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per ottenere messaggi di una chat con paginazione
|
||||||
|
// 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.img')
|
||||||
|
.populate('replyTo', 'text senderId')
|
||||||
|
.sort({ createdAt: -1 }) // -1 = più recente prima
|
||||||
|
.limit(limit)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per creare messaggio di sistema
|
||||||
|
MessageSchema.statics.createSystemMessage = async function(idapp, chatId, text, action = null) {
|
||||||
|
const message = new this({
|
||||||
|
idapp,
|
||||||
|
chatId,
|
||||||
|
senderId: null, // Sistema
|
||||||
|
text,
|
||||||
|
type: 'system',
|
||||||
|
metadata: {
|
||||||
|
systemAction: action
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return message.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per contare messaggi non letti
|
||||||
|
MessageSchema.statics.countUnreadForUser = async function(idapp, chatId, userId) {
|
||||||
|
return this.countDocuments({
|
||||||
|
idapp,
|
||||||
|
chatId,
|
||||||
|
isDeleted: false,
|
||||||
|
senderId: { $ne: userId },
|
||||||
|
'readBy.userId': { $ne: userId }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook post-save per aggiornare la chat
|
||||||
|
MessageSchema.post('save', async function(doc) {
|
||||||
|
try {
|
||||||
|
const Chat = mongoose.model('Chat');
|
||||||
|
const chat = await Chat.findById(doc.chatId);
|
||||||
|
|
||||||
|
if (chat) {
|
||||||
|
await chat.updateLastMessage(doc);
|
||||||
|
await chat.incrementUnread(doc.senderId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore aggiornamento chat dopo messaggio:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const Message = mongoose.model('Message', MessageSchema);
|
||||||
|
|
||||||
|
module.exports = Message;
|
||||||
539
src/models/Ride.js
Normal file
539
src/models/Ride.js
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
// Schema per le coordinate geografiche
|
||||||
|
const CoordinatesSchema = new Schema(
|
||||||
|
{
|
||||||
|
lat: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
lng: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Schema per una località (partenza, destinazione, waypoint)
|
||||||
|
const LocationSchema = new Schema(
|
||||||
|
{
|
||||||
|
city: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
province: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
region: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
country: {
|
||||||
|
type: String,
|
||||||
|
default: 'Italia',
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
postalCode: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
coordinates: {
|
||||||
|
type: CoordinatesSchema,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Schema per i waypoint (tappe intermedie)
|
||||||
|
const WaypointSchema = new Schema({
|
||||||
|
/*_id: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},*/
|
||||||
|
location: {
|
||||||
|
type: LocationSchema,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
estimatedArrival: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
stopDuration: {
|
||||||
|
type: Number, // minuti di sosta
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
}, { _id: false }); // 👈 AGGIUNGI QUESTO
|
||||||
|
|
||||||
|
// Schema per la ricorrenza del viaggio
|
||||||
|
const RecurrenceSchema = new Schema(
|
||||||
|
{
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ['once', 'weekly', 'custom_days', 'custom_dates'],
|
||||||
|
default: 'once',
|
||||||
|
},
|
||||||
|
daysOfWeek: [
|
||||||
|
{
|
||||||
|
type: Number,
|
||||||
|
min: 0,
|
||||||
|
max: 6,
|
||||||
|
// 0 = Domenica, 1 = Lunedì, ..., 6 = Sabato
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customDates: [
|
||||||
|
{
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
startDate: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
excludedDates: [
|
||||||
|
{
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Schema per i passeggeri
|
||||||
|
const PassengersSchema = new Schema(
|
||||||
|
{
|
||||||
|
available: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
min: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Schema per il veicolo
|
||||||
|
const VehicleSchema = new Schema(
|
||||||
|
{
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'],
|
||||||
|
default: 'auto',
|
||||||
|
},
|
||||||
|
brand: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
colorHex: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
year: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
licensePlate: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
seats: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
},
|
||||||
|
photos: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Schema per le preferenze di viaggio
|
||||||
|
const RidePreferencesSchema = new Schema(
|
||||||
|
{
|
||||||
|
smoking: {
|
||||||
|
type: String,
|
||||||
|
enum: ['yes', 'no', 'outside_only'],
|
||||||
|
default: 'no',
|
||||||
|
},
|
||||||
|
pets: {
|
||||||
|
type: String,
|
||||||
|
enum: ['no', 'small', 'medium', 'large', 'all'],
|
||||||
|
default: 'no',
|
||||||
|
},
|
||||||
|
luggage: {
|
||||||
|
type: String,
|
||||||
|
enum: ['none', 'small', 'medium', 'large'],
|
||||||
|
default: 'medium',
|
||||||
|
},
|
||||||
|
packages: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
maxPackageSize: {
|
||||||
|
type: String,
|
||||||
|
enum: ['small', 'medium', 'large', 'xlarge'],
|
||||||
|
default: 'medium',
|
||||||
|
},
|
||||||
|
music: {
|
||||||
|
type: String,
|
||||||
|
enum: ['no_music', 'quiet', 'moderate', 'loud', 'passenger_choice'],
|
||||||
|
default: 'moderate',
|
||||||
|
},
|
||||||
|
conversation: {
|
||||||
|
type: String,
|
||||||
|
enum: ['quiet', 'moderate', 'chatty'],
|
||||||
|
default: 'moderate',
|
||||||
|
},
|
||||||
|
foodAllowed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
childrenFriendly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
wheelchairAccessible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
otherPreferences: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Schema per il contributo/pagamento
|
||||||
|
const ContributionItemSchema = new Schema({
|
||||||
|
contribTypeId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Contribtype',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: Number,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
pricePerKm: {
|
||||||
|
type: Number,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ContributionSchema = new Schema(
|
||||||
|
{
|
||||||
|
contribTypes: [ContributionItemSchema],
|
||||||
|
negotiable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
freeForStudents: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
freeForElders: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Schema principale del Ride
|
||||||
|
const RideSchema = new Schema(
|
||||||
|
{
|
||||||
|
idapp: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ['offer', 'request'],
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
// offer = 🟢 Offerta passaggio (sono conducente)
|
||||||
|
// request = 🔴 Richiesta passaggio (cerco passaggio)
|
||||||
|
},
|
||||||
|
departure: {
|
||||||
|
type: LocationSchema,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
type: LocationSchema,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
waypoints: [WaypointSchema],
|
||||||
|
dateTime: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
flexibleTime: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
flexibleMinutes: {
|
||||||
|
type: Number,
|
||||||
|
default: 30,
|
||||||
|
min: 0,
|
||||||
|
max: 180,
|
||||||
|
},
|
||||||
|
recurrence: {
|
||||||
|
type: RecurrenceSchema,
|
||||||
|
default: () => ({ type: 'once' }),
|
||||||
|
},
|
||||||
|
passengers: {
|
||||||
|
type: PassengersSchema,
|
||||||
|
required: function () {
|
||||||
|
return this.type === 'offer';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seatsNeeded: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
default: 1,
|
||||||
|
// Solo per type = 'request'
|
||||||
|
},
|
||||||
|
vehicle: {
|
||||||
|
type: VehicleSchema,
|
||||||
|
required: function () {
|
||||||
|
return this.type === 'offer';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
type: RidePreferencesSchema,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
contribution: {
|
||||||
|
type: ContributionSchema,
|
||||||
|
default: () => ({ contribTypes: [] }),
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ['draft', 'active', 'full', 'in_progress', 'completed', 'cancelled', 'expired'],
|
||||||
|
default: 'active',
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
estimatedDistance: {
|
||||||
|
type: Number, // in km
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
estimatedDuration: {
|
||||||
|
type: Number, // in minuti
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
routePolyline: {
|
||||||
|
type: String, // Polyline encoded per visualizzare il percorso
|
||||||
|
},
|
||||||
|
confirmedPassengers: [
|
||||||
|
{
|
||||||
|
userId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
seats: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
pickupPoint: LocationSchema,
|
||||||
|
dropoffPoint: LocationSchema,
|
||||||
|
confirmedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
views: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
isFeatured: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 1000,
|
||||||
|
},
|
||||||
|
cancellationReason: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
cancelledAt: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
toJSON: { virtuals: true },
|
||||||
|
toObject: { virtuals: true },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Indici per ricerche ottimizzate
|
||||||
|
RideSchema.index({ 'departure.city': 1, 'destination.city': 1 });
|
||||||
|
RideSchema.index({ 'departure.coordinates': '2dsphere' });
|
||||||
|
RideSchema.index({ 'destination.coordinates': '2dsphere' });
|
||||||
|
RideSchema.index({ 'waypoints.location.city': 1 });
|
||||||
|
RideSchema.index({ dateTime: 1, status: 1 });
|
||||||
|
RideSchema.index({ idapp: 1, status: 1, dateTime: 1 });
|
||||||
|
|
||||||
|
// Virtual per verificare se il viaggio è pieno
|
||||||
|
RideSchema.virtual('isFull').get(function () {
|
||||||
|
if (this.type === 'request') return false;
|
||||||
|
// ⚠️ 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 () {
|
||||||
|
if (!this.confirmedPassengers) return 0;
|
||||||
|
return this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Virtual per ottenere tutte le città del percorso
|
||||||
|
RideSchema.virtual('allCities').get(function () {
|
||||||
|
const cities = [this.departure.city];
|
||||||
|
if (this.waypoints && this.waypoints.length > 0) {
|
||||||
|
this.waypoints.sort((a, b) => a.order - b.order).forEach((wp) => cities.push(wp.location.city));
|
||||||
|
}
|
||||||
|
cities.push(this.destination.city);
|
||||||
|
return cities;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metodo per verificare se passa per una città
|
||||||
|
RideSchema.methods.passesThrough = function (cityName) {
|
||||||
|
const normalizedCity = cityName.toLowerCase().trim();
|
||||||
|
return this.allCities.some(
|
||||||
|
(city) => city.toLowerCase().trim().includes(normalizedCity) || normalizedCity.includes(city.toLowerCase().trim())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per aggiornare posti disponibili
|
||||||
|
RideSchema.methods.updateAvailableSeats = function () {
|
||||||
|
// ⚠️ 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) {
|
||||||
|
this.status = 'full';
|
||||||
|
} else if (this.status === 'full') {
|
||||||
|
this.status = 'active';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-save hook
|
||||||
|
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) {
|
||||||
|
this.status = 'full';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metodi statici per ricerche comuni
|
||||||
|
RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinationCity, options = {}) {
|
||||||
|
const query = {
|
||||||
|
idapp,
|
||||||
|
status: { $in: ['active', 'full'] },
|
||||||
|
dateTime: { $gte: new Date() },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (departureCity) {
|
||||||
|
query['departure.city'] = new RegExp(departureCity, 'i');
|
||||||
|
}
|
||||||
|
if (destinationCity) {
|
||||||
|
query['destination.city'] = new RegExp(destinationCity, 'i');
|
||||||
|
}
|
||||||
|
if (options.type) {
|
||||||
|
query.type = options.type;
|
||||||
|
}
|
||||||
|
if (options.date) {
|
||||||
|
const startOfDay = new Date(options.date);
|
||||||
|
startOfDay.setHours(0, 0, 0, 0);
|
||||||
|
const endOfDay = new Date(options.date);
|
||||||
|
endOfDay.setHours(23, 59, 59, 999);
|
||||||
|
query.dateTime = { $gte: startOfDay, $lte: endOfDay };
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.find(query)
|
||||||
|
.populate('userId', 'username name surname profile.driverProfile.averageRating')
|
||||||
|
.sort({ dateTime: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ricerca viaggi che passano per una città intermedia
|
||||||
|
RideSchema.statics.findPassingThrough = function (idapp, cityName, options = {}) {
|
||||||
|
const cityRegex = new RegExp(cityName, 'i');
|
||||||
|
const query = {
|
||||||
|
idapp,
|
||||||
|
status: { $in: ['active'] },
|
||||||
|
dateTime: { $gte: new Date() },
|
||||||
|
$or: [{ 'departure.city': cityRegex }, { 'destination.city': cityRegex }, { 'waypoints.location.city': cityRegex }],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.type) {
|
||||||
|
query.type = options.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.find(query)
|
||||||
|
.populate('userId', 'username name surname profile.driverProfile.averageRating')
|
||||||
|
.sort({ dateTime: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const Ride = mongoose.model('Ride', RideSchema);
|
||||||
|
|
||||||
|
module.exports = Ride;
|
||||||
296
src/models/RideRequest.js
Normal file
296
src/models/RideRequest.js
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
// Schema per le coordinate
|
||||||
|
const CoordinatesSchema = new Schema({
|
||||||
|
lat: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lng: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}, { _id: false });
|
||||||
|
|
||||||
|
// Schema per località
|
||||||
|
const LocationSchema = new Schema({
|
||||||
|
city: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
province: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
coordinates: {
|
||||||
|
type: CoordinatesSchema,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}, { _id: false });
|
||||||
|
|
||||||
|
const RideRequestSchema = new Schema({
|
||||||
|
idapp: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
rideId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Ride',
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
passengerId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
driverId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 500
|
||||||
|
},
|
||||||
|
pickupPoint: {
|
||||||
|
type: LocationSchema
|
||||||
|
},
|
||||||
|
dropoffPoint: {
|
||||||
|
type: LocationSchema
|
||||||
|
},
|
||||||
|
useOriginalRoute: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
// true = usa partenza/destinazione originali del ride
|
||||||
|
},
|
||||||
|
seatsRequested: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
min: 1,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
hasLuggage: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
luggageSize: {
|
||||||
|
type: String,
|
||||||
|
enum: ['small', 'medium', 'large'],
|
||||||
|
default: 'small'
|
||||||
|
},
|
||||||
|
hasPackages: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
packageDescription: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 200
|
||||||
|
},
|
||||||
|
hasPets: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
petType: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
petSize: {
|
||||||
|
type: String,
|
||||||
|
enum: ['small', 'medium', 'large']
|
||||||
|
},
|
||||||
|
specialNeeds: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 300
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ['pending', 'accepted', 'rejected', 'cancelled', 'expired', 'completed'],
|
||||||
|
default: 'pending',
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
responseMessage: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 500
|
||||||
|
},
|
||||||
|
respondedAt: {
|
||||||
|
type: Date
|
||||||
|
},
|
||||||
|
contribution: {
|
||||||
|
agreed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
contribTypeId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Contribtype'
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: Number,
|
||||||
|
min: 0
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancelledBy: {
|
||||||
|
type: String,
|
||||||
|
enum: ['passenger', 'driver']
|
||||||
|
},
|
||||||
|
cancellationReason: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
cancelledAt: {
|
||||||
|
type: Date
|
||||||
|
},
|
||||||
|
completedAt: {
|
||||||
|
type: Date
|
||||||
|
},
|
||||||
|
feedbackGiven: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true,
|
||||||
|
toJSON: { virtuals: true },
|
||||||
|
toObject: { virtuals: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Indici composti per ricerche ottimizzate
|
||||||
|
RideRequestSchema.index({ rideId: 1, status: 1 });
|
||||||
|
RideRequestSchema.index({ passengerId: 1, status: 1 });
|
||||||
|
RideRequestSchema.index({ driverId: 1, status: 1 });
|
||||||
|
RideRequestSchema.index({ idapp: 1, createdAt: -1 });
|
||||||
|
|
||||||
|
// Virtual per verificare se la richiesta può essere cancellata
|
||||||
|
RideRequestSchema.virtual('canCancel').get(function() {
|
||||||
|
return ['pending', 'accepted'].includes(this.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Virtual per verificare se è in attesa
|
||||||
|
RideRequestSchema.virtual('isPending').get(function() {
|
||||||
|
return this.status === 'pending';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metodo per accettare la richiesta
|
||||||
|
RideRequestSchema.methods.accept = async function(responseMessage = '') {
|
||||||
|
this.status = 'accepted';
|
||||||
|
this.responseMessage = responseMessage;
|
||||||
|
this.respondedAt = new Date();
|
||||||
|
|
||||||
|
// Aggiorna il ride con il passeggero confermato
|
||||||
|
const Ride = mongoose.model('Ride');
|
||||||
|
const ride = await Ride.findById(this.rideId);
|
||||||
|
|
||||||
|
if (ride) {
|
||||||
|
ride.confirmedPassengers.push({
|
||||||
|
userId: this.passengerId,
|
||||||
|
seats: this.seatsRequested,
|
||||||
|
pickupPoint: this.pickupPoint || ride.departure,
|
||||||
|
dropoffPoint: this.dropoffPoint || ride.destination,
|
||||||
|
confirmedAt: new Date()
|
||||||
|
});
|
||||||
|
await ride.updateAvailableSeats();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per rifiutare la richiesta
|
||||||
|
RideRequestSchema.methods.reject = function(responseMessage = '') {
|
||||||
|
this.status = 'rejected';
|
||||||
|
this.responseMessage = responseMessage;
|
||||||
|
this.respondedAt = new Date();
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo per cancellare la richiesta
|
||||||
|
RideRequestSchema.methods.cancel = async function(cancelledBy, reason = '') {
|
||||||
|
this.status = 'cancelled';
|
||||||
|
this.cancelledBy = cancelledBy;
|
||||||
|
this.cancellationReason = reason;
|
||||||
|
this.cancelledAt = new Date();
|
||||||
|
|
||||||
|
// Se era accettata, rimuovi il passeggero dal ride
|
||||||
|
if (this.status === 'accepted') {
|
||||||
|
const Ride = mongoose.model('Ride');
|
||||||
|
const ride = await Ride.findById(this.rideId);
|
||||||
|
|
||||||
|
if (ride) {
|
||||||
|
ride.confirmedPassengers = ride.confirmedPassengers.filter(
|
||||||
|
p => p.userId.toString() !== this.passengerId.toString()
|
||||||
|
);
|
||||||
|
await ride.updateAvailableSeats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per ottenere richieste pendenti di un conducente
|
||||||
|
RideRequestSchema.statics.getPendingForDriver = function(idapp, driverId) {
|
||||||
|
return this.find({
|
||||||
|
idapp,
|
||||||
|
driverId,
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
.populate('passengerId', 'username name surname email')
|
||||||
|
.populate('rideId', 'departure destination dateTime')
|
||||||
|
.sort({ createdAt: -1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Metodo statico per ottenere richieste di un passeggero
|
||||||
|
RideRequestSchema.statics.getByPassenger = function(idapp, passengerId, status = null) {
|
||||||
|
const query = { idapp, passengerId };
|
||||||
|
if (status) {
|
||||||
|
query.status = status;
|
||||||
|
}
|
||||||
|
return this.find(query)
|
||||||
|
.populate('rideId')
|
||||||
|
.populate('driverId', 'username name surname')
|
||||||
|
.sort({ createdAt: -1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-save hook per validazioni
|
||||||
|
RideRequestSchema.pre('save', async function(next) {
|
||||||
|
if (this.isNew) {
|
||||||
|
// Verifica che il ride esista e abbia posti disponibili
|
||||||
|
const Ride = mongoose.model('Ride');
|
||||||
|
const ride = await Ride.findById(this.rideId);
|
||||||
|
|
||||||
|
if (!ride) {
|
||||||
|
throw new Error('Viaggio non trovato');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ride.type === 'offer' && ride.passengers.available < this.seatsRequested) {
|
||||||
|
throw new Error('Posti non sufficienti per questo viaggio');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ride.userId.toString() === this.passengerId.toString()) {
|
||||||
|
throw new Error('Non puoi richiedere un passaggio per il tuo stesso viaggio');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imposta il driverId dal ride
|
||||||
|
this.driverId = ride.userId;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const RideRequest = mongoose.model('RideRequest', RideRequestSchema);
|
||||||
|
|
||||||
|
module.exports = RideRequest;
|
||||||
@@ -56,11 +56,11 @@ ContribtypeSchema.statics.findAllIdApp = async function (idapp) {
|
|||||||
return await Contribtype.find(myfind).lean();
|
return await Contribtype.find(myfind).lean();
|
||||||
};
|
};
|
||||||
|
|
||||||
const Contribtype = mongoose.model('Contribtype', ContribtypeSchema);
|
const Contribtype = mongoose.models.Contribtype || mongoose.model('Contribtype', ContribtypeSchema);
|
||||||
|
|
||||||
Contribtype.createIndexes()
|
Contribtype.createIndexes()
|
||||||
.then(() => { })
|
.then(() => { })
|
||||||
.catch((err) => { throw err; });
|
.catch((err) => { throw err; });
|
||||||
|
|
||||||
|
|
||||||
module.exports = { Contribtype };
|
module.exports = { Contribtype };
|
||||||
@@ -175,6 +175,7 @@ const SiteSchema = new Schema({
|
|||||||
enableEcommerce: { type: Boolean, default: false },
|
enableEcommerce: { type: Boolean, default: false },
|
||||||
enableAI: { type: Boolean, default: false },
|
enableAI: { type: Boolean, default: false },
|
||||||
enablePoster: { type: Boolean, default: false },
|
enablePoster: { type: Boolean, default: false },
|
||||||
|
enableTrasporti: { type: Boolean, default: false },
|
||||||
enableGroups: { type: Boolean, default: false },
|
enableGroups: { type: Boolean, default: false },
|
||||||
enableCircuits: { type: Boolean, default: false },
|
enableCircuits: { type: Boolean, default: false },
|
||||||
enableGoods: { type: Boolean, default: false },
|
enableGoods: { type: Boolean, default: false },
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const mongoose = require('mongoose').set('debug', false);
|
const mongoose = require('mongoose').set('debug', false);
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
@@ -285,6 +287,10 @@ const UserSchema = new mongoose.Schema(
|
|||||||
cell: {
|
cell: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
cellVerified: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
country_pay: {
|
country_pay: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
@@ -584,6 +590,262 @@ const UserSchema = new mongoose.Schema(
|
|||||||
],
|
],
|
||||||
version: { type: Number },
|
version: { type: Number },
|
||||||
insert_circuito_ita: { type: Boolean },
|
insert_circuito_ita: { type: Boolean },
|
||||||
|
|
||||||
|
// ============ DRIVER PROFILE ============
|
||||||
|
driverProfile: {
|
||||||
|
isDriver: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
bio: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
maxlength: 500,
|
||||||
|
},
|
||||||
|
vehicles: [
|
||||||
|
{
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'auto',
|
||||||
|
},
|
||||||
|
brand: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
colorHex: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
year: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
seats: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
max: 50,
|
||||||
|
},
|
||||||
|
licensePlate: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
photos: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isDefault: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isVerified: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ridesCompletedAsDriver: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
ridesCompletedAsPassenger: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
averageRating: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
|
totalRatings: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
verifiedDriver: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
licenseVerified: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
licenseNumber: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
licenseExpiry: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
memberSince: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
responseRate: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
responseTime: {
|
||||||
|
type: String,
|
||||||
|
default: 'within_day',
|
||||||
|
},
|
||||||
|
totalKmShared: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
co2Saved: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
// kg di CO2 risparmiati
|
||||||
|
},
|
||||||
|
badges: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
earnedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
level: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
min: 1,
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============ PREFERENCES ============
|
||||||
|
preferences: {
|
||||||
|
// Preferenze di viaggio
|
||||||
|
smoking: {
|
||||||
|
type: String,
|
||||||
|
default: 'no',
|
||||||
|
},
|
||||||
|
pets: {
|
||||||
|
type: String,
|
||||||
|
default: 'small',
|
||||||
|
},
|
||||||
|
music: {
|
||||||
|
type: String,
|
||||||
|
default: 'moderate',
|
||||||
|
},
|
||||||
|
conversation: {
|
||||||
|
type: String,
|
||||||
|
default: 'moderate',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Notifiche
|
||||||
|
notifications: {
|
||||||
|
rideRequests: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
rideAccepted: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
rideReminders: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
marketing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
pushEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
emailEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Privacy
|
||||||
|
privacy: {
|
||||||
|
showEmail: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showPhone: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showLastName: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showRides: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Località preferite
|
||||||
|
favoriteLocations: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
city: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
coordinates: {
|
||||||
|
lat: Number,
|
||||||
|
lng: Number,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'other',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Lingue parlate
|
||||||
|
languages: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Metodo di pagamento preferito
|
||||||
|
preferredContribType: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Contribtype',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
updatedAt: { type: Date, default: Date.now },
|
updatedAt: { type: Date, default: Date.now },
|
||||||
},
|
},
|
||||||
@@ -7016,8 +7278,6 @@ UserSchema.statics.getUsersList = function (idapp) {
|
|||||||
}).lean();
|
}).lean();
|
||||||
};
|
};
|
||||||
|
|
||||||
const User = mongoose.model('User', UserSchema);
|
|
||||||
|
|
||||||
class Hero {
|
class Hero {
|
||||||
constructor(name, level) {
|
constructor(name, level) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -7074,6 +7334,8 @@ const FuncUsers = {
|
|||||||
|
|
||||||
UserSchema.index({ 'tokens.token': 1, 'tokens.access': 1, idapp: 1, deleted: 1, updatedAt: 1 });
|
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 = {
|
module.exports = {
|
||||||
User,
|
User,
|
||||||
Hero,
|
Hero,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const PageView = require('../models/PageView');
|
|||||||
|
|
||||||
const fal = require('@fal-ai/client');
|
const fal = require('@fal-ai/client');
|
||||||
|
|
||||||
|
|
||||||
const imageGenerator = require('../services/imageGenerator'); // Assicurati che il percorso sia corretto
|
const imageGenerator = require('../services/imageGenerator'); // Assicurati che il percorso sia corretto
|
||||||
|
|
||||||
const posterEditor = require('../services/PosterEditor'); // <--- Importa la nuova classe
|
const posterEditor = require('../services/PosterEditor'); // <--- Importa la nuova classe
|
||||||
@@ -33,6 +32,15 @@ const { MyElem } = require('../models/myelem');
|
|||||||
|
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const viaggiRoutes = require('../routes/viaggiRoutes');
|
||||||
|
router.use('/viaggi', viaggiRoutes);
|
||||||
|
|
||||||
|
// Importa le routes video
|
||||||
|
const videoRoutes = require('../routes/videoRoutes');
|
||||||
|
|
||||||
|
// Monta le routes video
|
||||||
|
router.use('/video', videoRoutes);
|
||||||
|
|
||||||
router.use('/templates', authenticate, templatesRouter);
|
router.use('/templates', authenticate, templatesRouter);
|
||||||
router.use('/posters', authenticate, postersRouter);
|
router.use('/posters', authenticate, postersRouter);
|
||||||
router.use('/assets', authenticate, assetsRouter);
|
router.use('/assets', authenticate, assetsRouter);
|
||||||
@@ -512,9 +520,16 @@ router.post('/chatbot', authenticate, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/generateposter', async (req, res) => {
|
router.post('/generateposter', async (req, res) => {
|
||||||
const {
|
const {
|
||||||
titolo, data, ora, luogo, descrizione, contatti, fotoDescrizione, stile,
|
titolo,
|
||||||
provider = 'hf' // Default a HF (Gratis)
|
data,
|
||||||
|
ora,
|
||||||
|
luogo,
|
||||||
|
descrizione,
|
||||||
|
contatti,
|
||||||
|
fotoDescrizione,
|
||||||
|
stile,
|
||||||
|
provider = 'hf', // Default a HF (Gratis)
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// 1. Prompt per l'AI: Chiediamo SOLO lo sfondo, VIETIAMO il testo.
|
// 1. Prompt per l'AI: Chiediamo SOLO lo sfondo, VIETIAMO il testo.
|
||||||
@@ -536,21 +551,50 @@ router.post('/generateposter', async (req, res) => {
|
|||||||
data,
|
data,
|
||||||
ora,
|
ora,
|
||||||
luogo,
|
luogo,
|
||||||
contatti
|
contatti,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
imageUrl: finalPosterBase64, // Restituisce l'immagine completa in base64
|
imageUrl: finalPosterBase64, // Restituisce l'immagine completa in base64
|
||||||
step: 'AI + Canvas Composition'
|
step: 'AI + Canvas Composition',
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Errore:', err.message);
|
console.error('Errore:', err.message);
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/users/search', authenticate, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { User } = require('../models/user');
|
||||||
|
|
||||||
|
const { q, idapp } = req.query;
|
||||||
|
|
||||||
|
if (!q || q.length < 2) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Query too short' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = q.trim();
|
||||||
|
|
||||||
|
const users = await User.find({
|
||||||
|
idapp,
|
||||||
|
$or: [
|
||||||
|
{ name: { $regex: query, $options: 'i' } },
|
||||||
|
{ surname: { $regex: query, $options: 'i' } },
|
||||||
|
{ username: { $regex: query, $options: 'i' } },
|
||||||
|
],
|
||||||
|
_id: { $ne: req.user?._id }, // escludi l'utente corrente se autenticato
|
||||||
|
})
|
||||||
|
.select('_id name surname username profile') // solo campi necessari
|
||||||
|
.limit(10); // evita overload
|
||||||
|
|
||||||
|
res.json({ success: true, data: users });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('User search error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -317,7 +317,11 @@ router.post('/', async (req, res) => {
|
|||||||
await telegrambot.askConfirmationUser(myuser.idapp, shared_consts.CallFunz.REGISTRATION, myuser);
|
await telegrambot.askConfirmationUser(myuser.idapp, shared_consts.CallFunz.REGISTRATION, myuser);
|
||||||
|
|
||||||
const { token, refreshToken, browser_random } = await myuser.generateAuthToken(req, browser_random);
|
const { token, refreshToken, browser_random } = await myuser.generateAuthToken(req, browser_random);
|
||||||
res.header('x-auth', token).header('x-refrtok', refreshToken).header('x-browser-random', browser_random).send(myuser);
|
res
|
||||||
|
.header('x-auth', token)
|
||||||
|
.header('x-refrtok', refreshToken)
|
||||||
|
.header('x-browser-random', browser_random)
|
||||||
|
.send(myuser);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,7 +372,11 @@ router.post('/', async (req, res) => {
|
|||||||
// if (!tools.testing()) {
|
// if (!tools.testing()) {
|
||||||
await sendemail.sendEmail_Registration(user.lang, user.email, user, user.idapp, user.linkreg);
|
await sendemail.sendEmail_Registration(user.lang, user.email, user, user.idapp, user.linkreg);
|
||||||
// }
|
// }
|
||||||
res.header('x-auth', ris.token).header('x-refrtok', ris.refreshToken).header('x-browser-random', ris.browser_random).send(user);
|
res
|
||||||
|
.header('x-auth', ris.token)
|
||||||
|
.header('x-refrtok', ris.refreshToken)
|
||||||
|
.header('x-browser-random', ris.browser_random)
|
||||||
|
.send(user);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -411,7 +419,9 @@ router.patch('/:id', authenticate, (req, res) => {
|
|||||||
|
|
||||||
if (!User.isAdmin(req.user.perm)) {
|
if (!User.isAdmin(req.user.perm)) {
|
||||||
// If without permissions, exit
|
// If without permissions, exit
|
||||||
return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
return res
|
||||||
|
.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED)
|
||||||
|
.send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
User.findByIdAndUpdate(id, { $set: body })
|
User.findByIdAndUpdate(id, { $set: body })
|
||||||
@@ -512,7 +522,7 @@ router.post('/profile', authenticate, (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if ìs a Notif to read
|
// Check if ìs a Notif to read
|
||||||
if (idnotif) {
|
if (idnotif) {
|
||||||
SendNotif.setNotifAsRead(idapp, usernameOrig, idnotif);
|
SendNotif.setNotifAsRead(idapp, usernameOrig, idnotif);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,9 +601,14 @@ router.post('/panel', authenticate, async (req, res) => {
|
|||||||
idapp = req.body.idapp;
|
idapp = req.body.idapp;
|
||||||
locale = req.body.locale;
|
locale = req.body.locale;
|
||||||
|
|
||||||
if (!req.user || !User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm)) {
|
if (
|
||||||
|
!req.user ||
|
||||||
|
(!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm))
|
||||||
|
) {
|
||||||
// If without permissions, exit
|
// If without permissions, exit
|
||||||
return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
return res
|
||||||
|
.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED)
|
||||||
|
.send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -660,6 +675,7 @@ router.post('/notifs', authenticate, async (req, res) => {
|
|||||||
router.post('/newtok', async (req, res) => {
|
router.post('/newtok', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const refreshToken = req.body.refreshToken;
|
const refreshToken = req.body.refreshToken;
|
||||||
|
const browser_random = req.body.br;
|
||||||
|
|
||||||
// return res.status(403).send({ error: 'Refresh token non valido' });
|
// return res.status(403).send({ error: 'Refresh token non valido' });
|
||||||
|
|
||||||
@@ -668,7 +684,7 @@ router.post('/newtok', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const recFound = await User.findByRefreshTokenAnyAccess(refreshToken);
|
const recFound = await User.findByRefreshTokenAnyAccess(refreshToken);
|
||||||
|
|
||||||
if (!recFound) {
|
if (!recFound) {
|
||||||
return res.status(403).send({ error: 'Refresh token non valido' });
|
return res.status(403).send({ error: 'Refresh token non valido' });
|
||||||
}
|
}
|
||||||
@@ -952,7 +968,9 @@ router.post('/friends/cmd', authenticate, async (req, res) => {
|
|||||||
usernameDest !== usernameLogged &&
|
usernameDest !== usernameLogged &&
|
||||||
(cmd === shared_consts.FRIENDSCMD.SETFRIEND || cmd === shared_consts.FRIENDSCMD.SETHANDSHAKE)
|
(cmd === shared_consts.FRIENDSCMD.SETFRIEND || cmd === shared_consts.FRIENDSCMD.SETHANDSHAKE)
|
||||||
) {
|
) {
|
||||||
return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
return res
|
||||||
|
.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED)
|
||||||
|
.send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,7 +1136,10 @@ async function eseguiDbOpUser(idapp, mydata, locale, req, res) {
|
|||||||
} else if (mydata.dbop === 'noNameSurname') {
|
} else if (mydata.dbop === 'noNameSurname') {
|
||||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noNameSurname': mydata.value } });
|
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noNameSurname': mydata.value } });
|
||||||
} else if (mydata.dbop === 'telegram_verification_skipped') {
|
} else if (mydata.dbop === 'telegram_verification_skipped') {
|
||||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.telegram_verification_skipped': mydata.value } });
|
await User.findOneAndUpdate(
|
||||||
|
{ _id: mydata._id },
|
||||||
|
{ $set: { 'profile.telegram_verification_skipped': mydata.value } }
|
||||||
|
);
|
||||||
} else if (mydata.dbop === 'pwdLikeAdmin') {
|
} else if (mydata.dbop === 'pwdLikeAdmin') {
|
||||||
await User.setPwdComeQuellaDellAdmin(mydata);
|
await User.setPwdComeQuellaDellAdmin(mydata);
|
||||||
} else if (mydata.dbop === 'ripristinaPwdPrec') {
|
} else if (mydata.dbop === 'ripristinaPwdPrec') {
|
||||||
@@ -1128,10 +1149,10 @@ async function eseguiDbOpUser(idapp, mydata, locale, req, res) {
|
|||||||
} else if (mydata.dbop === 'noComune') {
|
} else if (mydata.dbop === 'noComune') {
|
||||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noComune': mydata.value } });
|
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noComune': mydata.value } });
|
||||||
} else if (mydata.dbop === 'verifiedemail') {
|
} else if (mydata.dbop === 'verifiedemail') {
|
||||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'verified_email': mydata.value } });
|
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { verified_email: mydata.value } });
|
||||||
} else if (mydata.dbop === 'resendVerificationEmail') {
|
} else if (mydata.dbop === 'resendVerificationEmail') {
|
||||||
// Invia la email di Verifica email
|
// Invia la email di Verifica email
|
||||||
const ris = await sendemail.sendEmail_ReVerifyingEmail(mydata, idapp);
|
const ris = await sendemail.sendEmail_ReVerifyingEmail(mydata, idapp);
|
||||||
} else if (mydata.dbop === 'noCircIta') {
|
} else if (mydata.dbop === 'noCircIta') {
|
||||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noCircIta': mydata.value } });
|
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noCircIta': mydata.value } });
|
||||||
} else if (mydata.dbop === 'insert_circuito_ita') {
|
} else if (mydata.dbop === 'insert_circuito_ita') {
|
||||||
|
|||||||
37
src/routes/geoRoutes.js
Normal file
37
src/routes/geoRoutes.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const {
|
||||||
|
autocomplete,
|
||||||
|
geocode,
|
||||||
|
reverseGeocode,
|
||||||
|
getRoute,
|
||||||
|
getMatrix,
|
||||||
|
suggestWaypoints,
|
||||||
|
searchItalianCities,
|
||||||
|
getDistance,
|
||||||
|
getIsochrone
|
||||||
|
} = require('../controllers/geocodingController');
|
||||||
|
|
||||||
|
// Rate limiting opzionale
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
|
||||||
|
const geoLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 minuto
|
||||||
|
max: 60, // 60 richieste per minuto
|
||||||
|
message: { success: false, message: 'Troppe richieste, riprova tra poco' }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use(geoLimiter);
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
router.get('/autocomplete', autocomplete);
|
||||||
|
router.get('/geocode', geocode);
|
||||||
|
router.get('/reverse', reverseGeocode);
|
||||||
|
router.get('/route', getRoute);
|
||||||
|
router.post('/matrix', getMatrix);
|
||||||
|
router.get('/suggest-waypoints', suggestWaypoints);
|
||||||
|
router.get('/cities/it', searchItalianCities);
|
||||||
|
router.get('/distance', getDistance);
|
||||||
|
router.get('/isochrone', getIsochrone);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
1040
src/routes/viaggiRoutes.js
Normal file
1040
src/routes/viaggiRoutes.js
Normal file
File diff suppressed because it is too large
Load Diff
58
src/routes/videoRoutes.js
Normal file
58
src/routes/videoRoutes.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const VideoController = require('../controllers/VideoController');
|
||||||
|
const UploadMiddleware = require('../middleware/uploadMiddleware');
|
||||||
|
|
||||||
|
const {
|
||||||
|
authenticate,
|
||||||
|
authenticate_noerror,
|
||||||
|
authenticate_noerror_WithUser,
|
||||||
|
authenticate_noerror_WithUserLean,
|
||||||
|
} = require('../middleware/authenticate');
|
||||||
|
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Configurazione
|
||||||
|
const UPLOAD_PATH = process.env.VIDEO_UPLOAD_PATH || 'uploads/videos';
|
||||||
|
|
||||||
|
// Istanze
|
||||||
|
const videoController = new VideoController(UPLOAD_PATH);
|
||||||
|
const uploadMiddleware = new UploadMiddleware(UPLOAD_PATH);
|
||||||
|
|
||||||
|
// ============ FOLDER ROUTES ============
|
||||||
|
router.get('/folders', authenticate, videoController.getFolders);
|
||||||
|
router.post('/folders', authenticate, videoController.createFolder);
|
||||||
|
router.put('/folders/:folderPath(*)', authenticate, videoController.renameFolder);
|
||||||
|
router.delete('/folders/:folderPath(*)', authenticate, videoController.deleteFolder);
|
||||||
|
|
||||||
|
// ============ VIDEO ROUTES ============
|
||||||
|
router.get('/videos', authenticate, videoController.getVideos);
|
||||||
|
router.get('/videos/:folder/:filename', authenticate, videoController.getVideo);
|
||||||
|
|
||||||
|
// Upload
|
||||||
|
router.post(
|
||||||
|
'/videos/upload',
|
||||||
|
uploadMiddleware.single('video'),
|
||||||
|
videoController.uploadVideo
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/videos/upload-multiple',
|
||||||
|
uploadMiddleware.multiple('videos', 10),
|
||||||
|
videoController.uploadVideos
|
||||||
|
);
|
||||||
|
|
||||||
|
// Modifica
|
||||||
|
router.put('/videos/:folder/:filename/rename', authenticate, videoController.renameVideo);
|
||||||
|
router.put('/videos/:folder/:filename/move', authenticate, videoController.moveVideo);
|
||||||
|
|
||||||
|
// Elimina
|
||||||
|
router.delete('/videos/:folder/:filename', authenticate, videoController.deleteVideo);
|
||||||
|
|
||||||
|
// Stream
|
||||||
|
router.get('/stream/:folder/:filename', authenticate, videoController.streamVideo);
|
||||||
|
|
||||||
|
// Error Handler
|
||||||
|
router.use(VideoController.errorHandler);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ function setupExpress(app, corsOptions) {
|
|||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
app.use(morgan('dev'));
|
app.use(morgan('dev'));
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
app.set('trust proxy', true);
|
app.set('trust proxy', (process.env.NODE_ENV === 'development') ? false : true);
|
||||||
|
|
||||||
// parser
|
// parser
|
||||||
app.use(express.json({ limit: '100mb' }));
|
app.use(express.json({ limit: '100mb' }));
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ connectToDatabase(connectionUrl, options)
|
|||||||
|
|
||||||
const { MyEvent } = require('./models/myevent');
|
const { MyEvent } = require('./models/myevent');
|
||||||
|
|
||||||
|
app.use('/videos', express.static(path.join(__dirname, 'uploads/videos')));
|
||||||
|
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
|
|
||||||
@@ -1062,6 +1064,7 @@ connectToDatabase(connectionUrl, options)
|
|||||||
const NOCORS = false;
|
const NOCORS = false;
|
||||||
|
|
||||||
const { domains, domainsAllowed } = parseDomains();
|
const { domains, domainsAllowed } = parseDomains();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.log('domains:', domains);
|
console.log('domains:', domains);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const registerFonts = async () => {
|
|||||||
{ file: 'PlayfairDisplay-Regular.ttf', family: 'Playfair Display', weight: '400' }
|
{ file: 'PlayfairDisplay-Regular.ttf', family: 'Playfair Display', weight: '400' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
for (const font of fontMappings) {
|
for (const font of fontMappings) {
|
||||||
const fontPath = path.join(FONTS_DIR, font.file);
|
const fontPath = path.join(FONTS_DIR, font.file);
|
||||||
try {
|
try {
|
||||||
@@ -708,7 +709,7 @@ class PosterRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Path locale
|
// Path locale
|
||||||
if (url.startsWith('/uploads') || url.startsWith('./uploads')) {
|
if (url.startsWith('/upload') || url.startsWith('./upload')) {
|
||||||
const localPath = url.startsWith('/')
|
const localPath = url.startsWith('/')
|
||||||
? path.join(process.cwd(), url)
|
? path.join(process.cwd(), url)
|
||||||
: url;
|
: url;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.2.86
|
1.2.87
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user