ciastoman

AUXI Reporter

Apr 4th, 2026
538
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 38.58 KB | Gaming | 0 0
  1. <!DOCTYPE html>
  2. <html lang="pl">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Driver Statistics — Raport</title>
  7. <style>
  8.   :root {
  9.     --bg: #0d0f14;
  10.     --bg2: #13161e;
  11.     --bg3: #1a1e2a;
  12.     --border: #2a2f3f;
  13.     --accent: #f0a500;
  14.     --accent2: #3dd6f5;
  15.     --danger: #e05252;
  16.     --success: #4cca8a;
  17.     --text: #d0d6e8;
  18.     --text-dim: #6b7494;
  19.     --mono: 'Consolas', 'Cascadia Code', 'Courier New', monospace;
  20.     --body: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
  21.   }
  22.  
  23.   *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  24.  
  25.   body {
  26.     background: var(--bg);
  27.     color: var(--text);
  28.     font-family: var(--body);
  29.     font-size: 14px;
  30.     min-height: 100vh;
  31.   }
  32.  
  33.   /* HEADER */
  34.   .header {
  35.     background: var(--bg2);
  36.     border-bottom: 1px solid var(--border);
  37.     padding: 20px 32px;
  38.     display: flex;
  39.     align-items: center;
  40.     gap: 20px;
  41.     position: sticky;
  42.     top: 0;
  43.     z-index: 100;
  44.   }
  45.   .header-icon {
  46.     font-size: 28px;
  47.   }
  48.   .header-title {
  49.     font-family: var(--mono);
  50.     font-size: 18px;
  51.     color: var(--accent);
  52.     letter-spacing: 2px;
  53.     text-transform: uppercase;
  54.   }
  55.   .header-sub {
  56.     font-size: 11px;
  57.     color: var(--text-dim);
  58.     font-family: var(--mono);
  59.     letter-spacing: 1px;
  60.     margin-top: 2px;
  61.   }
  62.   .header-stats {
  63.     margin-left: auto;
  64.     display: flex;
  65.     gap: 24px;
  66.   }
  67.   .hstat {
  68.     text-align: right;
  69.   }
  70.   .hstat-val {
  71.     font-family: var(--mono);
  72.     font-size: 18px;
  73.     color: var(--accent2);
  74.   }
  75.   .hstat-label {
  76.     font-size: 10px;
  77.     color: var(--text-dim);
  78.     text-transform: uppercase;
  79.     letter-spacing: 1px;
  80.   }
  81.  
  82.   /* FILTERS */
  83.   .filters {
  84.     padding: 18px 32px;
  85.     background: var(--bg2);
  86.     border-bottom: 1px solid var(--border);
  87.     display: flex;
  88.     gap: 16px;
  89.     flex-wrap: wrap;
  90.     align-items: flex-end;
  91.   }
  92.   .filter-group {
  93.     display: flex;
  94.     flex-direction: column;
  95.     gap: 5px;
  96.   }
  97.   .filter-group label {
  98.     font-size: 10px;
  99.     color: var(--text-dim);
  100.     text-transform: uppercase;
  101.     letter-spacing: 1px;
  102.     font-family: var(--mono);
  103.   }
  104.   select, input[type="date"] {
  105.     background: var(--bg3);
  106.     border: 1px solid var(--border);
  107.     color: var(--text);
  108.     font-family: var(--mono);
  109.     font-size: 13px;
  110.     padding: 7px 12px;
  111.     border-radius: 4px;
  112.     outline: none;
  113.     min-width: 180px;
  114.     cursor: pointer;
  115.     transition: border-color .2s;
  116.   }
  117.   select:focus, input[type="date"]:focus {
  118.     border-color: var(--accent);
  119.   }
  120.   .btn-reset {
  121.     background: transparent;
  122.     border: 1px solid var(--border);
  123.     color: var(--text-dim);
  124.     font-family: var(--mono);
  125.     font-size: 12px;
  126.     padding: 7px 16px;
  127.     border-radius: 4px;
  128.     cursor: pointer;
  129.     letter-spacing: 1px;
  130.     transition: all .2s;
  131.     align-self: flex-end;
  132.   }
  133.   .btn-reset:hover {
  134.     border-color: var(--accent);
  135.     color: var(--accent);
  136.   }
  137.  
  138.   /* VIEW TOGGLE */
  139.   .view-toggle {
  140.     margin-left: auto;
  141.     display: flex;
  142.     gap: 8px;
  143.     align-self: flex-end;
  144.   }
  145.   .btn-view {
  146.     background: var(--bg3);
  147.     border: 1px solid var(--border);
  148.     color: var(--text-dim);
  149.     font-family: var(--mono);
  150.     font-size: 11px;
  151.     padding: 7px 14px;
  152.     border-radius: 4px;
  153.     cursor: pointer;
  154.     letter-spacing: 1px;
  155.     transition: all .2s;
  156.   }
  157.   .btn-view.active {
  158.     background: var(--accent);
  159.     border-color: var(--accent);
  160.     color: #000;
  161.     font-weight: 700;
  162.   }
  163.  
  164.   /* SUMMARY CARDS */
  165.   .summary-cards {
  166.     display: grid;
  167.     grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  168.     gap: 12px;
  169.     padding: 20px 32px;
  170.   }
  171.   .card {
  172.     background: var(--bg2);
  173.     border: 1px solid var(--border);
  174.     border-radius: 6px;
  175.     padding: 14px 16px;
  176.     position: relative;
  177.     overflow: hidden;
  178.   }
  179.   .card::before {
  180.     content: '';
  181.     position: absolute;
  182.     top: 0; left: 0; right: 0;
  183.     height: 2px;
  184.     background: var(--accent);
  185.   }
  186.   .card.blue::before { background: var(--accent2); }
  187.   .card.green::before { background: var(--success); }
  188.   .card.red::before { background: var(--danger); }
  189.   .card-val {
  190.     font-family: var(--mono);
  191.     font-size: 22px;
  192.     color: var(--accent);
  193.     line-height: 1;
  194.   }
  195.   .card.blue .card-val { color: var(--accent2); }
  196.   .card.green .card-val { color: var(--success); }
  197.   .card.red .card-val { color: var(--danger); }
  198.   .card-label {
  199.     font-size: 10px;
  200.     color: var(--text-dim);
  201.     text-transform: uppercase;
  202.     letter-spacing: 1px;
  203.     margin-top: 5px;
  204.   }
  205.  
  206.   /* TABLE */
  207.   .table-wrap {
  208.     padding: 0 32px 40px;
  209.     overflow-x: auto;
  210.   }
  211.  
  212.   /* GROUP VIEW */
  213.   .group-section {
  214.     margin-bottom: 32px;
  215.   }
  216.   .group-header {
  217.     display: flex;
  218.     align-items: center;
  219.     gap: 12px;
  220.     padding: 10px 0 8px;
  221.     border-bottom: 1px solid var(--border);
  222.     margin-bottom: 8px;
  223.     cursor: pointer;
  224.     user-select: none;
  225.   }
  226.   .group-header:hover .group-name { color: var(--accent); }
  227.   .group-name {
  228.     font-family: var(--mono);
  229.     font-size: 14px;
  230.     color: var(--accent2);
  231.     letter-spacing: 1px;
  232.     transition: color .2s;
  233.   }
  234.   .group-badge {
  235.     font-family: var(--mono);
  236.     font-size: 10px;
  237.     background: var(--bg3);
  238.     border: 1px solid var(--border);
  239.     padding: 2px 8px;
  240.     border-radius: 10px;
  241.     color: var(--text-dim);
  242.   }
  243.   .group-chevron {
  244.     margin-left: auto;
  245.     color: var(--text-dim);
  246.     font-size: 12px;
  247.     transition: transform .2s;
  248.   }
  249.   .group-chevron.open { transform: rotate(180deg); }
  250.  
  251.   table {
  252.     width: 100%;
  253.     border-collapse: collapse;
  254.     font-size: 13px;
  255.   }
  256.   thead th {
  257.     background: var(--bg3);
  258.     color: var(--text-dim);
  259.     font-family: var(--mono);
  260.     font-size: 10px;
  261.     text-transform: uppercase;
  262.     letter-spacing: 1px;
  263.     padding: 8px 12px;
  264.     text-align: left;
  265.     border-bottom: 1px solid var(--border);
  266.     white-space: nowrap;
  267.     cursor: pointer;
  268.     user-select: none;
  269.     transition: color .2s;
  270.   }
  271.   thead th:hover { color: var(--accent); }
  272.   thead th .sort-icon { margin-left: 4px; opacity: 0.4; }
  273.   thead th.sort-asc .sort-icon::after { content: '▲'; opacity: 1; color: var(--accent); }
  274.   thead th.sort-desc .sort-icon::after { content: '▼'; opacity: 1; color: var(--accent); }
  275.  
  276.   tbody tr {
  277.     border-bottom: 1px solid rgba(42,47,63,0.5);
  278.     transition: background .15s;
  279.   }
  280.   tbody tr:hover { background: rgba(255,165,0,0.04); }
  281.   td {
  282.     padding: 8px 12px;
  283.     white-space: nowrap;
  284.   }
  285.   td.mono { font-family: var(--mono); }
  286.   td.num { text-align: right; font-family: var(--mono); }
  287.   td.map-name {
  288.     font-family: var(--mono);
  289.     color: var(--accent2);
  290.     font-size: 12px;
  291.   }
  292.   .pill {
  293.     display: inline-block;
  294.     padding: 2px 8px;
  295.     border-radius: 3px;
  296.     font-family: var(--mono);
  297.     font-size: 11px;
  298.     font-weight: 700;
  299.   }
  300.   .pill-ok { background: rgba(76,202,138,0.15); color: var(--success); }
  301.   .pill-warn { background: rgba(240,165,0,0.15); color: var(--accent); }
  302.   .pill-danger { background: rgba(224,82,82,0.15); color: var(--danger); }
  303.  
  304.   /* FLAT VIEW */
  305.   #flat-table thead th:first-child { width: 180px; }
  306.  
  307.   /* No results */
  308.   .no-results {
  309.     text-align: center;
  310.     padding: 60px;
  311.     color: var(--text-dim);
  312.     font-family: var(--mono);
  313.     letter-spacing: 2px;
  314.   }
  315.   .no-results .icon { font-size: 40px; margin-bottom: 12px; }
  316.  
  317.   /* Scrollbar */
  318.   ::-webkit-scrollbar { width: 6px; height: 6px; }
  319.   ::-webkit-scrollbar-track { background: var(--bg); }
  320.   ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
  321.   ::-webkit-scrollbar-thumb:hover { background: var(--accent); }
  322.  
  323.   /* Anim */
  324.   @keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
  325.   .fade-in { animation: fadeIn .3s ease both; }
  326.  
  327.   .collapsed { display: none; }
  328.  
  329.   /* FILE PICKER */
  330.   #file-overlay {
  331.     position: fixed; inset: 0;
  332.     background: var(--bg);
  333.     display: flex; align-items: center; justify-content: center;
  334.     z-index: 999;
  335.     flex-direction: column;
  336.     gap: 24px;
  337.   }
  338.   #file-overlay.hidden { display: none; }
  339.   .file-drop-zone {
  340.     border: 2px dashed var(--border);
  341.     border-radius: 12px;
  342.     padding: 60px 80px;
  343.     text-align: center;
  344.     cursor: pointer;
  345.     transition: border-color .2s, background .2s;
  346.     position: relative;
  347.   }
  348.   .file-drop-zone:hover, .file-drop-zone.drag-over {
  349.     border-color: var(--accent);
  350.     background: rgba(240,165,0,0.05);
  351.   }
  352.   .file-drop-zone input[type=file] {
  353.     position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%;
  354.   }
  355.   .drop-icon { font-size: 48px; margin-bottom: 16px; }
  356.   .drop-title {
  357.     font-family: var(--mono);
  358.     font-size: 16px;
  359.     color: var(--accent);
  360.     letter-spacing: 2px;
  361.     text-transform: uppercase;
  362.     margin-bottom: 8px;
  363.   }
  364.   .drop-sub {
  365.     font-size: 12px;
  366.     color: var(--text-dim);
  367.     font-family: var(--mono);
  368.     letter-spacing: 1px;
  369.   }
  370.   .overlay-logo {
  371.     font-family: var(--mono);
  372.     font-size: 22px;
  373.     color: var(--accent);
  374.     letter-spacing: 3px;
  375.     text-transform: uppercase;
  376.   }
  377.   #file-error {
  378.     font-family: var(--mono);
  379.     font-size: 12px;
  380.     color: var(--danger);
  381.     letter-spacing: 1px;
  382.     display: none;
  383.   }
  384.   .btn-load-another {
  385.     background: transparent;
  386.     border: 1px solid var(--border);
  387.     color: var(--text-dim);
  388.     font-family: var(--mono);
  389.     font-size: 11px;
  390.     padding: 6px 14px;
  391.     border-radius: 4px;
  392.     cursor: pointer;
  393.     letter-spacing: 1px;
  394.     transition: all .2s;
  395.     margin-left: 12px;
  396.   }
  397.   .btn-load-another:hover { border-color: var(--accent); color: var(--accent); }
  398.   #main-content { display: none; }
  399. </style>
  400. </head>
  401. <body>
  402.  
  403. <div id="file-overlay">
  404.   <div class="overlay-logo">🚌 Driver Statistics</div>
  405.   <div class="file-drop-zone" id="drop-zone">
  406.     <input type="file" id="file-input" accept=".dat,.xml">
  407.     <div class="drop-icon">📂</div>
  408.     <div class="drop-title">Wczytaj plik danych</div>
  409.     <div class="drop-sub">Kliknij lub przeciągnij plik .dat / .xml</div>
  410.   </div>
  411.   <div id="file-error">⚠ Nie udało się odczytać pliku. Upewnij się, że to prawidłowy plik XML.</div>
  412. </div>
  413.  
  414. <div id="main-content">
  415.  
  416.   <div class="header-icon">🚌</div>
  417.   <div>
  418.     <div class="header-title">Driver Statistics</div>
  419.     <div class="header-sub" id="date-range-label">Wczytywanie danych...</div>
  420.   </div>
  421.   <div class="header-stats">
  422.     <div class="hstat">
  423.       <div class="hstat-val" id="hstat-sessions">—</div>
  424.       <div class="hstat-label">Sesji</div>
  425.     </div>
  426.     <div class="hstat">
  427.       <div class="hstat-val" id="hstat-maps">—</div>
  428.       <div class="hstat-label">Map</div>
  429.     </div>
  430.   </div>
  431. </div>
  432.  
  433. <div class="filters">
  434.   <div class="filter-group">
  435.     <label>🗺 Mapa</label>
  436.     <select id="filter-map">
  437.       <option value="">Wszystkie mapy</option>
  438.     </select>
  439.   </div>
  440.   <div class="filter-group">
  441.     <label>📅 Data od</label>
  442.     <input type="date" id="filter-date-from">
  443.   </div>
  444.   <div class="filter-group">
  445.     <label>📅 Data do</label>
  446.     <input type="date" id="filter-date-to">
  447.   </div>
  448.   <button class="btn-reset" onclick="resetFilters()">✕ Resetuj</button>
  449.   <div class="view-toggle">
  450.     <button class="btn-view active" id="btn-grouped" onclick="setView('grouped')">Pogrupowane</button>
  451.     <button class="btn-view" id="btn-flat" onclick="setView('flat')">Wszystkie sesje</button>
  452.     <button class="btn-view" id="btn-summary" onclick="setView('summary')">Sumy per mapa</button>
  453.   </div>
  454. </div>
  455.  
  456. <div class="summary-cards" id="summary-cards"></div>
  457. <div class="table-wrap" id="table-wrap"></div>
  458. </div><!-- /main-content -->
  459.  
  460. <script>
  461.  
  462. // =============================================================================
  463. // SEKCJA 1: WCZYTYWANIE PLIKU
  464. // Używamy File API — wbudowanego w przeglądarkę mechanizmu do odczytu plików
  465. // z dysku użytkownika. Bez tego nie można otworzyć pliku lokalnie bez serwera.
  466. // =============================================================================
  467.  
  468. // Pobieramy referencje do elementów HTML, żeby móc nimi sterować z JS.
  469. // getElementById zwraca element o podanym id="" ze strony.
  470. const fileInput = document.getElementById('file-input');  // input type=file (ukryty)
  471. const dropZone  = document.getElementById('drop-zone');   // ramka "przeciągnij plik"
  472. const overlay   = document.getElementById('file-overlay'); // cały ekran powitalny
  473. const fileError = document.getElementById('file-error');  // komunikat błędu
  474.  
  475. // Obsługa kliknięcia / wyboru pliku przez okno dialogowe.
  476. // Zdarzenie 'change' odpala się gdy użytkownik wybierze plik.
  477. // e.target.files[0] to pierwszy wybrany plik (obsługujemy tylko jeden).
  478. fileInput.addEventListener('change', e => {
  479.   if (e.target.files[0]) readFile(e.target.files[0]);
  480. });
  481.  
  482. // Obsługa przeciągania pliku (drag & drop) na strefę upuszczania.
  483. // 'dragover' — kursor z plikiem jest nad strefą (musimy wywołać preventDefault,
  484. //              żeby przeglądarka nie otwierała pliku sama w zakładce)
  485. // 'dragleave' — kursor opuścił strefę — usuwamy efekt wizualny
  486. // 'drop'      — użytkownik upuścił plik — pobieramy go z e.dataTransfer.files
  487. dropZone.addEventListener('dragover',  e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
  488. dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
  489. dropZone.addEventListener('drop', e => {
  490.   e.preventDefault();
  491.   dropZone.classList.remove('drag-over');
  492.   if (e.dataTransfer.files[0]) readFile(e.dataTransfer.files[0]);
  493. });
  494.  
  495. // Odczyt zawartości pliku jako tekstu (UTF-8).
  496. // FileReader to wbudowany obiekt przeglądarki działający asynchronicznie —
  497. // nie blokuje strony podczas czytania. Gdy skończy, wywołuje callback onload.
  498. function readFile(file) {
  499.   const reader = new FileReader();
  500.   // onload odpala się po pomyślnym wczytaniu — e.target.result to treść pliku jako string
  501.   reader.onload = e => {
  502.     try {
  503.       loadData(e.target.result, file.name);
  504.     } catch(err) {
  505.       // Jeśli XML jest uszkodzony lub plik nie jest właściwym formatem — pokaż błąd
  506.       showFileError();
  507.     }
  508.   };
  509.   reader.onerror = showFileError; // Błąd I/O (np. brak dostępu do pliku)
  510.   reader.readAsText(file, 'utf-8');
  511. }
  512.  
  513. // Pokazuje czerwony komunikat błędu pod strefą upuszczania
  514. function showFileError() {
  515.   fileError.style.display = 'block';
  516. }
  517.  
  518.  
  519. // =============================================================================
  520. // SEKCJA 2: FUNKCJE POMOCNICZE (formatowanie danych)
  521. // =============================================================================
  522.  
  523. // Parsowanie czasu w formacie ISO 8601 (np. "PT1H30M15S") na sekundy.
  524. // Format: P = Period, T = Time, H = godziny, M = minuty, S = sekundy.
  525. // Regex (?:(\d+)H)? oznacza "opcjonalnie: cyfry przed H" — każda część jest opcjonalna.
  526. // Operator + zamienia string na liczbę; ||0 zabezpiecza gdy dana część nie wystąpi.
  527. function parseDuration(s) {
  528.   if (!s) return 0;
  529.   const m = s.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
  530.   if (!m) return 0;
  531.   return (+(m[1]||0)) * 3600   // godziny → sekundy
  532.        + (+(m[2]||0)) * 60     // minuty → sekundy
  533.        + (+(m[3]||0));          // sekundy
  534. }
  535.  
  536. // Zamienia liczbę sekund z powrotem na czytelny tekst (np. 5400 → "1h 30m").
  537. // Math.floor() zaokrągla w dół (np. 3661 sekund to 1h 1m, nie 1h 1m 1s).
  538. // Modulo (%) wyciąga resztę z dzielenia, np. 3661 % 3600 = 61 sekund → 1 minuta.
  539. function fmtDuration(secs) {
  540.   const h = Math.floor(secs / 3600);
  541.   const m = Math.floor((secs % 3600) / 60);
  542.   const s = secs % 60;
  543.   if (h > 0) return `${h}h ${m}m`;
  544.   if (m > 0) return `${m}m ${s}s`;
  545.   return `${s}s`;
  546. }
  547.  
  548. // Usuwa prefix "maps/" i końcowy ukośnik z nazwy mapy (np. "maps/golczewo 2.0/" → "golczewo 2.0").
  549. // replace() z /\/$/ to wyrażenie regularne dopasowujące ukośnik na końcu stringa.
  550. function fmtMapName(raw) {
  551.   return raw.replace('maps/', '').replace(/\/$/, '');
  552. }
  553.  
  554. // Formatuje liczbę zmiennoprzecinkową z podaną liczbą miejsc po przecinku,
  555. // zamieniając kropkę na przecinek (polska notacja dziesiętna).
  556. function fmtNum(n, dec = 2) {
  557.   return n.toFixed(dec).replace('.', ',');
  558. }
  559.  
  560. // Zamienia obiekt Date na string "RRRR-MM-DD" (pierwsze 10 znaków z formatu ISO).
  561. function fmtDate(d) {
  562.   return d.toISOString().slice(0, 10);
  563. }
  564.  
  565.  
  566. // =============================================================================
  567. // SEKCJA 3: STAN APLIKACJI
  568. // Zmienne globalne przechowujące aktualny stan widoku i filtrów.
  569. // "Stan" to w uproszczeniu: co aktualnie widzisz i jakie masz ustawione filtry.
  570. // =============================================================================
  571.  
  572. let currentView = 'grouped'; // aktywny widok: 'grouped' | 'flat' | 'summary'
  573. let sortCol = null, sortDir = 1; // (nieużywane globalnie — każdy widok ma własne)
  574. let filterMap  = '';   // aktualnie wybrana mapa (pusty string = wszystkie)
  575. let filterFrom = '';   // data początkowa filtra
  576. let filterTo   = '';   // data końcowa filtra
  577. let allSessions = [];  // tablica wszystkich sesji wczytanych z pliku
  578.  
  579. // Podpinamy słuchaczy zdarzeń do kontrolek filtrów.
  580. // Za każdym razem gdy użytkownik zmieni wartość, aktualizujemy zmienną i przerysowujemy widok.
  581. const mapSel = document.getElementById('filter-map');
  582. mapSel.addEventListener('change', e => { filterMap = e.target.value; render(); });
  583. document.getElementById('filter-date-from').addEventListener('change', e => { filterFrom = e.target.value; render(); });
  584. document.getElementById('filter-date-to').addEventListener('change', e => { filterTo = e.target.value; render(); });
  585.  
  586.  
  587. // =============================================================================
  588. // SEKCJA 4: WCZYTYWANIE I PARSOWANIE DANYCH XML
  589. // =============================================================================
  590.  
  591. // Główna funkcja wywoływana po odczytaniu pliku przez FileReader.
  592. // xmlText — zawartość pliku jako string, filename — nazwa pliku do wyświetlenia w nagłówku.
  593. function loadData(xmlText, filename) {
  594.   fileError.style.display = 'none'; // ukryj ewentualny poprzedni błąd
  595.  
  596.   // DOMParser to wbudowany parser przeglądarki — zamienia string XML na drzewo DOM,
  597.   // po którym można nawigować tak samo jak po strukturze HTML strony.
  598.   const parser = new DOMParser();
  599.   const doc = parser.parseFromString(xmlText, 'application/xml');
  600.  
  601.   // Jeśli XML jest nieprawidłowy, przeglądarka wstawia element <parsererror>.
  602.   // Rzucamy wyjątek, który złapie blok catch w readFile() i pokaże błąd.
  603.   if (doc.querySelector('parsererror')) throw new Error('XML parse error');
  604.  
  605.   // querySelectorAll zwraca wszystkie węzły XML o nazwie tblMapStatistics jako tablicę.
  606.   // Array.from() jest potrzebne, bo querySelectorAll zwraca NodeList, nie Array.
  607.   const allNodes = Array.from(doc.querySelectorAll('tblMapStatistics'));
  608.  
  609.   // Mapujemy każdy węzeł XML na płaski obiekt JavaScript — łatwiej z nim pracować.
  610.   // querySelector('tagName').textContent odczytuje zawartość tekstową danego elementu.
  611.   // Operator + przed stringiem zamienia go na liczbę (np. +'16' === 16).
  612.   // parseFloat obsługuje liczby zmiennoprzecinkowe.
  613.   allSessions = allNodes.map(n => ({
  614.     mapRaw:       n.querySelector('mapName').textContent,                       // oryginalna nazwa z XML
  615.     map:          fmtMapName(n.querySelector('mapName').textContent),           // oczyszczona nazwa
  616.     date:         new Date(n.querySelector('date').textContent),                // obiekt Date do sortowania
  617.     dateStr:      n.querySelector('date').textContent.slice(0, 10),            // string "RRRR-MM-DD" do wyświetlania
  618.     timeActive:   parseDuration(n.querySelector('timeActive').textContent),    // czas jazdy w sekundach
  619.     km:           parseFloat(n.querySelector('kilometersDriven').textContent), // kilometry
  620.     stopsPassedBy: +n.querySelector('stopsPassedBy').textContent,              // przystanki obsłużone
  621.     stopsMissed:   +n.querySelector('stopsMissed').textContent,                // przystanki ominięte
  622.     stopsTooEarly: +n.querySelector('stopsTooEarly').textContent,              // odjazdy za wcześnie
  623.     stopsTooLate:  +n.querySelector('stopsTooLate').textContent,               // odjazdy za późno
  624.     passengers:    +n.querySelector('passengersServed').textContent,           // liczba pasażerów
  625.     revenue:      parseFloat(n.querySelector('revenueEarned').textContent),    // przychód
  626.   }))
  627.   // Odrzucamy "puste" sesje (0 km i 0 pasażerów) — to zwykle przypadkowe uruchomienia gry.
  628.   // filter() zwraca nową tablicę zawierającą tylko elementy, dla których callback zwraca true.
  629.   .filter(s => !(s.km === 0 && s.passengers === 0));
  630.  
  631.   // Sortujemy chronologicznie — odejmowanie dwóch obiektów Date zwraca różnicę w ms.
  632.   // Jeśli wynik ujemny, a idzie przed b; jeśli dodatni — odwrotnie.
  633.   allSessions.sort((a, b) => a.date - b.date);
  634.  
  635.   // Budujemy listę unikalnych map do dropdownu filtra.
  636.   // new Set() usuwa duplikaty, spread [...] zamienia Set z powrotem na tablicę.
  637.   mapSel.innerHTML = '<option value="">Wszystkie mapy</option>'; // reset dropdownu
  638.   const maps = [...new Set(allSessions.map(s => s.map))].sort();
  639.   maps.forEach(m => {
  640.     const o = document.createElement('option');
  641.     o.value = m;
  642.     o.textContent = m;
  643.     mapSel.appendChild(o);
  644.   });
  645.  
  646.   // Ustawiamy domyślny zakres dat na cały dostępny przedział danych.
  647.   const dates = allSessions.map(s => s.dateStr);
  648.   filterMap = '';
  649.   filterFrom = dates[0];
  650.   filterTo   = dates[dates.length - 1];
  651.   document.getElementById('filter-date-from').value = filterFrom;
  652.   document.getElementById('filter-date-to').value   = filterTo;
  653.  
  654.   // Aktualizujemy nagłówek strony.
  655.   document.getElementById('date-range-label').textContent =
  656.     `${filename}  ·  ${dates[0]} — ${dates[dates.length - 1]}  ·  ${allSessions.length} sesji`;
  657.   document.getElementById('hstat-sessions').textContent = allSessions.length;
  658.   document.getElementById('hstat-maps').textContent     = maps.length;
  659.  
  660.   // Ukrywamy ekran powitalny i pokazujemy właściwą treść raportu.
  661.   // classList.add/remove/toggle manipulują klasami CSS elementu.
  662.   overlay.classList.add('hidden');
  663.   document.getElementById('main-content').style.display = 'block';
  664.  
  665.   render(); // pierwsze rysowanie widoku
  666. }
  667.  
  668.  
  669. // =============================================================================
  670. // SEKCJA 5: OBSŁUGA FILTRÓW I WIDOKÓW
  671. // =============================================================================
  672.  
  673. // Resetuje wszystkie filtry do wartości domyślnych (pełny zakres danych).
  674. // Odczytuje daty bezpośrednio z wartości inputów (pewniejsze niż ze zmiennych).
  675. function resetFilters() {
  676.   const dateInputFrom = document.getElementById('filter-date-from');
  677.   const dateInputTo   = document.getElementById('filter-date-to');
  678.   // Pobieramy min i max z aktualnych sesji
  679.   const dates = allSessions.map(s => s.dateStr);
  680.   filterMap  = '';
  681.   filterFrom = dates[0];
  682.   filterTo   = dates[dates.length - 1];
  683.   mapSel.value        = '';
  684.   dateInputFrom.value = filterFrom;
  685.   dateInputTo.value   = filterTo;
  686.   render();
  687. }
  688.  
  689. // Przełącza aktywny widok i aktualizuje wygląd przycisków.
  690. // classList.toggle(cls, condition) — dodaje klasę gdy condition=true, usuwa gdy false.
  691. function setView(v) {
  692.   currentView = v;
  693.   document.getElementById('btn-grouped').classList.toggle('active', v === 'grouped');
  694.   document.getElementById('btn-flat').classList.toggle('active', v === 'flat');
  695.   document.getElementById('btn-summary').classList.toggle('active', v === 'summary');
  696.   render();
  697. }
  698.  
  699. // Zwraca przefiltrowaną tablicę sesji na podstawie aktualnych ustawień filtrów.
  700. // Porównanie stringów dat (format RRRR-MM-DD) działa leksykograficznie — poprawnie!
  701. // Np. '2025-10-15' < '2025-11-01' jest prawdą, bo porównujemy znak po znaku.
  702. function getFiltered() {
  703.  return allSessions.filter(s => {
  704.     if (filterMap  && s.map     !== filterMap)  return false; // filtr mapy
  705.     if (filterFrom && s.dateStr <  filterFrom)  return false; // dolna granica dat
  706.     if (filterTo   && s.dateStr >  filterTo)    return false; // górna granica dat
  707.     return true;
  708.   });
  709. }
  710.  
  711.  
  712. // =============================================================================
  713. // SEKCJA 6: KARTY PODSUMOWANIA (górny rząd kafelków z liczbami)
  714. // =============================================================================
  715.  
  716. // Oblicza sumy dla aktualnie widocznych sesji i wstawia karty do DOM.
  717. // reduce() to metoda tablicy — "składa" wszystkie elementy w jedną wartość.
  718. // Np. [1,2,3].reduce((suma, x) => suma + x, 0) === 6
  719. function renderCards(sessions) {
  720.   const totalKm     = sessions.reduce((a, s) => a + s.km, 0);
  721.   const totalSecs   = sessions.reduce((a, s) => a + s.timeActive, 0);
  722.   const totalPass   = sessions.reduce((a, s) => a + s.passengers, 0);
  723.   const totalRev    = sessions.reduce((a, s) => a + s.revenue, 0);
  724.   const totalMissed = sessions.reduce((a, s) => a + s.stopsMissed, 0);
  725.   const totalLate   = sessions.reduce((a, s) => a + s.stopsTooLate, 0);
  726.   const totalEarly  = sessions.reduce((a, s) => a + s.stopsTooEarly, 0);
  727.   // new Set().size zlicza unikalne wartości — ile różnych map jest w wynikach
  728.   const mapCount    = new Set(sessions.map(s => s.map)).size;
  729.  
  730.   // Template literals (backtick ``) pozwalają wstawiać wyrażenia JS w ${} wewnątrz stringa.
  731.   // innerHTML zastępuje całą zawartość elementu nowym HTML.
  732.   document.getElementById('summary-cards').innerHTML = `
  733.     <div class="card fade-in"><div class="card-val">${sessions.length}</div><div class="card-label">Sesji</div></div>
  734.     <div class="card blue fade-in"><div class="card-val">${mapCount}</div><div class="card-label">Map</div></div>
  735.     <div class="card fade-in"><div class="card-val">${fmtNum(totalKm, 1)} km</div><div class="card-label">Łączny dystans</div></div>
  736.     <div class="card blue fade-in"><div class="card-val">${fmtDuration(totalSecs)}</div><div class="card-label">Łączny czas</div></div>
  737.     <div class="card green fade-in"><div class="card-val">${totalPass.toLocaleString('pl')}</div><div class="card-label">Pasażerów</div></div>
  738.     <div class="card green fade-in"><div class="card-val">${fmtNum(totalRev, 0)}</div><div class="card-label">Przychód</div></div>
  739.     <div class="card red fade-in"><div class="card-val">${totalMissed}</div><div class="card-label">Przystanki ominięte</div></div>
  740.     <div class="card fade-in"><div class="card-val">${totalLate}</div><div class="card-label">Za późno</div></div>
  741.     <div class="card fade-in"><div class="card-val">${totalEarly}</div><div class="card-label">Za wcześnie</div></div>
  742.   `;
  743. }
  744.  
  745.  
  746. // =============================================================================
  747. // SEKCJA 7: GŁÓWNA FUNKCJA RENDEROWANIA
  748. // Wywoływana za każdym razem gdy coś się zmienia (filtr, widok, wczytanie pliku).
  749. // =============================================================================
  750.  
  751. function render() {
  752.   const sessions = getFiltered(); // pobierz przefiltrowane dane
  753.  
  754.   renderCards(sessions); // odśwież karty podsumowania
  755.  
  756.   // Aktualizuj liczniki w nagłówku
  757.   document.getElementById('hstat-sessions').textContent = sessions.length;
  758.   document.getElementById('hstat-maps').textContent     = new Set(sessions.map(s => s.map)).size;
  759.  
  760.   // Jeśli po filtrach nic nie zostało — pokaż komunikat zamiast pustej tabeli
  761.   if (sessions.length === 0) {
  762.     document.getElementById('table-wrap').innerHTML =
  763.       `<div class="no-results"><div class="icon">🚫</div>BRAK WYNIKÓW</div>`;
  764.     return; // return przerywa funkcję — dalszy kod się nie wykona
  765.   }
  766.  
  767.   // Wywołaj odpowiednią funkcję renderowania w zależności od aktywnego widoku
  768.   if      (currentView === 'grouped') renderGrouped(sessions);
  769.   else if (currentView === 'flat')    renderFlat(sessions);
  770.   else                                renderSummary(sessions);
  771. }
  772.  
  773.  
  774. // =============================================================================
  775. // SEKCJA 8: WIDOK POGRUPOWANY (sesje zgrupowane po nazwie mapy)
  776. // =============================================================================
  777.  
  778. function renderGrouped(sessions) {
  779.   // Grupujemy sesje w obiekt słownikowy: { "nazwa mapy": [sesja1, sesja2, ...] }
  780.   // Jeśli klucz jeszcze nie istnieje, inicjalizujemy go pustą tablicą.
  781.   const byMap = {};
  782.   sessions.forEach(s => {
  783.     if (!byMap[s.map]) byMap[s.map] = [];
  784.     byMap[s.map].push(s);
  785.   });
  786.  
  787.   // Sortujemy nazwy map alfabetycznie dla przejrzystości
  788.   const mapsSorted = Object.keys(byMap).sort();
  789.  
  790.   let html = '';
  791.   mapsSorted.forEach(map => {
  792.     const rows = byMap[map];
  793.     // Generujemy unikalny id dla każdej grupy (do zwijania/rozwijania).
  794.     // replace(/[^a-z0-9]/gi, '_') zamienia wszystkie znaki inne niż litery/cyfry na _
  795.     // żeby id było prawidłowym identyfikatorem HTML.
  796.     const id = `grp-${map.replace(/[^a-z0-9]/gi, '_')}`;
  797.  
  798.     html += `
  799.     <div class="group-section fade-in">
  800.       <div class="group-header" onclick="toggleGroup('${id}')">
  801.         <span class="group-name">🗺 ${map}</span>
  802.         <span class="group-badge">${rows.length} sesji</span>
  803.         <span class="group-badge">${fmtNum(rows.reduce((a, r) => a + r.km, 0), 1)} km</span>
  804.         <span class="group-badge">${rows.reduce((a, r) => a + r.passengers, 0)} pasażerów</span>
  805.         <span class="group-chevron open" id="chev-${id}">▼</span>
  806.       </div>
  807.       <div id="${id}">
  808.         <table>
  809.           <thead><tr>
  810.             <th>Data</th><th>Czas</th><th>Km</th>
  811.             <th>Przystanki</th><th>Ominięte</th><th>Za wcześnie</th><th>Za późno</th>
  812.             <th>Pasażerowie</th><th>Przychód</th>
  813.           </tr></thead>
  814.           <tbody>
  815.             ${rows.map(r => `<tr>
  816.               <td class="mono">${r.dateStr}</td>
  817.               <td class="mono">${fmtDuration(r.timeActive)}</td>
  818.               <td class="num">${fmtNum(r.km, 2)}</td>
  819.               <td class="num">${r.stopsPassedBy}</td>
  820.               <td class="num">${r.stopsMissed  > 0 ? `<span class="pill pill-danger">${r.stopsMissed}</span>`  : '—'}</td>
  821.               <td class="num">${r.stopsTooEarly > 0 ? `<span class="pill pill-warn">${r.stopsTooEarly}</span>` : '—'}</td>
  822.               <td class="num">${r.stopsTooLate  > 0 ? `<span class="pill pill-warn">${r.stopsTooLate}</span>`  : '—'}</td>
  823.               <td class="num">${r.passengers}</td>
  824.               <td class="num">${fmtNum(r.revenue, 2)}</td>
  825.             </tr>`).join('')}
  826.           </tbody>
  827.         </table>
  828.       </div>
  829.     </div>`;
  830.   });
  831.  
  832.   document.getElementById('table-wrap').innerHTML = html;
  833. }
  834.  
  835. // Zwija lub rozwija sekcję grupy klikając w jej nagłówek.
  836. // classList.toggle() — dodaje klasę jeśli jej nie ma, usuwa jeśli jest.
  837. // Klasa CSS "collapsed" ma display:none — ukrywa element.
  838. function toggleGroup(id) {
  839.   const el   = document.getElementById(id);
  840.   const chev = document.getElementById('chev-' + id);
  841.   el.classList.toggle('collapsed');   // ukryj/pokaż zawartość
  842.   chev.classList.toggle('open');      // obróć strzałkę (▼ / ▲ przez CSS transform)
  843. }
  844.  
  845.  
  846. // =============================================================================
  847. // SEKCJA 9: WIDOK PŁASKI (wszystkie sesje w jednej tabeli, z sortowaniem)
  848. // =============================================================================
  849.  
  850. // Zmienne stanu sortowania dla tego widoku — poza funkcją, żeby przeżyły re-render.
  851. let flatSortCol = 'date'; // domyślnie sortuj po dacie
  852. let flatSortDir = 1;      // 1 = rosnąco, -1 = malejąco
  853.  
  854. function renderFlat(sessions) {
  855.   // Definicja kolumn: klucz w obiekcie sesji + etykieta nagłówka.
  856.   // Przechowywanie ich w tablicy pozwala generować th i td w jednej pętli.
  857.   const cols = [
  858.     { key: 'map',          label: 'Mapa' },
  859.     { key: 'date',         label: 'Data' },
  860.     { key: 'timeActive',   label: 'Czas' },
  861.     { key: 'km',           label: 'Km' },
  862.     { key: 'stopsPassedBy',label: 'Przystanki' },
  863.     { key: 'stopsMissed',  label: 'Ominięte' },
  864.     { key: 'stopsTooEarly',label: 'Za wcześnie' },
  865.     { key: 'stopsTooLate', label: 'Za późno' },
  866.     { key: 'passengers',   label: 'Pasażerowie' },
  867.     { key: 'revenue',      label: 'Przychód' },
  868.   ];
  869.  
  870.   // Tworzymy kopię tablicy ([...sessions]) przed sortowaniem — sort() modyfikuje tablicę
  871.   // w miejscu, a my nie chcemy zmieniać oryginalnej kolejności allSessions.
  872.   const sorted = [...sessions].sort((a, b) => {
  873.     let av = a[flatSortCol], bv = b[flatSortCol];
  874.     // Dla stringów używamy localeCompare (obsługuje polskie znaki: ą, ę, ó itp.)
  875.     // Dla liczb i dat zwykłe odejmowanie da właściwą kolejność.
  876.     if (typeof av === 'string') return flatSortDir * av.localeCompare(bv);
  877.     return flatSortDir * (av - bv);
  878.   });
  879.  
  880.   // Generujemy nagłówki tabeli. Aktywna kolumna dostaje klasę sort-asc lub sort-desc
  881.   // (CSS dodaje strzałkę ▲ / ▼ przez pseudoelement ::after).
  882.   const thHTML = cols.map(c => {
  883.     const cls = flatSortCol === c.key
  884.       ? (flatSortDir === 1 ? 'sort-asc' : 'sort-desc')
  885.       : '';
  886.     return `<th class="${cls}" onclick="flatSort('${c.key}')">${c.label}<span class="sort-icon"></span></th>`;
  887.   }).join('');
  888.  
  889.   const rowsHTML = sorted.map(r => `<tr>
  890.     <td class="map-name">${r.map}</td>
  891.     <td class="mono">${r.dateStr}</td>
  892.     <td class="mono">${fmtDuration(r.timeActive)}</td>
  893.     <td class="num">${fmtNum(r.km, 2)}</td>
  894.     <td class="num">${r.stopsPassedBy}</td>
  895.     <td class="num">${r.stopsMissed  > 0 ? `<span class="pill pill-danger">${r.stopsMissed}</span>`  : '—'}</td>
  896.     <td class="num">${r.stopsTooEarly > 0 ? `<span class="pill pill-warn">${r.stopsTooEarly}</span>` : '—'}</td>
  897.     <td class="num">${r.stopsTooLate  > 0 ? `<span class="pill pill-warn">${r.stopsTooLate}</span>`  : '—'}</td>
  898.     <td class="num">${r.passengers}</td>
  899.     <td class="num">${fmtNum(r.revenue, 2)}</td>
  900.   </tr>`).join('');
  901.  
  902.   document.getElementById('table-wrap').innerHTML = `
  903.   <table id="flat-table" class="fade-in">
  904.     <thead><tr>${thHTML}</tr></thead>
  905.     <tbody>${rowsHTML}</tbody>
  906.   </table>`;
  907. }
  908.  
  909. // Obsługa kliknięcia w nagłówek kolumny.
  910. // Jeśli kliknięto tę samą kolumnę co poprzednio — odwróć kierunek (*= -1).
  911. // Jeśli nową — ustaw ją jako aktywną i zacznij od rosnącego.
  912. // window. jest tu wymagane, bo funkcja jest wywoływana z atrybutu onclick w HTML
  913. // (inline onclick ma dostęp tylko do globalnego zasięgu window).
  914. window.flatSort = function(col) {
  915.   if (flatSortCol === col) flatSortDir *= -1;
  916.   else { flatSortCol = col; flatSortDir = 1; }
  917.   render();
  918. };
  919.  
  920.  
  921. // =============================================================================
  922. // SEKCJA 10: WIDOK SUMY PER MAPA (zagregowane statystyki dla każdej mapy)
  923. // =============================================================================
  924.  
  925. let sumSortCol = 'sessions'; // domyślnie sortuj po liczbie sesji
  926. let sumSortDir = -1;         // -1 = malejąco (najpopularniejsze mapy na górze)
  927.  
  928. function renderSummary(sessions) {
  929.   // Agregacja — "składamy" wszystkie sesje w jeden rekord per mapa.
  930.   // Podobnie jak w renderGrouped, ale zamiast przechowywać sesje, sumujemy wartości.
  931.   const byMap = {};
  932.   sessions.forEach(s => {
  933.     if (!byMap[s.map]) {
  934.       // Inicjalizacja rekordu dla nowej mapy
  935.       byMap[s.map] = { map: s.map, sessions: 0, km: 0, time: 0,
  936.                        passengers: 0, revenue: 0, missed: 0, early: 0, late: 0, stops: 0 };
  937.     }
  938.     const g = byMap[s.map];
  939.     g.sessions++;
  940.     g.km         += s.km;
  941.     g.time       += s.timeActive;
  942.     g.passengers += s.passengers;
  943.     g.revenue    += s.revenue;
  944.     g.missed     += s.stopsMissed;
  945.     g.early      += s.stopsTooEarly;
  946.     g.late       += s.stopsTooLate;
  947.     g.stops      += s.stopsPassedBy;
  948.   });
  949.  
  950.   // Definicja kolumn z funkcją formatowania (fmt) — każda kolumna wie jak wyświetlić swoją wartość.
  951.   // Takie podejście (dane + logika w jednym miejscu) nazywa się "data-driven rendering".
  952.   const cols = [
  953.     { key: 'map',       label: 'Mapa',         fmt: r => r.map },
  954.     { key: 'sessions',  label: 'Sesji',         fmt: r => r.sessions },
  955.     { key: 'km',        label: 'Łączne km',     fmt: r => fmtNum(r.km, 1) + ' km' },
  956.     { key: 'time',      label: 'Łączny czas',   fmt: r => fmtDuration(r.time) },
  957.     { key: 'passengers',label: 'Pasażerowie',   fmt: r => r.passengers.toLocaleString('pl') },
  958.     { key: 'revenue',   label: 'Przychód',      fmt: r => fmtNum(r.revenue, 2) },
  959.     { key: 'stops',     label: 'Przystanki',    fmt: r => r.stops },
  960.     { key: 'missed',    label: 'Ominięte',      fmt: r => r.missed > 0 ? `<span class="pill pill-danger">${r.missed}</span>` : '0' },
  961.     { key: 'early',     label: 'Za wcześnie',   fmt: r => r.early  > 0 ? `<span class="pill pill-warn">${r.early}</span>`   : '0' },
  962.     { key: 'late',      label: 'Za późno',      fmt: r => r.late   > 0 ? `<span class="pill pill-warn">${r.late}</span>`    : '0' },
  963.   ];
  964.  
  965.   // Object.values() zwraca tablicę wartości ze słownika (same rekordy, bez kluczy).
  966.   const rows = Object.values(byMap).sort((a, b) => {
  967.     let av = a[sumSortCol], bv = b[sumSortCol];
  968.     if (typeof av === 'string') return sumSortDir * av.localeCompare(bv);
  969.     return sumSortDir * (av - bv);
  970.   });
  971.  
  972.   const thHTML = cols.map(c => {
  973.     const cls = sumSortCol === c.key
  974.       ? (sumSortDir === 1 ? 'sort-asc' : 'sort-desc')
  975.       : '';
  976.     return `<th class="${cls}" onclick="sumSort('${c.key}')">${c.label}<span class="sort-icon"></span></th>`;
  977.   }).join('');
  978.  
  979.   // cols.map((c, i) => ...) — i to indeks kolumny; pierwsza kolumna (mapa) dostaje
  980.   // inną klasę CSS niż pozostałe (wyrównanie do lewej zamiast do prawej).
  981.   const rowsHTML = rows.map(r => `<tr>
  982.     ${cols.map((c, i) => `<td class="${i === 0 ? 'map-name' : 'num'}">${c.fmt(r)}</td>`).join('')}
  983.   </tr>`).join('');
  984.  
  985.   document.getElementById('table-wrap').innerHTML = `
  986.   <table class="fade-in">
  987.     <thead><tr>${thHTML}</tr></thead>
  988.     <tbody>${rowsHTML}</tbody>
  989.   </table>`;
  990. }
  991.  
  992. // Analogicznie do flatSort — obsługa kliknięcia w nagłówek tabeli sumarycznej.
  993. // Domyślny kierunek to malejący (-1), bo przy statystykach zwykle interesuje nas "top".
  994. window.sumSort = function(col) {
  995.   if (sumSortCol === col) sumSortDir *= -1;
  996.   else { sumSortCol = col; sumSortDir = -1; }
  997.   render();
  998. };
  999.  
  1000. // Pierwsze wywołanie render() — nic jeszcze nie wyświetla (allSessions jest puste),
  1001. // ale inicjalizuje karty i nagłówki w stanie "oczekiwania na plik".
  1002. render();
  1003. </script>
  1004. </body>
  1005. </html>
  1006.  
Tags: OMSI2
Advertisement
Add Comment
Please, Sign In to add comment