- aggiornamento di tante cose...

- generazione Volantini
- pagina RIS
This commit is contained in:
Surya Paolo
2025-12-17 10:07:51 +01:00
parent 037ff6f7f9
commit 3d87c336de
43 changed files with 7123 additions and 518 deletions

View File

@@ -41,3 +41,7 @@ CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpY
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7" DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
OLLAMA_URL=http://localhost:11434 OLLAMA_URL=http://localhost:11434
OLLAMA_DEFAULT_MODEL=llama3.2:3b OLLAMA_DEFAULT_MODEL=llama3.2:3b
GROK_API="xai-PcNM5obgPaETtmnfDWPZk235D75ZgxENU2QmeqPfMQCHh9dwCDVeRRe0oVVA2YOpiUDh1uJieZsMasja"
REPLICATE_API_TOKEN="r8_AVhM6igwvoOnUA65cHVZdhEDfTqBVk94WTB0u"
FAL_KEY="7d251c88-21b5-4b55-8b3e-4bafd910f99f:b81c0a36a25b052f26eb8ac226c7efff"
HF_TOKEN="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"

View File

@@ -365,7 +365,7 @@ html(lang="it")
//- Intro //- Intro
.intro-text .intro-text
| Ciao <strong>#{usernameMembro}</strong>,<br> | Ciao <strong>#{usernameMembro}</strong>,<br>
| complimenti! Sei stato abilitato al Circuito RIS del tuo territorio da #{usernameInvitante}. | complimenti! Sei stato abilitato #{nomeTerritorio} da #{usernameInvitante}.
if linkProfiloAdmin if linkProfiloAdmin
.divider(style="margin: 16px 0;") .divider(style="margin: 16px 0;")
@@ -379,7 +379,7 @@ html(lang="it")
.congrats-icon ✅ .congrats-icon ✅
h3 Abilitazione Completata h3 Abilitazione Completata
p(style="font-size: 15px; color: #555; margin-top: 8px;") p(style="font-size: 15px; color: #555; margin-top: 8px;")
| Ora puoi utilizzare i RIS per i tuoi scambi nella comunità | Ora puoi utilizzare i #{symbol} per i tuoi scambi nella comunità
.territory-name 📍 #{nomeTerritorio} .territory-name 📍 #{nomeTerritorio}
//- Info comunità //- Info comunità
@@ -448,7 +448,7 @@ html(lang="it")
.step-number 1 .step-number 1
.step-content .step-content
h5 Esplora la Piattaforma h5 Esplora la Piattaforma
p Familiarizza con gli annunci, i membri e le funzionalità del Circuito RIS p Familiarizza con gli annunci, i membri e le funzionalità del #{nomeTerritorio}
.step-item .step-item
.step-number 2 .step-number 2
.step-content .step-content

View File

@@ -1 +1 @@
=`Richiesta ingresso di ${usernameMembro} - ${nomeMembro} ${cognomeMembro} su ${nomeTerritorio} in ${nomeapp}` =`Abilitazione avvenuta su ${nomeTerritorio} in ${nomeapp} - (${usernameMembro})`

View File

@@ -300,7 +300,7 @@ html(lang="it")
//- Intro //- Intro
.intro-text .intro-text
| Ciao <strong>#{nomeFacilitatore}</strong>,<br> | Ciao <strong>#{nomeFacilitatore}</strong>,<br>
| un nuovo membro richiede l'abilitazione alla fiducia al Circuito RIS del tuo territorio! | un nuovo membro richiede l'abilitazione alla fiducia al Circuito del tuo territorio!
//- Card richiesta //- Card richiesta
.request-card .request-card
@@ -384,12 +384,12 @@ html(lang="it")
span.responsibility-icon 👥 span.responsibility-icon 👥
span.responsibility-text span.responsibility-text
strong Integrazione: strong Integrazione:
| Supporta il nuovo membro nell'attivazione e utilizzo del Circuito RIS locale | Supporta il nuovo membro nell'attivazione e utilizzo del Circuito locale
//- Info box //- Info box
.info-box .info-box
p p
| ✓ Dopo l'abilitazione, #{usernameMembro} potrà accedere al Circuito RIS di #{nomeTerritorio} | ✓ Dopo l'abilitazione, #{usernameMembro} potrà accedere al #{nomeTerritorio}
p p
| ✓ Il membro riceverà una notifica automatica dell'avvenuta attivazione | ✓ Il membro riceverà una notifica automatica dell'avvenuta attivazione

View File

@@ -16,11 +16,13 @@
"author": "Surya", "author": "Surya",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fal-ai/client": "^1.7.2",
"axios": "^1.13.0", "axios": "^1.13.0",
"basic-ftp": "^5.0.5", "basic-ftp": "^5.0.5",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"canvas": "^3.2.0",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"compress-pdf": "^0.5.3", "compress-pdf": "^0.5.3",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
@@ -38,6 +40,7 @@
"formidable": "^3.5.2", "formidable": "^3.5.2",
"fs-extra": "^11.3.2", "fs-extra": "^11.3.2",
"ghostscript4js": "^3.2.3", "ghostscript4js": "^3.2.3",
"groq-sdk": "^0.37.0",
"helmet": "^8.1.0", "helmet": "^8.1.0",
"i18n": "^0.15.1", "i18n": "^0.15.1",
"image-downloader": "^4.3.0", "image-downloader": "^4.3.0",
@@ -59,7 +62,7 @@
"node-telegram-bot-api": "^0.66.0", "node-telegram-bot-api": "^0.66.0",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"npm-check-updates": "^17.1.15", "npm-check-updates": "^17.1.15",
"openai": "^4.86.2", "openai": "^4.104.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"pem": "^1.14.8", "pem": "^1.14.8",
@@ -67,6 +70,7 @@
"pug": "^3.0.3", "pug": "^3.0.3",
"puppeteer": "^24.9.0", "puppeteer": "^24.9.0",
"rate-limiter-flexible": "^5.0.5", "rate-limiter-flexible": "^5.0.5",
"replicate": "^1.4.0",
"request": "^2.88", "request": "^2.88",
"sanitize-html": "^2.14.0", "sanitize-html": "^2.14.0",
"save": "^2.9.0", "save": "^2.9.0",

View File

@@ -0,0 +1,398 @@
const Asset = require('../models/Asset');
const imageGenerator = require('../services/imageGenerator');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs').promises;
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
const assetController = {
// POST /assets/upload
async upload(req, res) {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: 'Nessun file caricato'
});
}
const { category = 'other', tags, description, isReusable = true } = req.body;
const file = req.file;
// Ottieni dimensioni immagine
let dimensions = {};
try {
const metadata = await sharp(file.path).metadata();
dimensions = { width: metadata.width, height: metadata.height };
} catch (e) {
console.warn('Cannot read image dimensions');
}
// Genera thumbnail
const thumbDir = path.join(UPLOAD_DIR, 'thumbs');
await fs.mkdir(thumbDir, { recursive: true });
const thumbName = `thumb_${file.filename}`;
const thumbPath = path.join(thumbDir, thumbName);
try {
await sharp(file.path)
.resize(300, 300, { fit: 'cover' })
.jpeg({ quality: 80 })
.toFile(thumbPath);
} catch (e) {
console.warn('Cannot create thumbnail');
}
const asset = new Asset({
type: 'image',
category,
sourceType: 'upload',
file: {
path: file.path,
url: `/uploads/${file.filename}`,
thumbnailPath: thumbPath,
thumbnailUrl: `/uploads/thumbs/${thumbName}`,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
dimensions
},
metadata: {
userId: req.user._id,
tags: tags ? tags.split(',').map(t => t.trim()) : [],
description,
isReusable: isReusable === 'true' || isReusable === true
}
});
await asset.save();
res.status(201).json({
success: true,
data: asset
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /assets/upload-multiple
async uploadMultiple(req, res) {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
error: 'Nessun file caricato'
});
}
const { category = 'other' } = req.body;
const assets = [];
for (const file of req.files) {
let dimensions = {};
try {
const metadata = await sharp(file.path).metadata();
dimensions = { width: metadata.width, height: metadata.height };
} catch (e) {}
const asset = new Asset({
type: 'image',
category,
sourceType: 'upload',
file: {
path: file.path,
url: `/uploads/${file.filename}`,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
dimensions
},
metadata: {
userId: req.user._id
}
});
await asset.save();
assets.push(asset);
}
res.status(201).json({
success: true,
data: assets
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /assets/generate-ai
async generateAi(req, res) {
try {
const {
prompt,
negativePrompt,
provider = 'hf',
category = 'other',
aspectRatio = '9:16',
model,
seed,
steps,
cfg
} = req.body;
if (!prompt) {
return res.status(400).json({
success: false,
error: 'Prompt richiesto'
});
}
const startTime = Date.now();
const imageUrl = await imageGenerator.generate(provider, prompt, {
negativePrompt,
aspectRatio,
model,
seed,
steps,
cfg
});
const generationTime = Date.now() - startTime;
// Salva file
const fileName = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.jpg`;
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
await fs.mkdir(path.dirname(filePath), { recursive: true });
let fileSize = 0;
let dimensions = {};
if (imageUrl.startsWith('data:')) {
const base64Data = imageUrl.replace(/^data:image\/\w+;base64,/, '');
const buffer = Buffer.from(base64Data, 'base64');
await fs.writeFile(filePath, buffer);
fileSize = buffer.length;
const metadata = await sharp(buffer).metadata();
dimensions = { width: metadata.width, height: metadata.height };
} else {
const fetch = require('node-fetch');
const response = await fetch(imageUrl);
const buffer = await response.buffer();
await fs.writeFile(filePath, buffer);
fileSize = buffer.length;
const metadata = await sharp(buffer).metadata();
dimensions = { width: metadata.width, height: metadata.height };
}
const asset = new Asset({
type: 'image',
category,
sourceType: 'ai',
file: {
path: filePath,
url: `/uploads/ai-generated/${fileName}`,
mimeType: 'image/jpeg',
size: fileSize,
dimensions
},
aiGeneration: {
prompt,
negativePrompt,
provider,
model,
seed,
steps,
cfg,
requestedSize: aspectRatio,
generationTime
},
metadata: {
userId: req.user._id,
isReusable: true
}
});
await asset.save();
res.status(201).json({
success: true,
data: asset
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /assets
async list(req, res) {
try {
const {
category,
sourceType,
page = 1,
limit = 50
} = req.query;
const query = {
'metadata.userId': req.user._id,
status: 'ready'
};
if (category) query.category = category;
if (sourceType) query.sourceType = sourceType;
const [assets, total] = await Promise.all([
Asset.find(query)
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(parseInt(limit)),
Asset.countDocuments(query)
]);
res.json({
success: true,
data: assets,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /assets/:id
async getById(req, res) {
try {
const asset = await Asset.findById(req.params.id);
if (!asset) {
return res.status(404).json({
success: false,
error: 'Asset non trovato'
});
}
res.json({
success: true,
data: asset
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /assets/:id/file
async getFile(req, res) {
try {
const asset = await Asset.findById(req.params.id);
if (!asset || !asset.file?.path) {
return res.status(404).json({
success: false,
error: 'File non trovato'
});
}
res.sendFile(path.resolve(asset.file.path));
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /assets/:id/thumbnail
async getThumbnail(req, res) {
try {
const asset = await Asset.findById(req.params.id);
if (!asset) {
return res.status(404).json({
success: false,
error: 'Asset non trovato'
});
}
const thumbPath = asset.file?.thumbnailPath || asset.file?.path;
if (!thumbPath) {
return res.status(404).json({
success: false,
error: 'Thumbnail non disponibile'
});
}
res.sendFile(path.resolve(thumbPath));
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// DELETE /assets/:id
async delete(req, res) {
try {
const asset = await Asset.findById(req.params.id);
if (!asset) {
return res.status(404).json({
success: false,
error: 'Asset non trovato'
});
}
if (asset.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
// Elimina file
try {
if (asset.file?.path) await fs.unlink(asset.file.path);
if (asset.file?.thumbnailPath) await fs.unlink(asset.file.thumbnailPath);
} catch (e) {
console.warn('File deletion warning:', e.message);
}
await Asset.deleteOne({ _id: asset._id });
res.json({
success: true,
message: 'Asset eliminato'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
};
module.exports = assetController;

View File

@@ -0,0 +1,647 @@
const Poster = require('../models/Poster');
const Template = require('../models/Template');
const Asset = require('../models/Asset');
const posterRenderer = require('../services/posterRenderer');
const imageGenerator = require('../services/imageGenerator');
const path = require('path');
const fs = require('fs').promises;
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
const posterController = {
// POST /posters
async create(req, res) {
try {
const {
templateId,
name,
description,
content,
assets,
layerOverrides,
autoRender = false
} = req.body;
// Carica template
const template = await Template.findById(templateId);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
// Valida contenuti richiesti
const requiredLayers = template.layers.filter(l => l.required);
for (const layer of requiredLayers) {
if (layer.type === 'title' && !content?.title) {
return res.status(400).json({
success: false,
error: `Campo richiesto: ${layer.type}`
});
}
}
const poster = new Poster({
templateId,
templateSnapshot: template.toObject(), // Snapshot per retrocompatibilità
name: name || content?.title || 'Nuova Locandina',
description,
status: 'draft',
content: content || {},
assets: assets || {},
layerOverrides: layerOverrides || {},
renderEngineVersion: '1.0.0',
metadata: {
userId: req.user._id
}
});
await poster.save();
// Incrementa uso template
await template.incrementUsage();
// Auto-render se richiesto
if (autoRender) {
await posterController._renderPoster(poster);
await poster.save();
}
res.status(201).json({
success: true,
data: poster
});
} catch (error) {
console.error('Poster create error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters
async list(req, res) {
try {
const {
status,
templateId,
search,
page = 1,
limit = 20,
sortBy = 'createdAt',
sortOrder = 'desc'
} = req.query;
const query = { 'metadata.userId': req.user._id };
if (status) query.status = status;
if (templateId) query.templateId = templateId;
if (search) query.$text = { $search: search };
const sort = { [sortBy]: sortOrder === 'asc' ? 1 : -1 };
const [posters, total] = await Promise.all([
Poster.find(query)
.populate('templateId', 'name templateType thumbnailUrl')
.sort(sort)
.skip((page - 1) * limit)
.limit(parseInt(limit))
.select('-templateSnapshot -history'),
Poster.countDocuments(query)
]);
res.json({
success: true,
data: posters,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters/favorites
async listFavorites(req, res) {
try {
const posters = await Poster.findFavorites(req.user._id);
res.json({
success: true,
data: posters
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters/recent
async listRecent(req, res) {
try {
const { limit = 10 } = req.query;
const posters = await Poster.findRecent(req.user._id, parseInt(limit));
res.json({
success: true,
data: posters
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters/:id
async getById(req, res) {
try {
const poster = await Poster.findById(req.params.id)
.populate('templateId')
.populate('assets.backgroundImage.assetId')
.populate('assets.mainImage.assetId')
.populate('assets.logos.assetId');
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
// Check ownership
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Accesso negato'
});
}
res.json({
success: true,
data: poster
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// PUT /posters/:id
async update(req, res) {
try {
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
const updateFields = [
'name', 'description', 'content', 'assets', 'layerOverrides'
];
updateFields.forEach(field => {
if (req.body[field] !== undefined) {
poster[field] = req.body[field];
}
});
// Invalida render precedente se contenuto modificato
if (req.body.content || req.body.assets || req.body.layerOverrides) {
poster.status = 'draft';
poster.addHistory('updated', { fields: Object.keys(req.body) });
}
await poster.save();
res.json({
success: true,
data: poster
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// DELETE /posters/:id
async delete(req, res) {
try {
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
// Elimina file renderizzati
if (poster.renderOutput) {
const filesToDelete = [
poster.renderOutput.png?.path,
poster.renderOutput.jpg?.path,
poster.renderOutput.webp?.path
].filter(Boolean);
for (const filePath of filesToDelete) {
try {
await fs.unlink(filePath);
} catch (e) {
console.warn('File not found:', filePath);
}
}
}
await Poster.deleteOne({ _id: poster._id });
res.json({
success: true,
message: 'Poster eliminato'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /posters/:id/render
async render(req, res) {
try {
const poster = await Poster.findById(req.params.id)
.populate('templateId');
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
poster.status = 'processing';
await poster.save();
try {
await posterController._renderPoster(poster);
await poster.save();
res.json({
success: true,
data: {
status: poster.status,
renderOutput: poster.renderOutput
}
});
} catch (renderError) {
poster.setError(renderError.message);
await poster.save();
throw renderError;
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /posters/:id/regenerate-ai
async regenerateAi(req, res) {
try {
const { assetType, prompt, provider = 'hf' } = req.body;
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
// Genera nuova immagine AI
const startTime = Date.now();
const imageUrl = await imageGenerator.generate(provider, prompt);
const generationTime = Date.now() - startTime;
// Salva su filesystem
const fileName = `${poster._id}_${assetType}_${Date.now()}.jpg`;
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
await fs.mkdir(path.dirname(filePath), { recursive: true });
// Se è base64, converti
let savedPath;
if (imageUrl.startsWith('data:')) {
const base64Data = imageUrl.replace(/^data:image\/\w+;base64,/, '');
await fs.writeFile(filePath, base64Data, 'base64');
savedPath = filePath;
} else {
// Se è URL, scarica
const fetch = require('node-fetch');
const response = await fetch(imageUrl);
const buffer = await response.buffer();
await fs.writeFile(filePath, buffer);
savedPath = filePath;
}
// Aggiorna asset nel poster
const assetData = {
sourceType: 'ai',
url: `/uploads/ai-generated/${fileName}`,
mimeType: 'image/jpeg',
aiParams: {
prompt,
provider,
generatedAt: new Date()
}
};
if (assetType === 'backgroundImage') {
poster.assets.backgroundImage = assetData;
poster.addHistory('ai_background_generated', { provider, duration: generationTime });
} else if (assetType === 'mainImage') {
poster.assets.mainImage = assetData;
poster.addHistory('ai_main_generated', { provider, duration: generationTime });
}
poster.status = 'draft';
await poster.save();
res.json({
success: true,
data: {
assetType,
asset: assetData
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters/:id/download/:format
async download(req, res) {
try {
const { format } = req.params;
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
const outputFile = poster.renderOutput?.[format];
if (!outputFile?.path) {
return res.status(404).json({
success: false,
error: `Formato ${format} non disponibile`
});
}
// Incrementa download count
await poster.incrementDownload();
const fileName = `${poster.name.replace(/[^a-z0-9]/gi, '_')}_poster.${format}`;
res.download(outputFile.path, fileName);
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /posters/:id/favorite
async toggleFavorite(req, res) {
try {
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
await poster.toggleFavorite();
res.json({
success: true,
data: {
isFavorite: poster.metadata.isFavorite
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /posters/quick-generate (compatibile con la tua bozza)
async quickGenerate(req, res) {
try {
const {
templateId,
titolo,
descrizione,
data,
ora,
luogo,
contatti,
fotoDescrizione,
stile,
provider = 'hf',
aspectRatio = '9:16'
} = req.body;
// Validazione base
if (!titolo || !data || !luogo) {
return res.status(400).json({
success: false,
error: 'Compila titolo, data e luogo'
});
}
// Usa template default o quello specificato
let template;
if (templateId) {
template = await Template.findById(templateId);
} else {
// Template default per quick-generate
template = await Template.findOne({
templateType: 'quick-generate',
isActive: true
});
}
// Genera prompt per AI background
const aiPrompt = `Vertical event poster background, ${stile || 'modern style, vivid colors'}. Subject: ${fotoDescrizione || 'abstract artistic shapes'}. Composition: Central empty space suitable for text overlay. NO TEXT, NO LETTERS, clean illustration, high quality, 4k.`;
// Genera immagine AI
const startTime = Date.now();
const rawImageUrl = await imageGenerator.generate(provider, aiPrompt);
const generationTime = Date.now() - startTime;
// Salva asset generato
const fileName = `quick_${Date.now()}.jpg`;
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
await fs.mkdir(path.dirname(filePath), { recursive: true });
if (rawImageUrl.startsWith('data:')) {
const base64Data = rawImageUrl.replace(/^data:image\/\w+;base64,/, '');
await fs.writeFile(filePath, base64Data, 'base64');
}
// Crea poster
const poster = new Poster({
templateId: template?._id,
name: titolo,
status: 'processing',
content: {
title: titolo,
subtitle: descrizione,
eventDate: data,
eventTime: ora,
location: luogo,
contacts: contatti
},
assets: {
backgroundImage: {
sourceType: 'ai',
url: `/uploads/ai-generated/${fileName}`,
mimeType: 'image/jpeg',
aiParams: {
prompt: aiPrompt,
provider,
generatedAt: new Date()
}
}
},
originalPrompt: aiPrompt,
styleUsed: stile,
aspectRatio,
provider,
metadata: {
userId: req.user._id
}
});
poster.addHistory('ai_background_generated', { provider, duration: generationTime });
// Render con testi sovrapposti
await posterController._renderPoster(poster, { useQuickRender: true });
await poster.save();
res.json({
success: true,
data: {
posterId: poster._id,
imageUrl: poster.renderOutput?.png?.url || rawImageUrl,
status: poster.status
}
});
} catch (error) {
console.error('Quick generate error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
},
// Helper interno: renderizza poster
async _renderPoster(poster, options = {}) {
const template = poster.templateId || poster.templateSnapshot;
const result = await posterRenderer.render({
template,
content: poster.content,
assets: poster.assets,
layerOverrides: Object.fromEntries(poster.layerOverrides || new Map()),
outputDir: path.join(UPLOAD_DIR, 'posters', 'final'),
posterId: poster._id.toString()
});
poster.setRenderOutput({
png: {
path: result.pngPath,
url: `/uploads/posters/final/${path.basename(result.pngPath)}`,
size: result.pngSize
},
jpg: {
path: result.jpgPath,
url: `/uploads/posters/final/${path.basename(result.jpgPath)}`,
size: result.jpgSize,
quality: 95
},
dimensions: result.dimensions,
duration: result.duration
});
}
};
module.exports = posterController;

View File

@@ -0,0 +1,383 @@
const Template = require('../models/Template');
// Presets formati standard
const FORMAT_PRESETS = {
'A4': { width: 2480, height: 3508, dpi: 300 },
'A4-landscape': { width: 3508, height: 2480, dpi: 300 },
'A3': { width: 3508, height: 4961, dpi: 300 },
'A3-landscape': { width: 4961, height: 3508, dpi: 300 },
'instagram-post': { width: 1080, height: 1080, dpi: 72 },
'instagram-story': { width: 1080, height: 1920, dpi: 72 },
'instagram-portrait': { width: 1080, height: 1350, dpi: 72 },
'facebook-post': { width: 1200, height: 630, dpi: 72 },
'facebook-event': { width: 1920, height: 1080, dpi: 72 },
'twitter-post': { width: 1200, height: 675, dpi: 72 },
'poster-24x36': { width: 7200, height: 10800, dpi: 300 },
'flyer-5x7': { width: 1500, height: 2100, dpi: 300 }
};
const templateController = {
// POST /templates
async create(req, res) {
try {
const {
name,
templateType,
description,
format,
safeArea,
backgroundColor,
layers,
logoSlots,
palette,
typography,
defaultAiPromptHints,
metadata
} = req.body;
// Applica preset se specificato
let finalFormat = format;
if (format?.preset && FORMAT_PRESETS[format.preset]) {
finalFormat = {
...FORMAT_PRESETS[format.preset],
preset: format.preset,
unit: 'px'
};
}
// Valida layers
if (!layers || !Array.isArray(layers) || layers.length === 0) {
return res.status(400).json({
success: false,
error: 'Almeno un layer è richiesto'
});
}
// Assicura ID unici per layer
const layersWithIds = layers.map((layer, idx) => ({
...layer,
id: layer.id || `layer_${layer.type}_${idx}`
}));
const template = new Template({
name,
templateType,
description,
format: finalFormat,
safeArea: safeArea || {},
backgroundColor: backgroundColor || '#1a1a2e',
layers: layersWithIds,
logoSlots: logoSlots || { enabled: false, slots: [] },
palette: palette || {},
typography: typography || {},
defaultAiPromptHints: defaultAiPromptHints || {},
metadata: {
...metadata,
author: req.user?.name || 'System'
},
userId: req.user?._id
});
await template.save();
res.status(201).json({
success: true,
data: template
});
} catch (error) {
console.error('Template create error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /templates
async list(req, res) {
try {
const {
type,
search,
tags,
page = 1,
limit = 20,
sortBy = 'createdAt',
sortOrder = 'desc'
} = req.query;
const query = { isActive: true };
if (type) {
query.templateType = type;
}
if (tags) {
const tagArray = tags.split(',').map(t => t.trim());
query['metadata.tags'] = { $in: tagArray };
}
if (search) {
query.$text = { $search: search };
}
// Se utente autenticato, mostra anche i suoi privati
if (req.user) {
query.$or = [
{ 'metadata.isPublic': true },
{ userId: req.user._id }
];
} else {
query['metadata.isPublic'] = true;
}
const sort = { [sortBy]: sortOrder === 'asc' ? 1 : -1 };
const [templates, total] = await Promise.all([
Template.find(query)
.sort(sort)
.skip((page - 1) * limit)
.limit(parseInt(limit))
.select('-layers -logoSlots'), // Escludi dati pesanti per list
Template.countDocuments(query)
]);
res.json({
success: true,
data: templates,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('Template list error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /templates/types
async getTypes(req, res) {
try {
const types = await Template.distinct('templateType', { isActive: true });
res.json({
success: true,
data: types.sort()
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /templates/presets
async getFormatPresets(req, res) {
res.json({
success: true,
data: FORMAT_PRESETS
});
},
// GET /templates/:id
async getById(req, res) {
try {
const template = await Template.findById(req.params.id);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
// Check accesso
if (!template.metadata.isPublic &&
(!req.user || template.userId?.toString() !== req.user._id.toString())) {
return res.status(403).json({
success: false,
error: 'Accesso negato'
});
}
res.json({
success: true,
data: template
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// PUT /templates/:id
async update(req, res) {
try {
const template = await Template.findById(req.params.id);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
// Check ownership
if (template.userId?.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato a modificare questo template'
});
}
const updateFields = [
'name', 'description', 'templateType', 'format', 'safeArea',
'backgroundColor', 'layers', 'logoSlots', 'palette',
'typography', 'defaultAiPromptHints', 'metadata', 'isActive'
];
updateFields.forEach(field => {
if (req.body[field] !== undefined) {
template[field] = req.body[field];
}
});
// Incrementa versione
if (template.metadata) {
const version = template.metadata.version || '1.0.0';
const parts = version.split('.').map(Number);
parts[2]++;
template.metadata.version = parts.join('.');
}
await template.save();
res.json({
success: true,
data: template
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// DELETE /templates/:id
async delete(req, res) {
try {
const template = await Template.findById(req.params.id);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
if (template.userId?.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
// Soft delete
template.isActive = false;
await template.save();
res.json({
success: true,
message: 'Template eliminato'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /templates/:id/duplicate
async duplicate(req, res) {
try {
const original = await Template.findById(req.params.id);
if (!original) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
const duplicateData = original.toObject();
delete duplicateData._id;
delete duplicateData.createdAt;
delete duplicateData.updatedAt;
duplicateData.name = `${original.name} (copia)`;
duplicateData.userId = req.user._id;
duplicateData.metadata = {
...duplicateData.metadata,
isPublic: false,
usageCount: 0,
author: req.user?.name || 'System',
version: '1.0.0'
};
const duplicate = new Template(duplicateData);
await duplicate.save();
res.status(201).json({
success: true,
data: duplicate
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /templates/:id/preview
async getPreview(req, res) {
try {
const template = await Template.findById(req.params.id)
.select('previewUrl thumbnailUrl name');
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
res.json({
success: true,
data: {
previewUrl: template.previewUrl,
thumbnailUrl: template.thumbnailUrl,
name: template.name
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
};
module.exports = templateController;

45
src/data/asset.json Normal file
View File

@@ -0,0 +1,45 @@
{
"_id": "asset_bg_001",
"type": "image",
"category": "background",
"sourceType": "ai",
"file": {
"path": "/uploads/assets/backgrounds/forest_autumn_001.jpg",
"url": "/api/assets/asset_bg_001/file",
"thumbnailPath": "/uploads/assets/backgrounds/thumbs/forest_autumn_001_thumb.jpg",
"thumbnailUrl": "/api/assets/asset_bg_001/thumbnail",
"originalName": null,
"mimeType": "image/jpeg",
"size": 2458000,
"dimensions": { "width": 2480, "height": 3508 }
},
"aiGeneration": {
"prompt": "Mystical autumn forest at golden hour...",
"negativePrompt": "text, letters, words...",
"provider": "hf",
"model": "FLUX.1-dev",
"seed": 8847291,
"steps": 35,
"cfg": 7.5,
"requestedSize": "1024x1536",
"actualSize": "1024x1536",
"generationTime": 12500,
"cost": 0
},
"usage": {
"usedInPosters": ["poster_sagra_funghi_2025_001"],
"usedInTemplates": [],
"usageCount": 1
},
"metadata": {
"userId": "user_001",
"tags": ["forest", "autumn", "background", "nature"],
"isReusable": true
},
"createdAt": "2025-01-15T10:25:00.000Z"
}

150
src/data/poster.json Normal file
View File

@@ -0,0 +1,150 @@
{
"_id": "poster_sagra_funghi_2025_001",
"templateId": "template_raccolta_funghi_001",
"name": "Sagra del Fungo Porcino 2025",
"status": "completed",
"content": {
"title": "SAGRA DEL FUNGO PORCINO",
"subtitle": "XXV Edizione - Tradizione e Sapori del Bosco",
"eventDate": "15-16-17 Ottobre 2025",
"eventTime": "10:00 - 23:00",
"location": "Parco delle Querce, Borgo Montano (PG)",
"contacts": "Tel: 0742 123456 | info@sagrafungoporcino.it | www.sagrafungoporcino.it",
"extraText": [
"Ingresso Libero",
"Stand Gastronomici • Musica dal Vivo • Mercatino Artigianale"
]
},
"assets": {
"backgroundImage": {
"id": "asset_bg_001",
"sourceType": "ai",
"url": "/uploads/posters/poster_sagra_2025_bg.jpg",
"thumbnailUrl": "/uploads/posters/thumbs/poster_sagra_2025_bg_thumb.jpg",
"mimeType": "image/jpeg",
"size": 2458000,
"dimensions": { "width": 2480, "height": 3508 },
"aiParams": {
"prompt": "Mystical autumn forest at golden hour, morning mist between ancient oak trees, forest floor covered with porcini mushrooms, warm orange and golden light filtering through leaves, photorealistic, cinematic composition, National Geographic style, 8k quality",
"negativePrompt": "text, letters, words, watermark, signature, blurry, low quality, cartoon, anime",
"provider": "hf",
"model": "FLUX.1-dev",
"seed": 8847291,
"steps": 35,
"cfg": 7.5,
"size": "1024x1536",
"generatedAt": "2025-01-15T10:25:00.000Z"
}
},
"mainImage": {
"id": "asset_main_001",
"sourceType": "upload",
"url": "/uploads/assets/porcini_basket_hero.jpg",
"thumbnailUrl": "/uploads/assets/thumbs/porcini_basket_hero_thumb.jpg",
"originalName": "IMG_20241015_porcini.jpg",
"mimeType": "image/jpeg",
"size": 1845000,
"dimensions": { "width": 1920, "height": 1280 },
"uploadedAt": "2025-01-15T10:20:00.000Z"
},
"logos": [
{
"id": "asset_logo_001",
"slotId": "logo_slot_1",
"sourceType": "upload",
"url": "/uploads/logos/comune_borgomontano.png",
"originalName": "logo_comune.png",
"mimeType": "image/png",
"size": 45000
},
{
"id": "asset_logo_002",
"slotId": "logo_slot_2",
"sourceType": "upload",
"url": "/uploads/logos/proloco_borgomontano.png",
"originalName": "logo_proloco.png",
"mimeType": "image/png",
"size": 38000
},
{
"id": "asset_logo_003",
"slotId": "logo_slot_3",
"sourceType": "ai",
"url": "/uploads/logos/ai_generated_mushroom_logo.png",
"mimeType": "image/png",
"size": 52000,
"aiParams": {
"prompt": "Minimal vector logo of a porcini mushroom, flat design, golden brown color, white background, simple elegant icon",
"provider": "ideogram",
"model": "ideogram-v2"
}
}
]
},
"layerOverrides": {
"layer_title": {
"style": {
"fontSize": 78,
"color": "#fff8e7"
}
},
"layer_event_date": {
"style": {
"color": "#ffa502"
}
}
},
"renderOutput": {
"png": {
"path": "/uploads/posters/final/poster_sagra_2025_final.png",
"size": 8945000,
"url": "/api/posters/poster_sagra_funghi_2025_001/download/png"
},
"jpg": {
"path": "/uploads/posters/final/poster_sagra_2025_final.jpg",
"quality": 95,
"size": 2145000,
"url": "/api/posters/poster_sagra_funghi_2025_001/download/jpg"
},
"dimensions": {
"width": 2480,
"height": 3508
},
"renderedAt": "2025-01-15T10:30:00.000Z"
},
"renderEngineVersion": "1.0.0",
"history": [
{
"action": "created",
"timestamp": "2025-01-15T10:15:00.000Z",
"userId": "user_001"
},
{
"action": "ai_background_generated",
"timestamp": "2025-01-15T10:25:00.000Z",
"details": { "provider": "hf", "duration": 12500 }
},
{
"action": "rendered",
"timestamp": "2025-01-15T10:30:00.000Z",
"details": { "duration": 3200 }
}
],
"metadata": {
"userId": "user_001",
"projectId": "project_eventi_2025",
"tags": ["sagra", "fungo", "autunno", "2025"],
"isPublic": false,
"isFavorite": true
},
"createdAt": "2025-01-15T10:15:00.000Z",
"updatedAt": "2025-01-15T10:30:00.000Z"
}

272
src/data/template.json Normal file
View File

@@ -0,0 +1,272 @@
{
"_id": "template_raccolta_funghi_001",
"name": "Raccolta Funghi Autunnale",
"templateType": "outdoor-event",
"description": "Template per eventi all'aperto legati alla natura",
"format": {
"preset": "A4",
"width": 2480,
"height": 3508,
"unit": "px",
"dpi": 300
},
"safeArea": {
"top": 0.04,
"right": 0.04,
"bottom": 0.04,
"left": 0.04
},
"backgroundColor": "#1a1a2e",
"layers": [
{
"id": "layer_bg",
"type": "backgroundImage",
"zIndex": 0,
"position": { "x": 0, "y": 0, "w": 1, "h": 1 },
"anchor": "top-left",
"required": false,
"fallback": {
"type": "gradient",
"direction": "to-bottom",
"colors": ["#2d3436", "#636e72"]
},
"style": {
"opacity": 1,
"blur": 0,
"objectFit": "cover",
"overlay": {
"enabled": true,
"type": "gradient",
"direction": "to-bottom",
"stops": [
{ "position": 0, "color": "rgba(0,0,0,0)" },
{ "position": 0.5, "color": "rgba(0,0,0,0.3)" },
{ "position": 1, "color": "rgba(0,0,0,0.85)" }
]
}
}
},
{
"id": "layer_main_image",
"type": "mainImage",
"zIndex": 1,
"position": { "x": 0.5, "y": 0.28, "w": 0.85, "h": 0.38 },
"anchor": "center",
"required": false,
"style": {
"borderRadius": 24,
"objectFit": "cover",
"shadow": {
"enabled": true,
"blur": 40,
"spread": 0,
"offsetX": 0,
"offsetY": 20,
"color": "rgba(0,0,0,0.6)"
},
"border": {
"enabled": false,
"width": 4,
"color": "#ffffff"
}
}
},
{
"id": "layer_title",
"type": "title",
"zIndex": 10,
"position": { "x": 0.5, "y": 0.54, "w": 0.92, "h": 0.12 },
"anchor": "center",
"required": true,
"maxLines": 2,
"style": {
"fontFamily": "Montserrat",
"fontWeight": 900,
"fontSize": 82,
"fontSizeMin": 48,
"fontSizeMax": 120,
"autoFit": true,
"color": "#ffffff",
"textAlign": "center",
"textTransform": "uppercase",
"letterSpacing": 6,
"lineHeight": 1.05,
"shadow": {
"enabled": true,
"blur": 15,
"offsetX": 3,
"offsetY": 3,
"color": "rgba(0,0,0,0.9)"
},
"stroke": {
"enabled": true,
"width": 3,
"color": "rgba(0,0,0,0.5)"
}
}
},
{
"id": "layer_subtitle",
"type": "subtitle",
"zIndex": 10,
"position": { "x": 0.5, "y": 0.635, "w": 0.85, "h": 0.05 },
"anchor": "center",
"required": false,
"style": {
"fontFamily": "Open Sans",
"fontWeight": 400,
"fontSize": 32,
"color": "#f0f0f0",
"textAlign": "center",
"letterSpacing": 2,
"lineHeight": 1.3,
"shadow": {
"enabled": true,
"blur": 8,
"offsetX": 1,
"offsetY": 1,
"color": "rgba(0,0,0,0.7)"
}
}
},
{
"id": "layer_event_date",
"type": "eventDate",
"zIndex": 10,
"position": { "x": 0.5, "y": 0.72, "w": 0.9, "h": 0.06 },
"anchor": "center",
"required": true,
"style": {
"fontFamily": "Bebas Neue",
"fontWeight": 400,
"fontSize": 56,
"color": "#ffd700",
"textAlign": "center",
"letterSpacing": 4,
"textTransform": "uppercase",
"shadow": {
"enabled": true,
"blur": 10,
"offsetX": 2,
"offsetY": 2,
"color": "rgba(0,0,0,0.8)"
}
}
},
{
"id": "layer_location",
"type": "location",
"zIndex": 10,
"position": { "x": 0.5, "y": 0.79, "w": 0.85, "h": 0.05 },
"anchor": "center",
"required": true,
"icon": {
"enabled": true,
"name": "location_on",
"size": 28,
"color": "#e74c3c"
},
"style": {
"fontFamily": "Open Sans",
"fontWeight": 600,
"fontSize": 28,
"color": "#ffffff",
"textAlign": "center",
"letterSpacing": 1
}
},
{
"id": "layer_contacts",
"type": "contacts",
"zIndex": 10,
"position": { "x": 0.5, "y": 0.86, "w": 0.9, "h": 0.04 },
"anchor": "center",
"required": false,
"style": {
"fontFamily": "Open Sans",
"fontWeight": 400,
"fontSize": 22,
"color": "#cccccc",
"textAlign": "center",
"letterSpacing": 0.5
}
},
{
"id": "layer_extra_text",
"type": "extraText",
"zIndex": 10,
"position": { "x": 0.5, "y": 0.91, "w": 0.85, "h": 0.03 },
"anchor": "center",
"required": false,
"style": {
"fontFamily": "Open Sans",
"fontWeight": 300,
"fontSize": 18,
"fontStyle": "italic",
"color": "#aaaaaa",
"textAlign": "center"
}
}
],
"logoSlots": {
"enabled": true,
"maxCount": 3,
"collapseIfEmpty": true,
"slots": [
{
"id": "logo_slot_1",
"position": { "x": 0.12, "y": 0.96, "w": 0.12, "h": 0.05 },
"anchor": "bottom-left",
"style": { "objectFit": "contain", "opacity": 0.9 }
},
{
"id": "logo_slot_2",
"position": { "x": 0.5, "y": 0.96, "w": 0.12, "h": 0.05 },
"anchor": "bottom-center",
"style": { "objectFit": "contain", "opacity": 0.9 }
},
{
"id": "logo_slot_3",
"position": { "x": 0.88, "y": 0.96, "w": 0.12, "h": 0.05 },
"anchor": "bottom-right",
"style": { "objectFit": "contain", "opacity": 0.9 }
}
]
},
"palette": {
"primary": "#e94560",
"secondary": "#0f3460",
"accent": "#ffd700",
"background": "#1a1a2e",
"text": "#ffffff",
"textSecondary": "#cccccc",
"textMuted": "#888888"
},
"typography": {
"titleFont": "Montserrat",
"headingFont": "Bebas Neue",
"bodyFont": "Open Sans",
"accentFont": "Playfair Display"
},
"defaultAiPromptHints": {
"backgroundImage": "atmospheric outdoor scene, nature, forest, autumn colors, cinematic lighting, no text, no letters",
"mainImage": "detailed illustration, high quality, vibrant colors, no text"
},
"metadata": {
"author": "System",
"version": "1.0.0",
"tags": ["natura", "outdoor", "autunno", "sagra"]
},
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}

42
src/middleware/upload.js Normal file
View File

@@ -0,0 +1,42 @@
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, UPLOAD_DIR);
},
filename: (req, file, cb) => {
const uniqueId = crypto.randomBytes(8).toString('hex');
const ext = path.extname(file.originalname);
cb(null, `${Date.now()}_${uniqueId}${ext}`);
}
});
const fileFilter = (req, file, cb) => {
const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/svg+xml'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Tipo file non supportato'), false);
}
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 20 * 1024 * 1024 // 20MB max
}
});
module.exports = upload;

137
src/models/Asset.js Normal file
View File

@@ -0,0 +1,137 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Sub-schema: File Info
const FileInfoSchema = new Schema({
path: { type: String, required: true },
url: { type: String },
thumbnailPath: { type: String },
thumbnailUrl: { type: String },
originalName: { type: String },
mimeType: { type: String, required: true },
size: { type: Number }, // bytes
dimensions: {
width: { type: Number },
height: { type: Number }
}
}, { _id: false });
// Sub-schema: AI Generation Params
const AiGenerationSchema = new Schema({
prompt: { type: String, required: true },
negativePrompt: { type: String },
provider: {
type: String,
required: true,
enum: ['hf', 'fal', 'ideogram', 'openai', 'stability', 'midjourney']
},
model: { type: String },
seed: { type: Number },
steps: { type: Number },
cfg: { type: Number },
requestedSize: { type: String },
actualSize: { type: String },
aspectRatio: { type: String },
styleType: { type: String },
generationTime: { type: Number }, // ms
cost: { type: Number, default: 0 },
rawResponse: { type: Schema.Types.Mixed }
}, { _id: false });
// Sub-schema: Usage Tracking
const UsageTrackingSchema = new Schema({
usedInPosters: [{ type: Schema.Types.ObjectId, ref: 'Poster' }],
usedInTemplates: [{ type: Schema.Types.ObjectId, ref: 'Template' }],
usageCount: { type: Number, default: 0 }
}, { _id: false });
// Sub-schema: Asset Metadata
const AssetMetadataSchema = new Schema({
userId: { type: Schema.Types.ObjectId, ref: 'User', index: true },
projectId: { type: Schema.Types.ObjectId, ref: 'Project' },
tags: [{ type: String }],
description: { type: String },
isReusable: { type: Boolean, default: true },
isPublic: { type: Boolean, default: false }
}, { _id: false });
// MAIN SCHEMA: Asset
const AssetSchema = new Schema({
type: {
type: String,
required: true,
enum: ['image', 'logo', 'icon', 'font']
},
category: {
type: String,
required: true,
enum: ['background', 'main', 'logo', 'decoration', 'overlay', 'other'],
index: true
},
sourceType: {
type: String,
required: true,
enum: ['upload', 'ai', 'library', 'url'],
index: true
},
file: { type: FileInfoSchema, required: true },
aiGeneration: { type: AiGenerationSchema },
usage: { type: UsageTrackingSchema, default: () => ({}) },
metadata: { type: AssetMetadataSchema, default: () => ({}) },
status: {
type: String,
enum: ['processing', 'ready', 'error', 'deleted'],
default: 'ready'
},
errorMessage: { type: String }
}, {
timestamps: true,
toJSON: { virtuals: true }
});
// Indexes
AssetSchema.index({ 'metadata.userId': 1, category: 1 });
AssetSchema.index({ 'metadata.tags': 1 });
AssetSchema.index({ sourceType: 1, status: 1 });
// Virtual: isAiGenerated
AssetSchema.virtual('isAiGenerated').get(function() {
return this.sourceType === 'ai';
});
// Methods
AssetSchema.methods.addUsage = async function(posterId, type = 'poster') {
if (type === 'poster' && !this.usage.usedInPosters.includes(posterId)) {
this.usage.usedInPosters.push(posterId);
} else if (type === 'template' && !this.usage.usedInTemplates.includes(posterId)) {
this.usage.usedInTemplates.push(posterId);
}
this.usage.usageCount = this.usage.usedInPosters.length + this.usage.usedInTemplates.length;
return this.save();
};
AssetSchema.methods.getPublicUrl = function() {
return this.file.url || `/api/assets/${this._id}/file`;
};
// Statics
AssetSchema.statics.findByUser = function(userId, category = null) {
const query = { 'metadata.userId': userId, status: 'ready' };
if (category) query.category = category;
return this.find(query).sort({ createdAt: -1 });
};
AssetSchema.statics.findReusable = function(userId, category = null) {
const query = {
'metadata.userId': userId,
'metadata.isReusable': true,
status: 'ready'
};
if (category) query.category = category;
return this.find(query).sort({ 'usage.usageCount': -1 });
};
module.exports = mongoose.model('Asset', AssetSchema);

262
src/models/Poster.js Normal file
View File

@@ -0,0 +1,262 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Sub-schema: Content
const PosterContentSchema = new Schema({
title: { type: String, maxlength: 500 },
subtitle: { type: String, maxlength: 500 },
eventDate: { type: String, maxlength: 200 },
eventTime: { type: String, maxlength: 100 },
location: { type: String, maxlength: 500 },
contacts: { type: String, maxlength: 1000 },
extraText: [{ type: String }],
customFields: { type: Map, of: String }
}, { _id: false });
// Sub-schema: Asset AI Params (embedded)
const EmbeddedAiParamsSchema = new Schema({
prompt: { type: String },
negativePrompt: { type: String },
provider: { type: String },
model: { type: String },
seed: { type: Number },
steps: { type: Number },
cfg: { type: Number },
size: { type: String },
generatedAt: { type: Date }
}, { _id: false });
// Sub-schema: Poster Asset Reference
const PosterAssetSchema = new Schema({
id: { type: String },
assetId: { type: Schema.Types.ObjectId, ref: 'Asset' },
slotId: { type: String }, // per loghi
sourceType: { type: String, enum: ['upload', 'ai', 'library', 'url'] },
url: { type: String },
thumbnailUrl: { type: String },
originalName: { type: String },
mimeType: { type: String },
size: { type: Number },
dimensions: {
width: { type: Number },
height: { type: Number }
},
aiParams: EmbeddedAiParamsSchema
}, { _id: false });
// Sub-schema: Assets Container
const PosterAssetsSchema = new Schema({
backgroundImage: PosterAssetSchema,
mainImage: PosterAssetSchema,
logos: [PosterAssetSchema]
}, { _id: false });
// Sub-schema: Layer Override Style
const LayerOverrideStyleSchema = new Schema({
fontSize: { type: Number },
color: { type: String },
fontWeight: { type: Number },
opacity: { type: Number },
// altri override possibili
}, { _id: false });
// Sub-schema: Layer Override
const LayerOverrideSchema = new Schema({
position: {
x: { type: Number },
y: { type: Number },
w: { type: Number },
h: { type: Number }
},
visible: { type: Boolean },
style: LayerOverrideStyleSchema
}, { _id: false });
// Sub-schema: Render Output File
const RenderOutputFileSchema = new Schema({
path: { type: String, required: true },
url: { type: String },
size: { type: Number },
quality: { type: Number }
}, { _id: false });
// Sub-schema: Render Output
const RenderOutputSchema = new Schema({
png: RenderOutputFileSchema,
jpg: RenderOutputFileSchema,
webp: RenderOutputFileSchema,
pdf: RenderOutputFileSchema,
dimensions: {
width: { type: Number },
height: { type: Number }
},
renderedAt: { type: Date }
}, { _id: false });
// Sub-schema: History Entry
const HistoryEntrySchema = new Schema({
action: {
type: String,
required: true,
enum: ['created', 'updated', 'ai_background_generated', 'ai_main_generated', 'rendered', 'downloaded', 'shared', 'deleted']
},
timestamp: { type: Date, default: Date.now },
userId: { type: Schema.Types.ObjectId, ref: 'User' },
details: { type: Schema.Types.Mixed }
}, { _id: false });
// Sub-schema: Poster Metadata
const PosterMetadataSchema = new Schema({
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true },
projectId: { type: Schema.Types.ObjectId, ref: 'Project' },
tags: [{ type: String }],
isPublic: { type: Boolean, default: false },
isFavorite: { type: Boolean, default: false },
viewCount: { type: Number, default: 0 },
downloadCount: { type: Number, default: 0 }
}, { _id: false });
// MAIN SCHEMA: Poster
const PosterSchema = new Schema({
templateId: {
type: Schema.Types.ObjectId,
ref: 'Template',
required: true,
index: true
},
templateSnapshot: { type: Schema.Types.Mixed }, // copia del template al momento della creazione
name: { type: String, required: true, trim: true, maxlength: 300 },
description: { type: String, maxlength: 1000 },
status: {
type: String,
enum: ['draft', 'processing', 'completed', 'error'],
default: 'draft',
index: true
},
content: { type: PosterContentSchema, required: true },
assets: { type: PosterAssetsSchema, default: () => ({}) },
layerOverrides: { type: Map, of: LayerOverrideSchema, default: () => new Map() },
renderOutput: RenderOutputSchema,
renderEngineVersion: { type: String, default: '1.0.0' },
history: [HistoryEntrySchema],
metadata: { type: PosterMetadataSchema, required: true },
errorMessage: { type: String },
// Campi dalla tua bozza originale
originalPrompt: { type: String }, // prompt completo usato
styleUsed: { type: String },
aspectRatio: { type: String },
provider: { type: String }
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Indexes
PosterSchema.index({ 'metadata.userId': 1, status: 1 });
PosterSchema.index({ 'metadata.tags': 1 });
PosterSchema.index({ 'metadata.isFavorite': 1, 'metadata.userId': 1 });
PosterSchema.index({ createdAt: -1 });
PosterSchema.index({ name: 'text', description: 'text' });
// Virtual: isCompleted
PosterSchema.virtual('isCompleted').get(function() {
return this.status === 'completed' && this.renderOutput?.png?.path;
});
// Virtual: downloadUrl
PosterSchema.virtual('downloadUrl').get(function() {
if (this.renderOutput?.png?.path) {
return `/api/posters/${this._id}/download/png`;
}
return null;
});
// Pre-save: aggiorna history
PosterSchema.pre('save', function(next) {
if (this.isNew) {
this.history = this.history || [];
this.history.push({
action: 'created',
timestamp: new Date(),
userId: this.metadata.userId
});
}
next();
});
// Methods
PosterSchema.methods.addHistory = function(action, details = {}) {
this.history.push({
action,
timestamp: new Date(),
userId: this.metadata.userId,
details
});
return this;
};
PosterSchema.methods.setRenderOutput = function(outputData) {
this.renderOutput = {
...outputData,
renderedAt: new Date()
};
this.status = 'completed';
this.addHistory('rendered', { duration: outputData.duration });
return this;
};
PosterSchema.methods.setError = function(errorMessage) {
this.status = 'error';
this.errorMessage = errorMessage;
return this;
};
PosterSchema.methods.incrementDownload = async function() {
this.metadata.downloadCount = (this.metadata.downloadCount || 0) + 1;
this.addHistory('downloaded');
return this.save();
};
PosterSchema.methods.toggleFavorite = async function() {
this.metadata.isFavorite = !this.metadata.isFavorite;
return this.save();
};
// Statics
PosterSchema.statics.findByUser = function(userId, options = {}) {
const query = { 'metadata.userId': userId };
if (options.status) query.status = options.status;
if (options.isFavorite) query['metadata.isFavorite'] = true;
return this.find(query)
.populate('templateId', 'name templateType thumbnailUrl')
.sort({ createdAt: -1 })
.limit(options.limit || 50);
};
PosterSchema.statics.findFavorites = function(userId) {
return this.find({
'metadata.userId': userId,
'metadata.isFavorite': true
}).sort({ updatedAt: -1 });
};
PosterSchema.statics.findRecent = function(userId, limit = 10) {
return this.find({
'metadata.userId': userId,
status: 'completed'
})
.sort({ createdAt: -1 })
.limit(limit)
.select('name renderOutput.png.url thumbnailUrl createdAt');
};
module.exports = mongoose.model('Poster', PosterSchema);

253
src/models/Template.js Normal file
View File

@@ -0,0 +1,253 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Sub-schema: Posizione layer
const PositionSchema = new Schema({
x: { type: Number, required: true, min: 0, max: 1 },
y: { type: Number, required: true, min: 0, max: 1 },
w: { type: Number, required: true, min: 0, max: 1 },
h: { type: Number, required: true, min: 0, max: 1 }
}, { _id: false });
// Sub-schema: Ombra
const ShadowSchema = new Schema({
enabled: { type: Boolean, default: false },
blur: { type: Number, default: 10 },
spread: { type: Number, default: 0 },
offsetX: { type: Number, default: 0 },
offsetY: { type: Number, default: 4 },
color: { type: String, default: 'rgba(0,0,0,0.5)' }
}, { _id: false });
// Sub-schema: Stroke
const StrokeSchema = new Schema({
enabled: { type: Boolean, default: false },
width: { type: Number, default: 2 },
color: { type: String, default: '#000000' }
}, { _id: false });
// Sub-schema: Border
const BorderSchema = new Schema({
enabled: { type: Boolean, default: false },
width: { type: Number, default: 2 },
color: { type: String, default: '#ffffff' },
style: { type: String, enum: ['solid', 'dashed', 'dotted'], default: 'solid' }
}, { _id: false });
// Sub-schema: Gradient Stop
const GradientStopSchema = new Schema({
position: { type: Number, required: true, min: 0, max: 1 },
color: { type: String, required: true }
}, { _id: false });
// Sub-schema: Overlay
const OverlaySchema = new Schema({
enabled: { type: Boolean, default: false },
type: { type: String, enum: ['solid', 'gradient'], default: 'gradient' },
color: { type: String },
direction: { type: String, default: 'to-bottom' },
stops: [GradientStopSchema]
}, { _id: false });
// Sub-schema: Fallback
const FallbackSchema = new Schema({
type: { type: String, enum: ['solid', 'gradient'], default: 'solid' },
color: { type: String },
direction: { type: String },
colors: [{ type: String }]
}, { _id: false });
// Sub-schema: Icon
const IconSchema = new Schema({
enabled: { type: Boolean, default: false },
name: { type: String },
size: { type: Number, default: 24 },
color: { type: String, default: '#ffffff' }
}, { _id: false });
// Sub-schema: Stile Layer (unificato per immagini e testi)
const LayerStyleSchema = new Schema({
// Comuni
opacity: { type: Number, default: 1, min: 0, max: 1 },
// Per immagini
objectFit: { type: String, enum: ['cover', 'contain', 'fill', 'none'], default: 'cover' },
blur: { type: Number, default: 0 },
borderRadius: { type: Number, default: 0 },
overlay: OverlaySchema,
border: BorderSchema,
// Per testi
fontFamily: { type: String },
fontWeight: { type: Number, default: 400 },
fontSize: { type: Number },
fontSizeMin: { type: Number },
fontSizeMax: { type: Number },
autoFit: { type: Boolean, default: false },
fontStyle: { type: String, enum: ['normal', 'italic'], default: 'normal' },
color: { type: String },
textAlign: { type: String, enum: ['left', 'center', 'right'], default: 'center' },
textTransform: { type: String, enum: ['none', 'uppercase', 'lowercase', 'capitalize'], default: 'none' },
letterSpacing: { type: Number, default: 0 },
lineHeight: { type: Number, default: 1.2 },
// Effetti
shadow: ShadowSchema,
stroke: StrokeSchema
}, { _id: false });
// Sub-schema: Layer
const LayerSchema = new Schema({
id: { type: String, required: true },
type: {
type: String,
required: true,
enum: ['backgroundImage', 'mainImage', 'logo', 'title', 'subtitle', 'eventDate', 'eventTime', 'location', 'contacts', 'extraText', 'customText', 'customImage', 'shape', 'divider']
},
zIndex: { type: Number, default: 0 },
position: { type: PositionSchema, required: true },
anchor: {
type: String,
enum: ['top-left', 'top-center', 'top-right', 'center-left', 'center', 'center-right', 'bottom-left', 'bottom-center', 'bottom-right'],
default: 'center'
},
required: { type: Boolean, default: false },
visible: { type: Boolean, default: true },
locked: { type: Boolean, default: false },
maxLines: { type: Number },
fallback: FallbackSchema,
icon: IconSchema,
style: { type: LayerStyleSchema, default: () => ({}) }
}, { _id: false });
// Sub-schema: Logo Slot
const LogoSlotSchema = new Schema({
id: { type: String, required: true },
position: { type: PositionSchema, required: true },
anchor: { type: String, default: 'center' },
style: { type: LayerStyleSchema, default: () => ({}) }
}, { _id: false });
// Sub-schema: Logo Slots Config
const LogoSlotsConfigSchema = new Schema({
enabled: { type: Boolean, default: true },
maxCount: { type: Number, default: 3, min: 1, max: 10 },
collapseIfEmpty: { type: Boolean, default: true },
slots: [LogoSlotSchema]
}, { _id: false });
// Sub-schema: Format
const FormatSchema = new Schema({
preset: { type: String, default: 'custom' }, // A4, A3, Instagram, Facebook, custom
width: { type: Number, required: true },
height: { type: Number, required: true },
unit: { type: String, enum: ['px', 'mm', 'in'], default: 'px' },
dpi: { type: Number, default: 300 }
}, { _id: false });
// Sub-schema: Safe Area
const SafeAreaSchema = new Schema({
top: { type: Number, default: 0, min: 0, max: 0.5 },
right: { type: Number, default: 0, min: 0, max: 0.5 },
bottom: { type: Number, default: 0, min: 0, max: 0.5 },
left: { type: Number, default: 0, min: 0, max: 0.5 }
}, { _id: false });
// Sub-schema: Palette
const PaletteSchema = new Schema({
primary: { type: String, default: '#e94560' },
secondary: { type: String, default: '#0f3460' },
accent: { type: String, default: '#ffd700' },
background: { type: String, default: '#1a1a2e' },
text: { type: String, default: '#ffffff' },
textSecondary: { type: String, default: '#cccccc' },
textMuted: { type: String, default: '#888888' }
}, { _id: false });
// Sub-schema: Typography
const TypographySchema = new Schema({
titleFont: { type: String, default: 'Montserrat' },
headingFont: { type: String, default: 'Bebas Neue' },
bodyFont: { type: String, default: 'Open Sans' },
accentFont: { type: String, default: 'Playfair Display' }
}, { _id: false });
// Sub-schema: AI Prompt Hints
const AiPromptHintsSchema = new Schema({
backgroundImage: { type: String },
mainImage: { type: String }
}, { _id: false });
// Sub-schema: Metadata
const TemplateMetadataSchema = new Schema({
author: { type: String, default: 'System' },
version: { type: String, default: '1.0.0' },
tags: [{ type: String }],
isPublic: { type: Boolean, default: false },
usageCount: { type: Number, default: 0 }
}, { _id: false });
// MAIN SCHEMA: Template
const TemplateSchema = new Schema({
name: { type: String, required: true, trim: true, maxlength: 200 },
templateType: { type: String, required: true, trim: true, index: true },
description: { type: String, maxlength: 1000 },
format: { type: FormatSchema, required: true },
safeArea: { type: SafeAreaSchema, default: () => ({}) },
backgroundColor: { type: String, default: '#1a1a2e' },
layers: { type: [LayerSchema], required: true, validate: [arr => arr.length > 0, 'Almeno un layer richiesto'] },
logoSlots: { type: LogoSlotsConfigSchema, default: () => ({}) },
palette: { type: PaletteSchema, default: () => ({}) },
typography: { type: TypographySchema, default: () => ({}) },
defaultAiPromptHints: { type: AiPromptHintsSchema, default: () => ({}) },
previewUrl: { type: String },
thumbnailUrl: { type: String },
metadata: { type: TemplateMetadataSchema, default: () => ({}) },
isActive: { type: Boolean, default: true },
userId: { type: Schema.Types.ObjectId, ref: 'User', index: true }
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Indexes
TemplateSchema.index({ templateType: 1, isActive: 1 });
TemplateSchema.index({ 'metadata.tags': 1 });
TemplateSchema.index({ name: 'text', description: 'text', templateType: 'text' });
// Virtual: layer count
TemplateSchema.virtual('layerCount').get(function() {
return this.layers ? this.layers.length : 0;
});
// Methods
TemplateSchema.methods.getLayerById = function(layerId) {
return this.layers.find(l => l.id === layerId);
};
TemplateSchema.methods.getLayersByType = function(type) {
return this.layers.filter(l => l.type === type);
};
TemplateSchema.methods.incrementUsage = async function() {
this.metadata.usageCount = (this.metadata.usageCount || 0) + 1;
return this.save();
};
// Statics
TemplateSchema.statics.findByType = function(templateType) {
return this.find({ templateType, isActive: true }).sort({ 'metadata.usageCount': -1 });
};
TemplateSchema.statics.findPublic = function() {
return this.find({ 'metadata.isPublic': true, isActive: true });
};
module.exports = mongoose.model('Template', TemplateSchema);

View File

@@ -32,6 +32,12 @@ const AccountSchema = new Schema({
numtransactions: { numtransactions: {
type: Number, type: Number,
}, },
sent: {
type: Number,
},
received: {
type: Number,
},
username: { username: {
type: String, type: String,
}, },
@@ -242,6 +248,12 @@ AccountSchema.statics.addtoSaldo = async function (myaccount, amount, mitt) {
myaccountupdate.saldo = myaccount.saldo; myaccountupdate.saldo = myaccount.saldo;
myaccountupdate.totTransato = myaccount.totTransato; myaccountupdate.totTransato = myaccount.totTransato;
myaccountupdate.numtransactions = myaccount.numtransactions; myaccountupdate.numtransactions = myaccount.numtransactions;
if (amount > 0) {
myaccountupdate.received += 1;
} else {
myaccountupdate.sent += 1;
}
myaccountupdate.date_updated = myaccount.date_updated; myaccountupdate.date_updated = myaccount.date_updated;
const ris = await Account.updateOne( const ris = await Account.updateOne(
@@ -324,6 +336,8 @@ AccountSchema.statics.getAccountByUsernameAndCircuitId = async function (
username_admin_abilitante: '', username_admin_abilitante: '',
qta_maxConcessa: 0, qta_maxConcessa: 0,
totTransato: 0, totTransato: 0,
sent: 0,
received: 0,
numtransactions: 0, numtransactions: 0,
totTransato_pend: 0, totTransato_pend: 0,
}); });

View File

@@ -87,6 +87,9 @@ const CircuitSchema = new Schema({
totTransato: { totTransato: {
type: Number, type: Number,
}, },
numTransazioni: {
type: Number,
},
nome_valuta: { nome_valuta: {
type: String, type: String,
maxlength: 20, maxlength: 20,
@@ -327,6 +330,7 @@ CircuitSchema.statics.getWhatToShow = function (idapp, username) {
numMembers: 1, numMembers: 1,
totCircolante: 1, totCircolante: 1,
totTransato: 1, totTransato: 1,
numTransazioni: 1,
systemUserId: 1, systemUserId: 1,
createdBy: 1, createdBy: 1,
date_created: 1, date_created: 1,
@@ -412,6 +416,7 @@ CircuitSchema.statics.getWhatToShow_Unknown = function (idapp, username) {
nome_valuta: 1, nome_valuta: 1,
totCircolante: 1, totCircolante: 1,
totTransato: 1, totTransato: 1,
numTransazioni: 1,
fido_scoperto_default: 1, fido_scoperto_default: 1,
fido_scoperto_default_grp: 1, fido_scoperto_default_grp: 1,
qta_max_default_grp: 1, qta_max_default_grp: 1,
@@ -825,6 +830,7 @@ CircuitSchema.statics.sendCoins = async function (onlycheck, idapp, usernameOrig
const circolanteAtt = this.getCircolanteSingolaTransaz(accountorigTable, accountdestTable); const circolanteAtt = this.getCircolanteSingolaTransaz(accountorigTable, accountdestTable);
// Somma di tutte le transazioni // Somma di tutte le transazioni
circuittable.numTransazioni += 1;
circuittable.totTransato += myqty; circuittable.totTransato += myqty;
// circuittable.totCircolante = circuittable.totCircolante + (circolanteAtt - circolantePrec); // circuittable.totCircolante = circuittable.totCircolante + (circolanteAtt - circolantePrec);
circuittable.totCircolante = await Account.calcTotCircolante(idapp, circuittable._id); circuittable.totCircolante = await Account.calcTotCircolante(idapp, circuittable._id);
@@ -901,7 +907,14 @@ CircuitSchema.statics.sendCoins = async function (onlycheck, idapp, usernameOrig
let myuserDest = await User.getUserByUsername(idapp, extrarec.dest); let myuserDest = await User.getUserByUsername(idapp, extrarec.dest);
// Invia una email al destinatario ! // Invia una email al destinatario !
await sendemail.sendEmail_RisRicevuti(myuserDest.lang, myuserDest, myuserDest.email, idapp, paramsrec, extrarec); await sendemail.sendEmail_RisRicevuti(
myuserDest.lang,
myuserDest,
myuserDest.email,
idapp,
paramsrec,
extrarec
);
} else if (extrarec.groupdest || extrarec.contoComDest) { } else if (extrarec.groupdest || extrarec.contoComDest) {
const groupDestoContoCom = extrarec.groupdest const groupDestoContoCom = extrarec.groupdest
? extrarec.groupdest ? extrarec.groupdest
@@ -1047,16 +1060,16 @@ CircuitSchema.statics.getListAdminsByCircuitPath = async function (idapp, circui
let adminObjects = circuit && circuit.admins ? circuit.admins : []; let adminObjects = circuit && circuit.admins ? circuit.admins : [];
// Aggiungi USER_ADMIN_CIRCUITS come oggetti // Aggiungi USER_ADMIN_CIRCUITS come oggetti
let systemAdmins = shared_consts.USER_ADMIN_CIRCUITS.map(username => ({ let systemAdmins = shared_consts.USER_ADMIN_CIRCUITS.map((username) => ({
username, username,
date: null, date: null,
_id: null _id: null,
})); }));
// Unisci e rimuovi duplicati per username // Unisci e rimuovi duplicati per username
let allAdmins = [...adminObjects, ...systemAdmins]; let allAdmins = [...adminObjects, ...systemAdmins];
let uniqueAdmins = allAdmins.filter((admin, index, self) => let uniqueAdmins = allAdmins.filter(
index === self.findIndex(a => a.username === admin.username) (admin, index, self) => index === self.findIndex((a) => a.username === admin.username)
); );
return uniqueAdmins; return uniqueAdmins;
@@ -1190,6 +1203,7 @@ CircuitSchema.statics.createCircuitIfNotExist = async function (req, idapp, prov
qta_max_default_grp: shared_consts.CIRCUIT_PARAMS.SCOPERTO_MAX_GRP, qta_max_default_grp: shared_consts.CIRCUIT_PARAMS.SCOPERTO_MAX_GRP,
valuta_per_euro: 1, valuta_per_euro: 1,
totTransato: 0, totTransato: 0,
numTransazioni: 0,
totCircolante: 0, totCircolante: 0,
date_created: new Date(), date_created: new Date(),
admins: admins.map((username) => ({ username })), admins: admins.map((username) => ({ username })),
@@ -1388,7 +1402,12 @@ CircuitSchema.statics.setFido = async function (idapp, username, circuitName, gr
const ris = await Account.updateFido(idapp, username, groupname, circuitId, fido, username_action); const ris = await Account.updateFido(idapp, username, groupname, circuitId, fido, username_action);
if (ris) { if (ris) {
return { qta_maxConcessa: qtamax, fidoConcesso: fido, username_admin_abilitante: username_action, changed: variato || (ris && ris.modifiedCount > 0) }; return {
qta_maxConcessa: qtamax,
fidoConcesso: fido,
username_admin_abilitante: username_action,
changed: variato || (ris && ris.modifiedCount > 0),
};
} }
} }
} }
@@ -1441,7 +1460,7 @@ CircuitSchema.statics.getFido = async function (idapp, username, circuitName, gr
return null; return null;
}; };
CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) { CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi, options) {
const { User } = require('../models/user'); const { User } = require('../models/user');
const { MyGroup } = require('../models/mygroup'); const { MyGroup } = require('../models/mygroup');
const { SendNotif } = require('../models/sendnotif'); const { SendNotif } = require('../models/sendnotif');
@@ -1540,7 +1559,7 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
let numtransazionitot = 0; let numtransazionitot = 0;
const arrcircuits = await Circuit.find({ idapp }).lean(); const arrcircuits = await Circuit.find({ idapp });
for (const circuit of arrcircuits) { for (const circuit of arrcircuits) {
let strusersnotinaCircuit = ''; let strusersnotinaCircuit = '';
let strusersnotExist = ''; let strusersnotExist = '';
@@ -1620,6 +1639,16 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
_id: null, _id: null,
numtransactions: { $sum: 1 }, numtransactions: { $sum: 1 },
totTransato: { $sum: { $abs: '$amount' } }, totTransato: { $sum: { $abs: '$amount' } },
sentCount: {
$sum: {
$cond: [{ $eq: ['$accountFromId', account._id] }, 1, 0],
},
},
receivedCount: {
$sum: {
$cond: [{ $eq: ['$accountToId', account._id] }, 1, 0],
},
},
saldo: { saldo: {
$sum: { $sum: {
$cond: [ $cond: [
@@ -1636,6 +1665,8 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
]); ]);
let numtransactions = result && result.length > 0 ? result[0].numtransactions : 0; let numtransactions = result && result.length > 0 ? result[0].numtransactions : 0;
let sentCount = result && result.length > 0 ? result[0].sentCount : 0;
let receivedCount = result && result.length > 0 ? result[0].receivedCount : 0;
let totTransato = result && result.length > 0 ? result[0].totTransato : 0; let totTransato = result && result.length > 0 ? result[0].totTransato : 0;
let saldo = result && result.length > 0 ? result[0].saldo : 0; let saldo = result && result.length > 0 ? result[0].saldo : 0;
@@ -1679,6 +1710,8 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
if (correggi) await Account.findOneAndUpdate({ _id: account._id }, { $set: { totTransato } }); if (correggi) await Account.findOneAndUpdate({ _id: account._id }, { $set: { totTransato } });
} }
await Account.findOneAndUpdate({ _id: account._id }, { $set: { sent: sentCount, received: receivedCount } });
saldotot += account.saldo; saldotot += account.saldo;
// if (account.totTransato === NaN || account.totTransato === undefined) // if (account.totTransato === NaN || account.totTransato === undefined)
@@ -1693,6 +1726,11 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
// await account.calcPending(); // await account.calcPending();
ind++; ind++;
} // FINE ACCOUNT
if (options?.setnumtransaction) {
circuit.numTransazioni = numtransazionitot;
await circuit.save(); // salva su db
} }
let numaccounts = accounts.length; let numaccounts = accounts.length;
@@ -1876,6 +1914,11 @@ CircuitSchema.statics.getCircuitiExtraProvinciali = async function (idapp) {
return circuits; return circuits;
}; };
CircuitSchema.statics.ricalcolaNumTransazioni = async function (circuitId) {
const Circuit = this;
// +TODO: Ricalcola il numero delle transazioni avvenute
};
CircuitSchema.statics.getCircuitoItalia = async function (idapp) { CircuitSchema.statics.getCircuitoItalia = async function (idapp) {
const Circuit = this; const Circuit = this;
@@ -1884,6 +1927,13 @@ CircuitSchema.statics.getCircuitoItalia = async function (idapp) {
return circuit; return circuit;
}; };
CircuitSchema.statics.getSymbolByCircuitId = async function (circuitId) {
const Circuit = this;
const circuit = await Circuit.findOne({ _id: circuitId }, { symbol: 1});
return circuit?.symbol || '';
};
CircuitSchema.statics.isEnableToReceiveEmailByExtraRec = async function (idapp, recnotif) { CircuitSchema.statics.isEnableToReceiveEmailByExtraRec = async function (idapp, recnotif) {
let ricevo = true; let ricevo = true;
if (recnotif.tag === 'setfido') { if (recnotif.tag === 'setfido') {

View File

@@ -106,6 +106,8 @@ MovementSchema.statics.addMov = async function (
idOrdersCart idOrdersCart
) { ) {
try { try {
const { Circuit } = require('./circuit');
// Only positive values // Only positive values
amount = Math.abs(amount); amount = Math.abs(amount);

View File

@@ -174,6 +174,7 @@ const SiteSchema = new Schema({
bookingEvents: { type: Boolean, default: false }, bookingEvents: { type: Boolean, default: false },
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 },
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 },

View File

@@ -202,7 +202,7 @@ class CronMod {
} else if (mydata.dbop === 'RewriteCategESubCateg') { } else if (mydata.dbop === 'RewriteCategESubCateg') {
const migration = require('../populate/migration-categories'); const migration = require('../populate/migration-categories');
ris = await migration.aggiornaCategorieESottoCategorie() ris = await migration.aggiornaCategorieESottoCategorie();
} else if (mydata.dbop === 'ReplaceUsername') { } else if (mydata.dbop === 'ReplaceUsername') {
if (User.isAdmin(req.user.perm)) { if (User.isAdmin(req.user.perm)) {
ris = globalTables.replaceUsername(req.body.idapp, mydata.search_username, mydata.replace_username); ris = globalTables.replaceUsername(req.body.idapp, mydata.search_username, mydata.replace_username);
@@ -270,6 +270,8 @@ class CronMod {
await Order.RemoveDeletedOrdersInOrderscart(); await Order.RemoveDeletedOrdersInOrderscart();
} else if (mydata.dbop === 'CheckTransazioniCircuiti') { } else if (mydata.dbop === 'CheckTransazioniCircuiti') {
await Circuit.CheckTransazioniCircuiti(false); await Circuit.CheckTransazioniCircuiti(false);
} else if (mydata.dbop === 'CalcNumTransCircuiti') {
await Circuit.CheckTransazioniCircuiti(false, { setnumtransaction: true });
} else if (mydata.dbop === 'CorreggiTransazioniCircuiti') { } else if (mydata.dbop === 'CorreggiTransazioniCircuiti') {
await Circuit.CheckTransazioniCircuiti(true); await Circuit.CheckTransazioniCircuiti(true);
} else if (mydata.dbop === 'RemovePendentTransactions') { } else if (mydata.dbop === 'RemovePendentTransactions') {

View File

@@ -2,8 +2,22 @@ const express = require('express');
const { authenticate, authenticate_noerror } = require('../middleware/authenticate'); const { authenticate, authenticate_noerror } = require('../middleware/authenticate');
const router = express.Router(); const router = express.Router();
const templatesRouter = require('../routes/templates');
const postersRouter = require('../routes/posters');
const assetsRouter = require('../routes/assets');
const PageView = require('../models/PageView'); const PageView = require('../models/PageView');
// const { Groq } = require('groq-sdk');
const fal = require('@fal-ai/client');
const imageGenerator = require('../services/imageGenerator'); // Assicurati che il percorso sia corretto
const posterEditor = require('../services/PosterEditor'); // <--- Importa la nuova classe
const multer = require('multer'); const multer = require('multer');
const XLSX = require('xlsx'); const XLSX = require('xlsx');
@@ -19,6 +33,10 @@ const { MyElem } = require('../models/myelem');
const axios = require('axios'); const axios = require('axios');
router.use('/templates', authenticate, templatesRouter);
router.use('/posters', authenticate, postersRouter);
router.use('/assets', authenticate, assetsRouter);
router.post('/test-lungo', authenticate, (req, res) => { router.post('/test-lungo', authenticate, (req, res) => {
const timeout = req.body.timeout; const timeout = req.body.timeout;
@@ -389,7 +407,6 @@ router.post('/search-books', authenticate, async (req, res) => {
let productfind = null; let productfind = null;
for (let field of book) { for (let field of book) {
field = field.trim(); field = field.trim();
let valido = typeof field === 'string' && field.length > 4 && field.length < 50; let valido = typeof field === 'string' && field.length > 4 && field.length < 50;
if (valido) { if (valido) {
@@ -494,4 +511,46 @@ router.post('/chatbot', authenticate, async (req, res) => {
} }
}); });
router.post('/generateposter', async (req, res) => {
const {
titolo, data, ora, luogo, descrizione, contatti, fotoDescrizione, stile,
provider = 'hf' // Default a HF (Gratis)
} = req.body;
// 1. Prompt per l'AI: Chiediamo SOLO lo sfondo, VIETIAMO il testo.
// Questo garantisce che Flux si concentri sulla bellezza dell'immagine.
const promptAI = `Vertical event poster background, ${stile || 'modern style, vivid colors'}.
Subject: ${fotoDescrizione || 'abstract artistic shapes'}.
Composition: Central empty space or clean layout suitable for overlaying text later.
NO TEXT, NO LETTERS, clean illustration, high quality, 4k.`;
try {
console.log('1. Generazione Sfondo AI...');
// Genera solo l'immagine base
const rawImageUrl = await imageGenerator.generate(provider, promptAI);
console.log('2. Composizione Grafica Testi...');
// Sovrapponi i testi con Canvas
const finalPosterBase64 = await posterEditor.createPoster(rawImageUrl, {
titolo,
data,
ora,
luogo,
contatti
});
res.json({
success: true,
imageUrl: finalPosterBase64, // Restituisce l'immagine completa in base64
step: 'AI + Canvas Composition'
});
} catch (err) {
console.error('Errore:', err.message);
res.status(500).json({ error: err.message });
}
});
module.exports = router; module.exports = router;

21
src/routes/assets.js Normal file
View File

@@ -0,0 +1,21 @@
const express = require('express');
const router = express.Router();
const assetController = require('../controllers/assetController');
const { authenticate } = require('../middleware/authenticate');
const upload = require('../middleware/upload');
// Upload
router.post('/upload', authenticate, upload.single('file'), assetController.upload);
router.post('/upload-multiple', authenticate, upload.array('files', 10), assetController.uploadMultiple);
// AI Generation
router.post('/generate-ai', authenticate, assetController.generateAi);
// CRUD
router.get('/', authenticate, assetController.list);
router.get('/:id', assetController.getById);
router.get('/:id/file', assetController.getFile);
router.get('/:id/thumbnail', assetController.getThumbnail);
router.delete('/:id', authenticate, assetController.delete);
module.exports = router;

25
src/routes/posters.js Normal file
View File

@@ -0,0 +1,25 @@
const express = require('express');
const router = express.Router();
const posterController = require('../controllers/posterController');
const { authenticate } = require('../middleware/authenticate');
const upload = require('../middleware/upload');
// CRUD Posters
router.post('/', authenticate, posterController.create);
router.get('/', authenticate, posterController.list);
router.get('/favorites', authenticate, posterController.listFavorites);
router.get('/recent', authenticate, posterController.listRecent);
router.get('/:id', authenticate, posterController.getById);
router.put('/:id', authenticate, posterController.update);
router.delete('/:id', authenticate, posterController.delete);
// Azioni speciali
router.post('/:id/render', authenticate, posterController.render);
router.post('/:id/regenerate-ai', authenticate, posterController.regenerateAi);
router.get('/:id/download/:format', posterController.download);
router.post('/:id/favorite', authenticate, posterController.toggleFavorite);
// Quick generate (come nella tua bozza)
router.post('/quick-generate', authenticate, posterController.quickGenerate);
module.exports = router;

17
src/routes/templates.js Normal file
View File

@@ -0,0 +1,17 @@
const express = require('express');
const router = express.Router();
const templateController = require('../controllers/templateController');
const { authenticate } = require('../middleware/authenticate');
// CRUD Templates
router.post('/', authenticate, templateController.create);
router.get('/', templateController.list);
router.get('/types', templateController.getTypes);
router.get('/presets', templateController.getFormatPresets);
router.get('/:id', templateController.getById);
router.put('/:id', authenticate, templateController.update);
router.delete('/:id', authenticate, templateController.delete);
router.post('/:id/duplicate', authenticate, templateController.duplicate);
router.get('/:id/preview', templateController.getPreview);
module.exports = router;

View File

@@ -0,0 +1,33 @@
const Template = require('../models/Template');
const templateSeeds = require('../templates/template-seeds');
const MONGODB_URI = process.env.MONGODB_URI || '';
async function seedTemplates() {
try {
// await mongoose.connect(MONGODB_URI);
// Opzionale: rimuovi template esistenti con stessi templateType
const existingTypes = templateSeeds.map(t => t.templateType);
await Template.deleteMany({ templateType: { $in: existingTypes } });
console.log('✓ Template esistenti rimossi');
// Inserisci nuovi template
const result = await Template.insertMany(templateSeeds);
console.log(`${result.length} template inseriti con successo`);
// Log dei template creati
result.forEach(t => {
console.log(` - ${t.name} (${t.templateType})`);
});
// await mongoose.disconnect();
console.log('✓ Disconnesso da MongoDB');
process.exit(0);
} catch (error) {
console.error('✗ Errore seeding:', error);
process.exit(1);
}
}
seedTemplates();

View File

@@ -14,4 +14,3 @@ const seedTemplates = async () => {
}; };
seedTemplates(); seedTemplates();
s

View File

@@ -368,17 +368,17 @@ function checkifSendEmail() {
module.exports = { module.exports = {
sendEmail_base_e_manager: async function (idapp, template, to, mylocalsconf, replyTo, transport, previewonly) { sendEmail_base_e_manager: async function (idapp, template, to, mylocalsconf, replyTo, transport, previewonly) {
await this.sendEmail_base(template, to, mylocalsconf, replyTo, transport, previewonly); await this.sendEmail_base(idapp, template, to, mylocalsconf, replyTo, transport, previewonly);
await this.sendEmail_base(template, tools.getAdminEmailByIdApp(idapp), mylocalsconf, '', transport, previewonly); await this.sendEmail_base(idapp, template, tools.getAdminEmailByIdApp(idapp), mylocalsconf, '', transport, previewonly);
if (tools.isManagAndAdminDifferent(idapp)) { if (tools.isManagAndAdminDifferent(idapp)) {
const email = tools.getManagerEmailByIdApp(idapp); const email = tools.getManagerEmailByIdApp(idapp);
await this.sendEmail_base(template, email, mylocalsconf, '', transport, previewonly); await this.sendEmail_base(idapp, template, email, mylocalsconf, '', transport, previewonly);
} }
}, },
sendEmail_base: async function (template, to, mylocalsconf, replyTo, transport, previewonly) { sendEmail_base: async function (idapp, template, to, mylocalsconf, replyTo, transport, previewonly) {
if (to === '') return false; if (to === '') return false;
// console.log('mylocalsconf', mylocalsconf); // console.log('mylocalsconf', mylocalsconf);
@@ -389,9 +389,17 @@ module.exports = {
if (!replyTo) replyTo = ''; if (!replyTo) replyTo = '';
const emailSender = mylocalsconf.dataemail.from;
let senderName = '';
if (idapp) {
senderName = tools.getNomeAppByIdApp(mylocalsconf.idapp);
}
const emailcompleta = senderName ? `"${senderName}" <${emailSender}>` : emailSender;
const paramemail = { const paramemail = {
message: { message: {
from: mylocalsconf.dataemail.from, // sender address from: emailcompleta,
headers: { headers: {
'Reply-To': replyTo, 'Reply-To': replyTo,
}, },
@@ -457,9 +465,12 @@ module.exports = {
sendEmail_Normale: async function (mylocalsconf, to, subject, html, replyTo) { sendEmail_Normale: async function (mylocalsconf, to, subject, html, replyTo) {
try { try {
const emailSender = tools.getEmailByIdApp(mylocalsconf.idapp);
const senderName = tools.getNomeAppByIdApp(mylocalsconf.idapp);
// setup e-mail data with unicode symbols // setup e-mail data with unicode symbols
var mailOptions = { var mailOptions = {
from: tools.getEmailByIdApp(mylocalsconf.idapp), // sender address from: `"${senderName}" <${emailSender}>`,
dataemail: await this.getdataemail(mylocalsconf.idapp), dataemail: await this.getdataemail(mylocalsconf.idapp),
to: to, to: to,
generateTextFromHTML: true, generateTextFromHTML: true,
@@ -498,12 +509,12 @@ module.exports = {
try { try {
const reg = require('./reg/registration'); const reg = require('./reg/registration');
const idverif = reg.getlinkregByEmail(idapp, email, username); const idverif = reg.getlinkregByEmail(idapp, email, username);
await User.setLinkToVerifiedEmail(idapp, username, idverif); await User.setLinkToVerifiedEmail(idapp, username, idverif);
const strlinkreg = tools.getHostByIdApp(idapp) + process.env.CHECKREVERIF_EMAIL + `/?idapp=${idapp}&idlink=${idverif}`; const strlinkreg =
tools.getHostByIdApp(idapp) + process.env.CHECKREVERIF_EMAIL + `/?idapp=${idapp}&idlink=${idverif}`;
return strlinkreg; return strlinkreg;
} catch (e) { } catch (e) {
console.error('ERROR getlinkVerifyEmail'); console.error('ERROR getlinkVerifyEmail');
@@ -522,15 +533,22 @@ module.exports = {
}, },
getLinkAbilitaCircuito: function (idapp, user, data) { getLinkAbilitaCircuito: function (idapp, user, data) {
if (data.token_circuito_da_ammettere) { if (data.token_circuito_da_ammettere) {
const strlink = tools.getHostByIdApp(idapp) + `/abcirc/${data.cmd}/${data.token_circuito_da_ammettere}/${user.username}/${data.myusername}`; const strlink =
tools.getHostByIdApp(idapp) +
`/abcirc/${data.cmd}/${data.token_circuito_da_ammettere}/${user.username}/${data.myusername}`;
return strlink; return strlink;
} }
return ''; return '';
}, },
getPathEmail(idapp, email_template) { getPathEmail(idapp, email_template) {
const RISO_TEMPLATES = ['reg_notifica_all_invitante', 'reg_email_benvenuto_ammesso', 'reg_chiedi_ammettere_all_invitante', const RISO_TEMPLATES = [
'circuit_chiedi_facilitatori_di_entrare', 'circuit_abilitato_al_fido_membro']; 'reg_notifica_all_invitante',
'reg_email_benvenuto_ammesso',
'reg_chiedi_ammettere_all_invitante',
'circuit_chiedi_facilitatori_di_entrare',
'circuit_abilitato_al_fido_membro',
];
if (idapp === '13') { if (idapp === '13') {
if (RISO_TEMPLATES.includes(email_template)) { if (RISO_TEMPLATES.includes(email_template)) {
@@ -585,34 +603,24 @@ module.exports = {
} }
//Invia una email al nuovo utente //Invia una email al nuovo utente
await this.sendEmail_base(quale_email_inviare, emailto, mylocalsconf, tools.getreplyToEmailByIdApp(idapp)); await this.sendEmail_base(idapp, quale_email_inviare, emailto, mylocalsconf, tools.getreplyToEmailByIdApp(idapp));
if (user.verified_email && user.aportador_solidario && user.verified_by_aportador) { if (user.verified_email && user.aportador_solidario && user.verified_by_aportador) {
const pathemail = this.getPathEmail(idapp, 'reg_notifica_all_invitante'); const pathemail = this.getPathEmail(idapp, 'reg_notifica_all_invitante');
// Manda anche una email al suo Invitante // Manda anche una email al suo Invitante
const recaportador = await User.getUserShortDataByUsername(idapp, user.aportador_solidario); const recaportador = await User.getUserShortDataByUsername(idapp, user.aportador_solidario);
const ris = await this.sendEmail_base( const ris = await this.sendEmail_base(idapp, pathemail + '/' + tools.LANGADMIN, recaportador.email, mylocalsconf, '');
pathemail + '/' + tools.LANGADMIN,
recaportador.email,
mylocalsconf,
''
);
} else if (user.aportador_solidario && !user.verified_by_aportador) { } else if (user.aportador_solidario && !user.verified_by_aportador) {
const pathemail = this.getPathEmail(idapp, 'reg_chiedi_ammettere_all_invitante'); const pathemail = this.getPathEmail(idapp, 'reg_chiedi_ammettere_all_invitante');
// Manda una email al suo Invitante per chiedere di essere ammesso // Manda una email al suo Invitante per chiedere di essere ammesso
const recaportador = await User.getUserShortDataByUsername(idapp, user.aportador_solidario); const recaportador = await User.getUserShortDataByUsername(idapp, user.aportador_solidario);
const ris = await this.sendEmail_base( const ris = await this.sendEmail_base(idapp, pathemail + '/' + tools.LANGADMIN, recaportador.email, mylocalsconf, '');
pathemail + '/' + tools.LANGADMIN,
recaportador.email,
mylocalsconf,
''
);
} }
// Send to the Admin an Email // Send to the Admin an Email
const ris = await this.sendEmail_base( const ris = await this.sendEmail_base(idapp,
'admin/registration/' + tools.LANGADMIN, 'admin/registration/' + tools.LANGADMIN,
tools.getAdminEmailByIdApp(idapp), tools.getAdminEmailByIdApp(idapp),
mylocalsconf, mylocalsconf,
@@ -654,7 +662,7 @@ module.exports = {
messaggioPersonalizzato: dati.messaggioPersonalizzato, messaggioPersonalizzato: dati.messaggioPersonalizzato,
}; };
const ris = await this.sendEmail_base('invitaamico/' + lang, emailto, mylocalsconf, ''); const ris = await this.sendEmail_base(idapp, 'invitaamico/' + lang, emailto, mylocalsconf, '');
await telegrambot.notifyToTelegram(telegrambot.phase.INVITA_AMICO, mylocalsconf); await telegrambot.notifyToTelegram(telegrambot.phase.INVITA_AMICO, mylocalsconf);
@@ -681,7 +689,7 @@ module.exports = {
const quale_email_inviare = this.getPathEmail(idapp, 'reg_email_benvenuto_ammesso') + '/' + lang; const quale_email_inviare = this.getPathEmail(idapp, 'reg_email_benvenuto_ammesso') + '/' + lang;
const ris = await this.sendEmail_base(quale_email_inviare, emailto, mylocalsconf, ''); const ris = await this.sendEmail_base(idapp, quale_email_inviare, emailto, mylocalsconf, '');
await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf); await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf);
@@ -715,7 +723,7 @@ module.exports = {
const quale_email_inviare = this.getPathEmail(idapp, 'reg_resend_email_to_verifiyng') + '/' + lang; const quale_email_inviare = this.getPathEmail(idapp, 'reg_resend_email_to_verifiyng') + '/' + lang;
const ris = await this.sendEmail_base(quale_email_inviare, email, mylocalsconf, ''); const ris = await this.sendEmail_base(idapp, quale_email_inviare, email, mylocalsconf, '');
return ris; return ris;
} catch (e) { } catch (e) {
@@ -724,6 +732,8 @@ module.exports = {
}, },
sendEmail_Utente_Abilitato_Circuito_FidoConcesso: async function (lang, emailto, user, idapp, dati) { sendEmail_Utente_Abilitato_Circuito_FidoConcesso: async function (lang, emailto, user, idapp, dati) {
try { try {
const { Circuit } = require('../models/circuit');
let mylocalsconf = { let mylocalsconf = {
idapp, idapp,
dataemail: await this.getdataemail(idapp), dataemail: await this.getdataemail(idapp),
@@ -735,6 +745,7 @@ module.exports = {
usernameInvitante: dati.usernameInvitante, usernameInvitante: dati.usernameInvitante,
linkProfiloAdmin: tools.getLinkUserProfile(idapp, dati.usernameInvitante), linkProfiloAdmin: tools.getLinkUserProfile(idapp, dati.usernameInvitante),
user, user,
symbol: await Circuit.getSymbolByCircuitId(dati.circuitId),
usernameMembro: user.username, usernameMembro: user.username,
nomeTerritorio: dati.nomeTerritorio, nomeTerritorio: dati.nomeTerritorio,
linkTelegramTerritorio: dati.link_group, linkTelegramTerritorio: dati.link_group,
@@ -742,7 +753,7 @@ module.exports = {
const quale_email_inviare = this.getPathEmail(idapp, 'circuit_abilitato_al_fido_membro') + '/' + lang; const quale_email_inviare = this.getPathEmail(idapp, 'circuit_abilitato_al_fido_membro') + '/' + lang;
const ris = await this.sendEmail_base(quale_email_inviare, emailto, mylocalsconf, ''); const ris = await this.sendEmail_base(idapp, quale_email_inviare, emailto, mylocalsconf, '');
await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf); await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf);
@@ -751,7 +762,14 @@ module.exports = {
console.error('Err sendEmail_Utente_Ammesso', e); console.error('Err sendEmail_Utente_Ammesso', e);
} }
}, },
sendEmail_Richiesta_Al_Facilitatore_Di_FarEntrare_AlCircuito: async function (lang, emailto, user, userInvitante, idapp, dati) { sendEmail_Richiesta_Al_Facilitatore_Di_FarEntrare_AlCircuito: async function (
lang,
emailto,
user,
userInvitante,
idapp,
dati
) {
try { try {
// dati.circuitId // dati.circuitId
// dati.groupname // dati.groupname
@@ -759,6 +777,8 @@ module.exports = {
const linkAbilitazione = this.getLinkAbilitaCircuito(idapp, user, dati); const linkAbilitazione = this.getLinkAbilitaCircuito(idapp, user, dati);
const { Circuit } = require('../models/circuit');
let mylocalsconf = { let mylocalsconf = {
idapp, idapp,
dataemail: await this.getdataemail(idapp), dataemail: await this.getdataemail(idapp),
@@ -780,6 +800,7 @@ module.exports = {
comuneResidenza: user.profile.resid_str_comune, comuneResidenza: user.profile.resid_str_comune,
provinciaResidenza: user.profile.resid_province, provinciaResidenza: user.profile.resid_province,
user, user,
symbol: await Circuit.getSymbolByCircuitId(dati.circuitId),
linkAbilitazione: linkAbilitazione, linkAbilitazione: linkAbilitazione,
linkProfiloMembro: tools.getLinkUserProfile(idapp, user.username), linkProfiloMembro: tools.getLinkUserProfile(idapp, user.username),
linkProfiloInvitante: tools.getLinkUserProfile(idapp, user.aportador_solidario), linkProfiloInvitante: tools.getLinkUserProfile(idapp, user.aportador_solidario),
@@ -789,7 +810,7 @@ module.exports = {
const quale_email_inviare = this.getPathEmail(idapp, 'circuit_chiedi_facilitatori_di_entrare') + '/' + lang; const quale_email_inviare = this.getPathEmail(idapp, 'circuit_chiedi_facilitatori_di_entrare') + '/' + lang;
const ris = await this.sendEmail_base(quale_email_inviare, emailto, mylocalsconf, ''); const ris = await this.sendEmail_base(idapp, quale_email_inviare, emailto, mylocalsconf, '');
// await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf); // await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf);
@@ -817,6 +838,7 @@ module.exports = {
mylocalsconf = this.setParamsForTemplate(iscritto, mylocalsconf); mylocalsconf = this.setParamsForTemplate(iscritto, mylocalsconf);
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'iscrizione_conacreis/' + lang, 'iscrizione_conacreis/' + lang,
emailto, emailto,
mylocalsconf, mylocalsconf,
@@ -825,6 +847,7 @@ module.exports = {
// Send to the Admin an Email // Send to the Admin an Email
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'admin/iscrizione_conacreis/' + tools.LANGADMIN, 'admin/iscrizione_conacreis/' + tools.LANGADMIN,
tools.getAdminEmailByIdApp(idapp), tools.getAdminEmailByIdApp(idapp),
mylocalsconf, mylocalsconf,
@@ -845,6 +868,7 @@ module.exports = {
if (tools.isManagAndAdminDifferent(idapp)) { if (tools.isManagAndAdminDifferent(idapp)) {
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'admin/iscrizione_conacreis/' + tools.LANGADMIN, 'admin/iscrizione_conacreis/' + tools.LANGADMIN,
tools.getManagerEmailByIdApp(idapp), tools.getManagerEmailByIdApp(idapp),
mylocalsconf, mylocalsconf,
@@ -868,7 +892,7 @@ module.exports = {
mylocalsconf = this.setParamsForTemplate(user, mylocalsconf); mylocalsconf = this.setParamsForTemplate(user, mylocalsconf);
await this.sendEmail_base('resetpwd/' + lang, emailto, mylocalsconf, ''); await this.sendEmail_base(idapp, 'resetpwd/' + lang, emailto, mylocalsconf, '');
}, },
sendEmail_RisRicevuti: async function (lang, userDest, emailto, idapp, myrec, extrarec) { sendEmail_RisRicevuti: async function (lang, userDest, emailto, idapp, myrec, extrarec) {
@@ -895,7 +919,7 @@ module.exports = {
mylocalsconf = this.setParamsForTemplate(userDest, mylocalsconf); mylocalsconf = this.setParamsForTemplate(userDest, mylocalsconf);
await this.sendEmail_base('risricevuti/' + lang, emailto, mylocalsconf, ''); await this.sendEmail_base(idapp, 'risricevuti/' + lang, emailto, mylocalsconf, '');
}, },
sendEmail_Booking: async function (res, lang, emailto, user, idapp, recbooking) { sendEmail_Booking: async function (res, lang, emailto, user, idapp, recbooking) {
@@ -933,6 +957,7 @@ module.exports = {
} }
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'booking/' + texthtml + '/' + lang, 'booking/' + texthtml + '/' + lang,
emailto, emailto,
mylocalsconf, mylocalsconf,
@@ -941,6 +966,7 @@ module.exports = {
// Send Email also to the Admin // Send Email also to the Admin
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'admin/' + texthtml + '/' + tools.LANGADMIN, 'admin/' + texthtml + '/' + tools.LANGADMIN,
tools.getAdminEmailByIdApp(idapp), tools.getAdminEmailByIdApp(idapp),
mylocalsconf, mylocalsconf,
@@ -949,6 +975,7 @@ module.exports = {
if (tools.isManagAndAdminDifferent(idapp)) { if (tools.isManagAndAdminDifferent(idapp)) {
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'admin/' + texthtml + '/' + tools.LANGADMIN, 'admin/' + texthtml + '/' + tools.LANGADMIN,
tools.getManagerEmailByIdApp(idapp), tools.getManagerEmailByIdApp(idapp),
mylocalsconf, mylocalsconf,
@@ -1046,6 +1073,7 @@ module.exports = {
telegrambot.sendMsgTelegramToTheManagers(idapp, msgtelegram); telegrambot.sendMsgTelegramToTheManagers(idapp, msgtelegram);
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'booking/cancelbooking/' + lang, 'booking/cancelbooking/' + lang,
emailto, emailto,
mylocalsconf, mylocalsconf,
@@ -1054,6 +1082,7 @@ module.exports = {
// Send Email also to the Admin // Send Email also to the Admin
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'admin/cancelbooking/' + tools.LANGADMIN, 'admin/cancelbooking/' + tools.LANGADMIN,
tools.getAdminEmailByIdApp(idapp), tools.getAdminEmailByIdApp(idapp),
mylocalsconf, mylocalsconf,
@@ -1062,6 +1091,7 @@ module.exports = {
if (tools.isManagAndAdminDifferent(idapp)) { if (tools.isManagAndAdminDifferent(idapp)) {
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'admin/cancelbooking/' + tools.LANGADMIN, 'admin/cancelbooking/' + tools.LANGADMIN,
tools.getManagerEmailByIdApp(idapp), tools.getManagerEmailByIdApp(idapp),
mylocalsconf, mylocalsconf,
@@ -1093,7 +1123,7 @@ module.exports = {
if (mylocalsconf.infoevent !== '') replyto = user.email; if (mylocalsconf.infoevent !== '') replyto = user.email;
else replyto = tools.getreplyToEmailByIdApp(idapp); else replyto = tools.getreplyToEmailByIdApp(idapp);
return await this.sendEmail_base('msg/sendmsg/' + lang, emailto, mylocalsconf, replyto); return await this.sendEmail_base(idapp, 'msg/sendmsg/' + lang, emailto, mylocalsconf, replyto);
// Send Email also to the Admin // Send Email also to the Admin
// this.sendEmail_base('admin/sendmsg/' + lang, tools.getAdminEmailByIdApp(idapp), mylocalsconf); // this.sendEmail_base('admin/sendmsg/' + lang, tools.getAdminEmailByIdApp(idapp), mylocalsconf);
@@ -1215,6 +1245,7 @@ module.exports = {
if (sendnews) { if (sendnews) {
// Send to the Admin an Email // Send to the Admin an Email
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'admin/added_to_newsletter/' + tools.LANGADMIN, 'admin/added_to_newsletter/' + tools.LANGADMIN,
tools.getAdminEmailByIdApp(idapp), tools.getAdminEmailByIdApp(idapp),
mylocalsconf, mylocalsconf,
@@ -1223,6 +1254,7 @@ module.exports = {
if (tools.isManagAndAdminDifferent(idapp)) { if (tools.isManagAndAdminDifferent(idapp)) {
await this.sendEmail_base( await this.sendEmail_base(
idapp,
'admin/added_to_newsletter/' + tools.LANGADMIN, 'admin/added_to_newsletter/' + tools.LANGADMIN,
tools.getManagerEmailByIdApp(idapp), tools.getManagerEmailByIdApp(idapp),
mylocalsconf, mylocalsconf,
@@ -1521,6 +1553,7 @@ module.exports = {
if (status !== shared_consts.OrderStatus.CANCELED && status !== shared_consts.OrderStatus.COMPLETED) { if (status !== shared_consts.OrderStatus.CANCELED && status !== shared_consts.OrderStatus.COMPLETED) {
const esito = await this.sendEmail_base( const esito = await this.sendEmail_base(
idapp,
'ecommerce/' + ordertype + '/' + lang, 'ecommerce/' + ordertype + '/' + lang,
mylocalsconf.emailto, mylocalsconf.emailto,
mylocalsconf, mylocalsconf,
@@ -1617,6 +1650,7 @@ module.exports = {
// Send Email to the User // Send Email to the User
// console.log('-> Invio Email (', mynewsrec.numemail_sent, '/', mynewsrec.numemail_tot, ')'); // console.log('-> Invio Email (', mynewsrec.numemail_sent, '/', mynewsrec.numemail_tot, ')');
const esito = await this.sendEmail_base( const esito = await this.sendEmail_base(
idapp,
'newsletter/' + lang, 'newsletter/' + lang,
mylocalsconf.emailto, mylocalsconf.emailto,
mylocalsconf, mylocalsconf,
@@ -1756,6 +1790,7 @@ module.exports = {
console.log('-> Invio Email TEST a', mylocalsconf.emailto, 'previewonly', previewonly); console.log('-> Invio Email TEST a', mylocalsconf.emailto, 'previewonly', previewonly);
return await this.sendEmail_base( return await this.sendEmail_base(
idapp,
'newsletter/' + lang, 'newsletter/' + lang,
mylocalsconf.emailto, mylocalsconf.emailto,
mylocalsconf, mylocalsconf,
@@ -1796,6 +1831,7 @@ module.exports = {
console.log('-> Invio Email ' + mylocalsconf.subject + ' a', mylocalsconf.emailto, 'in corso...'); console.log('-> Invio Email ' + mylocalsconf.subject + ' a', mylocalsconf.emailto, 'in corso...');
const risult = await this.sendEmail_base( const risult = await this.sendEmail_base(
idapp,
'newsletter/' + userto.lang, 'newsletter/' + userto.lang,
mylocalsconf.emailto, mylocalsconf.emailto,
mylocalsconf, mylocalsconf,

View File

@@ -120,6 +120,10 @@ async function runStartupTasks() {
await inizia(); await inizia();
if (true) {
// const Seed = require('../scripts/seedTemplates');
}
// 4) reset job pendenti // 4) reset job pendenti
await resetProcessingJob(); await resetProcessingJob();

View File

@@ -59,6 +59,10 @@ function setupRouters(app) {
}); });
}); });
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
return true; return true;
} }

View File

@@ -0,0 +1,151 @@
const { createCanvas, loadImage } = require('canvas');
class PosterEditor {
/**
* Crea poster con testi sovrapposti (compatibile con tua API originale)
*/
async createPoster(backgroundImageUrl, data) {
const { titolo, data: eventDate, ora, luogo, contatti } = data;
const width = 1080;
const height = 1920;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
// Carica e disegna background
try {
let img;
if (backgroundImageUrl.startsWith('data:')) {
img = await loadImage(backgroundImageUrl);
} else {
const fetch = require('node-fetch');
const response = await fetch(backgroundImageUrl);
const buffer = await response.buffer();
img = await loadImage(buffer);
}
// Cover fit
const imgRatio = img.width / img.height;
const canvasRatio = width / height;
let dw, dh, dx, dy;
if (imgRatio > canvasRatio) {
dh = height;
dw = height * imgRatio;
dx = (width - dw) / 2;
dy = 0;
} else {
dw = width;
dh = width / imgRatio;
dx = 0;
dy = (height - dh) / 2;
}
ctx.drawImage(img, dx, dy, dw, dh);
} catch (e) {
// Fallback colore solido
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, width, height);
}
// Overlay gradient
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(0.5, 'rgba(0,0,0,0.3)');
gradient.addColorStop(1, 'rgba(0,0,0,0.85)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
// Titolo
if (titolo) {
ctx.save();
ctx.font = 'bold 68px Arial, sans-serif';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(0,0,0,0.9)';
ctx.shadowBlur = 20;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
// Word wrap manuale
const maxWidth = width * 0.9;
const lines = this._wrapText(ctx, titolo.toUpperCase(), maxWidth);
const lineHeight = 80;
const startY = height * 0.50 - ((lines.length - 1) * lineHeight) / 2;
lines.forEach((line, i) => {
ctx.fillText(line, width / 2, startY + i * lineHeight);
});
ctx.restore();
}
// Data e ora
if (eventDate) {
ctx.save();
ctx.font = 'bold 44px Arial, sans-serif';
ctx.fillStyle = '#ffd700';
ctx.textAlign = 'center';
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 10;
const dateText = ora ? `${eventDate} • ORE ${ora}` : eventDate;
ctx.fillText(dateText.toUpperCase(), width / 2, height * 0.68);
ctx.restore();
}
// Luogo
if (luogo) {
ctx.save();
ctx.font = '600 30px Arial, sans-serif';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.fillText(`📍 ${luogo}`, width / 2, height * 0.76);
ctx.restore();
}
// Contatti
if (contatti) {
ctx.save();
ctx.font = '400 24px Arial, sans-serif';
ctx.fillStyle = '#cccccc';
ctx.textAlign = 'center';
ctx.fillText(contatti, width / 2, height * 0.85);
ctx.restore();
}
// Ritorna base64
return canvas.toDataURL('image/png');
}
/**
* Word wrap utility
*/
_wrapText(ctx, text, maxWidth) {
const words = text.split(' ');
const lines = [];
let currentLine = '';
words.forEach(word => {
const testLine = currentLine ? `${currentLine} ${word}` : word;
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
});
if (currentLine) {
lines.push(currentLine);
}
return lines;
}
}
module.exports = new PosterEditor();

View File

@@ -0,0 +1,154 @@
const fetch = require('node-fetch');
class ImageGenerator {
constructor() {
this.falKey = process.env.FAL_KEY;
this.hfToken = process.env.HF_TOKEN;
this.ideogramKey = process.env.IDEOGRAM_KEY;
}
async generate(provider, prompt, options = {}) {
const {
negativePrompt,
aspectRatio = '9:16',
model,
seed,
steps,
cfg
} = options;
switch (provider) {
case 'ideogram':
return this._generateIdeogram(prompt, { aspectRatio });
case 'fal':
return this._generateFal(prompt, { aspectRatio, seed, steps, cfg });
case 'hf':
default:
return this._generateHuggingFace(prompt, { negativePrompt });
}
}
// Ideogram V2 (via Fal.ai) - Ottimo per testo
async _generateIdeogram(prompt, options = {}) {
console.log('--- Generazione Ideogram V2 ---');
const response = await fetch('https://fal.run/fal-ai/ideogram/v2', {
method: 'POST',
headers: {
Authorization: `Key ${this.falKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt,
aspect_ratio: options.aspectRatio || '9:16',
style_type: 'DESIGN',
expand_prompt: true
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ideogram error: ${response.status} - ${errorText}`);
}
const result = await response.json();
const imageUrl = result.images?.[0]?.url;
if (!imageUrl) throw new Error('Ideogram: nessun URL restituito');
return imageUrl;
}
// Flux Dev (via Fal.ai)
async _generateFal(prompt, options = {}) {
console.log('--- Generazione Fal Flux Dev ---');
const imageSizeMap = {
'9:16': 'portrait_16_9',
'16:9': 'landscape_16_9',
'1:1': 'square'
};
const response = await fetch('https://fal.run/fal-ai/flux/dev', {
method: 'POST',
headers: {
Authorization: `Key ${this.falKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt,
image_size: imageSizeMap[options.aspectRatio] || 'portrait_16_9',
num_images: 1,
enable_safety_checker: false,
seed: options.seed,
num_inference_steps: options.steps || 28,
guidance_scale: options.cfg || 7.5
}),
});
if (!response.ok) {
throw new Error(`Fal error: ${response.status}`);
}
const result = await response.json();
return result.images?.[0]?.url;
}
// Flux Dev (via Hugging Face) - GRATIS
async _generateHuggingFace(prompt, options = {}) {
console.log('--- Generazione HuggingFace (Gratis) ---');
const response = await fetch(
'https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-dev',
{
headers: {
Authorization: `Bearer ${this.hfToken}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
inputs: prompt,
parameters: {
negative_prompt: options.negativePrompt
}
}),
}
);
if (!response.ok) {
const err = await response.text();
throw new Error(`HuggingFace error: ${response.status} - ${err}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return `data:image/jpeg;base64,${buffer.toString('base64')}`;
}
// Utility: verifica disponibilità provider
async checkProvider(provider) {
const checks = {
hf: !!this.hfToken,
fal: !!this.falKey,
ideogram: !!this.falKey // Ideogram via Fal
};
return checks[provider] || false;
}
// Lista provider disponibili
getAvailableProviders() {
const providers = [];
if (this.hfToken) {
providers.push({ id: 'hf', name: 'HuggingFace (Gratis)', cost: 'free' });
}
if (this.falKey) {
providers.push({ id: 'fal', name: 'Fal Flux Dev', cost: 'paid' });
providers.push({ id: 'ideogram', name: 'Ideogram V2', cost: 'paid' });
}
return providers;
}
}
module.exports = new ImageGenerator();

View File

@@ -0,0 +1,870 @@
const { createCanvas, loadImage, registerFont } = require('canvas');
const fs = require('fs').promises;
const path = require('path');
const sharp = require('sharp');
// Registra font personalizzati
const FONTS_DIR = process.env.FONTS_DIR || './fonts';
const registerFonts = async () => {
const fontMappings = [
{ file: 'Montserrat-Black.ttf', family: 'Montserrat', weight: '900' },
{ file: 'Montserrat-Bold.ttf', family: 'Montserrat', weight: '700' },
{ file: 'Montserrat-Regular.ttf', family: 'Montserrat', weight: '400' },
{ file: 'BebasNeue-Regular.ttf', family: 'Bebas Neue', weight: '400' },
{ file: 'OpenSans-Bold.ttf', family: 'Open Sans', weight: '700' },
{ file: 'OpenSans-SemiBold.ttf', family: 'Open Sans', weight: '600' },
{ file: 'OpenSans-Regular.ttf', family: 'Open Sans', weight: '400' },
{ file: 'OpenSans-Light.ttf', family: 'Open Sans', weight: '300' },
{ file: 'PlayfairDisplay-Bold.ttf', family: 'Playfair Display', weight: '700' },
{ file: 'PlayfairDisplay-Regular.ttf', family: 'Playfair Display', weight: '400' }
];
for (const font of fontMappings) {
const fontPath = path.join(FONTS_DIR, font.file);
try {
await fs.access(fontPath);
registerFont(fontPath, { family: font.family, weight: font.weight });
} catch (e) {
// Font non trovato, usa fallback
}
}
};
// Inizializza fonts al caricamento modulo
registerFonts().catch(console.warn);
class PosterRenderer {
constructor() {
this.version = '1.0.0';
}
/**
* Render principale
*/
async render(options) {
const {
template,
content,
assets,
layerOverrides = {},
outputDir,
posterId
} = options;
const startTime = Date.now();
// Dimensioni canvas
const width = template.format?.width || 2480;
const height = template.format?.height || 3508;
// Crea canvas
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
// 1. Disegna background
await this._drawBackground(ctx, template, assets, width, height);
// 2. Ordina layer per zIndex
const sortedLayers = [...(template.layers || [])]
.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
// 3. Disegna ogni layer
for (const layer of sortedLayers) {
if (layer.visible === false) continue;
const override = layerOverrides[layer.id] || {};
const mergedLayer = this._mergeLayerOverride(layer, override);
await this._drawLayer(ctx, mergedLayer, content, assets, width, height, template);
}
// 4. Disegna loghi
if (template.logoSlots?.enabled && assets?.logos?.length > 0) {
await this._drawLogos(ctx, template.logoSlots, assets.logos, width, height);
}
// 5. Salva output
await fs.mkdir(outputDir, { recursive: true });
const baseName = `poster_${posterId}_${Date.now()}`;
const pngPath = path.join(outputDir, `${baseName}.png`);
const jpgPath = path.join(outputDir, `${baseName}.jpg`);
// Salva PNG
const pngBuffer = canvas.toBuffer('image/png');
await fs.writeFile(pngPath, pngBuffer);
// Salva JPG con Sharp (migliore qualità)
await sharp(pngBuffer)
.jpeg({ quality: 95, progressive: true })
.toFile(jpgPath);
const [pngStats, jpgStats] = await Promise.all([
fs.stat(pngPath),
fs.stat(jpgPath)
]);
return {
pngPath,
jpgPath,
pngSize: pngStats.size,
jpgSize: jpgStats.size,
dimensions: { width, height },
duration: Date.now() - startTime,
engineVersion: this.version
};
}
/**
* Disegna background
*/
async _drawBackground(ctx, template, assets, width, height) {
// Colore di sfondo base
ctx.fillStyle = template.backgroundColor || '#1a1a2e';
ctx.fillRect(0, 0, width, height);
// Background image
const bgAsset = assets?.backgroundImage;
const bgLayer = template.layers?.find(l => l.type === 'backgroundImage');
if (bgAsset?.url) {
try {
const img = await this._loadImageFromUrl(bgAsset.url);
// Calcola dimensioni per cover
const imgRatio = img.width / img.height;
const canvasRatio = width / height;
let drawWidth, drawHeight, drawX, drawY;
if (imgRatio > canvasRatio) {
drawHeight = height;
drawWidth = height * imgRatio;
drawX = (width - drawWidth) / 2;
drawY = 0;
} else {
drawWidth = width;
drawHeight = width / imgRatio;
drawX = 0;
drawY = (height - drawHeight) / 2;
}
// Applica blur se definito
if (bgLayer?.style?.blur > 0) {
ctx.filter = `blur(${bgLayer.style.blur}px)`;
}
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
ctx.filter = 'none';
// Applica overlay gradient
const overlay = bgLayer?.style?.overlay;
if (overlay?.enabled) {
this._drawOverlay(ctx, overlay, width, height);
}
} catch (e) {
console.warn('Background image load failed:', e.message);
// Usa fallback
if (bgLayer?.fallback) {
this._drawFallback(ctx, bgLayer.fallback, width, height);
}
}
} else if (bgLayer?.fallback) {
this._drawFallback(ctx, bgLayer.fallback, width, height);
}
}
/**
* Disegna overlay gradient
*/
_drawOverlay(ctx, overlay, width, height) {
if (overlay.type === 'solid') {
ctx.fillStyle = overlay.color || 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, width, height);
return;
}
// Gradient
let gradient;
const dir = overlay.direction || 'to-bottom';
if (dir === 'to-bottom') {
gradient = ctx.createLinearGradient(0, 0, 0, height);
} else if (dir === 'to-top') {
gradient = ctx.createLinearGradient(0, height, 0, 0);
} else if (dir === 'to-right') {
gradient = ctx.createLinearGradient(0, 0, width, 0);
} else if (dir === 'to-left') {
gradient = ctx.createLinearGradient(width, 0, 0, 0);
} else if (dir === 'to-bottom-right') {
gradient = ctx.createLinearGradient(0, 0, width, height);
} else {
gradient = ctx.createLinearGradient(0, 0, 0, height);
}
if (overlay.stops) {
overlay.stops.forEach(stop => {
gradient.addColorStop(stop.position, stop.color);
});
} else {
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0.7)');
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
/**
* Disegna fallback
*/
_drawFallback(ctx, fallback, width, height) {
if (fallback.type === 'solid') {
ctx.fillStyle = fallback.color || '#333333';
ctx.fillRect(0, 0, width, height);
} else if (fallback.type === 'gradient' && fallback.colors) {
const gradient = ctx.createLinearGradient(0, 0, 0, height);
fallback.colors.forEach((color, i) => {
gradient.addColorStop(i / (fallback.colors.length - 1), color);
});
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
}
/**
* Disegna singolo layer
*/
async _drawLayer(ctx, layer, content, assets, canvasWidth, canvasHeight, template) {
const pos = this._calculatePosition(layer.position, layer.anchor, canvasWidth, canvasHeight);
switch (layer.type) {
case 'backgroundImage':
// Già gestito in _drawBackground
break;
case 'mainImage':
await this._drawMainImage(ctx, assets?.mainImage, pos, layer.style);
break;
case 'title':
this._drawText(ctx, content?.title, pos, layer.style, template.palette);
break;
case 'subtitle':
this._drawText(ctx, content?.subtitle, pos, layer.style, template.palette);
break;
case 'eventDate':
const dateText = content?.eventTime
? `${content.eventDate}${content.eventTime}`
: content?.eventDate;
this._drawText(ctx, dateText, pos, layer.style, template.palette);
break;
case 'eventTime':
this._drawText(ctx, content?.eventTime, pos, layer.style, template.palette);
break;
case 'location':
this._drawTextWithIcon(ctx, content?.location, pos, layer, template.palette);
break;
case 'contacts':
this._drawText(ctx, content?.contacts, pos, layer.style, template.palette);
break;
case 'extraText':
const extraTexts = Array.isArray(content?.extraText)
? content.extraText.join(' • ')
: content?.extraText;
this._drawText(ctx, extraTexts, pos, layer.style, template.palette);
break;
case 'customText':
const customValue = content?.customFields?.get(layer.id);
this._drawText(ctx, customValue, pos, layer.style, template.palette);
break;
case 'divider':
this._drawDivider(ctx, pos, layer.style);
break;
case 'shape':
this._drawShape(ctx, pos, layer.style);
break;
default:
console.warn(`Layer type non gestito: ${layer.type}`);
}
}
/**
* Calcola posizione assoluta da coordinate relative
*/
_calculatePosition(position, anchor, canvasWidth, canvasHeight) {
const relX = position.x || 0;
const relY = position.y || 0;
const relW = position.w || 1;
const relH = position.h || 0.1;
const absW = relW * canvasWidth;
const absH = relH * canvasHeight;
let absX = relX * canvasWidth;
let absY = relY * canvasHeight;
// Aggiusta per anchor
switch (anchor) {
case 'top-center':
absX -= absW / 2;
break;
case 'top-right':
absX -= absW;
break;
case 'center-left':
absY -= absH / 2;
break;
case 'center':
absX -= absW / 2;
absY -= absH / 2;
break;
case 'center-right':
absX -= absW;
absY -= absH / 2;
break;
case 'bottom-left':
absY -= absH;
break;
case 'bottom-center':
absX -= absW / 2;
absY -= absH;
break;
case 'bottom-right':
absX -= absW;
absY -= absH;
break;
// top-left è default, nessun aggiustamento
}
return { x: absX, y: absY, w: absW, h: absH };
}
/**
* Disegna main image
*/
async _drawMainImage(ctx, asset, pos, style = {}) {
if (!asset?.url) return;
try {
const img = await this._loadImageFromUrl(asset.url);
ctx.save();
// Border radius (clip)
const radius = style.borderRadius || 0;
if (radius > 0) {
this._roundRect(ctx, pos.x, pos.y, pos.w, pos.h, radius);
ctx.clip();
}
// Shadow
if (style.shadow?.enabled) {
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.5)';
ctx.shadowBlur = style.shadow.blur || 20;
ctx.shadowOffsetX = style.shadow.offsetX || 0;
ctx.shadowOffsetY = style.shadow.offsetY || 10;
}
// Calcola dimensioni per object-fit
const { sx, sy, sw, sh, dx, dy, dw, dh } = this._calculateObjectFit(
img.width, img.height, pos.w, pos.h, style.objectFit || 'cover'
);
ctx.drawImage(img, sx, sy, sw, sh, pos.x + dx, pos.y + dy, dw, dh);
// Border
if (style.border?.enabled) {
ctx.strokeStyle = style.border.color || '#ffffff';
ctx.lineWidth = style.border.width || 2;
if (radius > 0) {
this._roundRect(ctx, pos.x, pos.y, pos.w, pos.h, radius);
} else {
ctx.strokeRect(pos.x, pos.y, pos.w, pos.h);
}
ctx.stroke();
}
ctx.restore();
} catch (e) {
console.warn('Main image load failed:', e.message);
}
}
/**
* Calcola object-fit
*/
_calculateObjectFit(imgW, imgH, boxW, boxH, fit) {
let sx = 0, sy = 0, sw = imgW, sh = imgH;
let dx = 0, dy = 0, dw = boxW, dh = boxH;
const imgRatio = imgW / imgH;
const boxRatio = boxW / boxH;
if (fit === 'cover') {
if (imgRatio > boxRatio) {
sw = imgH * boxRatio;
sx = (imgW - sw) / 2;
} else {
sh = imgW / boxRatio;
sy = (imgH - sh) / 2;
}
} else if (fit === 'contain') {
if (imgRatio > boxRatio) {
dh = boxW / imgRatio;
dy = (boxH - dh) / 2;
} else {
dw = boxH * imgRatio;
dx = (boxW - dw) / 2;
}
}
// 'fill' usa valori default
return { sx, sy, sw, sh, dx, dy, dw, dh };
}
/**
* Disegna testo
*/
_drawText(ctx, text, pos, style = {}, palette = {}) {
if (!text) return;
ctx.save();
// Font
const fontWeight = style.fontWeight || 400;
const fontSize = style.fontSize || 48;
const fontFamily = style.fontFamily || 'Open Sans';
ctx.font = `${fontWeight} ${fontSize}px "${fontFamily}"`;
// Colore
ctx.fillStyle = style.color || palette.text || '#ffffff';
// Allineamento
const align = style.textAlign || 'center';
ctx.textAlign = align;
ctx.textBaseline = 'middle';
// Transform
let displayText = text;
if (style.textTransform === 'uppercase') {
displayText = text.toUpperCase();
} else if (style.textTransform === 'lowercase') {
displayText = text.toLowerCase();
} else if (style.textTransform === 'capitalize') {
displayText = text.replace(/\b\w/g, c => c.toUpperCase());
}
// Calcola X in base ad allineamento
let textX;
if (align === 'center') {
textX = pos.x + pos.w / 2;
} else if (align === 'right') {
textX = pos.x + pos.w;
} else {
textX = pos.x;
}
const textY = pos.y + pos.h / 2;
// Letter spacing (manuale)
if (style.letterSpacing && style.letterSpacing > 0) {
this._drawTextWithSpacing(ctx, displayText, textX, textY, style, pos);
} else {
// Shadow
if (style.shadow?.enabled) {
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.8)';
ctx.shadowBlur = style.shadow.blur || 10;
ctx.shadowOffsetX = style.shadow.offsetX || 2;
ctx.shadowOffsetY = style.shadow.offsetY || 2;
}
// Stroke
if (style.stroke?.enabled) {
ctx.strokeStyle = style.stroke.color || 'rgba(0,0,0,0.5)';
ctx.lineWidth = style.stroke.width || 2;
ctx.strokeText(displayText, textX, textY);
}
// Fill
ctx.fillText(displayText, textX, textY);
}
ctx.restore();
}
/**
* Disegna testo con letter-spacing
*/
_drawTextWithSpacing(ctx, text, x, y, style, pos) {
const spacing = style.letterSpacing || 0;
const chars = text.split('');
// Calcola larghezza totale
let totalWidth = 0;
chars.forEach(char => {
totalWidth += ctx.measureText(char).width + spacing;
});
totalWidth -= spacing; // Rimuovi ultimo spacing
// Calcola startX in base ad allineamento
let startX;
if (style.textAlign === 'center') {
startX = x - totalWidth / 2;
} else if (style.textAlign === 'right') {
startX = x - totalWidth;
} else {
startX = x;
}
// Shadow
if (style.shadow?.enabled) {
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.8)';
ctx.shadowBlur = style.shadow.blur || 10;
ctx.shadowOffsetX = style.shadow.offsetX || 2;
ctx.shadowOffsetY = style.shadow.offsetY || 2;
}
// Disegna ogni carattere
ctx.textAlign = 'left';
let currentX = startX;
chars.forEach(char => {
if (style.stroke?.enabled) {
ctx.strokeStyle = style.stroke.color || 'rgba(0,0,0,0.5)';
ctx.lineWidth = style.stroke.width || 2;
ctx.strokeText(char, currentX, y);
}
ctx.fillText(char, currentX, y);
currentX += ctx.measureText(char).width + spacing;
});
}
/**
* Disegna testo con icona
*/
_drawTextWithIcon(ctx, text, pos, layer, palette) {
if (!text) return;
const icon = layer.icon;
const style = layer.style || {};
// Se icona abilitata, disegna simbolo prima del testo
if (icon?.enabled) {
ctx.save();
const iconSize = icon.size || 24;
const iconColor = icon.color || palette?.accent || '#e74c3c';
// Disegna simbolo location semplificato
ctx.fillStyle = iconColor;
ctx.font = `${iconSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const iconChar = '📍'; // Emoji o usa font icon
const textWithIcon = `${iconChar} ${text}`;
// Ora disegna testo normale con icona
this._drawText(ctx, textWithIcon, pos, style, palette);
ctx.restore();
} else {
this._drawText(ctx, text, pos, style, palette);
}
}
/**
* Disegna loghi
*/
async _drawLogos(ctx, logoSlots, logos, canvasWidth, canvasHeight) {
const slots = logoSlots.slots || [];
const maxCount = Math.min(logos.length, logoSlots.maxCount || 3, slots.length);
for (let i = 0; i < maxCount; i++) {
const logo = logos[i];
const slot = slots[i];
if (!logo?.url || !slot) continue;
try {
const img = await this._loadImageFromUrl(logo.url);
const pos = this._calculatePosition(slot.position, slot.anchor, canvasWidth, canvasHeight);
ctx.save();
// Opacity
ctx.globalAlpha = slot.style?.opacity ?? 0.9;
// Object fit contain per loghi
const { sx, sy, sw, sh, dx, dy, dw, dh } = this._calculateObjectFit(
img.width, img.height, pos.w, pos.h, 'contain'
);
ctx.drawImage(img, sx, sy, sw, sh, pos.x + dx, pos.y + dy, dw, dh);
ctx.restore();
} catch (e) {
console.warn(`Logo ${i} load failed:`, e.message);
}
}
}
/**
* Disegna divider
*/
_drawDivider(ctx, pos, style = {}) {
ctx.save();
ctx.strokeStyle = style.color || '#ffffff';
ctx.lineWidth = style.width || 2;
ctx.globalAlpha = style.opacity || 0.5;
ctx.beginPath();
ctx.moveTo(pos.x, pos.y + pos.h / 2);
ctx.lineTo(pos.x + pos.w, pos.y + pos.h / 2);
ctx.stroke();
ctx.restore();
}
/**
* Disegna shape
*/
_drawShape(ctx, pos, style = {}) {
ctx.save();
ctx.fillStyle = style.fill || 'rgba(255,255,255,0.1)';
ctx.strokeStyle = style.stroke || 'transparent';
ctx.lineWidth = style.strokeWidth || 0;
ctx.globalAlpha = style.opacity || 1;
const shape = style.shape || 'rectangle';
const radius = style.borderRadius || 0;
if (shape === 'rectangle') {
if (radius > 0) {
this._roundRect(ctx, pos.x, pos.y, pos.w, pos.h, radius);
ctx.fill();
if (style.strokeWidth) ctx.stroke();
} else {
ctx.fillRect(pos.x, pos.y, pos.w, pos.h);
if (style.strokeWidth) ctx.strokeRect(pos.x, pos.y, pos.w, pos.h);
}
} else if (shape === 'circle' || shape === 'ellipse') {
ctx.beginPath();
ctx.ellipse(
pos.x + pos.w / 2,
pos.y + pos.h / 2,
pos.w / 2,
pos.h / 2,
0, 0, Math.PI * 2
);
ctx.fill();
if (style.strokeWidth) ctx.stroke();
}
ctx.restore();
}
/**
* Utility: rounded rectangle
*/
_roundRect(ctx, x, y, w, h, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + w - radius, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
ctx.lineTo(x + w, y + h - radius);
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
ctx.lineTo(x + radius, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
/**
* Utility: carica immagine da URL o path locale
*/
async _loadImageFromUrl(url) {
if (!url) throw new Error('URL mancante');
// Base64
if (url.startsWith('data:')) {
return loadImage(url);
}
// Path locale
if (url.startsWith('/uploads') || url.startsWith('./uploads')) {
const localPath = url.startsWith('/')
? path.join(process.cwd(), url)
: url;
return loadImage(localPath);
}
// URL remoto
if (url.startsWith('http://') || url.startsWith('https://')) {
const fetch = require('node-fetch');
const response = await fetch(url);
const buffer = await response.buffer();
return loadImage(buffer);
}
// Assume path locale
return loadImage(url);
}
/**
* Merge layer con override
*/
_mergeLayerOverride(layer, override) {
if (!override || Object.keys(override).length === 0) {
return layer;
}
return {
...layer,
position: override.position ? { ...layer.position, ...override.position } : layer.position,
visible: override.visible !== undefined ? override.visible : layer.visible,
style: override.style ? { ...layer.style, ...override.style } : layer.style
};
}
/**
* Quick render (semplificato per quick-generate)
*/
async quickRender(options) {
const {
backgroundUrl,
content,
outputPath,
width = 1080,
height = 1920
} = options;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
// Background
if (backgroundUrl) {
try {
const img = await this._loadImageFromUrl(backgroundUrl);
const imgRatio = img.width / img.height;
const canvasRatio = width / height;
let dw, dh, dx, dy;
if (imgRatio > canvasRatio) {
dh = height;
dw = height * imgRatio;
dx = (width - dw) / 2;
dy = 0;
} else {
dw = width;
dh = width / imgRatio;
dx = 0;
dy = (height - dh) / 2;
}
ctx.drawImage(img, dx, dy, dw, dh);
} catch (e) {
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, width, height);
}
}
// Overlay gradient
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(0.4, 'rgba(0,0,0,0.2)');
gradient.addColorStop(0.7, 'rgba(0,0,0,0.6)');
gradient.addColorStop(1, 'rgba(0,0,0,0.85)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
// Title
if (content.title) {
ctx.save();
ctx.font = 'bold 72px "Montserrat", sans-serif';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 15;
ctx.shadowOffsetY = 4;
ctx.fillText(content.title.toUpperCase(), width / 2, height * 0.52);
ctx.restore();
}
// Subtitle
if (content.subtitle) {
ctx.save();
ctx.font = '400 32px "Open Sans", sans-serif';
ctx.fillStyle = '#f0f0f0';
ctx.textAlign = 'center';
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 8;
ctx.fillText(content.subtitle, width / 2, height * 0.60);
ctx.restore();
}
// Date
if (content.eventDate) {
ctx.save();
ctx.font = '400 48px "Bebas Neue", sans-serif';
ctx.fillStyle = '#ffd700';
ctx.textAlign = 'center';
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 10;
const dateText = content.eventTime
? `${content.eventDate} • ORE ${content.eventTime}`
: content.eventDate;
ctx.fillText(dateText.toUpperCase(), width / 2, height * 0.70);
ctx.restore();
}
// Location
if (content.location) {
ctx.save();
ctx.font = '600 28px "Open Sans", sans-serif';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.fillText(`📍 ${content.location}`, width / 2, height * 0.78);
ctx.restore();
}
// Contacts
if (content.contacts) {
ctx.save();
ctx.font = '400 22px "Open Sans", sans-serif';
ctx.fillStyle = '#cccccc';
ctx.textAlign = 'center';
ctx.fillText(content.contacts, width / 2, height * 0.86);
ctx.restore();
}
// Salva
const buffer = canvas.toBuffer('image/png');
await fs.writeFile(outputPath, buffer);
return {
path: outputPath,
size: buffer.length,
dimensions: { width, height }
};
}
}
module.exports = new PosterRenderer();

View File

@@ -1048,6 +1048,7 @@ const MyTelegramBot = {
token_circuito_da_ammettere: token, token_circuito_da_ammettere: token,
nomeTerritorio: mycircuit.name, nomeTerritorio: mycircuit.name,
myusername: userDest, myusername: userDest,
circuitId: mycircuit._id,
}; };
// if (usersmanagers) { // if (usersmanagers) {
// for (const recadminCirc of usersmanagers) { // for (const recadminCirc of usersmanagers) {

File diff suppressed because it is too large Load Diff

View File

@@ -1198,6 +1198,7 @@ module.exports = {
let paramsObj = { let paramsObj = {
usernameDest, usernameDest,
circuitnameDest: circuitname, circuitnameDest: circuitname,
circuitId: myreccircuit ? myreccircuit._id : ''
path, path,
username_action: username_action, username_action: username_action,
singleadmin_username: usernameDest, singleadmin_username: usernameDest,
@@ -6433,4 +6434,5 @@ module.exports = {
// Usa padding di 3 cifre per minor e patch (supporta fino a 999) // Usa padding di 3 cifre per minor e patch (supporta fino a 999)
return major * 1000000 + minor * 1000 + patch; return major * 1000000 + minor * 1000 + patch;
}, },
}; };

View File

@@ -450,6 +450,7 @@ module.exports = {
usernameInvitante: paramsObj.extrarec?.username_admin_abilitante, usernameInvitante: paramsObj.extrarec?.username_admin_abilitante,
nomeTerritorio: paramsObj.circuitnameDest, nomeTerritorio: paramsObj.circuitnameDest,
link_group: paramsObj.extrarec?.link_group, link_group: paramsObj.extrarec?.link_group,
circuitId: paramsObj.circuitId,
}; };
await sendemail.sendEmail_Utente_Abilitato_Circuito_FidoConcesso(usertosend.lang, usertosend.email, usertosend, params.idapp, dati); await sendemail.sendEmail_Utente_Abilitato_Circuito_FidoConcesso(usertosend.lang, usertosend.email, usertosend, params.idapp, dati);
} }

View File

@@ -1283,7 +1283,6 @@ module.exports = {
DASHBOARD: 140, DASHBOARD: 140,
DASHGROUP: 145, DASHGROUP: 145,
MOVEMENTS: 148, MOVEMENTS: 148,
CSENDRISTO: 150,
STATUSREG: 160, STATUSREG: 160,
CHECKIFISLOGGED: 170, CHECKIFISLOGGED: 170,
INFO_VERSION: 180, INFO_VERSION: 180,

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

1134
yarn.lock

File diff suppressed because it is too large Load Diff