TruckersMP Report Page Enhancer

Enhances the TruckersMP report page with better visualization and statistics

  1. // ==UserScript==
  2. // @name TruckersMP Report Page Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Enhances the TruckersMP report page with better visualization and statistics
  6. // @author NoobFly
  7. // @match https://truckersmp.com/reports
  8. // @grant GM_addStyle
  9. // @license GNU GPLv3
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Add custom CSS
  17. GM_addStyle(`
  18. .tm-enhanced-container {
  19. margin: 20px 0;
  20. padding: 20px;
  21. background-color: #333;
  22. border-radius: 8px;
  23. box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  24. }
  25.  
  26. .tm-dashboard {
  27. display: grid;
  28. grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  29. gap: 20px;
  30. margin-bottom: 20px;
  31. }
  32.  
  33. .tm-card {
  34. background-color: #444;
  35. border-radius: 8px;
  36. padding: 15px;
  37. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  38. }
  39.  
  40. .tm-card h3 {
  41. margin-top: 0;
  42. border-bottom: 1px solid #555;
  43. padding-bottom: 10px;
  44. color: #72c02c;
  45. }
  46.  
  47. .tm-stats {
  48. display: flex;
  49. flex-wrap: wrap;
  50. gap: 10px;
  51. }
  52.  
  53. .tm-stat-item {
  54. background-color: #555;
  55. border-radius: 4px;
  56. padding: 10px;
  57. flex: 1;
  58. min-width: 120px;
  59. text-align: center;
  60. }
  61.  
  62. .tm-stat-value {
  63. font-size: 24px;
  64. font-weight: bold;
  65. color: #72c02c;
  66. }
  67.  
  68. .tm-stat-label {
  69. font-size: 14px;
  70. color: #ccc;
  71. }
  72.  
  73. .tm-table {
  74. width: 100%;
  75. border-collapse: collapse;
  76. margin-top: 15px;
  77. }
  78.  
  79. .tm-table th, .tm-table td {
  80. border: 1px solid #555;
  81. padding: 8px 12px;
  82. text-align: left;
  83. }
  84.  
  85. .tm-table th {
  86. background-color: #505050;
  87. color: #fff;
  88. }
  89.  
  90. .tm-table tr:nth-child(even) {
  91. background-color: #3a3a3a;
  92. }
  93.  
  94. .tm-chart-container {
  95. height: 300px;
  96. margin-top: 15px;
  97. }
  98.  
  99. .tm-loading {
  100. text-align: center;
  101. padding: 20px;
  102. font-size: 18px;
  103. color: #ccc;
  104. }
  105.  
  106. .tm-filter-bar {
  107. display: flex;
  108. gap: 10px;
  109. margin-bottom: 15px;
  110. flex-wrap: wrap;
  111. }
  112.  
  113. .tm-filter-btn {
  114. background-color: #444;
  115. border: 1px solid #555;
  116. color: white;
  117. padding: 8px 12px;
  118. border-radius: 4px;
  119. cursor: pointer;
  120. }
  121.  
  122. .tm-filter-btn.active {
  123. background-color: #72c02c;
  124. border-color: #72c02c;
  125. }
  126.  
  127. .tm-search {
  128. padding: 8px 12px;
  129. border-radius: 4px;
  130. border: 1px solid #555;
  131. background-color: #444;
  132. color: white;
  133. margin-left: auto;
  134. }
  135.  
  136. .tm-search::placeholder {
  137. color: #aaa;
  138. }
  139.  
  140. .tm-status-new { color: #3498db; }
  141. .tm-status-accepted { color: #2ecc71; }
  142. .tm-status-declined { color: #e74c3c; }
  143.  
  144. .tm-detailed-container {
  145. margin-top: 20px;
  146. }
  147.  
  148. .tab-button {
  149. padding: 10px 15px;
  150. background-color: #444;
  151. border: none;
  152. border-radius: 4px 4px 0 0;
  153. cursor: pointer;
  154. margin-right: 5px;
  155. color: white;
  156. }
  157.  
  158. .tab-button.active {
  159. background-color: #72c02c;
  160. color: white;
  161. }
  162.  
  163. .tab-content {
  164. display: none;
  165. padding: 20px;
  166. background-color: #333;
  167. border-radius: 0 0 4px 4px;
  168. }
  169.  
  170. .tab-content.active {
  171. display: block;
  172. }
  173.  
  174. #tm-new-report-btn .btn-primary {
  175. background-color: #72c02c;
  176. border-color: #5ca21c;
  177. }
  178.  
  179. #tm-new-report-btn .btn-primary:hover {
  180. background-color: #62b21c;
  181. }
  182. `);
  183.  
  184. // Initialize main variables
  185. let allReports = [];
  186. let currentPage = 1;
  187. let totalPages = 1;
  188. let isLoading = false;
  189.  
  190. // Main function to start the enhancement
  191. function enhanceReportsPage() {
  192. // Add container for our enhanced UI
  193. const container = document.createElement('div');
  194. container.className = 'tm-enhanced-container';
  195. container.innerHTML = `
  196. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
  197. <h2 style="color: #72c02c; margin: 0; font-size: 24px; letter-spacing: 0.5px; text-transform: uppercase; font-weight: 600;">TruckersMP Report Dashboard</h2>
  198. <div id="tm-new-report-btn"></div>
  199. </div>
  200. <div class="tm-loading">Loading all your reports... This may take a moment.</div>
  201. <div id="tm-dashboard" class="tm-dashboard" style="display: none;"></div>
  202. <div id="tm-tabs" style="margin-top: 20px; display: none;">
  203. <button class="tab-button active" data-tab="reports">All Reports</button>
  204. <button class="tab-button" data-tab="categories">Categories</button>
  205. <button class="tab-button" data-tab="repeated-players">Repeated Players</button>
  206. <button class="tab-button" data-tab="statistics">Statistics</button>
  207. </div>
  208. <div id="tm-tab-content" style="display: none;">
  209. <div id="reports-tab" class="tab-content active">
  210. <div class="tm-filter-bar">
  211. <button class="tm-filter-btn active" data-filter="all">All</button>
  212. <button class="tm-filter-btn" data-filter="new">New</button>
  213. <button class="tm-filter-btn" data-filter="accepted">Accepted</button>
  214. <button class="tm-filter-btn" data-filter="declined">Declined</button>
  215. <input type="text" class="tm-search" placeholder="Search reports...">
  216. </div>
  217. <div id="tm-all-reports"></div>
  218. </div>
  219. <div id="categories-tab" class="tab-content">
  220. <div id="tm-categories"></div>
  221. </div>
  222. <div id="repeated-players-tab" class="tab-content">
  223. <div id="tm-repeated-players"></div>
  224. </div>
  225. <div id="statistics-tab" class="tab-content">
  226. <div class="tm-dashboard">
  227. <div class="tm-card">
  228. <h3>Report Status Distribution</h3>
  229. <div class="tm-chart-container">
  230. <canvas id="status-chart"></canvas>
  231. </div>
  232. </div>
  233. <div class="tm-card">
  234. <h3>Categories Distribution</h3>
  235. <div class="tm-chart-container">
  236. <canvas id="categories-chart"></canvas>
  237. </div>
  238. </div>
  239. </div>
  240. </div>
  241. </div>
  242. `;
  243.  
  244. // Insert after the report summary at the top
  245. const insertPoint = document.querySelector('.row.padding-top-5');
  246. insertPoint.parentNode.insertBefore(container, insertPoint);
  247.  
  248. // Set up tab switching
  249. const tabButtons = container.querySelectorAll('.tab-button');
  250. tabButtons.forEach(button => {
  251. button.addEventListener('click', function() {
  252. // Remove active class from all buttons and contents
  253. tabButtons.forEach(btn => btn.classList.remove('active'));
  254. container.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
  255.  
  256. // Add active class to clicked button and corresponding content
  257. this.classList.add('active');
  258. document.getElementById(`${this.dataset.tab}-tab`).classList.add('active');
  259. });
  260. });
  261.  
  262. // Move the New Report button
  263. const newReportButton = document.querySelector('a.btn.btn-primary.pull-right');
  264. if (newReportButton) {
  265. document.getElementById('tm-new-report-btn').appendChild(newReportButton);
  266. }
  267.  
  268. // Hide the original report listing
  269. const originalTable = document.querySelector('.row.padding-top-5');
  270. if (originalTable) {
  271. originalTable.style.display = 'none';
  272. }
  273.  
  274. // Start fetching reports
  275. fetchAllReports();
  276. }
  277.  
  278. // Function to fetch all reports from all pages
  279. async function fetchAllReports() {
  280. try {
  281. // Get the total number of pages
  282. const paginationLinks = document.querySelectorAll('.pagination li a');
  283. if (paginationLinks.length > 0) {
  284. const lastPageLink = paginationLinks[paginationLinks.length - 2];
  285. if (lastPageLink && lastPageLink.href) {
  286. const pageMatch = lastPageLink.href.match(/page=(\d+)/);
  287. if (pageMatch && pageMatch[1]) {
  288. totalPages = parseInt(pageMatch[1]);
  289. }
  290. }
  291. }
  292.  
  293. // Add the current page's reports
  294. parseReportsFromCurrentPage();
  295.  
  296. // Fetch all other pages
  297. const fetchPromises = [];
  298. for (let page = 1; page <= totalPages; page++) {
  299. if (page !== currentPage) { // Skip current page as we already have it
  300. fetchPromises.push(fetchReportPage(page));
  301. }
  302. }
  303.  
  304. await Promise.all(fetchPromises);
  305.  
  306. // Process and display the data
  307. processReportData();
  308.  
  309. } catch (error) {
  310. console.error('Error fetching reports:', error);
  311. document.querySelector('.tm-loading').innerHTML = 'Error loading reports. Please try refreshing the page.';
  312. }
  313. }
  314.  
  315. // Function to fetch a specific page of reports
  316. async function fetchReportPage(page) {
  317. try {
  318. const response = await fetch(`https://truckersmp.com/reports?page=${page}`);
  319. const html = await response.text();
  320. const parser = new DOMParser();
  321. const doc = parser.parseFromString(html, 'text/html');
  322.  
  323. // Parse reports from this page
  324. const reportRows = doc.querySelectorAll('table.table tbody tr');
  325. reportRows.forEach(row => {
  326. const cells = row.querySelectorAll('td');
  327. if (cells.length >= 9) {
  328. const reportLink = cells[8].querySelector('a').href;
  329. const reportId = reportLink.split('/').pop();
  330.  
  331. allReports.push({
  332. id: reportId,
  333. reporter: cells[0].textContent.trim(),
  334. perpetrator: cells[1].textContent.trim(),
  335. server: cells[2].textContent.trim(),
  336. reason: cells[3].textContent.trim(),
  337. language: cells[4].textContent.trim(),
  338. isClaimed: cells[5].textContent.trim(),
  339. status: cells[6].textContent.trim(),
  340. updatedAt: cells[7].textContent.trim(),
  341. link: reportLink
  342. });
  343. }
  344. });
  345. } catch (error) {
  346. console.error(`Error fetching page ${page}:`, error);
  347. }
  348. }
  349.  
  350. // Function to parse reports from the current page
  351. function parseReportsFromCurrentPage() {
  352. const reportRows = document.querySelectorAll('table.table tbody tr');
  353. reportRows.forEach(row => {
  354. const cells = row.querySelectorAll('td');
  355. if (cells.length >= 9) {
  356. const reportLink = cells[8].querySelector('a').href;
  357. const reportId = reportLink.split('/').pop();
  358.  
  359. allReports.push({
  360. id: reportId,
  361. reporter: cells[0].textContent.trim(),
  362. perpetrator: cells[1].textContent.trim(),
  363. server: cells[2].textContent.trim(),
  364. reason: cells[3].textContent.trim(),
  365. language: cells[4].textContent.trim(),
  366. isClaimed: cells[5].textContent.trim(),
  367. status: cells[6].textContent.trim(),
  368. updatedAt: cells[7].textContent.trim(),
  369. link: reportLink
  370. });
  371. }
  372. });
  373. }
  374.  
  375. // Process the report data and create visualizations
  376. function processReportData() {
  377. // Hide loading indicator and show content
  378. document.querySelector('.tm-loading').style.display = 'none';
  379. document.getElementById('tm-dashboard').style.display = 'grid';
  380. document.getElementById('tm-tabs').style.display = 'block';
  381. document.getElementById('tm-tab-content').style.display = 'block';
  382.  
  383. // Basic statistics summary
  384. createStatsSummary();
  385.  
  386. // Create the all reports table
  387. createAllReportsTable();
  388.  
  389. // Create categories breakdown
  390. createCategoriesBreakdown();
  391.  
  392. // Create repeated players list
  393. createRepeatedPlayersList();
  394.  
  395. // Create charts
  396. createStatusChart();
  397. createCategoriesChart();
  398.  
  399. // Make sure the original report list stays hidden
  400. // This is in case anything caused it to show again
  401. const originalTable = document.querySelector('.row.padding-top-5');
  402. if (originalTable) {
  403. originalTable.style.display = 'none';
  404. }
  405. }
  406.  
  407. // Create statistics summary cards
  408. function createStatsSummary() {
  409. const dashboard = document.getElementById('tm-dashboard');
  410.  
  411. // Count reports by status
  412. const statusCounts = {
  413. 'New': 0,
  414. 'Accepted': 0,
  415. 'Declined': 0
  416. };
  417.  
  418. allReports.forEach(report => {
  419. const status = report.status.trim();
  420. if (statusCounts.hasOwnProperty(status)) {
  421. statusCounts[status]++;
  422. }
  423. });
  424.  
  425. // Get unique categories and languages
  426. const categories = [...new Set(allReports.map(report => report.reason))];
  427. const languages = [...new Set(allReports.map(report => report.language))];
  428.  
  429. // Count unique reported players
  430. const uniquePlayers = new Set(allReports.map(report => report.perpetrator)).size;
  431.  
  432. dashboard.innerHTML = `
  433. <div class="tm-card">
  434. <h3>Report Summary</h3>
  435. <div class="tm-stats">
  436. <div class="tm-stat-item">
  437. <div class="tm-stat-value">${allReports.length}</div>
  438. <div class="tm-stat-label">Total Reports</div>
  439. </div>
  440. <div class="tm-stat-item">
  441. <div class="tm-stat-value">${statusCounts['New']}</div>
  442. <div class="tm-stat-label">New</div>
  443. </div>
  444. <div class="tm-stat-item">
  445. <div class="tm-stat-value">${statusCounts['Accepted']}</div>
  446. <div class="tm-stat-label">Accepted</div>
  447. </div>
  448. <div class="tm-stat-item">
  449. <div class="tm-stat-value">${statusCounts['Declined']}</div>
  450. <div class="tm-stat-label">Declined</div>
  451. </div>
  452. </div>
  453. </div>
  454. <div class="tm-card">
  455. <h3>Player Statistics</h3>
  456. <div class="tm-stats">
  457. <div class="tm-stat-item">
  458. <div class="tm-stat-value">${uniquePlayers}</div>
  459. <div class="tm-stat-label">Unique Players</div>
  460. </div>
  461. <div class="tm-stat-item">
  462. <div class="tm-stat-value">${categories.length}</div>
  463. <div class="tm-stat-label">Categories</div>
  464. </div>
  465. <div class="tm-stat-item">
  466. <div class="tm-stat-value">${languages.length}</div>
  467. <div class="tm-stat-label">Languages</div>
  468. </div>
  469. <div class="tm-stat-item">
  470. <div class="tm-stat-value">${(statusCounts['Accepted'] / (statusCounts['Accepted'] + statusCounts['Declined']) * 100).toFixed(1)}%</div>
  471. <div class="tm-stat-label">Acceptance Rate</div>
  472. </div>
  473. </div>
  474. </div>
  475. `;
  476. }
  477.  
  478. // Rapor oluşturma fonksiyonunda değişiklik yapacağız
  479. function createAllReportsTable() {
  480. const container = document.getElementById('tm-all-reports');
  481.  
  482. // Gelişmiş tarih çözümleme ve sıralama
  483. const sortedReports = [...allReports].sort((a, b) => {
  484. // Tarih formatını dönüştürme
  485. const dateA = parseDetailedReportDate(a.updatedAt);
  486. const dateB = parseDetailedReportDate(b.updatedAt);
  487.  
  488. // En son güncellenen en üstte olacak şekilde sıralama
  489. return dateB - dateA;
  490. });
  491.  
  492. // Tabloyu oluşturma
  493. const tableHTML = `
  494. <table class="tm-table">
  495. <thead>
  496. <tr>
  497. <th>ID</th>
  498. <th>Perpetrator</th>
  499. <th>Server</th>
  500. <th>Reason</th>
  501. <th>Language</th>
  502. <th>Is claimed?</th>
  503. <th>Status</th>
  504. <th>Updated</th>
  505. <th>Action</th>
  506. </tr>
  507. </thead>
  508. <tbody>
  509. ${sortedReports.map(report => `
  510. <tr data-status="${report.status.toLowerCase().trim()}">
  511. <td>${report.id}</td>
  512. <td>${report.perpetrator}</td>
  513. <td>${report.server}</td>
  514. <td>${report.reason}</td>
  515. <td>${report.language}</td>
  516. <td class="${report.isClaimed.includes('Yes') ? 'tm-yes' : 'tm-no'}">${report.isClaimed}</td>
  517. <td class="tm-status-${report.status.toLowerCase().trim()}">${report.status}</td>
  518. <td>${report.updatedAt}</td>
  519. <td><a href="${report.link}" target="_blank">View</a></td>
  520. </tr>
  521. `).join('')}
  522. </tbody>
  523. </table>
  524. `;
  525.  
  526. container.innerHTML = tableHTML;
  527.  
  528. // Filtreleme butonları kurulumu
  529. const filterButtons = document.querySelectorAll('.tm-filter-btn');
  530. filterButtons.forEach(button => {
  531. button.addEventListener('click', function() {
  532. // Aktif butonu güncelleme
  533. filterButtons.forEach(btn => btn.classList.remove('active'));
  534. this.classList.add('active');
  535.  
  536. // Filtreyi uygulama
  537. const filter = this.dataset.filter;
  538. const rows = container.querySelectorAll('tbody tr');
  539.  
  540. rows.forEach(row => {
  541. if (filter === 'all') {
  542. row.style.display = '';
  543. } else {
  544. row.style.display = row.dataset.status === filter ? '' : 'none';
  545. }
  546. });
  547. });
  548. });
  549.  
  550. // Arama kurulumu
  551. const searchInput = document.querySelector('.tm-search');
  552. searchInput.addEventListener('input', function() {
  553. const searchTerm = this.value.toLowerCase();
  554. const rows = container.querySelectorAll('tbody tr');
  555.  
  556. rows.forEach(row => {
  557. const text = row.textContent.toLowerCase();
  558. row.style.display = text.includes(searchTerm) ? '' : 'none';
  559. });
  560. });
  561. }
  562.  
  563. // Gelişmiş tarih çözümleme fonksiyonu - tüm farklı tarih formatlarını işler
  564. function parseDetailedReportDate(dateString) {
  565. const now = new Date();
  566. const currentYear = now.getFullYear();
  567.  
  568. // "Today" formatı işleme (ör: "Today, 17:25")
  569. if (dateString.includes('Today')) {
  570. const timeMatch = dateString.match(/(\d{1,2}):(\d{1,2})/);
  571. if (timeMatch) {
  572. const hours = parseInt(timeMatch[1], 10);
  573. const minutes = parseInt(timeMatch[2], 10);
  574. const today = new Date();
  575. today.setHours(hours, minutes, 0, 0);
  576. return today;
  577. }
  578. return new Date(); // Sadece "Today" içeriyorsa
  579. }
  580.  
  581. // "Yesterday" formatı işleme (ör: "Yesterday, 15:30")
  582. if (dateString.includes('Yesterday')) {
  583. const timeMatch = dateString.match(/(\d{1,2}):(\d{1,2})/);
  584. const yesterday = new Date();
  585. yesterday.setDate(yesterday.getDate() - 1);
  586.  
  587. if (timeMatch) {
  588. const hours = parseInt(timeMatch[1], 10);
  589. const minutes = parseInt(timeMatch[2], 10);
  590. yesterday.setHours(hours, minutes, 0, 0);
  591. }
  592. return yesterday;
  593. }
  594.  
  595. // "DD Mon HH:MM" formatını işleme (ör: "01 Mar 22:49")
  596. const shortDateRegex = /(\d{1,2})\s+([A-Za-z]{3})\s+(\d{1,2}):(\d{1,2})/;
  597. const shortDateMatch = dateString.match(shortDateRegex);
  598. if (shortDateMatch) {
  599. const day = parseInt(shortDateMatch[1], 10);
  600. const month = getMonthNumber(shortDateMatch[2]);
  601. const hours = parseInt(shortDateMatch[3], 10);
  602. const minutes = parseInt(shortDateMatch[4], 10);
  603.  
  604. return new Date(currentYear, month, day, hours, minutes, 0);
  605. }
  606.  
  607. // "DD Mon YYYY HH:MM" formatını işleme (ör: "10 Dec 2024 19:28")
  608. const longDateRegex = /(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{1,2}):(\d{1,2})/;
  609. const longDateMatch = dateString.match(longDateRegex);
  610. if (longDateMatch) {
  611. const day = parseInt(longDateMatch[1], 10);
  612. const month = getMonthNumber(longDateMatch[2]);
  613. const year = parseInt(longDateMatch[3], 10);
  614. const hours = parseInt(longDateMatch[4], 10);
  615. const minutes = parseInt(longDateMatch[5], 10);
  616.  
  617. return new Date(year, month, day, hours, minutes, 0);
  618. }
  619.  
  620. // Standart tarih formatını işleme (ör: "23/01/2023 15:30")
  621. const standardDateRegex = /(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{1,2})/;
  622. const standardMatch = dateString.match(standardDateRegex);
  623. if (standardMatch) {
  624. const day = parseInt(standardMatch[1], 10);
  625. const month = parseInt(standardMatch[2], 10) - 1; // Ay 0-11 arasında
  626. const year = parseInt(standardMatch[3], 10);
  627. const hours = parseInt(standardMatch[4], 10);
  628. const minutes = parseInt(standardMatch[5], 10);
  629.  
  630. return new Date(year, month, day, hours, minutes, 0);
  631. }
  632.  
  633. // Eğer hiçbir format eşleşmezse, original stringi Date objesine çevirmeyi dene
  634. return new Date(dateString);
  635. }
  636.  
  637. // Ay adını sayıya çevirme yardımcı fonksiyonu
  638. function getMonthNumber(monthName) {
  639. const months = {
  640. 'jan': 0, 'feb': 1, 'mar': 2, 'apr': 3, 'may': 4, 'jun': 5,
  641. 'jul': 6, 'aug': 7, 'sep': 8, 'oct': 9, 'nov': 10, 'dec': 11
  642. };
  643.  
  644. return months[monthName.toLowerCase().substring(0, 3)] || 0;
  645. }
  646.  
  647. // Create categories breakdown
  648. function createCategoriesBreakdown() {
  649. const container = document.getElementById('tm-categories');
  650.  
  651. // Count categories
  652. const categoryCounts = {};
  653. allReports.forEach(report => {
  654. const category = report.reason;
  655. if (!categoryCounts[category]) {
  656. categoryCounts[category] = {
  657. total: 0,
  658. accepted: 0,
  659. declined: 0,
  660. new: 0
  661. };
  662. }
  663.  
  664. categoryCounts[category].total++;
  665.  
  666. const status = report.status.toLowerCase().trim();
  667. if (status === 'accepted') categoryCounts[category].accepted++;
  668. else if (status === 'declined') categoryCounts[category].declined++;
  669. else if (status === 'new') categoryCounts[category].new++;
  670. });
  671.  
  672. // Sort categories by total count
  673. const sortedCategories = Object.entries(categoryCounts)
  674. .sort((a, b) => b[1].total - a[1].total);
  675.  
  676. // Create the table
  677. const tableHTML = `
  678. <table class="tm-table">
  679. <thead>
  680. <tr>
  681. <th>Category</th>
  682. <th>Total</th>
  683. <th>New</th>
  684. <th>Accepted</th>
  685. <th>Declined</th>
  686. <th>Success Rate</th>
  687. </tr>
  688. </thead>
  689. <tbody>
  690. ${sortedCategories.map(([category, counts]) => `
  691. <tr>
  692. <td>${category}</td>
  693. <td>${counts.total}</td>
  694. <td>${counts.new}</td>
  695. <td>${counts.accepted}</td>
  696. <td>${counts.declined}</td>
  697. <td>${counts.accepted + counts.declined > 0 ?
  698. ((counts.accepted / (counts.accepted + counts.declined)) * 100).toFixed(1) + '%' :
  699. 'N/A'}</td>
  700. </tr>
  701. `).join('')}
  702. </tbody>
  703. </table>
  704. `;
  705.  
  706. container.innerHTML = tableHTML;
  707. }
  708.  
  709. // Create repeated players list
  710. function createRepeatedPlayersList() {
  711. const container = document.getElementById('tm-repeated-players');
  712.  
  713. // Count reports per player
  714. const playerCounts = {};
  715. allReports.forEach(report => {
  716. const player = report.perpetrator;
  717. if (!playerCounts[player]) {
  718. playerCounts[player] = {
  719. total: 0,
  720. accepted: 0,
  721. declined: 0,
  722. new: 0,
  723. categories: {}
  724. };
  725. }
  726.  
  727. playerCounts[player].total++;
  728.  
  729. const status = report.status.toLowerCase().trim();
  730. if (status === 'accepted') playerCounts[player].accepted++;
  731. else if (status === 'declined') playerCounts[player].declined++;
  732. else if (status === 'new') playerCounts[player].new++;
  733.  
  734. // Count categories for this player
  735. const category = report.reason;
  736. if (!playerCounts[player].categories[category]) {
  737. playerCounts[player].categories[category] = 0;
  738. }
  739. playerCounts[player].categories[category]++;
  740. });
  741.  
  742. // Filter players with more than 1 report
  743. const repeatedPlayers = Object.entries(playerCounts)
  744. .filter(([_, counts]) => counts.total > 1)
  745. .sort((a, b) => b[1].total - a[1].total);
  746.  
  747. // Create the table
  748. const tableHTML = `
  749. <table class="tm-table">
  750. <thead>
  751. <tr>
  752. <th>Player</th>
  753. <th>Total Reports</th>
  754. <th>New</th>
  755. <th>Accepted</th>
  756. <th>Declined</th>
  757. <th>Most Common Reason</th>
  758. </tr>
  759. </thead>
  760. <tbody>
  761. ${repeatedPlayers.map(([player, counts]) => {
  762. // Find most common category
  763. const mostCommonCategory = Object.entries(counts.categories)
  764. .sort((a, b) => b[1] - a[1])[0];
  765.  
  766. return `
  767. <tr>
  768. <td>${player}</td>
  769. <td>${counts.total}</td>
  770. <td>${counts.new}</td>
  771. <td>${counts.accepted}</td>
  772. <td>${counts.declined}</td>
  773. <td>${mostCommonCategory ? `${mostCommonCategory[0]} (${mostCommonCategory[1]})` : 'N/A'}</td>
  774. </tr>
  775. `;
  776. }).join('')}
  777. </tbody>
  778. </table>
  779. `;
  780.  
  781. container.innerHTML = tableHTML;
  782. }
  783.  
  784. // Create status distribution chart
  785. function createStatusChart() {
  786. const ctx = document.getElementById('status-chart').getContext('2d');
  787.  
  788. // Count status
  789. const statusCounts = {
  790. 'New': 0,
  791. 'Accepted': 0,
  792. 'Declined': 0
  793. };
  794.  
  795. allReports.forEach(report => {
  796. const status = report.status.trim();
  797. if (statusCounts.hasOwnProperty(status)) {
  798. statusCounts[status]++;
  799. }
  800. });
  801.  
  802. new Chart(ctx, {
  803. type: 'doughnut',
  804. data: {
  805. labels: Object.keys(statusCounts),
  806. datasets: [{
  807. data: Object.values(statusCounts),
  808. backgroundColor: [
  809. '#3498db', // Blue for New
  810. '#2ecc71', // Green for Accepted
  811. '#e74c3c' // Red for Declined
  812. ],
  813. borderWidth: 1
  814. }]
  815. },
  816. options: {
  817. responsive: true,
  818. maintainAspectRatio: false,
  819. plugins: {
  820. legend: {
  821. position: 'right',
  822. labels: {
  823. color: 'white'
  824. }
  825. }
  826. }
  827. }
  828. });
  829. }
  830.  
  831. // Create categories distribution chart
  832. function createCategoriesChart() {
  833. const ctx = document.getElementById('categories-chart').getContext('2d');
  834.  
  835. // Count categories
  836. const categoryCounts = {};
  837. allReports.forEach(report => {
  838. const category = report.reason;
  839. if (!categoryCounts[category]) {
  840. categoryCounts[category] = 0;
  841. }
  842. categoryCounts[category]++;
  843. });
  844.  
  845. // Sort and get top 5 categories
  846. const topCategories = Object.entries(categoryCounts)
  847. .sort((a, b) => b[1] - a[1])
  848. .slice(0, 5);
  849.  
  850. // Calculate 'Other' category
  851. const totalReports = allReports.length;
  852. const topCategoriesSum = topCategories.reduce((sum, [_, count]) => sum + count, 0);
  853. const otherCount = totalReports - topCategoriesSum;
  854.  
  855. // Prepare chart data
  856. const labels = [...topCategories.map(([category, _]) => {
  857. // Shorten long category names
  858. return category.length > 20 ? category.substring(0, 17) + '...' : category;
  859. })];
  860.  
  861. if (otherCount > 0) {
  862. labels.push('Other');
  863. }
  864.  
  865. const data = [...topCategories.map(([_, count]) => count)];
  866.  
  867. if (otherCount > 0) {
  868. data.push(otherCount);
  869. }
  870.  
  871. // Generate colors
  872. const colors = [
  873. '#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6', '#95a5a6'
  874. ];
  875.  
  876. new Chart(ctx, {
  877. type: 'bar',
  878. data: {
  879. labels: labels,
  880. datasets: [{
  881. label: 'Number of Reports',
  882. data: data,
  883. backgroundColor: colors,
  884. borderWidth: 1
  885. }]
  886. },
  887. options: {
  888. responsive: true,
  889. maintainAspectRatio: false,
  890. plugins: {
  891. legend: {
  892. display: false
  893. }
  894. },
  895. scales: {
  896. y: {
  897. beginAtZero: true,
  898. ticks: {
  899. color: 'white'
  900. },
  901. grid: {
  902. color: 'rgba(255, 255, 255, 0.1)'
  903. }
  904. },
  905. x: {
  906. ticks: {
  907. color: 'white'
  908. },
  909. grid: {
  910. color: 'rgba(255, 255, 255, 0.1)'
  911. }
  912. }
  913. }
  914. }
  915. });
  916. }
  917.  
  918. // Start enhancing the page
  919. enhanceReportsPage();
  920. })();