// ==UserScript==
// @name Bitcointalk Board Stats
// @namespace http://tampermonkey.net/
// @version 1.5.8
// @description Statistiche board Bitcointalk con grafici interattivi, download immagini, spinner "Dot Wave", sintesi testuale, distribuzione post, e utenti con maggior impatto
// @author Ace
// @match https://bitcointalk.org/*
// @grant GM_xmlhttpRequest
// @grant fetch
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Funzione per caricare Chart.js dinamicamente
async function loadChartJS() {
return new Promise((resolve, reject) => {
if (window.Chart) {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Funzione per aprire l'immagine del grafico in una nuova scheda
function openChartImage(canvasId, chartTitle) {
const canvas = document.getElementById(canvasId);
const image = canvas.toDataURL('image/png');
const newWindow = window.open('', '_blank');
newWindow.document.write(`
<html>
<head>
<title>${chartTitle}</title>
</head>
<body style="text-align: center; padding: 20px;">
<h3>${chartTitle}</h3>
<img src="${image}" style="max-width: 100%; border: 1px solid #ccc;" />
<p>Clicca con il tasto destro sull'immagine e seleziona "Salva immagine con nome" per scaricarla.</p>
</body>
</html>
`);
newWindow.document.close();
}
// Funzione per recuperare il nome di una board
async function fetchBoardName(boardId) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.ninjastic.space/boards`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (data.result !== "success" || !data.data) {
console.error("Errore API o dati mancanti:", data);
resolve(`Board ${boardId}`);
return;
}
// Cerca la board nell'array principale
for (const board of data.data) {
if (board.value == boardId) {
console.log("Board trovata:", board.title);
resolve(board.title || `Board ${boardId}`);
return;
}
// Cerca nelle child boards
if (board.children && board.children.length > 0) {
for (const child of board.children) {
if (child.value == boardId) {
console.log("Child board trovata:", child.title);
resolve(child.title || `Board ${boardId}`);
return;
}
}
}
}
// Se non trovato, restituisci un valore di default
resolve(`Board ${boardId}`);
} catch (err) {
console.error("Errore parsing board name:", err);
resolve(`Board ${boardId}`);
}
},
onerror: function(error) {
console.error("Errore richiesta board name:", error);
resolve(`Board ${boardId}`);
}
});
});
}
// Funzione per recuperare l'elenco delle board
async function fetchAllBoards() {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.ninjastic.space/boards`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (data.result !== "success" || !data.data) {
console.error("Errore API o dati mancanti:", data);
resolve([]);
return;
}
const boards = [];
const processBoards = (boardList, parentName = "") => {
boardList.forEach(board => {
const boardName = board.title || `Board ${board.value}`;
const displayName = parentName ? `${parentName} > ${boardName}` : boardName;
boards.push({
id: board.value,
name: boardName,
displayName: `${board.value} - ${boardName}`
});
if (board.children && board.children.length > 0) {
processBoards(board.children, boardName);
}
});
};
processBoards(data.data);
resolve(boards);
} catch (err) {
console.error("Errore parsing boards:", err);
resolve([]);
}
},
onerror: function(error) {
console.error("Errore richiesta boards:", error);
resolve([]);
}
});
});
}
// Funzione per recuperare le child boards
async function fetchChildBoards(boardId) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.ninjastic.space/boards`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (data.result !== "success") {
throw new Error("Errore API: " + (data.message || "Dati non validi"));
}
const childBoards = [];
const findChildren = (boards) => {
boards.forEach(board => {
if (board.parent === parseInt(boardId)) {
const boardTitle = board.title || `Board ${board.value}`;
childBoards.push({ id: board.value, title: boardTitle });
}
if (board.children && board.children.length > 0) {
findChildren(board.children);
}
});
};
findChildren(data.data);
resolve(childBoards);
} catch (err) {
console.error("Errore parsing child boards:", err);
reject(err);
}
},
onerror: function(error) {
console.error("Errore richiesta child boards:", error);
reject(error);
}
});
});
}
// Funzione per recuperare i dati Merit da più board
async function fetchMeritBatchData(boardIds, startDate, endDate) {
let allSenders = {};
let allReceivers = {};
for (const boardId of boardIds) {
try {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://beta.ninjastic.space/trpc/posts.posts_per_day_histogram,merits.merits_per_day_histogram,merits.top_merit_users,posts.top_users_by_post_count,posts.count_unique_users?batch=1&input=%7B%220%22%3A%7B%22date_min%22%3A%22${startDate}%22%2C%22date_max%22%3A%22${endDate}%22%2C%22board_id%22%3A${boardId}%2C%22interval%22%3A%221d%22%7D%2C%221%22%3A%7B%22date_min%22%3A%22${startDate}%22%2C%22date_max%22%3A%22${endDate}%22%2C%22board_id%22%3A${boardId}%2C%22interval%22%3A%221d%22%7D%2C%222%22%3A%7B%22date_min%22%3A%22${startDate}%22%2C%22date_max%22%3A%22${endDate}%22%2C%22board_id%22%3A${boardId}%7D%2C%223%22%3A%7B%22date_min%22%3A%22${startDate}%22%2C%22date_max%22%3A%22${endDate}%22%2C%22board_id%22%3A${boardId}%7D%2C%224%22%3A%7B%22date_min%22%3A%22${startDate}%22%2C%22date_max%22%3A%22${endDate}%22%2C%22board_id%22%3A${boardId}%7D%7D`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
resolve(data);
} catch (err) {
console.error("Errore parsing dati Merit per board " + boardId + ":", err);
reject(err);
}
},
onerror: function(error) {
console.error("Errore richiesta Merit per board " + boardId + ":", error);
reject(error);
}
});
});
// Estrai i dati corretti dalla risposta batch
const topSenders = response[2].result.data.top_senders || [];
const topReceivers = response[2].result.data.top_receivers || [];
// Aggrega i dati Merit
topSenders.forEach(user => {
if (!allSenders[user.user_uid]) {
allSenders[user.user_uid] = { sum: 0, user: user.user };
}
allSenders[user.user_uid].sum += user.sum;
});
topReceivers.forEach(user => {
if (!allReceivers[user.user_uid]) {
allReceivers[user.user_uid] = { sum: 0, user: user.user };
}
allReceivers[user.user_uid].sum += user.sum;
});
} catch (err) {
console.error("Errore durante il recupero dei dati Merit per board " + boardId + ":", err);
}
}
// Ordina e restituisce le top 10
const topSenders = Object.entries(allSenders)
.sort((a, b) => b[1].sum - a[1].sum)
.slice(0, 10)
.map(([uid, data]) => ({ user_uid: uid, user: data.user, sum: data.sum }));
const topReceivers = Object.entries(allReceivers)
.sort((a, b) => b[1].sum - a[1].sum)
.slice(0, 10)
.map(([uid, data]) => ({ user_uid: uid, user: data.user, sum: data.sum }));
return { sender: topSenders, receiver: topReceivers };
}
// Crea la pagina fittizia delle statistiche
function createStatPage() {
if (document.querySelector('#fake-stat-page')) return;
const page = document.createElement('div');
page.id = 'fake-stat-page';
page.style.position = 'fixed';
page.style.top = '0';
page.style.left = '0';
page.style.width = '100%';
page.style.height = '100%';
page.style.backgroundColor = 'rgba(0,0,0,0.5)';
page.style.zIndex = '9999';
page.style.overflowY = 'auto';
page.style.fontFamily = 'Verdana, Arial, sans-serif';
page.style.fontSize = '14px';
page.innerHTML = `
<div style="
max-width: 950px;
margin: 20px auto;
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
border: 1px solid #ddd;
">
<h2 style="text-align: center; margin-bottom: 20px; color: #2e3b4e;">Statistiche Board</h2>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Board:</label>
<select id="board-select" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; margin-bottom: 10px;">
<option value="">Caricamento board in corso...</option>
</select>
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<span style="margin-right: 10px;">oppure</span>
<input type="text" id="board-id" placeholder="Inserisci ID board manualmente" style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 3px;">
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Mese corrente (inizio):</label>
<input type="datetime-local" id="current-start" value="2025-08-01T00:00:00" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Mese corrente (fine):</label>
<input type="datetime-local" id="current-end" value="2025-08-31T23:59:59" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Mese precedente (inizio):</label>
<input type="datetime-local" id="previous-start" value="2025-07-01T00:00:00" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Mese precedente (fine):</label>
<input type="datetime-local" id="previous-end" value="2025-07-31T23:59:59" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px;">
</div>
<div style="margin-bottom: 20px;">
<label style="display: inline-flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="child-boards" style="margin-right: 8px;">
Includi child boards
</label>
</div>
<button id="generate-stats" style="
width: 100%;
padding: 10px;
font-weight: bold;
background: #2e3b4e;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
">Genera Statistiche</button>
<div id="stats-preview" style="margin-top: 20px; display: none;">
<h3 style="border-bottom: 1px solid #eee; padding-bottom: 5px;">Anteprima Tabella</h3>
<div id="preview-table" style="overflow-x: auto;"></div>
<div id="charts-container" style="margin-top: 20px;"></div>
</div>
<div id="stats-output" style="
margin-top: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 3px;
border: 1px solid #ddd;
min-height: 100px;
display: none;
">
<h3 style="border-bottom: 1px solid #eee; padding-bottom: 5px;">BBCode</h3>
<textarea id="bbcode-output" style="
width: 100%;
height: 300px;
padding: 10px;
font-family: monospace;
border: 1px solid #ccc;
border-radius: 3px;
resize: vertical;
"></textarea>
<button id="copy-bbcode" style="
display: block;
margin: 10px auto 0;
padding: 8px 16px;
background: #2e3b4e;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
">Copia BBCode</button>
</div>
<button id="close-page" style="
display: block;
margin: 20px auto 0;
padding: 8px 16px;
font-weight: bold;
background: #ccc;
border: none;
border-radius: 3px;
cursor: pointer;
">Chiudi</button>
</div>
`;
document.body.appendChild(page);
document.querySelector('#close-page').onclick = () => page.remove();
document.querySelector('#generate-stats').onclick = generateStats;
document.querySelector('#copy-bbcode').onclick = copyBBCode;
// Carica le board nel menu a tendina
fetchAllBoards().then(boards => {
const select = document.querySelector('#board-select');
select.innerHTML = '<option value="">Seleziona una board</option>';
boards.forEach(board => {
const option = document.createElement('option');
option.value = board.id;
option.textContent = board.displayName;
select.appendChild(option);
});
});
}
// Copia BBCode negli appunti
function copyBBCode() {
const textarea = document.querySelector('#bbcode-output');
textarea.select();
try {
navigator.clipboard.writeText(textarea.value)
.then(() => alert('BBCode copiato negli appunti!'))
.catch(() => {
document.execCommand('copy');
alert('BBCode copiato negli appunti!');
});
} catch (err) {
document.execCommand('copy');
alert('BBCode copiato negli appunti!');
}
}
// Ottieni il nome del mese da una data
function getMonthName(dateString) {
const months = ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'];
const date = new Date(dateString);
return months[date.getMonth()];
}
// Crea un grafico a barre
function createBarChart(canvasId, labels, data, title, label) {
const ctx = document.getElementById(canvasId).getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: label,
data: data,
backgroundColor: 'rgba(46, 59, 78, 0.7)',
borderColor: 'rgba(46, 59, 78, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: title,
font: {
size: 14
}
},
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// Crea un grafico a torta
function createPieChart(canvasId, labels, data, title) {
const ctx = document.getElementById(canvasId).getContext('2d');
new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: [
'rgba(46, 59, 78, 0.7)',
'rgba(70, 130, 180, 0.7)',
'rgba(123, 104, 238, 0.7)',
'rgba(50, 205, 50, 0.7)',
'rgba(255, 165, 0, 0.7)',
'rgba(255, 69, 0, 0.7)'
],
borderColor: [
'rgba(46, 59, 78, 1)',
'rgba(70, 130, 180, 1)',
'rgba(123, 104, 238, 1)',
'rgba(50, 205, 50, 1)',
'rgba(255, 165, 0, 1)',
'rgba(255, 69, 0, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: title,
font: {
size: 14
}
},
legend: {
position: 'right'
}
}
}
});
}
// Genera le statistiche con intestazione dinamica e Merit stats
async function generateStats() {
await loadChartJS();
const boardSelect = document.querySelector('#board-select');
const boardIdInput = document.querySelector('#board-id').value.trim();
const boardId = boardSelect.value || boardIdInput;
if (!boardId) {
alert('Seleziona o inserisci una board!');
return;
}
const currentStart = document.querySelector('#current-start').value;
const currentEnd = document.querySelector('#current-end').value;
const previousStart = document.querySelector('#previous-start').value;
const previousEnd = document.querySelector('#previous-end').value;
const childBoards = document.querySelector('#child-boards').checked;
if (!currentStart || !currentEnd || !previousStart || !previousEnd) {
alert('Compila tutti i campi!');
return;
}
// Nomi dei mesi per l'intestazione
const currentMonthName = getMonthName(currentStart);
const previousMonthName = getMonthName(previousStart);
// Recupera il nome della board
const boardName = await fetchBoardName(boardId);
const previewDiv = document.querySelector('#stats-preview');
const outputDiv = document.querySelector('#stats-output');
const chartsContainer = document.querySelector('#charts-container');
previewDiv.style.display = 'none';
outputDiv.style.display = 'none';
chartsContainer.innerHTML = '';
// Spinner di caricamento "Dot Wave"
const loadingMsg = document.createElement('div');
loadingMsg.style.textAlign = 'center';
loadingMsg.style.margin = '20px 0';
loadingMsg.innerHTML = `
<div class="lds-ellipsis">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<p style="color: #666; margin-top: 10px;">Caricamento dati in corso...</p>
<style>
.lds-ellipsis {
display: inline-block;
position: relative;
width: 64px;
height: 64px;
}
.lds-ellipsis div {
position: absolute;
top: 27px;
width: 11px;
height: 11px;
border-radius: 50%;
background: #2e3b4e;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 6px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 6px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 26px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 45px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(19px, 0);
}
}
</style>
`;
previewDiv.parentNode.insertBefore(loadingMsg, previewDiv);
try {
// Fetch dati mese corrente (post)
const currentUrl = `https://api.ninjastic.space/posts/authors?board=${boardId}&child_boards=${childBoards}&after_date=${currentStart}&before_date=${currentEnd}&limit=1000`;
const currentRes = await fetch(currentUrl);
const currentJson = await currentRes.json();
// Fetch dati mese precedente (post)
const previousUrl = `https://api.ninjastic.space/posts/authors?board=${boardId}&child_boards=${childBoards}&after_date=${previousStart}&before_date=${previousEnd}&limit=1000`;
const previousRes = await fetch(previousUrl);
const previousJson = await previousRes.json();
if (currentJson.result !== "success" || previousJson.result !== "success") {
loadingMsg.innerHTML = `<p style="color: red; text-align: center;">Errore API: ${currentJson.message || previousJson.message || "Sconosciuto"}</p>`;
return;
}
const currentAuthors = currentJson.data.authors;
const previousAuthors = previousJson.data.authors;
// Crea mappa post mese precedente (author_uid -> count)
const previousPostsMap = {};
previousAuthors.forEach(a => {
previousPostsMap[a.author_uid] = a.count || 0;
});
// Unisci e ordina gli utenti per post del mese corrente
const allAuthors = [...currentAuthors];
allAuthors.sort((a, b) => (b.count || 0) - (a.count || 0));
// Calcola i totali
const totalPostsCurrent = currentAuthors.reduce((sum, a) => sum + (a.count || 0), 0);
const boardIds = childBoards ? [boardId, ...(await fetchChildBoards(boardId)).map(b => b.id)] : [boardId];
const meritData = await fetchMeritBatchData(
boardIds,
currentStart.split('T')[0],
currentEnd.split('T')[0]
);
const totalMeritSent = meritData.sender.reduce((sum, u) => sum + u.sum, 0);
const totalMeritReceived = meritData.receiver.reduce((sum, u) => sum + u.sum, 0);
const avgMeritPerPost = totalPostsCurrent > 0 ? (totalMeritReceived / totalPostsCurrent).toFixed(2) : 0;
// Distribuzione post per child board (se attivate)
let childBoardDistribution = {};
if (childBoards) {
const childBoardsList = await fetchChildBoards(boardId);
for (const child of childBoardsList) {
const childUrl = `https://api.ninjastic.space/posts/authors?board=${child.id}&after_date=${currentStart}&before_date=${currentEnd}&limit=1000`;
const childRes = await fetch(childUrl);
const childJson = await childRes.json();
if (childJson.result === "success") {
const childPosts = childJson.data.authors.reduce((sum, a) => sum + (a.count || 0), 0);
childBoardDistribution[child.title] = childPosts;
}
}
}
// Anteprima tabella HTML (post)
let previewTable = `
<table style="width: 100%; border-collapse: collapse; margin-bottom: 15px; font-size: 13px;">
<tr style="background: #f0f0f0;">
<th style="padding: 8px; text-align: center; border: 1px solid #ddd; width: 5%;">Pos.</th>
<th style="padding: 8px; text-align: left; border: 1px solid #ddd; width: 25%;">User</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd; width: 10%;">Post (${currentMonthName})</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd; width: 10%;">Post (${previousMonthName})</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd; width: 10%;">Change</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd; width: 10%;">BPIP</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd; width: 10%;">Ninjastic</th>
</tr>
`;
allAuthors.forEach((a, index) => {
const username = a.author || 'Sconosciuto';
const userUid = a.author_uid || '';
const currentCount = a.count || 0;
const previousCount = previousPostsMap[userUid] || 0;
const diff = currentCount - previousCount;
const variationColor = diff >= 0 ? 'green' : 'red';
const variationText = diff >= 0 ? `▲${diff}` : `▼${Math.abs(diff)}`;
const userLink = userUid ? `<a href="https://bitcointalk.org/index.php?action=profile;u=${userUid}" target="_blank">${username}</a>` : username;
previewTable += `
<tr style="border: 1px solid #ddd;">
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${index + 1}.</td>
<td style="padding: 8px; border: 1px solid #ddd;">${userLink}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${currentCount}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${previousCount}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center; color: ${variationColor};">${variationText}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://bpip.org/Profile?p=${username}" target="_blank">
<img src="https://www.talkimg.com/images/2023/08/03/GXlZb.png" width="15">
</a>
</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://ninjastic.space/user/${username}" target="_blank">
<img src="https://talkimg.com/images/2023/08/03/GwdMz.png" width="15">
</a>
</td>
</tr>
`;
});
previewTable += `</table>`;
// BBCode con intestazione dinamica (post)
let bbcode = `[center][b][size=12pt]Statistiche Board[/size][/b][/center]
[center][i]Confronto: ${previousMonthName} vs ${currentMonthName}[/i][/center]
[table]
[tr]
[td][b]Pos.[/b][/td]
[td][b]User[/b][/td]
[td][b]Post (${currentMonthName})[/b][/td]
[td][b]Post (${previousMonthName})[/b][/td]
[td][b]Change[/b][/td]
[td][b]BPIP[/b][/td]
[td][b]Ninjastic[/b][/td]
[/tr]
`;
allAuthors.forEach((a, index) => {
const username = a.author || 'Sconosciuto';
const userUid = a.author_uid || '';
const currentCount = a.count || 0;
const previousCount = previousPostsMap[userUid] || 0;
const diff = currentCount - previousCount;
const variationColor = diff >= 0 ? 'green' : 'red';
const variationText = diff >= 0 ? `[color=${variationColor}]▲${diff}[/color]` : `[color=${variationColor}]▼${Math.abs(diff)}[/color]`;
const userLink = userUid ? `[url=https://bitcointalk.org/index.php?action=profile;u=${userUid}]${username}[/url]` : username;
bbcode += `[tr]
[td]${index + 1}.[/td]
[td]${userLink}[/td]
[td]${currentCount}[/td]
[td]${previousCount}[/td]
[td]${variationText}[/td]
[td][url=https://bpip.org/Profile?p=${username}][img width=15]https://www.talkimg.com/images/2023/08/03/GXlZb.png[/img][/url][/td]
[td][url=https://ninjastic.space/user/${username}][img width=15]https://talkimg.com/images/2023/08/03/GwdMz.png[/img][/url][/td]
[/tr]
`;
});
bbcode += `[/table]`;
// Analizza la distribuzione dei post per utente
const postDistribution = {
"1 post": 0,
"2-5 post": 0,
"6-10 post": 0,
"11-20 post": 0,
"21-50 post": 0,
">50 post": 0
};
allAuthors.forEach(author => {
const postCount = author.count || 0;
if (postCount === 1) postDistribution["1 post"]++;
else if (postCount >= 2 && postCount <= 5) postDistribution["2-5 post"]++;
else if (postCount >= 6 && postCount <= 10) postDistribution["6-10 post"]++;
else if (postCount >= 11 && postCount <= 20) postDistribution["11-20 post"]++;
else if (postCount >= 21 && postCount <= 50) postDistribution["21-50 post"]++;
else if (postCount > 50) postDistribution[">50 post"]++;
});
// Genera la tabella HTML per la distribuzione dei post
let postDistributionPreview = `
<div style="margin-top: 20px;">
<h4 style="text-align: center; margin-bottom: 10px;">Distribuzione Post per Utente (${currentMonthName})</h4>
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<tr style="background: #f0f0f0;">
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Intervallo Post</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Num. Utenti</th>
</tr>
`;
Object.entries(postDistribution).forEach(([range, count]) => {
postDistributionPreview += `
<tr style="border: 1px solid #ddd;">
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${range}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${count}</td>
</tr>
`;
});
postDistributionPreview += `</table></div>`;
// Genera il BBCode per la distribuzione dei post
let postDistributionBBCode = `
[center][b]Distribuzione Post per Utente (${currentMonthName})[/b][/center]
[table]
[tr]
[td][b]Intervallo Post[/b][/td]
[td][b]Num. Utenti[/b][/td]
[/tr]
`;
Object.entries(postDistribution).forEach(([range, count]) => {
postDistributionBBCode += `
[tr]
[td]${range}[/td]
[td]${count}[/td]
[/tr]
`;
});
postDistributionBBCode += `[/table]`;
// Anteprima tabelle Merit (affiancate)
let meritPreview = `
<div style="display: flex; gap: 20px; margin-top: 20px;">
<div style="flex: 1;">
<h4 style="text-align: center; margin-bottom: 10px;">Top 10 Merit Sender</h4>
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<tr style="background: #f0f0f0;">
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Pos.</th>
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">User</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Merit Inviati</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">BPIP</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Ninjastic</th>
</tr>
`;
meritData.sender.slice(0, 10).forEach((user, index) => {
meritPreview += `
<tr style="border: 1px solid #ddd;">
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${index + 1}.</td>
<td style="padding: 8px; border: 1px solid #ddd;">
<a href="https://bitcointalk.org/index.php?action=profile;u=${user.user_uid}" target="_blank">${user.user}</a>
</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${user.sum}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://bpip.org/Profile?p=${user.user}" target="_blank">
<img src="https://www.talkimg.com/images/2023/08/03/GXlZb.png" width="15">
</a>
</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://ninjastic.space/user/${user.user}" target="_blank">
<img src="https://talkimg.com/images/2023/08/03/GwdMz.png" width="15">
</a>
</td>
</tr>
`;
});
meritPreview += `</table></div>`;
// Tabella Merit Receiver
meritPreview += `
<div style="flex: 1;">
<h4 style="text-align: center; margin-bottom: 10px;">Top 10 Merit Receiver</h4>
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<tr style="background: #f0f0f0;">
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Pos.</th>
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">User</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Merit Ricevuti</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">BPIP</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Ninjastic</th>
</tr>
`;
meritData.receiver.slice(0, 10).forEach((user, index) => {
meritPreview += `
<tr style="border: 1px solid #ddd;">
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${index + 1}.</td>
<td style="padding: 8px; border: 1px solid #ddd;">
<a href="https://bitcointalk.org/index.php?action=profile;u=${user.user_uid}" target="_blank">${user.user}</a>
</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${user.sum}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://bpip.org/Profile?p=${user.user}" target="_blank">
<img src="https://www.talkimg.com/images/2023/08/03/GXlZb.png" width="15">
</a>
</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://ninjastic.space/user/${user.user}" target="_blank">
<img src="https://talkimg.com/images/2023/08/03/GwdMz.png" width="15">
</a>
</td>
</tr>
`;
});
meritPreview += `</table></div></div>`;
// BBCode per Merit (affiancate)
let meritBBCode = `
[table]
[tr]
[td][center][b]Top 10 Merit Sender[/b][/center][/td]
[td][center][b]Top 10 Merit Receiver[/b][/center][/td]
[/tr]
[tr]
[td]
[table]
[tr]
[td][b]Pos.[/b][/td]
[td][b]User[/b][/td]
[td][b]Merit Inviati[/b][/td]
[td][b]BPIP[/b][/td]
[td][b]Ninjastic[/b][/td]
[/tr]
`;
meritData.sender.slice(0, 10).forEach((user, index) => {
meritBBCode += `
[tr]
[td]${index + 1}.[/td]
[td][url=https://bitcointalk.org/index.php?action=profile;u=${user.user_uid}]${user.user}[/url][/td]
[td]${user.sum}[/td]
[td][url=https://bpip.org/Profile?p=${user.user}][img width=15]https://www.talkimg.com/images/2023/08/03/GXlZb.png[/img][/url][/td]
[td][url=https://ninjastic.space/user/${user.user}][img width=15]https://talkimg.com/images/2023/08/03/GwdMz.png[/img][/url][/td]
[/tr]
`;
});
meritBBCode += `
[/table]
[/td]
[td]
[table]
[tr]
[td][b]Pos.[/b][/td]
[td][b]User[/b][/td]
[td][b]Merit Ricevuti[/b][/td]
[td][b]BPIP[/b][/td]
[td][b]Ninjastic[/b][/td]
[/tr]
`;
meritData.receiver.slice(0, 10).forEach((user, index) => {
meritBBCode += `
[tr]
[td]${index + 1}.[/td]
[td][url=https://bitcointalk.org/index.php?action=profile;u=${user.user_uid}]${user.user}[/url][/td]
[td]${user.sum}[/td]
[td][url=https://bpip.org/Profile?p=${user.user}][img width=15]https://www.talkimg.com/images/2023/08/03/GXlZb.png[/img][/url][/td]
[td][url=https://ninjastic.space/user/${user.user}][img width=15]https://talkimg.com/images/2023/08/03/GwdMz.png[/img][/url][/td]
[/tr]
`;
});
meritBBCode += `
[/table]
[/td]
[/tr]
[/table]
`;
// Unisci i dati dei post e dei merit per calcolare il rateo
const userRateoMap = {};
allAuthors.forEach(author => {
const userUid = author.author_uid;
const postCount = author.count || 0;
const meritUser = meritData.receiver.find(u => u.user_uid == userUid);
const meritCount = meritUser ? meritUser.sum : 0;
const rateo = postCount > 0 ? (meritCount / postCount).toFixed(2) : 0;
userRateoMap[userUid] = {
user: author.author,
userUid,
postCount,
meritCount,
rateo: parseFloat(rateo)
};
});
// Ordina per rateo e prendi i primi 10 (filtra utenti con almeno 1 post)
const topRateoUsers = Object.values(userRateoMap)
.filter(user => user.postCount > 0)
.sort((a, b) => b.rateo - a.rateo)
.slice(0, 10);
// Genera la tabella HTML per il rateo
let rateoPreview = `
<div style="margin-top: 20px;">
<h4 style="text-align: center; margin-bottom: 10px;">Top 10 Utenti per Rateo Merit/Post</h4>
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<tr style="background: #f0f0f0;">
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Pos.</th>
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">User</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Post</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Merit Ricevuti</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Rateo</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">BPIP</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Ninjastic</th>
</tr>
`;
topRateoUsers.forEach((user, index) => {
rateoPreview += `
<tr style="border: 1px solid #ddd;">
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${index + 1}.</td>
<td style="padding: 8px; border: 1px solid #ddd;">
<a href="https://bitcointalk.org/index.php?action=profile;u=${user.userUid}" target="_blank">${user.user}</a>
</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${user.postCount}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${user.meritCount}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${user.rateo}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://bpip.org/Profile?p=${user.user}" target="_blank">
<img src="https://www.talkimg.com/images/2023/08/03/GXlZb.png" width="15">
</a>
</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://ninjastic.space/user/${user.user}" target="_blank">
<img src="https://talkimg.com/images/2023/08/03/GwdMz.png" width="15">
</a>
</td>
</tr>
`;
});
rateoPreview += `</table></div>`;
// Genera il BBCode per il rateo
let rateoBBCode = `
[center][b]Top 10 Utenti per Rateo Merit/Post[/b][/center]
[table]
[tr]
[td][b]Pos.[/b][/td]
[td][b]User[/b][/td]
[td][b]Post[/b][/td]
[td][b]Merit Ricevuti[/b][/td]
[td][b]Rateo[/b][/td]
[td][b]BPIP[/b][/td]
[td][b]Ninjastic[/b][/td]
[/tr]
`;
topRateoUsers.forEach((user, index) => {
rateoBBCode += `
[tr]
[td]${index + 1}.[/td]
[td][url=https://bitcointalk.org/index.php?action=profile;u=${user.userUid}]${user.user}[/url][/td]
[td]${user.postCount}[/td]
[td]${user.meritCount}[/td]
[td]${user.rateo}[/td]
[td][url=https://bpip.org/Profile?p=${user.user}][img width=15]https://www.talkimg.com/images/2023/08/03/GXlZb.png[/img][/url][/td]
[td][url=https://ninjastic.space/user/${user.user}][img width=15]https://talkimg.com/images/2023/08/03/GwdMz.png[/img][/url][/td]
[/tr]
`;
});
rateoBBCode += `[/table]`;
// Calcola gli utenti con il maggior impatto (Post + Merit)
const userImpactMap = {};
allAuthors.forEach(author => {
const userUid = author.author_uid;
const postCount = author.count || 0;
const meritUser = meritData.receiver.find(u => u.user_uid == userUid);
const meritCount = meritUser ? meritUser.sum : 0;
// Calcola il punteggio di impatto: (post * 0.5) + (merit * 1.5)
const impactScore = (postCount * 0.5) + (meritCount * 1.5);
userImpactMap[userUid] = {
user: author.author,
userUid,
postCount,
meritCount,
impactScore: impactScore.toFixed(2)
};
});
// Ordina per impatto e prendi i primi 10
const topImpactUsers = Object.values(userImpactMap)
.filter(user => user.postCount > 0 || user.meritCount > 0)
.sort((a, b) => parseFloat(b.impactScore) - parseFloat(a.impactScore))
.slice(0, 10);
// Genera la tabella HTML per l'impatto
let impactPreview = `
<div style="margin-top: 20px;">
<h4 style="text-align: center; margin-bottom: 10px;">Top 10 Utenti per Impatto (Post + Merit)</h4>
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<tr style="background: #f0f0f0;">
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Pos.</th>
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">User</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Post</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Merit Ricevuti</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Impatto</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">BPIP</th>
<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Ninjastic</th>
</tr>
`;
topImpactUsers.forEach((user, index) => {
impactPreview += `
<tr style="border: 1px solid #ddd;">
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${index + 1}.</td>
<td style="padding: 8px; border: 1px solid #ddd;">
<a href="https://bitcointalk.org/index.php?action=profile;u=${user.userUid}" target="_blank">${user.user}</a>
</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${user.postCount}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${user.meritCount}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">${user.impactScore}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://bpip.org/Profile?p=${user.user}" target="_blank">
<img src="https://www.talkimg.com/images/2023/08/03/GXlZb.png" width="15">
</a>
</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<a href="https://ninjastic.space/user/${user.user}" target="_blank">
<img src="https://talkimg.com/images/2023/08/03/GwdMz.png" width="15">
</a>
</td>
</tr>
`;
});
impactPreview += `</table></div>`;
// Genera il BBCode per l'impatto
let impactBBCode = `
[center][b]Top 10 Utenti per Impatto (Post + Merit)[/b][/center]
[table]
[tr]
[td][b]Pos.[/b][/td]
[td][b]User[/b][/td]
[td][b]Post[/b][/td]
[td][b]Merit Ricevuti[/b][/td]
[td][b]Impatto[/b][/td]
[td][b]BPIP[/b][/td]
[td][b]Ninjastic[/b][/td]
[/tr]
`;
topImpactUsers.forEach((user, index) => {
impactBBCode += `
[tr]
[td]${index + 1}.[/td]
[td][url=https://bitcointalk.org/index.php?action=profile;u=${user.userUid}]${user.user}[/url][/td]
[td]${user.postCount}[/td]
[td]${user.meritCount}[/td]
[td]${user.impactScore}[/td]
[td][url=https://bpip.org/Profile?p=${user.user}][img width=15]https://www.talkimg.com/images/2023/08/03/GXlZb.png[/img][/url][/td]
[td][url=https://ninjastic.space/user/${user.user}][img width=15]https://talkimg.com/images/2023/08/03/GwdMz.png[/img][/url][/td]
[/tr]
`;
});
impactBBCode += `[/table]`;
// Genera una sintesi testuale dei dati
let summaryText = `
<div style="margin-top: 20px; padding: 15px; background: #f9f9f9; border-radius: 5px; border: 1px solid #ddd;">
<h4 style="text-align: center; margin-bottom: 15px; color: #2e3b4e;">Sintesi degli Andamenti</h4>
<p style="margin-bottom: 10px;"><strong>Periodo analizzato:</strong> ${currentMonthName} (vs ${previousMonthName})</p>
<p style="margin-bottom: 10px;"><strong>Board:</strong> ${boardName} (ID: ${boardId}) ${childBoards ? "(incluse child boards)" : ""}</p>
<p style="margin-bottom: 10px;">
<strong>Post totali:</strong> ${totalPostsCurrent} (${currentMonthName})<br>
<strong>Merit totali inviati (solo nella board):</strong> ${totalMeritSent}<br>
<strong>Merit totali ricevuti (solo nella board):</strong> ${totalMeritReceived}<br>
<strong>Media Merit/Post:</strong> ${avgMeritPerPost}
</p>
${childBoards ?
`<p style="margin-bottom: 10px;"><strong>Distribuzione Post per Child Board:</strong></p>
<ul style="margin-bottom: 15px; padding-left: 20px;">
${Object.entries(childBoardDistribution).map(([title, posts]) =>
`<li><strong>${title}:</strong> ${posts} post</li>`
).join('')}
</ul>` : ''}
<p style="margin-bottom: 10px;"><strong>Utenti attivi:</strong> ${allAuthors.length} (di cui ${allAuthors.filter(a => (a.count || 0) >= 2).length} con almeno 2 post in ${currentMonthName})</p>
<h5 style="margin: 15px 0 10px 0; color: #2e3b4e;">Top Poster</h5>
<ul style="margin-bottom: 15px; padding-left: 20px;">
<li><strong>1°:</strong> ${allAuthors[0]?.author || "Nessuno"} (${allAuthors[0]?.count || 0} post in ${currentMonthName})</li>
<li><strong>2°:</strong> ${allAuthors[1]?.author || "Nessuno"} (${allAuthors[1]?.count || 0} post)</li>
<li><strong>3°:</strong> ${allAuthors[2]?.author || "Nessuno"} (${allAuthors[2]?.count || 0} post)</li>
</ul>
<h5 style="margin: 15px 0 10px 0; color: #2e3b4e;">Top Merit Receiver</h5>
<ul style="margin-bottom: 15px; padding-left: 20px;">
<li><strong>1°:</strong> ${meritData.receiver[0]?.user || "Nessuno"} (${meritData.receiver[0]?.sum || 0} merit)</li>
<li><strong>2°:</strong> ${meritData.receiver[1]?.user || "Nessuno"} (${meritData.receiver[1]?.sum || 0} merit)</li>
<li><strong>3°:</strong> ${meritData.receiver[2]?.user || "Nessuno"} (${meritData.receiver[2]?.sum || 0} merit)</li>
</ul>
<h5 style="margin: 15px 0 10px 0; color: #2e3b4e;">Distribuzione Post</h5>
<p style="margin-bottom: 10px;">
${postDistribution["1 post"]} utenti con 1 post,<br>
${postDistribution["2-5 post"]} utenti con 2-5 post,<br>
${postDistribution["6-10 post"]} utenti con 6-10 post,<br>
${postDistribution[">50 post"]} utenti con più di 50 post.
</p>
<h5 style="margin: 15px 0 10px 0; color: #2e3b4e;">Utenti con Maggior Impatto</h5>
<ul style="margin-bottom: 15px; padding-left: 20px;">
<li><strong>1°:</strong> ${topImpactUsers[0]?.user || "Nessuno"} (Impatto: ${topImpactUsers[0]?.impactScore || 0})</li>
<li><strong>2°:</strong> ${topImpactUsers[1]?.user || "Nessuno"} (Impatto: ${topImpactUsers[1]?.impactScore || 0})</li>
<li><strong>3°:</strong> ${topImpactUsers[2]?.user || "Nessuno"} (Impatto: ${topImpactUsers[2]?.impactScore || 0})</li>
</ul>
<h5 style="margin: 15px 0 10px 0; color: #2e3b4e;">Rateo Merit/Post</h5>
<p style="margin-bottom: 10px;">
${topRateoUsers[0]?.user || "Nessuno"} ha il rateo più alto: ${topRateoUsers[0]?.rateo || 0} merit per post.
</p>
</div>
`;
// Genera il BBCode per la sintesi
let summaryBBCode = `
[center][b][size=12pt]Sintesi degli Andamenti[/size][/b][/center]
[b]Periodo analizzato:[/b] ${currentMonthName} (vs ${previousMonthName})
[b]Board:[/b] ${boardName} (ID: ${boardId}) ${childBoards ? "(incluse child boards)" : ""}
[b]Post totali:[/b] ${totalPostsCurrent} (${currentMonthName})
[b]Merit totali inviati (solo nella board):[/b] ${totalMeritSent}
[b]Merit totali ricevuti (solo nella board):[/b] ${totalMeritReceived}
[b]Media Merit/Post:[/b] ${avgMeritPerPost}
${childBoards ?
`[center][b]Distribuzione Post per Child Board[/b][/center]
[list]
${Object.entries(childBoardDistribution).map(([title, posts]) =>
`[*]${title}: ${posts} post`
).join('\n')}
[/list]` : ''}
[b]Utenti attivi:[/b] ${allAuthors.length} (di cui ${allAuthors.filter(a => (a.count || 0) >= 2).length} con almeno 2 post in ${currentMonthName})
[center][b]Top Poster[/b][/center]
[list]
[*]1°: ${allAuthors[0]?.author || "Nessuno"} (${allAuthors[0]?.count || 0} post in ${currentMonthName})
[*]2°: ${allAuthors[1]?.author || "Nessuno"} (${allAuthors[1]?.count || 0} post)
[*]3°: ${allAuthors[2]?.author || "Nessuno"} (${allAuthors[2]?.count || 0} post)
[/list]
[center][b]Top Merit Receiver[/b][/center]
[list]
[*]1°: ${meritData.receiver[0]?.user || "Nessuno"} (${meritData.receiver[0]?.sum || 0} merit)
[*]2°: ${meritData.receiver[1]?.user || "Nessuno"} (${meritData.receiver[1]?.sum || 0} merit)
[*]3°: ${meritData.receiver[2]?.user || "Nessuno"} (${meritData.receiver[2]?.sum || 0} merit)
[/list]
[center][b]Distribuzione Post[/b][/center]
${postDistribution["1 post"]} utenti con 1 post
${postDistribution["2-5 post"]} utenti con 2-5 post
${postDistribution["6-10 post"]} utenti con 6-10 post
${postDistribution[">50 post"]} utenti con più di 50 post
[center][b]Utenti con Maggior Impatto[/b][/center]
[list]
[*]1°: ${topImpactUsers[0]?.user || "Nessuno"} (Impatto: ${topImpactUsers[0]?.impactScore || 0})
[*]2°: ${topImpactUsers[1]?.user || "Nessuno"} (Impatto: ${topImpactUsers[1]?.impactScore || 0})
[*]3°: ${topImpactUsers[2]?.user || "Nessuno"} (Impatto: ${topImpactUsers[2]?.impactScore || 0})
[/list]
[center][b]Rateo Merit/Post[/b][/center]
${topRateoUsers[0]?.user || "Nessuno"} ha il rateo più alto: ${topRateoUsers[0]?.rateo || 0} merit per post
`;
// Aggiungi i grafici con pulsanti di download
chartsContainer.innerHTML = `
<div style="margin-top: 20px;">
<h4 style="text-align: center; margin-bottom: 10px;">Distribuzione Post per Utente</h4>
<canvas id="postDistributionChart" style="max-height: 300px;"></canvas>
<button id="downloadPostDistributionChart"
style="display: block; margin: 10px auto; padding: 5px 10px; background: #2e3b4e; color: white; border: none; border-radius: 3px; cursor: pointer;">
Apri Grafico
</button>
</div>
<div style="display: flex; gap: 20px; margin-top: 20px;">
<div style="flex: 1;">
<h4 style="text-align: center; margin-bottom: 10px;">Top 10 Merit Sender</h4>
<canvas id="topSenderChart" style="max-height: 300px;"></canvas>
<button id="downloadTopSenderChart"
style="display: block; margin: 10px auto; padding: 5px 10px; background: #2e3b4e; color: white; border: none; border-radius: 3px; cursor: pointer;">
Apri Grafico
</button>
</div>
<div style="flex: 1;">
<h4 style="text-align: center; margin-bottom: 10px;">Top 10 Merit Receiver</h4>
<canvas id="topReceiverChart" style="max-height: 300px;"></canvas>
<button id="downloadTopReceiverChart"
style="display: block; margin: 10px auto; padding: 5px 10px; background: #2e3b4e; color: white; border: none; border-radius: 3px; cursor: pointer;">
Apri Grafico
</button>
</div>
</div>
<div style="margin-top: 20px;">
<h4 style="text-align: center; margin-bottom: 10px;">Top 10 Utenti per Rateo Merit/Post</h4>
<canvas id="topRateoChart" style="max-height: 300px;"></canvas>
<button id="downloadTopRateoChart"
style="display: block; margin: 10px auto; padding: 5px 10px; background: #2e3b4e; color: white; border: none; border-radius: 3px; cursor: pointer;">
Apri Grafico
</button>
</div>
<div style="margin-top: 20px;">
<h4 style="text-align: center; margin-bottom: 10px;">Top 10 Utenti per Impatto</h4>
<canvas id="topImpactChart" style="max-height: 300px;"></canvas>
<button id="downloadTopImpactChart"
style="display: block; margin: 10px auto; padding: 5px 10px; background: #2e3b4e; color: white; border: none; border-radius: 3px; cursor: pointer;">
Apri Grafico
</button>
</div>
`;
// Crea i grafici
setTimeout(() => {
// Distribuzione post (torta)
createPieChart(
'postDistributionChart',
Object.keys(postDistribution),
Object.values(postDistribution),
`Distribuzione Post per Utente (${currentMonthName})`
);
// Top Merit Sender (barre)
createBarChart(
'topSenderChart',
meritData.sender.slice(0, 10).map(u => u.user),
meritData.sender.slice(0, 10).map(u => u.sum),
'Top 10 Merit Sender',
'Merit Inviati'
);
// Top Merit Receiver (barre)
createBarChart(
'topReceiverChart',
meritData.receiver.slice(0, 10).map(u => u.user),
meritData.receiver.slice(0, 10).map(u => u.sum),
'Top 10 Merit Receiver',
'Merit Ricevuti'
);
// Top Rateo (barre)
createBarChart(
'topRateoChart',
topRateoUsers.map(u => u.user),
topRateoUsers.map(u => u.rateo),
'Top 10 Utenti per Rateo Merit/Post',
'Rateo'
);
// Top Impatto (barre)
createBarChart(
'topImpactChart',
topImpactUsers.map(u => u.user),
topImpactUsers.map(u => parseFloat(u.impactScore)),
'Top 10 Utenti per Impatto',
'Impatto'
);
// Aggiungi gli event listener ai pulsanti di download
document.getElementById('downloadPostDistributionChart').addEventListener('click', () => {
openChartImage('postDistributionChart', `Distribuzione_Post_per_Utente_${currentMonthName}`);
});
document.getElementById('downloadTopSenderChart').addEventListener('click', () => {
openChartImage('topSenderChart', 'Top_10_Merit_Sender');
});
document.getElementById('downloadTopReceiverChart').addEventListener('click', () => {
openChartImage('topReceiverChart', 'Top_10_Merit_Receiver');
});
document.getElementById('downloadTopRateoChart').addEventListener('click', () => {
openChartImage('topRateoChart', 'Top_10_Utenti_per_Rateo_Merit_Post');
});
document.getElementById('downloadTopImpactChart').addEventListener('click', () => {
openChartImage('topImpactChart', 'Top_10_Utenti_per_Impatto');
});
}, 500);
// Aggiungi la sintesi all'anteprima e al BBCode
previewTable = summaryText + previewTable;
bbcode = summaryBBCode + `\n\n` + bbcode;
// Mostra anteprima e BBCode
loadingMsg.remove();
previewDiv.style.display = 'block';
outputDiv.style.display = 'block';
document.querySelector('#preview-table').innerHTML = previewTable + postDistributionPreview + meritPreview + rateoPreview + impactPreview;
document.querySelector('#bbcode-output').value = bbcode + `\n\n${postDistributionBBCode}\n\n${meritBBCode}\n\n${rateoBBCode}\n\n${impactBBCode}`;
} catch (err) {
console.error("Errore generale durante il recupero dei dati:", err);
loadingMsg.innerHTML = '<p style="color: red; text-align: center;">Errore durante il caricamento dati. Vedi console per dettagli.</p>';
}
}
// Aggiunge pulsante Stat nella navbar
function addStatButton() {
const navbar = document.querySelector('table[style*="margin-left: 10px;"]');
if (!navbar) {
setTimeout(addStatButton, 1000);
return;
}
const lastCell = navbar.querySelector('td.maintab_last');
if (lastCell.querySelector('#stat-button')) return;
const statButton = document.createElement('td');
statButton.id = 'stat-button';
statButton.className = 'maintab_back';
statButton.innerHTML = `<a href="javascript:void(0)" class="maintab_link" style="padding: 0 10px;">Stats</a>`;
statButton.querySelector('a').onclick = createStatPage;
navbar.querySelector('tr').insertBefore(statButton, lastCell);
}
window.addEventListener('load', () => setTimeout(addStatButton, 1000));
window.copyBBCode = copyBBCode;
window.createStatPage = createStatPage;
})();