// ==UserScript==
// @name TruckersMP Report Page Enhancer
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Enhances the TruckersMP report page with better visualization and statistics
// @author NoobFly
// @match https://truckersmp.com/reports
// @grant GM_addStyle
// @license GNU GPLv3
// @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js
// ==/UserScript==
(function() {
'use strict';
// Add custom CSS
GM_addStyle(`
.tm-enhanced-container {
margin: 20px 0;
padding: 20px;
background-color: #333;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.tm-dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.tm-card {
background-color: #444;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.tm-card h3 {
margin-top: 0;
border-bottom: 1px solid #555;
padding-bottom: 10px;
color: #72c02c;
}
.tm-stats {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.tm-stat-item {
background-color: #555;
border-radius: 4px;
padding: 10px;
flex: 1;
min-width: 120px;
text-align: center;
}
.tm-stat-value {
font-size: 24px;
font-weight: bold;
color: #72c02c;
}
.tm-stat-label {
font-size: 14px;
color: #ccc;
}
.tm-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.tm-table th, .tm-table td {
border: 1px solid #555;
padding: 8px 12px;
text-align: left;
}
.tm-table th {
background-color: #505050;
color: #fff;
}
.tm-table tr:nth-child(even) {
background-color: #3a3a3a;
}
.tm-chart-container {
height: 300px;
margin-top: 15px;
}
.tm-loading {
text-align: center;
padding: 20px;
font-size: 18px;
color: #ccc;
}
.tm-filter-bar {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.tm-filter-btn {
background-color: #444;
border: 1px solid #555;
color: white;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
}
.tm-filter-btn.active {
background-color: #72c02c;
border-color: #72c02c;
}
.tm-search {
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #555;
background-color: #444;
color: white;
margin-left: auto;
}
.tm-search::placeholder {
color: #aaa;
}
.tm-status-new { color: #3498db; }
.tm-status-accepted { color: #2ecc71; }
.tm-status-declined { color: #e74c3c; }
.tm-detailed-container {
margin-top: 20px;
}
.tab-button {
padding: 10px 15px;
background-color: #444;
border: none;
border-radius: 4px 4px 0 0;
cursor: pointer;
margin-right: 5px;
color: white;
}
.tab-button.active {
background-color: #72c02c;
color: white;
}
.tab-content {
display: none;
padding: 20px;
background-color: #333;
border-radius: 0 0 4px 4px;
}
.tab-content.active {
display: block;
}
#tm-new-report-btn .btn-primary {
background-color: #72c02c;
border-color: #5ca21c;
}
#tm-new-report-btn .btn-primary:hover {
background-color: #62b21c;
}
`);
// Initialize main variables
let allReports = [];
let currentPage = 1;
let totalPages = 1;
let isLoading = false;
// Main function to start the enhancement
function enhanceReportsPage() {
// Add container for our enhanced UI
const container = document.createElement('div');
container.className = 'tm-enhanced-container';
container.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="color: #72c02c; margin: 0; font-size: 24px; letter-spacing: 0.5px; text-transform: uppercase; font-weight: 600;">TruckersMP Report Dashboard</h2>
<div id="tm-new-report-btn"></div>
</div>
<div class="tm-loading">Loading all your reports... This may take a moment.</div>
<div id="tm-dashboard" class="tm-dashboard" style="display: none;"></div>
<div id="tm-tabs" style="margin-top: 20px; display: none;">
<button class="tab-button active" data-tab="reports">All Reports</button>
<button class="tab-button" data-tab="categories">Categories</button>
<button class="tab-button" data-tab="repeated-players">Repeated Players</button>
<button class="tab-button" data-tab="statistics">Statistics</button>
</div>
<div id="tm-tab-content" style="display: none;">
<div id="reports-tab" class="tab-content active">
<div class="tm-filter-bar">
<button class="tm-filter-btn active" data-filter="all">All</button>
<button class="tm-filter-btn" data-filter="new">New</button>
<button class="tm-filter-btn" data-filter="accepted">Accepted</button>
<button class="tm-filter-btn" data-filter="declined">Declined</button>
<input type="text" class="tm-search" placeholder="Search reports...">
</div>
<div id="tm-all-reports"></div>
</div>
<div id="categories-tab" class="tab-content">
<div id="tm-categories"></div>
</div>
<div id="repeated-players-tab" class="tab-content">
<div id="tm-repeated-players"></div>
</div>
<div id="statistics-tab" class="tab-content">
<div class="tm-dashboard">
<div class="tm-card">
<h3>Report Status Distribution</h3>
<div class="tm-chart-container">
<canvas id="status-chart"></canvas>
</div>
</div>
<div class="tm-card">
<h3>Categories Distribution</h3>
<div class="tm-chart-container">
<canvas id="categories-chart"></canvas>
</div>
</div>
</div>
</div>
</div>
`;
// Insert after the report summary at the top
const insertPoint = document.querySelector('.row.padding-top-5');
insertPoint.parentNode.insertBefore(container, insertPoint);
// Set up tab switching
const tabButtons = container.querySelectorAll('.tab-button');
tabButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons and contents
tabButtons.forEach(btn => btn.classList.remove('active'));
container.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Add active class to clicked button and corresponding content
this.classList.add('active');
document.getElementById(`${this.dataset.tab}-tab`).classList.add('active');
});
});
// Move the New Report button
const newReportButton = document.querySelector('a.btn.btn-primary.pull-right');
if (newReportButton) {
document.getElementById('tm-new-report-btn').appendChild(newReportButton);
}
// Hide the original report listing
const originalTable = document.querySelector('.row.padding-top-5');
if (originalTable) {
originalTable.style.display = 'none';
}
// Start fetching reports
fetchAllReports();
}
// Function to fetch all reports from all pages
async function fetchAllReports() {
try {
// Get the total number of pages
const paginationLinks = document.querySelectorAll('.pagination li a');
if (paginationLinks.length > 0) {
const lastPageLink = paginationLinks[paginationLinks.length - 2];
if (lastPageLink && lastPageLink.href) {
const pageMatch = lastPageLink.href.match(/page=(\d+)/);
if (pageMatch && pageMatch[1]) {
totalPages = parseInt(pageMatch[1]);
}
}
}
// Add the current page's reports
parseReportsFromCurrentPage();
// Fetch all other pages
const fetchPromises = [];
for (let page = 1; page <= totalPages; page++) {
if (page !== currentPage) { // Skip current page as we already have it
fetchPromises.push(fetchReportPage(page));
}
}
await Promise.all(fetchPromises);
// Process and display the data
processReportData();
} catch (error) {
console.error('Error fetching reports:', error);
document.querySelector('.tm-loading').innerHTML = 'Error loading reports. Please try refreshing the page.';
}
}
// Function to fetch a specific page of reports
async function fetchReportPage(page) {
try {
const response = await fetch(`https://truckersmp.com/reports?page=${page}`);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Parse reports from this page
const reportRows = doc.querySelectorAll('table.table tbody tr');
reportRows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 9) {
const reportLink = cells[8].querySelector('a').href;
const reportId = reportLink.split('/').pop();
allReports.push({
id: reportId,
reporter: cells[0].textContent.trim(),
perpetrator: cells[1].textContent.trim(),
server: cells[2].textContent.trim(),
reason: cells[3].textContent.trim(),
language: cells[4].textContent.trim(),
isClaimed: cells[5].textContent.trim(),
status: cells[6].textContent.trim(),
updatedAt: cells[7].textContent.trim(),
link: reportLink
});
}
});
} catch (error) {
console.error(`Error fetching page ${page}:`, error);
}
}
// Function to parse reports from the current page
function parseReportsFromCurrentPage() {
const reportRows = document.querySelectorAll('table.table tbody tr');
reportRows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 9) {
const reportLink = cells[8].querySelector('a').href;
const reportId = reportLink.split('/').pop();
allReports.push({
id: reportId,
reporter: cells[0].textContent.trim(),
perpetrator: cells[1].textContent.trim(),
server: cells[2].textContent.trim(),
reason: cells[3].textContent.trim(),
language: cells[4].textContent.trim(),
isClaimed: cells[5].textContent.trim(),
status: cells[6].textContent.trim(),
updatedAt: cells[7].textContent.trim(),
link: reportLink
});
}
});
}
// Process the report data and create visualizations
function processReportData() {
// Hide loading indicator and show content
document.querySelector('.tm-loading').style.display = 'none';
document.getElementById('tm-dashboard').style.display = 'grid';
document.getElementById('tm-tabs').style.display = 'block';
document.getElementById('tm-tab-content').style.display = 'block';
// Basic statistics summary
createStatsSummary();
// Create the all reports table
createAllReportsTable();
// Create categories breakdown
createCategoriesBreakdown();
// Create repeated players list
createRepeatedPlayersList();
// Create charts
createStatusChart();
createCategoriesChart();
// Make sure the original report list stays hidden
// This is in case anything caused it to show again
const originalTable = document.querySelector('.row.padding-top-5');
if (originalTable) {
originalTable.style.display = 'none';
}
}
// Create statistics summary cards
function createStatsSummary() {
const dashboard = document.getElementById('tm-dashboard');
// Count reports by status
const statusCounts = {
'New': 0,
'Accepted': 0,
'Declined': 0
};
allReports.forEach(report => {
const status = report.status.trim();
if (statusCounts.hasOwnProperty(status)) {
statusCounts[status]++;
}
});
// Get unique categories and languages
const categories = [...new Set(allReports.map(report => report.reason))];
const languages = [...new Set(allReports.map(report => report.language))];
// Count unique reported players
const uniquePlayers = new Set(allReports.map(report => report.perpetrator)).size;
dashboard.innerHTML = `
<div class="tm-card">
<h3>Report Summary</h3>
<div class="tm-stats">
<div class="tm-stat-item">
<div class="tm-stat-value">${allReports.length}</div>
<div class="tm-stat-label">Total Reports</div>
</div>
<div class="tm-stat-item">
<div class="tm-stat-value">${statusCounts['New']}</div>
<div class="tm-stat-label">New</div>
</div>
<div class="tm-stat-item">
<div class="tm-stat-value">${statusCounts['Accepted']}</div>
<div class="tm-stat-label">Accepted</div>
</div>
<div class="tm-stat-item">
<div class="tm-stat-value">${statusCounts['Declined']}</div>
<div class="tm-stat-label">Declined</div>
</div>
</div>
</div>
<div class="tm-card">
<h3>Player Statistics</h3>
<div class="tm-stats">
<div class="tm-stat-item">
<div class="tm-stat-value">${uniquePlayers}</div>
<div class="tm-stat-label">Unique Players</div>
</div>
<div class="tm-stat-item">
<div class="tm-stat-value">${categories.length}</div>
<div class="tm-stat-label">Categories</div>
</div>
<div class="tm-stat-item">
<div class="tm-stat-value">${languages.length}</div>
<div class="tm-stat-label">Languages</div>
</div>
<div class="tm-stat-item">
<div class="tm-stat-value">${(statusCounts['Accepted'] / (statusCounts['Accepted'] + statusCounts['Declined']) * 100).toFixed(1)}%</div>
<div class="tm-stat-label">Acceptance Rate</div>
</div>
</div>
</div>
`;
}
// Rapor oluşturma fonksiyonunda değişiklik yapacağız
function createAllReportsTable() {
const container = document.getElementById('tm-all-reports');
// Gelişmiş tarih çözümleme ve sıralama
const sortedReports = [...allReports].sort((a, b) => {
// Tarih formatını dönüştürme
const dateA = parseDetailedReportDate(a.updatedAt);
const dateB = parseDetailedReportDate(b.updatedAt);
// En son güncellenen en üstte olacak şekilde sıralama
return dateB - dateA;
});
// Tabloyu oluşturma
const tableHTML = `
<table class="tm-table">
<thead>
<tr>
<th>ID</th>
<th>Perpetrator</th>
<th>Server</th>
<th>Reason</th>
<th>Language</th>
<th>Is claimed?</th>
<th>Status</th>
<th>Updated</th>
<th>Action</th>
</tr>
</thead>
<tbody>
${sortedReports.map(report => `
<tr data-status="${report.status.toLowerCase().trim()}">
<td>${report.id}</td>
<td>${report.perpetrator}</td>
<td>${report.server}</td>
<td>${report.reason}</td>
<td>${report.language}</td>
<td class="${report.isClaimed.includes('Yes') ? 'tm-yes' : 'tm-no'}">${report.isClaimed}</td>
<td class="tm-status-${report.status.toLowerCase().trim()}">${report.status}</td>
<td>${report.updatedAt}</td>
<td><a href="${report.link}" target="_blank">View</a></td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = tableHTML;
// Filtreleme butonları kurulumu
const filterButtons = document.querySelectorAll('.tm-filter-btn');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
// Aktif butonu güncelleme
filterButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// Filtreyi uygulama
const filter = this.dataset.filter;
const rows = container.querySelectorAll('tbody tr');
rows.forEach(row => {
if (filter === 'all') {
row.style.display = '';
} else {
row.style.display = row.dataset.status === filter ? '' : 'none';
}
});
});
});
// Arama kurulumu
const searchInput = document.querySelector('.tm-search');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const rows = container.querySelectorAll('tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
}
// Gelişmiş tarih çözümleme fonksiyonu - tüm farklı tarih formatlarını işler
function parseDetailedReportDate(dateString) {
const now = new Date();
const currentYear = now.getFullYear();
// "Today" formatı işleme (ör: "Today, 17:25")
if (dateString.includes('Today')) {
const timeMatch = dateString.match(/(\d{1,2}):(\d{1,2})/);
if (timeMatch) {
const hours = parseInt(timeMatch[1], 10);
const minutes = parseInt(timeMatch[2], 10);
const today = new Date();
today.setHours(hours, minutes, 0, 0);
return today;
}
return new Date(); // Sadece "Today" içeriyorsa
}
// "Yesterday" formatı işleme (ör: "Yesterday, 15:30")
if (dateString.includes('Yesterday')) {
const timeMatch = dateString.match(/(\d{1,2}):(\d{1,2})/);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
if (timeMatch) {
const hours = parseInt(timeMatch[1], 10);
const minutes = parseInt(timeMatch[2], 10);
yesterday.setHours(hours, minutes, 0, 0);
}
return yesterday;
}
// "DD Mon HH:MM" formatını işleme (ör: "01 Mar 22:49")
const shortDateRegex = /(\d{1,2})\s+([A-Za-z]{3})\s+(\d{1,2}):(\d{1,2})/;
const shortDateMatch = dateString.match(shortDateRegex);
if (shortDateMatch) {
const day = parseInt(shortDateMatch[1], 10);
const month = getMonthNumber(shortDateMatch[2]);
const hours = parseInt(shortDateMatch[3], 10);
const minutes = parseInt(shortDateMatch[4], 10);
return new Date(currentYear, month, day, hours, minutes, 0);
}
// "DD Mon YYYY HH:MM" formatını işleme (ör: "10 Dec 2024 19:28")
const longDateRegex = /(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{1,2}):(\d{1,2})/;
const longDateMatch = dateString.match(longDateRegex);
if (longDateMatch) {
const day = parseInt(longDateMatch[1], 10);
const month = getMonthNumber(longDateMatch[2]);
const year = parseInt(longDateMatch[3], 10);
const hours = parseInt(longDateMatch[4], 10);
const minutes = parseInt(longDateMatch[5], 10);
return new Date(year, month, day, hours, minutes, 0);
}
// Standart tarih formatını işleme (ör: "23/01/2023 15:30")
const standardDateRegex = /(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{1,2})/;
const standardMatch = dateString.match(standardDateRegex);
if (standardMatch) {
const day = parseInt(standardMatch[1], 10);
const month = parseInt(standardMatch[2], 10) - 1; // Ay 0-11 arasında
const year = parseInt(standardMatch[3], 10);
const hours = parseInt(standardMatch[4], 10);
const minutes = parseInt(standardMatch[5], 10);
return new Date(year, month, day, hours, minutes, 0);
}
// Eğer hiçbir format eşleşmezse, original stringi Date objesine çevirmeyi dene
return new Date(dateString);
}
// Ay adını sayıya çevirme yardımcı fonksiyonu
function getMonthNumber(monthName) {
const months = {
'jan': 0, 'feb': 1, 'mar': 2, 'apr': 3, 'may': 4, 'jun': 5,
'jul': 6, 'aug': 7, 'sep': 8, 'oct': 9, 'nov': 10, 'dec': 11
};
return months[monthName.toLowerCase().substring(0, 3)] || 0;
}
// Create categories breakdown
function createCategoriesBreakdown() {
const container = document.getElementById('tm-categories');
// Count categories
const categoryCounts = {};
allReports.forEach(report => {
const category = report.reason;
if (!categoryCounts[category]) {
categoryCounts[category] = {
total: 0,
accepted: 0,
declined: 0,
new: 0
};
}
categoryCounts[category].total++;
const status = report.status.toLowerCase().trim();
if (status === 'accepted') categoryCounts[category].accepted++;
else if (status === 'declined') categoryCounts[category].declined++;
else if (status === 'new') categoryCounts[category].new++;
});
// Sort categories by total count
const sortedCategories = Object.entries(categoryCounts)
.sort((a, b) => b[1].total - a[1].total);
// Create the table
const tableHTML = `
<table class="tm-table">
<thead>
<tr>
<th>Category</th>
<th>Total</th>
<th>New</th>
<th>Accepted</th>
<th>Declined</th>
<th>Success Rate</th>
</tr>
</thead>
<tbody>
${sortedCategories.map(([category, counts]) => `
<tr>
<td>${category}</td>
<td>${counts.total}</td>
<td>${counts.new}</td>
<td>${counts.accepted}</td>
<td>${counts.declined}</td>
<td>${counts.accepted + counts.declined > 0 ?
((counts.accepted / (counts.accepted + counts.declined)) * 100).toFixed(1) + '%' :
'N/A'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = tableHTML;
}
// Create repeated players list
function createRepeatedPlayersList() {
const container = document.getElementById('tm-repeated-players');
// Count reports per player
const playerCounts = {};
allReports.forEach(report => {
const player = report.perpetrator;
if (!playerCounts[player]) {
playerCounts[player] = {
total: 0,
accepted: 0,
declined: 0,
new: 0,
categories: {}
};
}
playerCounts[player].total++;
const status = report.status.toLowerCase().trim();
if (status === 'accepted') playerCounts[player].accepted++;
else if (status === 'declined') playerCounts[player].declined++;
else if (status === 'new') playerCounts[player].new++;
// Count categories for this player
const category = report.reason;
if (!playerCounts[player].categories[category]) {
playerCounts[player].categories[category] = 0;
}
playerCounts[player].categories[category]++;
});
// Filter players with more than 1 report
const repeatedPlayers = Object.entries(playerCounts)
.filter(([_, counts]) => counts.total > 1)
.sort((a, b) => b[1].total - a[1].total);
// Create the table
const tableHTML = `
<table class="tm-table">
<thead>
<tr>
<th>Player</th>
<th>Total Reports</th>
<th>New</th>
<th>Accepted</th>
<th>Declined</th>
<th>Most Common Reason</th>
</tr>
</thead>
<tbody>
${repeatedPlayers.map(([player, counts]) => {
// Find most common category
const mostCommonCategory = Object.entries(counts.categories)
.sort((a, b) => b[1] - a[1])[0];
return `
<tr>
<td>${player}</td>
<td>${counts.total}</td>
<td>${counts.new}</td>
<td>${counts.accepted}</td>
<td>${counts.declined}</td>
<td>${mostCommonCategory ? `${mostCommonCategory[0]} (${mostCommonCategory[1]})` : 'N/A'}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
container.innerHTML = tableHTML;
}
// Create status distribution chart
function createStatusChart() {
const ctx = document.getElementById('status-chart').getContext('2d');
// Count status
const statusCounts = {
'New': 0,
'Accepted': 0,
'Declined': 0
};
allReports.forEach(report => {
const status = report.status.trim();
if (statusCounts.hasOwnProperty(status)) {
statusCounts[status]++;
}
});
new Chart(ctx, {
type: 'doughnut',
data: {
labels: Object.keys(statusCounts),
datasets: [{
data: Object.values(statusCounts),
backgroundColor: [
'#3498db', // Blue for New
'#2ecc71', // Green for Accepted
'#e74c3c' // Red for Declined
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
color: 'white'
}
}
}
}
});
}
// Create categories distribution chart
function createCategoriesChart() {
const ctx = document.getElementById('categories-chart').getContext('2d');
// Count categories
const categoryCounts = {};
allReports.forEach(report => {
const category = report.reason;
if (!categoryCounts[category]) {
categoryCounts[category] = 0;
}
categoryCounts[category]++;
});
// Sort and get top 5 categories
const topCategories = Object.entries(categoryCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
// Calculate 'Other' category
const totalReports = allReports.length;
const topCategoriesSum = topCategories.reduce((sum, [_, count]) => sum + count, 0);
const otherCount = totalReports - topCategoriesSum;
// Prepare chart data
const labels = [...topCategories.map(([category, _]) => {
// Shorten long category names
return category.length > 20 ? category.substring(0, 17) + '...' : category;
})];
if (otherCount > 0) {
labels.push('Other');
}
const data = [...topCategories.map(([_, count]) => count)];
if (otherCount > 0) {
data.push(otherCount);
}
// Generate colors
const colors = [
'#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6', '#95a5a6'
];
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Number of Reports',
data: data,
backgroundColor: colors,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: 'white'
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
x: {
ticks: {
color: 'white'
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
}
}
}
});
}
// Start enhancing the page
enhanceReportsPage();
})();