3059 lines
138 KiB
Plaintext
3059 lines
138 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="<%= lang %>">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||
<title><%= squadronData.short_name || squadronData.tag_name %> - Squadron Profile | <%= botName %></title>
|
||
<meta name="description" content="View <%= squadronData.long_name || squadronData.tag_name || squadronData.squadron_name %>'s squadron statistics and member performance.">
|
||
<link rel="icon" type="image/png" href="/images/transparent_toothlessssss.png">
|
||
|
||
<!-- Fonts -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
||
<link rel="preload" href="/Fonts/symbols_skyquake.ttf" as="font" type="font/ttf" crossorigin="anonymous">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||
|
||
<!-- Tailwind CSS -->
|
||
<link rel="stylesheet" href="/css/output.css">
|
||
|
||
<!-- Font Awesome -->
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" media="print" onload="this.media='all'">
|
||
|
||
<style>
|
||
@font-face {
|
||
font-family: 'skyquakesymbols';
|
||
src: url('/Fonts/symbols_skyquake.ttf') format('truetype');
|
||
font-display: swap;
|
||
}
|
||
|
||
body {
|
||
background: #1b1b1b;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.text-accent { color: #F5F5DC; }
|
||
.text-muted { color: #90EE90; }
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, #F5F5DC 0%, #E8E8D0 100%);
|
||
box-shadow: 0 4px 20px rgba(245, 245, 220, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||
color: #1E1E1E;
|
||
font-weight: 700;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
box-shadow: 0 8px 25px rgba(245, 245, 220, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.season-recap-modal.hidden { display: none; }
|
||
.season-recap-modal { position: fixed; inset: 0; z-index: 1000; overflow-y: auto; }
|
||
.season-recap-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); }
|
||
.season-recap-content {
|
||
position: relative;
|
||
width: min(92vw, 1100px);
|
||
margin: 2vh auto;
|
||
background: #1a1f2e;
|
||
color: #e2e8f0;
|
||
padding: 0.85rem 1.25rem 1rem;
|
||
border-radius: 0.75rem;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||
}
|
||
.season-recap-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.6rem; }
|
||
.season-recap-header h2 { margin: 0; font-size: 1.2rem; }
|
||
.season-recap-close { background: transparent; border: 0; color: #e2e8f0; font-size: 1.75rem; cursor: pointer; line-height: 1; }
|
||
.season-recap-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1.25rem; margin-bottom: 0.5rem; }
|
||
.season-recap-controls > div { display: flex; flex-direction: column; }
|
||
.season-recap-body label { display: block; margin-bottom: 0.15rem; color: #94a3b8; font-size: 0.85rem; }
|
||
.season-recap-theme-group { display: inline-flex; gap: 0.5rem; font-size: 0.9rem; }
|
||
.season-recap-theme-group label { margin: 0; color: #e2e8f0; font-size: 0.9rem; cursor: pointer; }
|
||
#season-recap-btn .fa-image { margin-right: 0.45rem; }
|
||
.season-recap-body select {
|
||
padding: 0.4rem 0.6rem;
|
||
border-radius: 0.35rem;
|
||
margin-right: 0.5rem;
|
||
background: #0f172a;
|
||
color: #e2e8f0;
|
||
border: 1px solid rgba(144,238,144,0.3);
|
||
min-width: 12rem;
|
||
font: inherit;
|
||
}
|
||
.season-recap-body select option { background: #0f172a; color: #e2e8f0; }
|
||
#season-recap-status { margin: 0.75rem 0; color: #94a3b8; font-size: 0.9rem; }
|
||
#season-recap-image-wrap { margin-top: 0.75rem; }
|
||
#season-recap-image {
|
||
display: none;
|
||
max-width: 100%;
|
||
max-height: 75vh;
|
||
object-fit: contain;
|
||
border-radius: 0.35rem;
|
||
}
|
||
|
||
.squadron-container {
|
||
max-width: none;
|
||
margin: 0 auto;
|
||
padding: 5rem 1rem 2rem;
|
||
min-height: calc(100vh - 200px);
|
||
width: 100%;
|
||
}
|
||
|
||
.squadron-header {
|
||
background: linear-gradient(135deg, rgba(30, 30, 30, 0.8), rgba(40, 40, 40, 0.8));
|
||
border-radius: 1rem;
|
||
padding: 2rem;
|
||
margin-bottom: 2rem;
|
||
color: white;
|
||
text-align: center;
|
||
border: 1px solid rgba(144, 238, 144, 0.1);
|
||
}
|
||
|
||
.squadron-title-section {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 1rem;
|
||
margin-bottom: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.squadron-name {
|
||
font-family: 'skyquakesymbols', 'Inter', sans-serif !important;
|
||
font-size: 2.5rem;
|
||
font-weight: 700;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.squadron-stats-summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||
gap: 0.75rem;
|
||
margin-top: 1.5rem;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.squadron-stats-summary {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.squadron-stats-summary {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
.stat-card {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 0.5rem;
|
||
padding: 0.75rem;
|
||
text-align: center;
|
||
backdrop-filter: blur(10px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
min-height: 78px;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 1.3rem;
|
||
font-weight: 600;
|
||
display: block;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.8rem;
|
||
opacity: 0.8;
|
||
margin-top: 0.2rem;
|
||
}
|
||
|
||
.members-section {
|
||
background: rgba(30, 30, 30, 0.6);
|
||
border-radius: 1rem;
|
||
padding: 2rem;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(144, 238, 144, 0.1);
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 1.8rem;
|
||
font-weight: 600;
|
||
margin-bottom: 1.5rem;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.members-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: rgba(30, 30, 30, 0.8);
|
||
border-radius: 0.75rem;
|
||
overflow: hidden;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
border: 1px solid rgba(144, 238, 144, 0.1);
|
||
}
|
||
|
||
.members-table th {
|
||
background: linear-gradient(135deg, rgba(144, 238, 144, 0.2), rgba(0, 255, 107, 0.2));
|
||
color: #90EE90;
|
||
padding: 1.25rem;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
border-bottom: 1px solid rgba(144, 238, 144, 0.2);
|
||
font-size: 0.95rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
user-select: none;
|
||
}
|
||
|
||
.members-table th:hover {
|
||
background: linear-gradient(135deg, rgba(144, 238, 144, 0.3), rgba(0, 255, 107, 0.3));
|
||
}
|
||
|
||
.sortable {
|
||
position: relative;
|
||
}
|
||
|
||
.sort-icon {
|
||
font-size: 0.8rem;
|
||
opacity: 0.6;
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
.sortable.sorted-asc .sort-icon {
|
||
opacity: 1;
|
||
}
|
||
|
||
.sortable.sorted-asc .sort-icon::before {
|
||
content: "\f0de";
|
||
}
|
||
|
||
.sortable.sorted-desc .sort-icon {
|
||
opacity: 1;
|
||
}
|
||
|
||
.sortable.sorted-desc .sort-icon::before {
|
||
content: "\f0dd";
|
||
}
|
||
|
||
.members-table th:first-child {
|
||
text-align: left;
|
||
}
|
||
|
||
.members-table td {
|
||
padding: 1.25rem;
|
||
border-bottom: 1px solid rgba(144, 238, 144, 0.1);
|
||
background: rgba(30, 30, 30, 0.4);
|
||
color: #ffffff;
|
||
transition: all 0.3s ease;
|
||
text-align: center;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.members-table td:first-child {
|
||
text-align: left;
|
||
}
|
||
|
||
.members-table tr:hover td {
|
||
background: rgba(144, 238, 144, 0.05);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.player-name {
|
||
font-weight: 600;
|
||
color: #90EE90;
|
||
font-size: 1.1rem;
|
||
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
||
white-space: nowrap;
|
||
vertical-align: middle;
|
||
}
|
||
.player-name .pdm-details-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
border: 1px solid rgba(144,238,144,0.15);
|
||
color: rgba(144,238,144,0.5);
|
||
width: 22px; height: 22px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.65rem;
|
||
margin-left: 0.5rem;
|
||
vertical-align: middle;
|
||
padding: 0;
|
||
flex-shrink: 0;
|
||
transition: all 0.2s;
|
||
}
|
||
.player-name .pdm-details-btn:hover {
|
||
background: rgba(144,238,144,0.12);
|
||
color: #90EE90;
|
||
border-color: rgba(144,238,144,0.4);
|
||
}
|
||
|
||
.stat-cell {
|
||
font-weight: 600;
|
||
font-size: 1rem;
|
||
color: #90EE90;
|
||
text-shadow: 0 0 10px rgba(144, 238, 144, 0.5);
|
||
}
|
||
|
||
.back-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
color: var(--primary-color);
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
margin-bottom: 2rem;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.back-link:hover {
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
.responsive-table {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
/* Filter Controls Styling */
|
||
.leaderboard-controls {
|
||
background: rgba(30, 30, 30, 0.6);
|
||
border-radius: 1rem;
|
||
padding: 1.5rem;
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(144, 238, 144, 0.2);
|
||
}
|
||
|
||
.controls-row {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: end;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.filter-group label {
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.filter-select, .filter-input {
|
||
background: rgba(30, 30, 30, 0.9);
|
||
border: 2px solid rgba(144, 238, 144, 0.3);
|
||
border-radius: 0.75rem;
|
||
padding: 0.625rem 0.875rem;
|
||
color: #ffffff;
|
||
font-size: 0.95rem;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
min-width: 140px;
|
||
}
|
||
|
||
.filter-select:focus, .filter-input:focus {
|
||
outline: none;
|
||
border-color: #90EE90;
|
||
box-shadow: 0 0 20px rgba(144, 238, 144, 0.4);
|
||
}
|
||
|
||
.filter-select:hover, .filter-input:hover {
|
||
border-color: rgba(144, 238, 144, 0.5);
|
||
}
|
||
|
||
.filter-select option {
|
||
background: rgba(30, 30, 30, 0.95);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.btn-action {
|
||
background: linear-gradient(135deg, rgba(144, 238, 144, 0.3), rgba(0, 255, 107, 0.3));
|
||
border: 2px solid #90EE90;
|
||
color: #90EE90;
|
||
padding: 0.625rem 1.5rem;
|
||
border-radius: 0.75rem;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
transition: all 0.3s ease;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.btn-action:hover {
|
||
background: linear-gradient(135deg, rgba(144, 238, 144, 0.4), rgba(0, 255, 107, 0.4));
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 20px rgba(144, 238, 144, 0.4);
|
||
}
|
||
|
||
.clear-filters-btn {
|
||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(220, 38, 38, 0.2));
|
||
border: 2px solid rgba(239, 68, 68, 0.3);
|
||
color: #ef4444;
|
||
padding: 0.625rem 1.5rem;
|
||
border-radius: 0.75rem;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
transition: all 0.3s ease;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.clear-filters-btn:hover {
|
||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(220, 38, 38, 0.3));
|
||
border-color: #ef4444;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 20px rgba(239, 68, 68, 0.4);
|
||
}
|
||
|
||
.squadron-chart-section {
|
||
margin-top: 1.5rem;
|
||
padding-top: 1.5rem;
|
||
border-top: 1px solid rgba(144, 238, 144, 0.12);
|
||
}
|
||
|
||
.sq-time-filter-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: end;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
padding-bottom: 1rem;
|
||
border-bottom: 1px solid rgba(144, 238, 144, 0.15);
|
||
}
|
||
|
||
.sq-time-filter-bar .filter-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.sq-time-filter-bar label {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-weight: 500;
|
||
font-size: 0.85rem;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.sq-time-filter-bar select,
|
||
.sq-time-filter-bar input[type="date"] {
|
||
background: rgba(30, 30, 30, 0.9);
|
||
border: 1px solid rgba(144, 238, 144, 0.25);
|
||
border-radius: 0.5rem;
|
||
padding: 0.45rem 0.7rem;
|
||
color: #ffffff;
|
||
font-size: 0.9rem;
|
||
min-width: 140px;
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
font-family: inherit;
|
||
}
|
||
|
||
.sq-time-filter-bar select:focus,
|
||
.sq-time-filter-bar input[type="date"]:focus {
|
||
outline: none;
|
||
border-color: #90EE90;
|
||
box-shadow: 0 0 15px rgba(144, 238, 144, 0.3);
|
||
}
|
||
|
||
.sq-time-filter-bar select option {
|
||
background: rgba(30, 30, 30, 0.95);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.sq-time-filter-bar input[type="date"]::-webkit-calendar-picker-indicator {
|
||
filter: invert(1) opacity(0.6);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.sq-time-filter-bar .custom-range {
|
||
flex-direction: row;
|
||
align-items: end;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.sq-time-filter-bar .filter-reset-btn {
|
||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(220, 38, 38, 0.2));
|
||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||
color: #ef4444;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 0.5rem;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
font-size: 0.85rem;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.sq-time-filter-bar .filter-reset-btn:hover {
|
||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.35), rgba(220, 38, 38, 0.35));
|
||
border-color: #ef4444;
|
||
}
|
||
|
||
.chart-toggles-wrap {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.chart-toggle-group {
|
||
position: relative;
|
||
display: inline-flex;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 2rem;
|
||
padding: 3px;
|
||
border: 1px solid rgba(144, 238, 144, 0.2);
|
||
}
|
||
|
||
.chart-toggle-slider {
|
||
position: absolute;
|
||
top: 3px;
|
||
left: 3px;
|
||
height: calc(100% - 6px);
|
||
background: #90EE90;
|
||
border-radius: calc(2rem - 2px);
|
||
transition: left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.chart-toggle {
|
||
position: relative;
|
||
z-index: 1;
|
||
background: transparent;
|
||
border: none;
|
||
color: rgba(255, 255, 255, 0.45);
|
||
padding: 0.4rem 1.3rem;
|
||
border-radius: calc(2rem - 2px);
|
||
cursor: pointer;
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
transition: color 0.25s ease;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.chart-toggle.active {
|
||
color: #1b1b1b;
|
||
}
|
||
|
||
.chart-toggle:hover:not(.active) {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.chart-wrapper {
|
||
position: relative;
|
||
height: 220px;
|
||
}
|
||
|
||
.chart-loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 220px;
|
||
color: rgba(255, 255, 255, 0.35);
|
||
font-size: 0.88rem;
|
||
}
|
||
|
||
.chart-collapse {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-top: 0.75rem;
|
||
cursor: pointer;
|
||
color: rgba(144, 238, 144, 0.35);
|
||
transition: color 0.2s;
|
||
padding: 0.2rem;
|
||
}
|
||
|
||
.chart-collapse:hover {
|
||
color: rgba(144, 238, 144, 0.75);
|
||
}
|
||
|
||
/* View Toggle (cumulative/individual) */
|
||
.section-header-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.section-header-row .section-title {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.view-toggle-group {
|
||
position: relative;
|
||
display: inline-flex;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 2rem;
|
||
padding: 3px;
|
||
border: 1px solid rgba(144, 238, 144, 0.2);
|
||
}
|
||
|
||
.view-toggle-slider {
|
||
position: absolute;
|
||
top: 3px;
|
||
left: 3px;
|
||
height: calc(100% - 6px);
|
||
background: #90EE90;
|
||
border-radius: calc(2rem - 2px);
|
||
transition: left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.view-toggle {
|
||
position: relative;
|
||
z-index: 1;
|
||
background: transparent;
|
||
border: none;
|
||
color: rgba(255, 255, 255, 0.45);
|
||
padding: 0.35rem 1.1rem;
|
||
border-radius: calc(2rem - 2px);
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
transition: color 0.25s ease;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.view-toggle.active {
|
||
color: #1b1b1b;
|
||
}
|
||
|
||
.view-toggle:hover:not(.active) {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
/* Games section */
|
||
.sq-games-section {
|
||
background: rgba(30, 30, 30, 0.6);
|
||
border-radius: 1rem;
|
||
padding: 2rem;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(144, 238, 144, 0.1);
|
||
}
|
||
|
||
.sq-games-loading, .sq-games-error, .sq-no-games {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.sq-games-loading i {
|
||
font-size: 3rem;
|
||
color: #90EE90;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.sq-games-table {
|
||
width: 100%;
|
||
min-width: 1100px;
|
||
border-collapse: collapse;
|
||
background: rgba(30, 30, 30, 0.8);
|
||
border-radius: 0.75rem;
|
||
overflow: hidden;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
border: 1px solid rgba(144, 238, 144, 0.1);
|
||
table-layout: fixed;
|
||
}
|
||
|
||
.sq-games-table th {
|
||
background: linear-gradient(135deg, rgba(144, 238, 144, 0.2), rgba(0, 255, 107, 0.2));
|
||
color: #90EE90;
|
||
padding: 0.75rem;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
border-bottom: 1px solid rgba(144, 238, 144, 0.2);
|
||
font-size: 0.95rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.sq-games-table th:hover {
|
||
background: linear-gradient(135deg, rgba(144, 238, 144, 0.3), rgba(0, 255, 107, 0.3));
|
||
text-shadow: 0 0 15px rgba(144, 238, 144, 0.6);
|
||
}
|
||
|
||
.sq-games-table th.sortable::after {
|
||
content: '';
|
||
font-size: 0.8rem;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.sq-games-table th.sorted-asc::after {
|
||
content: ' ↑';
|
||
color: #90EE90;
|
||
opacity: 1;
|
||
}
|
||
|
||
.sq-games-table th.sorted-desc::after {
|
||
content: ' ↓';
|
||
color: #90EE90;
|
||
opacity: 1;
|
||
}
|
||
|
||
.sq-games-table th:first-child {
|
||
text-align: left;
|
||
}
|
||
|
||
/* Squadron games table column widths (8 columns) */
|
||
.sq-games-table th:nth-child(1),
|
||
.sq-games-table td:nth-child(1) { width: 15%; }
|
||
.sq-games-table th:nth-child(2),
|
||
.sq-games-table td:nth-child(2) { width: 22%; }
|
||
.sq-games-table th:nth-child(3),
|
||
.sq-games-table td:nth-child(3) { width: 10%; }
|
||
.sq-games-table th:nth-child(4),
|
||
.sq-games-table td:nth-child(4) { width: 10%; }
|
||
.sq-games-table th:nth-child(5),
|
||
.sq-games-table td:nth-child(5) { width: 10%; }
|
||
.sq-games-table th:nth-child(6),
|
||
.sq-games-table td:nth-child(6) { width: 10%; }
|
||
.sq-games-table th:nth-child(7),
|
||
.sq-games-table td:nth-child(7) { width: 10%; }
|
||
.sq-games-table th:nth-child(8),
|
||
.sq-games-table td:nth-child(8) { width: 13%; }
|
||
|
||
/* Per-row map background: paint on the tr so one image spans the row */
|
||
.sq-games-table tr[data-session-id] {
|
||
background-image:
|
||
linear-gradient(rgba(30, 30, 30, 0.92), rgba(30, 30, 30, 0.92)),
|
||
var(--bg-img, none);
|
||
background-size: cover;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
}
|
||
|
||
.sq-games-table tr[data-session-id]:hover {
|
||
background-image:
|
||
linear-gradient(rgba(144, 238, 144, 0.05), rgba(144, 238, 144, 0.05)),
|
||
linear-gradient(rgba(30, 30, 30, 0.82), rgba(30, 30, 30, 0.82)),
|
||
var(--bg-img, none);
|
||
}
|
||
|
||
.sq-games-table td {
|
||
padding: 1rem 0.625rem;
|
||
border-bottom: 1px solid rgba(144, 238, 144, 0.1);
|
||
background: transparent;
|
||
color: #ffffff;
|
||
transition: all 0.3s ease;
|
||
text-align: center;
|
||
}
|
||
|
||
.sq-games-table td:first-child {
|
||
text-align: left;
|
||
}
|
||
|
||
.sq-games-table tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.sq-game-date {
|
||
font-weight: 600;
|
||
color: #90EE90;
|
||
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
||
}
|
||
|
||
.sq-game-map {
|
||
font-weight: 600;
|
||
color: #A8E6CF;
|
||
text-shadow: 0 0 8px rgba(144, 238, 144, 0.2);
|
||
}
|
||
|
||
.sq-game-map i {
|
||
color: #90EE90;
|
||
font-size: 0.75rem;
|
||
margin-right: 0.4rem;
|
||
}
|
||
|
||
.sq-game-result {
|
||
font-weight: 700;
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 0.25rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.sq-game-result.win {
|
||
background: rgba(16, 185, 129, 0.2);
|
||
color: #10b981;
|
||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||
}
|
||
|
||
.sq-game-result.loss {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
color: #ef4444;
|
||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
}
|
||
|
||
.sq-game-result.draw {
|
||
background: rgba(245, 158, 11, 0.2);
|
||
color: #f59e0b;
|
||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||
}
|
||
|
||
.sq-games-filter-bar {
|
||
background: rgba(30, 30, 30, 0.9);
|
||
border-radius: 1rem;
|
||
padding: 1rem 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
border: 1px solid rgba(144, 238, 144, 0.15);
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.sq-games-filter-bar label {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.sq-games-filter-bar input[type="text"] {
|
||
background: rgba(30, 30, 30, 0.9);
|
||
border: 1px solid rgba(144, 238, 144, 0.25);
|
||
border-radius: 0.5rem;
|
||
padding: 0.5rem 0.8rem;
|
||
color: #ffffff;
|
||
font-size: 0.9rem;
|
||
min-width: 180px;
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.sq-games-filter-bar input[type="text"]:focus {
|
||
outline: none;
|
||
border-color: #90EE90;
|
||
box-shadow: 0 0 15px rgba(144, 238, 144, 0.3);
|
||
}
|
||
|
||
.sq-games-filter-bar input[type="text"]::placeholder {
|
||
color: rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
.sq-games-filter-count {
|
||
margin-left: auto;
|
||
color: #90EE90;
|
||
font-weight: 700;
|
||
font-size: 1rem;
|
||
text-shadow: 0 0 15px rgba(144, 238, 144, 0.4);
|
||
padding: 0.4rem 0.8rem;
|
||
background: rgba(144, 238, 144, 0.08);
|
||
border-radius: 0.5rem;
|
||
border: 1px solid rgba(144, 238, 144, 0.3);
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body class="text-white antialiased">
|
||
<%- include('partials/nav', { activePage: '' }) %>
|
||
|
||
<div class="squadron-container">
|
||
<a href="/squadrons" class="back-link">
|
||
<i class="fas fa-arrow-left"></i>
|
||
<%= t('squadrons.backToSquadronHub') %>
|
||
</a>
|
||
|
||
<div class="squadron-header">
|
||
<div class="squadron-title-section">
|
||
<h1 class="squadron-name" style="font-family: 'skyquakesymbols', 'Inter', sans-serif !important;"><%= squadronData.tag_name || squadronData.squadron_name %><br><span style="font-size: 2rem;"><%= squadronData.long_name || '' %></span></h1>
|
||
</div>
|
||
<div style="font-size: 0.9rem; opacity: 0.8; margin-top: 0.5rem; font-style: italic;">
|
||
<i class="fas fa-info-circle" style="margin-right: 0.5rem;"></i>
|
||
<%= t('common.recordingSince') %>
|
||
</div>
|
||
|
||
<%
|
||
const summaryKps = typeof squadronData.squadron_summary.kps === 'number'
|
||
? squadronData.squadron_summary.kps.toFixed(2)
|
||
: (Number(squadronData.squadron_summary.kps || 0)).toFixed(2);
|
||
%>
|
||
<div class="squadron-stats-summary">
|
||
<% if (squadronData.squadron_summary.points && squadronData.squadron_summary.points.has_points_data) { %>
|
||
<div class="stat-card">
|
||
<span class="stat-number"><%= squadronData.squadron_summary.points.total_points.toLocaleString() %></span>
|
||
<div class="stat-label"><%= t('squadrons.squadronPoints') %></div>
|
||
</div>
|
||
<% } %>
|
||
<div class="stat-card">
|
||
<span class="stat-number"><%= squadronData.squadron_summary.player_count %></span>
|
||
<div class="stat-label"><%= t('common.members') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-total-battles"><%= squadronData.squadron_summary.total_battles %></span>
|
||
<div class="stat-label"><%= t('common.totalBattles') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-wins"><%= squadronData.squadron_summary.wins %></span>
|
||
<div class="stat-label"><%= t('common.wins') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-win-rate"><%= squadronData.squadron_summary.win_rate.toFixed(1) %>%</span>
|
||
<div class="stat-label"><%= t('common.winRate') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-total-kills"><%= squadronData.squadron_summary.total_kills %></span>
|
||
<div class="stat-label"><%= t('common.totalKills') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-kdr"><%= squadronData.squadron_summary.kdr.toFixed(2) %></span>
|
||
<div class="stat-label"><%= t('common.kdr') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-kps"><%= summaryKps %></span>
|
||
<div class="stat-label">KPS</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-ground-kills"><%= squadronData.squadron_summary.ground_kills %></span>
|
||
<div class="stat-label"><%= t('common.groundKills') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-air-kills"><%= squadronData.squadron_summary.air_kills %></span>
|
||
<div class="stat-label"><%= t('common.airKills') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-assists"><%= squadronData.squadron_summary.assists %></span>
|
||
<div class="stat-label"><%= t('common.assists') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-deaths"><%= squadronData.squadron_summary.deaths %></span>
|
||
<div class="stat-label"><%= t('common.deaths') %></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number" id="sq-stat-captures"><%= squadronData.squadron_summary.captures %></span>
|
||
<div class="stat-label"><%= t('common.captures') %></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 1rem;">
|
||
<button type="button" id="season-recap-btn" class="btn btn-primary" <% if (!squadronData.clan_id) { %>disabled title="<%= t('seasonCard.buttonDisabledTitle') %>"<% } %>>
|
||
<i class="fas fa-image"></i> <%= t('seasonCard.buttonLabel') %>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="squadron-chart-section" id="squadronChartSection" data-squadron="<%= squadronData.long_name || squadronData.tag_name %>">
|
||
<div class="chart-toggles-wrap">
|
||
<div class="chart-toggle-group" id="chartToggles">
|
||
<div class="chart-toggle-slider" id="chartToggleSlider"></div>
|
||
<button class="chart-toggle" data-metric="win_rate"><%= t('common.winRate') %></button>
|
||
<button class="chart-toggle" data-metric="kdr"><%= t('common.kdr') %></button>
|
||
<button class="chart-toggle" data-metric="battles"><%= t('common.battles') %></button>
|
||
<button class="chart-toggle active" data-metric="rating"><%= t('common.rating') %></button>
|
||
</div>
|
||
</div>
|
||
<div class="chart-wrapper" id="chartWrapper">
|
||
<div class="chart-loading" id="chartLoading"><%= t('player.loadingChartData') %></div>
|
||
<canvas id="squadronChart" style="display:none;"></canvas>
|
||
</div>
|
||
<div style="display:flex;justify-content:center;margin-top:0.5rem;" id="sqChartScaleWrap">
|
||
<div class="chart-toggle-group" id="sqChartScaleToggles" style="margin:0;">
|
||
<div class="chart-toggle-slider" id="sqChartScaleSlider"></div>
|
||
<button class="chart-toggle" data-scale="alltime"><%= t('player.allTime') %></button>
|
||
<button class="chart-toggle active" data-scale="relative"><%= t('player.relative') %></button>
|
||
</div>
|
||
</div>
|
||
<div class="chart-collapse" id="chartCollapseBtn" title="<%= t('player.collapseChart') %>">
|
||
<i class="fas fa-chevron-up" id="chartCollapseIcon"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-header-row">
|
||
<h2 class="section-title" id="viewSectionTitle"><%= t('squadrons.squadronMembers') %></h2>
|
||
<div class="view-toggle-group" id="viewToggles">
|
||
<div class="view-toggle-slider" id="viewToggleSlider"></div>
|
||
<button class="view-toggle active" data-page="cumulative"><%= t('player.cumulative') %></button>
|
||
<button class="view-toggle" data-page="games"><%= t('player.individual') %></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Time/date filter bar — shared between cumulative and games pages, mounted via JS -->
|
||
<div class="sq-time-filter-bar" id="sq-time-filter-bar">
|
||
<div class="filter-group">
|
||
<label for="sq-filter-category"><i class="fas fa-filter" style="margin-right: 0.3rem;"></i><%= t('player.filterBy') %></label>
|
||
<select id="sq-filter-category" onchange="updateSquadronFilterCategory()">
|
||
<option value="all"><%= t('player.allTime') %></option>
|
||
<option value="date"><%= t('player.dateRange') %></option>
|
||
<option value="season"><%= t('player.season') %></option>
|
||
<option value="week"><%= t('player.week') %></option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="filter-group" id="sq-date-type-filter" style="display: none;">
|
||
<label for="sq-date-filter-type"><i class="fas fa-calendar-alt" style="margin-right: 0.3rem;"></i><%= t('player.dateType') %></label>
|
||
<select id="sq-date-filter-type" onchange="handleSquadronDateTypeChange()">
|
||
<option value="last7"><%= t('player.last7Days') %></option>
|
||
<option value="last30"><%= t('player.last30Days') %></option>
|
||
<option value="last90"><%= t('player.last90Days') %></option>
|
||
<option value="custom"><%= t('player.customRange') %></option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="filter-group custom-range" id="sq-custom-range-filters" style="display: none;">
|
||
<div class="filter-group">
|
||
<label for="sq-start-date"><%= t('player.from') %></label>
|
||
<input type="date" id="sq-start-date" onchange="applySquadronFilters()">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label for="sq-end-date"><%= t('player.to') %></label>
|
||
<input type="date" id="sq-end-date" onchange="applySquadronFilters()">
|
||
</div>
|
||
<div class="filter-group" id="sq-single-day-timeslot-filter" style="display: none;">
|
||
<label for="sq-single-day-timeslot"><%= t('player.timeslot') %></label>
|
||
<select id="sq-single-day-timeslot" onchange="applySquadronFilters()">
|
||
<option value="all"><%= t('player.fullDay') %></option>
|
||
<option value="na"><%= t('analytics.naTimeslot') %></option>
|
||
<option value="eu"><%= t('analytics.euTimeslot') %></option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-group" id="sq-season-select-filter" style="display: none;">
|
||
<label for="sq-season"><%= t('player.selectSeason') %></label>
|
||
<select id="sq-season" onchange="applySquadronFilters()">
|
||
<option value=""><%= t('player.pleaseSelect') %></option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="filter-group" id="sq-week-season-filter" style="display: none;">
|
||
<label for="sq-week-season-select"><%= t('player.selectSeason') %></label>
|
||
<select id="sq-week-season-select" onchange="updateSquadronWeekOptions()">
|
||
<option value=""><%= t('player.pleaseSelect') %></option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="filter-group" id="sq-week-select-filter" style="display: none;">
|
||
<label for="sq-week"><%= t('player.selectWeek') %></label>
|
||
<select id="sq-week" onchange="applySquadronFilters()">
|
||
<option value=""><%= t('player.pleaseSelect') %></option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="filter-group" id="sq-reset-btn-group" style="display: none;">
|
||
<label> </label>
|
||
<button class="filter-reset-btn" onclick="resetSquadronFilters()" type="button">
|
||
<i class="fas fa-times-circle"></i> <%= t('player.resetFilters') %>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="cumulative-page">
|
||
<div class="members-section">
|
||
<div class="sq-filter-mount" id="sq-filter-mount-cumulative"></div>
|
||
<% if (squadronData.players && squadronData.players.length > 0) { %>
|
||
<div class="responsive-table">
|
||
<table class="members-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="sortable" onclick="sortMembersTable('nick')" data-sort="nick">
|
||
<%= t('common.player') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('sqb_points')" data-sort="sqb_points">
|
||
<%= t('common.points') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('total_battles')" data-sort="total_battles">
|
||
<%= t('common.battles') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('wins')" data-sort="wins">
|
||
<%= t('common.wins') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('win_rate')" data-sort="win_rate">
|
||
<%= t('common.winRate') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('total_kills')" data-sort="total_kills">
|
||
<%= t('common.totalKills') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('kps')" data-sort="kps">
|
||
<%= t('common.kps') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('kdr')" data-sort="kdr">
|
||
<%= t('common.kdr') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('ground_kills')" data-sort="ground_kills">
|
||
<%= t('common.groundKills') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('air_kills')" data-sort="air_kills">
|
||
<%= t('common.airKills') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('assists')" data-sort="assists">
|
||
<%= t('common.assists') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('deaths')" data-sort="deaths">
|
||
<%= t('common.deaths') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
<th class="sortable" onclick="sortMembersTable('captures')" data-sort="captures">
|
||
<%= t('common.captures') %> <i class="fas fa-sort sort-icon"></i>
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sq-members-table-body">
|
||
<% squadronData.players.forEach(player => { %>
|
||
<tr class="row-link">
|
||
<td class="player-name"><a href="/players/<%= player.uid %>" class="row-link-overlay" aria-label="View <%= player.nick %>"></a><%= player.nick %><button class="pdm-details-btn" onclick="event.stopPropagation(); openPlayerDetailsModal('<%= player.uid %>', '<%= player.nick.replace(/'/g, "\\'") %>')" title="<%= t('squadrons.quickDetails') %>"><i class="fas fa-eye"></i></button></td>
|
||
<td class="stat-cell"><%= player.sqb_points || 0 %></td>
|
||
<td class="stat-cell"><%= player.total_battles %></td>
|
||
<td class="stat-cell"><%= player.wins %></td>
|
||
<td class="stat-cell"><%= player.win_rate.toFixed(1) %>%</td>
|
||
<td class="stat-cell"><%= player.total_kills %></td>
|
||
<td class="stat-cell"><%= player.total_battles ? (player.total_kills / player.total_battles).toFixed(2) : '0.00' %></td>
|
||
<td class="stat-cell"><%= player.kdr.toFixed(2) %></td>
|
||
<td class="stat-cell"><%= player.ground_kills || 0 %></td>
|
||
<td class="stat-cell"><%= player.air_kills || 0 %></td>
|
||
<td class="stat-cell"><%= player.assists %></td>
|
||
<td class="stat-cell"><%= player.deaths || 0 %></td>
|
||
<td class="stat-cell"><%= player.captures %></td>
|
||
</tr>
|
||
<% }); %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } else { %>
|
||
<div style="text-align: center; padding: 3rem; color: rgba(255, 255, 255, 0.6);">
|
||
<i class="fas fa-users" style="font-size: 3rem; margin-bottom: 1rem;"></i>
|
||
<h3><%= t('squadrons.noMembersFound') %></h3>
|
||
<p><%= t('squadrons.noRecordedMembers') %></p>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
<div id="games-page" style="display: none;">
|
||
<div class="sq-games-section">
|
||
<div class="sq-filter-mount" id="sq-filter-mount-games"></div>
|
||
<div class="sq-games-loading" id="sq-games-loading">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
<p><%= t('squadrons.loadingSquadronGames') %></p>
|
||
</div>
|
||
<div class="sq-games-content" id="sq-games-content" style="display: none;">
|
||
<div class="sq-games-filter-bar">
|
||
<label for="sq-player-search"><i class="fas fa-user" style="margin-right: 0.3rem;"></i><%= t('common.player') %>:</label>
|
||
<input type="text" id="sq-player-search" placeholder="<%= t('common.searchPlayerByName') %>" oninput="filterSquadronGamesDebounced()">
|
||
<label for="sq-map-search"><i class="fas fa-map-marker-alt" style="margin-right: 0.3rem;"></i><%= t('common.map') %>:</label>
|
||
<input type="text" id="sq-map-search" placeholder="<%= t('squadrons.searchMapPlaceholder') %>" oninput="filterSquadronGamesDebounced()">
|
||
<div class="sq-games-filter-count"><span id="sq-filtered-count">0</span> <%= t('player.gamesShown') %></div>
|
||
</div>
|
||
<div class="responsive-table">
|
||
<table class="sq-games-table" id="sq-games-table">
|
||
<thead>
|
||
<tr>
|
||
<th onclick="sortSqGamesTable(0, 'string')" class="sortable"><%= t('common.date') %></th>
|
||
<th onclick="sortSqGamesTable(1, 'string')" class="sortable"><%= t('common.map') %></th>
|
||
<th onclick="sortSqGamesTable(2, 'number')" class="sortable"><%= t('common.groundKills') %></th>
|
||
<th onclick="sortSqGamesTable(3, 'number')" class="sortable"><%= t('common.airKills') %></th>
|
||
<th onclick="sortSqGamesTable(4, 'number')" class="sortable"><%= t('common.assists') %></th>
|
||
<th onclick="sortSqGamesTable(5, 'number')" class="sortable"><%= t('common.captures') %></th>
|
||
<th onclick="sortSqGamesTable(6, 'number')" class="sortable"><%= t('common.deaths') %></th>
|
||
<th onclick="sortSqGamesTable(7, 'string')" class="sortable"><%= t('common.result') %></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sq-games-table-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div class="sq-games-error" id="sq-games-error" style="display: none;">
|
||
<i class="fas fa-exclamation-triangle" style="font-size: 3rem; color: #f59e0b; margin-bottom: 1rem;"></i>
|
||
<h3><%= t('common.failedToLoad') %></h3>
|
||
<button class="btn btn-primary" onclick="retryLoadSquadronGames()" style="margin-top: 1rem;"><%= t('squadrons.retryLoadGames') %></button>
|
||
</div>
|
||
<div class="sq-no-games" id="sq-no-games" style="display: none;">
|
||
<i class="fas fa-gamepad" style="font-size: 3rem; color: #d1d5db; margin-bottom: 1rem;"></i>
|
||
<h3><%= t('squadrons.noSquadronGames') %></h3>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<%- include('partials/footer') %>
|
||
|
||
<script>
|
||
window.__lang = '<%= lang %>';
|
||
window.__i18n = <%- localeJson %>;
|
||
window.__t = function(key) {
|
||
var parts = key.split('.'), obj = window.__i18n;
|
||
for (var i = 0; i < parts.length; i++) { obj = obj && obj[parts[i]]; }
|
||
return obj !== undefined ? obj : key;
|
||
};
|
||
window.switchLanguage = function(lang) {
|
||
var next = lang || (document.documentElement.lang === 'en' ? 'ru' : 'en');
|
||
if (next === document.documentElement.lang) return;
|
||
document.cookie = 'lang=' + next + ';path=/;max-age=31536000;SameSite=Lax';
|
||
window.location.reload();
|
||
};
|
||
</script>
|
||
<script src="/js/main.js?v=3"></script>
|
||
<script src="/js/api-client.js"></script>
|
||
<script src="/js/chart.umd.min.js"></script>
|
||
<div id="season-recap-modal" class="season-recap-modal hidden" role="dialog" aria-hidden="true">
|
||
<div class="season-recap-backdrop" data-close></div>
|
||
<div class="season-recap-content">
|
||
<header class="season-recap-header">
|
||
<h2><%= t('seasonCard.modalTitle') %></h2>
|
||
<button type="button" class="season-recap-close" data-close aria-label="Close">×</button>
|
||
</header>
|
||
<div class="season-recap-body">
|
||
<div class="season-recap-controls">
|
||
<div>
|
||
<label for="season-recap-select"><%= t('seasonCard.seasonLabel') %></label>
|
||
<select id="season-recap-select"></select>
|
||
</div>
|
||
<div>
|
||
<label><%= t('seasonCard.themeLabel') %></label>
|
||
<span class="season-recap-theme-group">
|
||
<label><input type="radio" name="season-recap-theme" value="dark" checked> <%= t('seasonCard.themeDark') %></label>
|
||
<label><input type="radio" name="season-recap-theme" value="light"> <%= t('seasonCard.themeLight') %></label>
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<label> </label>
|
||
<button type="button" id="season-recap-generate" class="btn btn-primary"><%= t('seasonCard.generate') %></button>
|
||
</div>
|
||
</div>
|
||
<div id="season-recap-status"></div>
|
||
<div id="season-recap-image-wrap">
|
||
<img id="season-recap-image" alt="Season recap card" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script src="/js/player-details-modal.js"></script>
|
||
<script src="/js/recap-modal.js" data-clan-id="<%= squadronData.clan_id || '' %>"></script>
|
||
<script src="/js/header-search.js?v=2"></script>
|
||
<script src="/js/vehicle-i18n.js"></script>
|
||
<script>
|
||
(function() {
|
||
const squadronName = document.getElementById('squadronChartSection').dataset.squadron;
|
||
let chart = null;
|
||
let historyData = null;
|
||
let currentMetric = 'rating';
|
||
let renderedMetric = null;
|
||
let chartOpen = true;
|
||
let chartFilterStart = null;
|
||
let chartFilterEnd = null;
|
||
let chartScaleMode = 'relative';
|
||
|
||
const metricConfig = {
|
||
win_rate: { label: __t('common.winRate'), color: '#90EE90', suffix: '%' },
|
||
kdr: { label: __t('common.kdr'), color: '#64b5f6', suffix: '' },
|
||
battles: { label: __t('common.battles'), color: '#ffb74d', suffix: '' },
|
||
rating: { label: __t('common.rating'), color: '#ce93d8', suffix: '' }
|
||
};
|
||
|
||
function updateSlider(btn) {
|
||
const slider = document.getElementById('chartToggleSlider');
|
||
const group = document.getElementById('chartToggles');
|
||
const btnRect = btn.getBoundingClientRect();
|
||
const groupRect = group.getBoundingClientRect();
|
||
slider.style.left = (btnRect.left - groupRect.left) + 'px';
|
||
slider.style.width = btnRect.width + 'px';
|
||
}
|
||
|
||
function updateScaleSlider(btn) {
|
||
const slider = document.getElementById('sqChartScaleSlider');
|
||
const group = document.getElementById('sqChartScaleToggles');
|
||
const btnRect = btn.getBoundingClientRect();
|
||
const groupRect = group.getBoundingClientRect();
|
||
slider.style.left = (btnRect.left - groupRect.left) + 'px';
|
||
slider.style.width = btnRect.width + 'px';
|
||
}
|
||
|
||
function formatPeriod(dateStr) {
|
||
const d = new Date(dateStr + 'T00:00:00Z');
|
||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
|
||
}
|
||
|
||
function periodToTimestamp(dateStr) {
|
||
return new Date(dateStr + 'T00:00:00Z').getTime();
|
||
}
|
||
|
||
function formatUnixLabel(ts) {
|
||
const d = new Date(ts);
|
||
return d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: 'UTC' }) + ' UTC';
|
||
}
|
||
|
||
function withinFilter(ts) {
|
||
if (chartFilterStart && ts < chartFilterStart.getTime()) return false;
|
||
if (chartFilterEnd && ts > chartFilterEnd.getTime()) return false;
|
||
return true;
|
||
}
|
||
|
||
function renderChart(metric) {
|
||
const config = metricConfig[metric];
|
||
const metricChanged = renderedMetric !== metric;
|
||
|
||
let dataPoints;
|
||
let allDataPoints;
|
||
let isHourly = false;
|
||
if (metric === 'rating' && historyData && historyData.rating_hourly && historyData.rating_hourly.length) {
|
||
isHourly = true;
|
||
allDataPoints = historyData.rating_hourly.map(d => ({
|
||
x: d.t * 1000,
|
||
y: d.rating,
|
||
label: formatUnixLabel(d.t * 1000),
|
||
}));
|
||
dataPoints = allDataPoints.filter(p => withinFilter(p.x));
|
||
} else if (historyData && historyData.history && historyData.history.length) {
|
||
allDataPoints = historyData.history
|
||
.filter(d => d[metric] != null)
|
||
.map(d => ({ x: periodToTimestamp(d.period), y: d[metric], label: formatPeriod(d.period) }));
|
||
dataPoints = allDataPoints.filter(p => withinFilter(p.x));
|
||
} else {
|
||
return;
|
||
}
|
||
if (!dataPoints.length) return;
|
||
|
||
// X-axis bounds:
|
||
// alltime: span the full history range (earliest → latest of allDataPoints)
|
||
// so a season filter highlights a slice without zooming the axis
|
||
// relative: span only the filtered range (zoomed)
|
||
const earliestTs = allDataPoints[0].x;
|
||
const latestTs = allDataPoints[allDataPoints.length - 1].x;
|
||
const earliestYear = new Date(earliestTs).getUTCFullYear();
|
||
const xMin = chartScaleMode === 'alltime' ? Date.UTC(earliestYear, 0, 1) : dataPoints[0].x;
|
||
const xMax = chartScaleMode === 'alltime' ? latestTs : dataPoints[dataPoints.length - 1].x;
|
||
// Hourly points are dense — hide the dots and keep just the line
|
||
const pointRadius = isHourly ? 0 : 2;
|
||
const pointHoverRadius = isHourly ? 3 : 4;
|
||
|
||
document.getElementById('chartLoading').style.display = 'none';
|
||
const canvas = document.getElementById('squadronChart');
|
||
canvas.style.display = '';
|
||
|
||
if (chart && metricChanged) {
|
||
chart.destroy();
|
||
chart = null;
|
||
}
|
||
|
||
if (chart) {
|
||
chart.data.datasets[0].data = dataPoints;
|
||
chart.data.datasets[0].label = config.label;
|
||
chart.data.datasets[0].borderColor = config.color;
|
||
chart.data.datasets[0].backgroundColor = config.color + '18';
|
||
chart.data.datasets[0].pointBackgroundColor = config.color;
|
||
chart.data.datasets[0].pointBorderColor = config.color;
|
||
chart.data.datasets[0].pointHoverBackgroundColor = config.color;
|
||
chart.data.datasets[0].pointHoverBorderColor = config.color;
|
||
chart.data.datasets[0].pointRadius = pointRadius;
|
||
chart.data.datasets[0].pointHoverRadius = pointHoverRadius;
|
||
chart.options.plugins.tooltip.borderColor = config.color;
|
||
chart.options.plugins.tooltip.callbacks.label = ctx => `${config.label}: ${ctx.parsed.y}${config.suffix}`;
|
||
chart.options.plugins.tooltip.callbacks.title = ctx => ctx[0].raw.label;
|
||
chart.options.scales.x.min = xMin;
|
||
chart.options.scales.x.max = xMax;
|
||
chart.options.scales.y.ticks.callback = v => parseFloat(v.toFixed(2)) + config.suffix;
|
||
// Keep the dense hourly rating series snappy, but animate the
|
||
// normal daily/filtered transitions like the player chart.
|
||
if (isHourly) chart.update('none');
|
||
else chart.update();
|
||
renderedMetric = metric;
|
||
return;
|
||
}
|
||
|
||
chart = new Chart(canvas.getContext('2d'), {
|
||
type: 'line',
|
||
data: {
|
||
datasets: [{
|
||
label: config.label,
|
||
data: dataPoints,
|
||
borderColor: config.color,
|
||
backgroundColor: config.color + '18',
|
||
borderWidth: 2,
|
||
pointBackgroundColor: config.color,
|
||
pointBorderColor: config.color,
|
||
pointHoverBackgroundColor: config.color,
|
||
pointHoverBorderColor: config.color,
|
||
pointRadius: pointRadius,
|
||
pointHoverRadius: pointHoverRadius,
|
||
tension: 0.15,
|
||
fill: true
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
animation: { duration: 300 },
|
||
parsing: false,
|
||
normalized: true,
|
||
plugins: {
|
||
legend: { display: false },
|
||
decimation: {
|
||
enabled: true,
|
||
algorithm: 'lttb',
|
||
samples: 350,
|
||
threshold: 350
|
||
},
|
||
tooltip: {
|
||
backgroundColor: 'rgba(15,15,15,0.92)',
|
||
borderColor: config.color,
|
||
borderWidth: 1,
|
||
titleColor: 'rgba(255,255,255,0.75)',
|
||
bodyColor: config.color,
|
||
padding: 10,
|
||
callbacks: {
|
||
title: ctx => ctx[0].raw.label,
|
||
label: ctx => `${config.label}: ${ctx.parsed.y}${config.suffix}`
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
type: 'linear',
|
||
min: xMin,
|
||
max: xMax,
|
||
grid: { color: 'rgba(255,255,255,0.04)' },
|
||
ticks: {
|
||
color: 'rgba(255,255,255,0.45)',
|
||
font: { size: 11 },
|
||
maxRotation: 45,
|
||
maxTicksLimit: 15,
|
||
autoSkip: true,
|
||
callback: function(val) {
|
||
const d = new Date(val);
|
||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
|
||
}
|
||
}
|
||
},
|
||
y: {
|
||
grid: { color: 'rgba(255,255,255,0.04)' },
|
||
ticks: {
|
||
color: 'rgba(255,255,255,0.45)',
|
||
font: { size: 11 },
|
||
callback: v => parseFloat(v.toFixed(2)) + config.suffix
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
renderedMetric = metric;
|
||
}
|
||
|
||
async function loadHistory() {
|
||
try {
|
||
historyData = await window.apiClient.request('/api/squadrons/' + encodeURIComponent(squadronName) + '/history');
|
||
if (!historyData.history || historyData.history.length === 0) {
|
||
document.getElementById('chartLoading').textContent = __t('player.noHistoricalData');
|
||
return;
|
||
}
|
||
renderChart(currentMetric);
|
||
} catch (e) {
|
||
console.error('Failed to load squadron history:', e);
|
||
document.getElementById('chartLoading').textContent = __t('player.chartUnavailable');
|
||
}
|
||
}
|
||
|
||
document.getElementById('chartToggles').addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.chart-toggle');
|
||
if (!btn) return;
|
||
document.querySelectorAll('#chartToggles .chart-toggle').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
updateSlider(btn);
|
||
currentMetric = btn.dataset.metric;
|
||
if (historyData) renderChart(currentMetric);
|
||
});
|
||
|
||
document.getElementById('sqChartScaleToggles').addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.chart-toggle');
|
||
if (!btn || !btn.dataset.scale) return;
|
||
document.querySelectorAll('#sqChartScaleToggles .chart-toggle').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
updateScaleSlider(btn);
|
||
chartScaleMode = btn.dataset.scale;
|
||
if (historyData) renderChart(currentMetric);
|
||
});
|
||
|
||
requestAnimationFrame(() => {
|
||
const activeBtn = document.querySelector('#chartToggles .chart-toggle.active');
|
||
if (activeBtn) updateSlider(activeBtn);
|
||
const activeScaleBtn = document.querySelector('#sqChartScaleToggles .chart-toggle.active');
|
||
if (activeScaleBtn) updateScaleSlider(activeScaleBtn);
|
||
});
|
||
|
||
document.getElementById('chartCollapseBtn').addEventListener('click', () => {
|
||
chartOpen = !chartOpen;
|
||
document.getElementById('chartToggles').style.display = chartOpen ? '' : 'none';
|
||
document.getElementById('chartWrapper').style.display = chartOpen ? '' : 'none';
|
||
document.getElementById('sqChartScaleWrap').style.display = chartOpen ? '' : 'none';
|
||
document.getElementById('chartCollapseIcon').className = chartOpen ? 'fas fa-chevron-up' : 'fas fa-chevron-down';
|
||
});
|
||
|
||
// Expose filter hook for the outer filter controller
|
||
window.updateSquadronChartFilter = function(startDate, endDate) {
|
||
chartFilterStart = startDate;
|
||
chartFilterEnd = endDate;
|
||
if (historyData) renderChart(currentMetric);
|
||
};
|
||
|
||
loadHistory();
|
||
})();
|
||
</script>
|
||
<script>
|
||
let currentSort = { field: '', direction: 'desc' };
|
||
|
||
function sortMembersTable(field) {
|
||
const table = document.querySelector('.members-table');
|
||
const tbody = table.querySelector('tbody');
|
||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||
|
||
if (currentSort.field === field) {
|
||
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
currentSort.field = field;
|
||
currentSort.direction = field === 'nick' ? 'asc' : 'desc';
|
||
}
|
||
|
||
document.querySelectorAll('.sortable').forEach(header => {
|
||
header.classList.remove('sorted-asc', 'sorted-desc');
|
||
});
|
||
|
||
const currentHeader = document.querySelector(`[data-sort="${field}"]`);
|
||
if (currentHeader) {
|
||
currentHeader.classList.add(currentSort.direction === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
||
}
|
||
|
||
rows.sort((a, b) => {
|
||
let valueA, valueB;
|
||
const cellIndex = Array.from(table.querySelectorAll('th')).findIndex(th => th.getAttribute('data-sort') === field);
|
||
const cellA = a.cells[cellIndex];
|
||
const cellB = b.cells[cellIndex];
|
||
|
||
if (field === 'nick') {
|
||
valueA = (cellA ? cellA.textContent : '').trim().toLowerCase();
|
||
valueB = (cellB ? cellB.textContent : '').trim().toLowerCase();
|
||
} else {
|
||
const rawA = cellA && cellA.dataset && cellA.dataset.sortValue != null
|
||
? String(cellA.dataset.sortValue)
|
||
: (cellA ? cellA.textContent : '');
|
||
const rawB = cellB && cellB.dataset && cellB.dataset.sortValue != null
|
||
? String(cellB.dataset.sortValue)
|
||
: (cellB ? cellB.textContent : '');
|
||
valueA = parseFloat(rawA.replace('%', '')) || 0;
|
||
valueB = parseFloat(rawB.replace('%', '')) || 0;
|
||
}
|
||
|
||
if (currentSort.direction === 'asc') {
|
||
return valueA > valueB ? 1 : valueA < valueB ? -1 : 0;
|
||
} else {
|
||
return valueA < valueB ? 1 : valueA > valueB ? -1 : 0;
|
||
}
|
||
});
|
||
|
||
tbody.innerHTML = '';
|
||
rows.forEach(row => tbody.appendChild(row));
|
||
}
|
||
|
||
</script>
|
||
<script>
|
||
(function() {
|
||
const squadronName = document.getElementById('squadronChartSection').dataset.squadron;
|
||
let sqGamesData = null;
|
||
let sqGamesLoadedKey = null;
|
||
let sqGamesTimestamp = null;
|
||
const SQ_GAMES_CACHE_DURATION = 5 * 60 * 1000;
|
||
let currentSqGamesSort = { column: 0, direction: 'desc' };
|
||
let sqCurrentFilterStart = null;
|
||
let sqCurrentFilterEnd = null;
|
||
let sqCurrentPage = 'cumulative';
|
||
let sqGamesRequestId = 0;
|
||
|
||
// View toggle
|
||
function updateViewSlider(btn) {
|
||
const slider = document.getElementById('viewToggleSlider');
|
||
const group = document.getElementById('viewToggles');
|
||
const btnRect = btn.getBoundingClientRect();
|
||
const groupRect = group.getBoundingClientRect();
|
||
slider.style.left = (btnRect.left - groupRect.left) + 'px';
|
||
slider.style.width = btnRect.width + 'px';
|
||
}
|
||
|
||
function mountFilterTo(pageName) {
|
||
const filterBar = document.getElementById('sq-time-filter-bar');
|
||
const mount = document.getElementById('sq-filter-mount-' + pageName);
|
||
if (filterBar && mount && filterBar.parentElement !== mount) {
|
||
mount.appendChild(filterBar);
|
||
}
|
||
}
|
||
|
||
function switchToPage(pageName) {
|
||
sqCurrentPage = pageName;
|
||
document.getElementById('cumulative-page').style.display = pageName === 'cumulative' ? 'block' : 'none';
|
||
document.getElementById('games-page').style.display = pageName === 'games' ? 'block' : 'none';
|
||
document.querySelectorAll('.view-toggle').forEach(b => {
|
||
b.classList.toggle('active', b.dataset.page === pageName);
|
||
});
|
||
const activeBtn = document.querySelector('.view-toggle[data-page="' + pageName + '"]');
|
||
if (activeBtn) updateViewSlider(activeBtn);
|
||
document.getElementById('viewSectionTitle').textContent = pageName === 'cumulative'
|
||
? __t('squadrons.squadronMembers')
|
||
: __t('squadrons.squadronGames');
|
||
mountFilterTo(pageName);
|
||
if (pageName === 'games') loadSquadronGames();
|
||
}
|
||
|
||
// Initial mount
|
||
mountFilterTo('cumulative');
|
||
|
||
document.getElementById('viewToggles').addEventListener('click', function(e) {
|
||
var btn = e.target.closest('.view-toggle');
|
||
if (!btn) return;
|
||
switchToPage(btn.dataset.page);
|
||
});
|
||
|
||
requestAnimationFrame(function() {
|
||
var activeBtn = document.querySelector('.view-toggle.active');
|
||
if (activeBtn) updateViewSlider(activeBtn);
|
||
});
|
||
|
||
function gamesCacheKey(start, end) {
|
||
return (start ? start.getTime() : 'null') + '_' + (end ? end.getTime() : 'null');
|
||
}
|
||
|
||
// Games loading
|
||
async function loadSquadronGames() {
|
||
var now = Date.now();
|
||
var key = gamesCacheKey(sqCurrentFilterStart, sqCurrentFilterEnd);
|
||
|
||
if (sqGamesLoadedKey === key && sqGamesData && sqGamesTimestamp) {
|
||
var dataAge = now - sqGamesTimestamp;
|
||
if (dataAge < SQ_GAMES_CACHE_DURATION) {
|
||
showGamesState(sqGamesData.games && sqGamesData.games.length > 0 ? 'content' : 'empty');
|
||
return;
|
||
}
|
||
sqGamesLoadedKey = null;
|
||
sqGamesData = null;
|
||
sqGamesTimestamp = null;
|
||
}
|
||
|
||
showGamesState('loading');
|
||
|
||
const thisRequest = ++sqGamesRequestId;
|
||
const startSnap = sqCurrentFilterStart;
|
||
const endSnap = sqCurrentFilterEnd;
|
||
|
||
try {
|
||
const data = await window.apiClient.getSquadronGames(squadronName, startSnap, endSnap);
|
||
if (thisRequest !== sqGamesRequestId) return;
|
||
sqGamesData = data;
|
||
|
||
if (!sqGamesData.games || sqGamesData.games.length === 0) {
|
||
showGamesState('empty');
|
||
sqGamesLoadedKey = gamesCacheKey(startSnap, endSnap);
|
||
sqGamesTimestamp = Date.now();
|
||
return;
|
||
}
|
||
|
||
displaySquadronGamesTable();
|
||
showGamesState('content');
|
||
sqGamesLoadedKey = gamesCacheKey(startSnap, endSnap);
|
||
sqGamesTimestamp = Date.now();
|
||
} catch (err) {
|
||
if (thisRequest !== sqGamesRequestId) return;
|
||
console.error('Error loading squadron games:', err);
|
||
showGamesState('error');
|
||
sqGamesLoadedKey = null;
|
||
}
|
||
}
|
||
|
||
// Expose for the outer filter controller
|
||
window.refreshSquadronGamesForFilter = function(startDate, endDate) {
|
||
sqCurrentFilterStart = startDate;
|
||
sqCurrentFilterEnd = endDate;
|
||
// Clear the games-page cached data on filter change so re-entry refetches
|
||
const key = gamesCacheKey(startDate, endDate);
|
||
if (sqGamesLoadedKey !== key) {
|
||
sqGamesData = null;
|
||
sqGamesLoadedKey = null;
|
||
sqGamesTimestamp = null;
|
||
}
|
||
if (sqCurrentPage === 'games') loadSquadronGames();
|
||
};
|
||
|
||
function showGamesState(state) {
|
||
document.getElementById('sq-games-loading').style.display = state === 'loading' ? 'block' : 'none';
|
||
document.getElementById('sq-games-content').style.display = state === 'content' ? 'block' : 'none';
|
||
document.getElementById('sq-games-error').style.display = state === 'error' ? 'block' : 'none';
|
||
document.getElementById('sq-no-games').style.display = state === 'empty' ? 'block' : 'none';
|
||
}
|
||
|
||
function getMapImage(mapName) {
|
||
if (!mapName) return null;
|
||
var clean = mapName.replace(/^\s*\[[^\]]+\]\s*/, '').trim();
|
||
var fileName = clean.replace(/\s+/g, '_').replace(/[^\w\u00C0-\u024F\-_.()]/g, '').replace(/_+/g, '_').replace(/^_|_$/g, '').toLowerCase();
|
||
return '/MAPS/' + fileName + '.jpg';
|
||
}
|
||
|
||
function cleanMapName(mapName) {
|
||
if (!mapName) return 'Unknown';
|
||
return mapName.replace(/^\s*\[[^\]]+\]\s*/, '').trim() || 'Unknown';
|
||
}
|
||
|
||
function displaySquadronGamesTable() {
|
||
var tableBody = document.getElementById('sq-games-table-body');
|
||
if (!tableBody || !sqGamesData.games) return;
|
||
|
||
sqGamesData.games.sort(function(a, b) { return (b.timestamp || 0) - (a.timestamp || 0); });
|
||
|
||
tableBody.innerHTML = sqGamesData.games.map(function(game, index) {
|
||
var date = new Date(game.timestamp * 1000);
|
||
var formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||
var result = game.result || 'Unknown';
|
||
var resultClass = result.toLowerCase();
|
||
var hasSession = game.session_id ? true : false;
|
||
var mapDisplay = cleanMapName(game.map_name);
|
||
var mapBg = getMapImage(game.map_name);
|
||
|
||
var rowStyle = (mapBg ? '--bg-img:url(\'' + mapBg + '\');' : '');
|
||
var rowClasses = hasSession ? 'row-link' : '';
|
||
var linkHtml = hasSession ? '<a href="/games/' + game.session_id + '" class="row-link-overlay" aria-label="View game"></a>' : '';
|
||
var playersAttr = (game.players || '').replace(/"/g, '"');
|
||
return '<tr data-game-index="' + index + '" data-players="' + playersAttr + '"' + (hasSession ? ' data-session-id="' + game.session_id + '"' : '') + ' class="' + rowClasses + '"' + (rowStyle ? ' style="' + rowStyle + '"' : '') + '>' +
|
||
'<td class="sq-game-date">' + linkHtml + formattedDate + '</td>' +
|
||
'<td class="sq-game-map"><i class="fas fa-map-marker-alt"></i>' + mapDisplay + '</td>' +
|
||
'<td class="stat-cell">' + (game.stats.ground_kills || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (game.stats.air_kills || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (game.stats.assists || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (game.stats.captures || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (game.stats.deaths || 0) + '</td>' +
|
||
'<td><span class="sq-game-result ' + resultClass + '">' + result + '</span></td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
|
||
currentSqGamesSort = { column: 0, direction: 'desc' };
|
||
var headers = document.getElementById('sq-games-table').querySelectorAll('th');
|
||
if (headers[0]) headers[0].classList.add('sorted-desc');
|
||
|
||
updateFilteredCount();
|
||
}
|
||
|
||
// Filtering (debounced so typing isn't blocked by per-keystroke DOM work)
|
||
var sqFilterTimer = null;
|
||
window.filterSquadronGamesDebounced = function() {
|
||
if (sqFilterTimer) clearTimeout(sqFilterTimer);
|
||
sqFilterTimer = setTimeout(function() {
|
||
sqFilterTimer = null;
|
||
window.filterSquadronGames();
|
||
}, 150);
|
||
};
|
||
|
||
window.filterSquadronGames = function() {
|
||
var playerSearch = document.getElementById('sq-player-search').value.toLowerCase();
|
||
var mapSearch = document.getElementById('sq-map-search').value.toLowerCase();
|
||
var rows = document.querySelectorAll('#sq-games-table-body tr');
|
||
|
||
rows.forEach(function(row) {
|
||
var players = (row.dataset.players || '').toLowerCase();
|
||
var map = row.cells[1] ? row.cells[1].textContent.toLowerCase() : '';
|
||
var matchPlayer = !playerSearch || players.indexOf(playerSearch) !== -1;
|
||
var matchMap = !mapSearch || map.indexOf(mapSearch) !== -1;
|
||
row.style.display = (matchPlayer && matchMap) ? '' : 'none';
|
||
});
|
||
|
||
updateFilteredCount();
|
||
};
|
||
|
||
function updateFilteredCount() {
|
||
var rows = document.querySelectorAll('#sq-games-table-body tr');
|
||
var visible = 0;
|
||
rows.forEach(function(row) {
|
||
if (row.style.display !== 'none') visible++;
|
||
});
|
||
var countEl = document.getElementById('sq-filtered-count');
|
||
if (countEl) countEl.textContent = visible;
|
||
}
|
||
|
||
// Sorting
|
||
window.sortSqGamesTable = function(columnIndex, dataType) {
|
||
var table = document.getElementById('sq-games-table');
|
||
var tbody = table.querySelector('tbody');
|
||
var headers = table.querySelectorAll('th');
|
||
var rows = Array.from(tbody.querySelectorAll('tr'));
|
||
|
||
if (currentSqGamesSort.column === columnIndex) {
|
||
currentSqGamesSort.direction = currentSqGamesSort.direction === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
currentSqGamesSort.column = columnIndex;
|
||
currentSqGamesSort.direction = dataType === 'number' ? 'desc' : 'asc';
|
||
}
|
||
|
||
headers.forEach(function(h) { h.classList.remove('sorted-asc', 'sorted-desc'); });
|
||
headers[columnIndex].classList.add('sorted-' + currentSqGamesSort.direction);
|
||
|
||
rows.sort(function(a, b) {
|
||
var valA, valB;
|
||
if (dataType === 'number') {
|
||
valA = parseFloat(a.cells[columnIndex].textContent.replace('%', '')) || 0;
|
||
valB = parseFloat(b.cells[columnIndex].textContent.replace('%', '')) || 0;
|
||
} else {
|
||
valA = a.cells[columnIndex].textContent.trim().toLowerCase();
|
||
valB = b.cells[columnIndex].textContent.trim().toLowerCase();
|
||
}
|
||
if (currentSqGamesSort.direction === 'asc') {
|
||
return valA > valB ? 1 : valA < valB ? -1 : 0;
|
||
} else {
|
||
return valA < valB ? 1 : valA > valB ? -1 : 0;
|
||
}
|
||
});
|
||
|
||
tbody.innerHTML = '';
|
||
rows.forEach(function(row) { tbody.appendChild(row); });
|
||
};
|
||
|
||
// Retry
|
||
window.retryLoadSquadronGames = function() {
|
||
sqGamesLoadedKey = null;
|
||
sqGamesData = null;
|
||
sqGamesTimestamp = null;
|
||
loadSquadronGames();
|
||
};
|
||
})();
|
||
</script>
|
||
|
||
<script src="/js/seasons-filter.js"></script>
|
||
<script>
|
||
(function() {
|
||
const squadronName = document.getElementById('squadronChartSection').dataset.squadron;
|
||
let sqRequestId = 0;
|
||
let initialPlayers = <%- JSON.stringify(squadronData.players || []) %>;
|
||
const initialSummary = <%- JSON.stringify(squadronData.squadron_summary || {}) %>;
|
||
let currentSquadronPlayers = [];
|
||
let performanceMetric = 'kdr';
|
||
let performanceTrendChart = null;
|
||
let performanceCombatRadarChart = null;
|
||
let performanceResultsRadarChart = null;
|
||
let performanceSort = { field: 'kdr', direction: 'desc' };
|
||
let performanceSelectedPlayers = [];
|
||
let performancePlayerGamesCache = new Map();
|
||
let performanceSparkCharts = new Map();
|
||
let performanceVehicleList = [];
|
||
let performanceVehicleTypes = {};
|
||
let performanceVehicleSearchTimeout = null;
|
||
let performancePlayerSearchTimeout = null;
|
||
let performanceSpecificVehicle = null;
|
||
let performanceFilterStart = null;
|
||
let performanceFilterEnd = null;
|
||
let performanceRenderToken = 0;
|
||
let performanceAvailableVehicles = [];
|
||
let performanceAvailableTypes = [];
|
||
const TIMESLOT_BOUNDS = {
|
||
all: { start: [0, 0, 0, 0], end: [23, 59, 59, 999] },
|
||
na: { start: [1, 0, 0, 0], end: [7, 59, 59, 999] },
|
||
eu: { start: [14, 0, 0, 0], end: [22, 59, 59, 999] },
|
||
};
|
||
const PERFORMANCE_METRICS = {
|
||
total_kills: { label: __t('common.totalKills'), color: '#90EE90', digits: 0 },
|
||
total_battles:{ label: __t('common.battles'), color: '#9fa8da', digits: 0 },
|
||
wins: { label: __t('common.wins'), color: '#aed581', digits: 0 },
|
||
kps: { label: __t('common.kps'), color: '#F5C16C', digits: 2 },
|
||
kdr: { label: __t('common.kdr'), color: '#64b5f6', digits: 2 },
|
||
win_rate: { label: __t('common.winRate'), color: '#ce93d8', digits: 1, suffix: '%' },
|
||
ground_kills: { label: __t('common.groundKills'), color: '#4dd0e1', digits: 0 },
|
||
air_kills: { label: __t('common.airKills'), color: '#ff8a65', digits: 0 },
|
||
assists: { label: __t('common.assists'), color: '#81c784', digits: 0 },
|
||
deaths: { label: __t('common.deaths'), color: '#ef9a9a', digits: 0 },
|
||
captures: { label: __t('common.captures'), color: '#ffd54f', digits: 0 },
|
||
};
|
||
const PERFORMANCE_COLOR_POOL = ['#90EE90', '#64b5f6', '#ffb74d', '#ce93d8', '#4dd0e1', '#ff8a65', '#81c784', '#ffd54f'];
|
||
const PERFORMANCE_HEADER_METRICS = new Set(['total_battles', 'wins', 'win_rate', 'total_kills', 'kps', 'kdr', 'ground_kills', 'air_kills', 'assists', 'deaths', 'captures']);
|
||
const PERFORMANCE_COMBAT_RADAR_KEYS = ['total_kills', 'kps', 'kdr', 'ground_kills', 'air_kills', 'assists'];
|
||
const PERFORMANCE_RESULTS_RADAR_KEYS = ['total_battles', 'wins', 'win_rate', 'captures', 'deaths'];
|
||
|
||
function getElement(id) { return document.getElementById(id); }
|
||
|
||
function parseUtcDateInput(value) {
|
||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || '');
|
||
if (!match) return null;
|
||
return new Date(Date.UTC(
|
||
Number(match[1]),
|
||
Number(match[2]) - 1,
|
||
Number(match[3]),
|
||
0, 0, 0, 0
|
||
));
|
||
}
|
||
|
||
function buildTimeslotBoundary(dateValue, slotKey, edge) {
|
||
const date = parseUtcDateInput(dateValue);
|
||
if (!date) return null;
|
||
const slot = TIMESLOT_BOUNDS[slotKey] || TIMESLOT_BOUNDS.all;
|
||
const parts = edge === 'start' ? slot.start : slot.end;
|
||
date.setUTCHours(parts[0], parts[1], parts[2], parts[3]);
|
||
return date;
|
||
}
|
||
|
||
function normalizePerformancePlayer(player) {
|
||
const totalBattles = Number(player && player.total_battles) || 0;
|
||
const totalKills = Number(player && player.total_kills) || 0;
|
||
const wins = Number(player && player.wins) || 0;
|
||
const deaths = Number(player && player.deaths) || 0;
|
||
const winRate = typeof player?.win_rate === 'number' ? player.win_rate : (Number(player && player.win_rate) || 0);
|
||
const kdr = typeof player?.kdr === 'number' ? player.kdr : (Number(player && player.kdr) || 0);
|
||
const kps = typeof player?.kps === 'number' ? player.kps : (totalBattles > 0 ? totalKills / totalBattles : 0);
|
||
return {
|
||
...player,
|
||
sqb_points: Number(player && player.sqb_points) || 0,
|
||
total_battles: totalBattles,
|
||
wins,
|
||
total_kills: totalKills,
|
||
ground_kills: Number(player && player.ground_kills) || 0,
|
||
air_kills: Number(player && player.air_kills) || 0,
|
||
assists: Number(player && player.assists) || 0,
|
||
captures: Number(player && player.captures) || 0,
|
||
deaths,
|
||
win_rate: winRate,
|
||
kdr,
|
||
kps,
|
||
};
|
||
}
|
||
|
||
function setCurrentPlayers(players) {
|
||
currentSquadronPlayers = (players || []).map(normalizePerformancePlayer);
|
||
}
|
||
|
||
function formatPerformanceValue(metricKey, rawValue) {
|
||
const config = PERFORMANCE_METRICS[metricKey] || { digits: 0, suffix: '' };
|
||
const value = Number(rawValue) || 0;
|
||
if (config.digits > 0) return value.toFixed(config.digits) + (config.suffix || '');
|
||
return Math.round(value).toLocaleString() + (config.suffix || '');
|
||
}
|
||
|
||
function performanceFilterKey() {
|
||
return `${performanceFilterStart ? performanceFilterStart.getTime() : 'all'}_${performanceFilterEnd ? performanceFilterEnd.getTime() : 'all'}`;
|
||
}
|
||
|
||
function playerGamesEndpoint(uid) {
|
||
let endpoint = `/api/player/${encodeURIComponent(uid)}/games`;
|
||
if (performanceFilterStart || performanceFilterEnd) {
|
||
const params = new URLSearchParams();
|
||
if (performanceFilterStart) params.append('start_date', performanceFilterStart.toISOString());
|
||
if (performanceFilterEnd) params.append('end_date', performanceFilterEnd.toISOString());
|
||
endpoint += `?${params.toString()}`;
|
||
}
|
||
return endpoint;
|
||
}
|
||
|
||
function performanceColor(index) {
|
||
return PERFORMANCE_COLOR_POOL[index % PERFORMANCE_COLOR_POOL.length];
|
||
}
|
||
|
||
function vehicleTypeLabel(code) {
|
||
switch (code) {
|
||
case 'F': return __t('analytics.compTypeFighters');
|
||
case 'B': return __t('analytics.compTypeBombers');
|
||
case 'H': return __t('analytics.compTypeHelicopters');
|
||
case 'L': return __t('analytics.compTypeLight');
|
||
case 'T': return __t('analytics.compTypeTanks');
|
||
case 'AA': return __t('analytics.compTypeSPAA');
|
||
default: return __t('analytics.compTypeUnknown');
|
||
}
|
||
}
|
||
|
||
async function ensurePerformanceVehicleMeta() {
|
||
if (!performanceVehicleList.length) {
|
||
const listRes = await window.apiClient.request('/api/analytics/vehicle-list');
|
||
performanceVehicleList = (listRes && listRes.vehicles) || [];
|
||
}
|
||
if (!Object.keys(performanceVehicleTypes).length) {
|
||
const typeRes = await window.apiClient.request('/api/i18n/vehicle-types');
|
||
performanceVehicleTypes = (typeRes && typeRes.types) || {};
|
||
}
|
||
if (window.vehicleI18n && window.vehicleI18n.ensureLoaded) {
|
||
await window.vehicleI18n.ensureLoaded();
|
||
}
|
||
}
|
||
|
||
function performanceVehicleDisplay(internal, fallback) {
|
||
if (!internal) return fallback || '';
|
||
if (window.vehicleI18n && window.vehicleI18n.translate) {
|
||
return window.vehicleI18n.translate(internal, fallback || internal);
|
||
}
|
||
return fallback || internal;
|
||
}
|
||
|
||
function getPerformanceSelectedType() {
|
||
const select = getElement('sq-performance-vehicle-type');
|
||
return select ? (select.value || 'all') : 'all';
|
||
}
|
||
|
||
function getPerformanceSelectedVehicleInternal() {
|
||
return performanceSpecificVehicle && performanceSpecificVehicle.internal
|
||
? String(performanceSpecificVehicle.internal).toLowerCase()
|
||
: null;
|
||
}
|
||
|
||
function gameMatchesPerformanceVehicleFilter(game) {
|
||
const internal = String(game && game.vehicle_internal || '').toLowerCase();
|
||
const specificInternal = getPerformanceSelectedVehicleInternal();
|
||
if (specificInternal) return internal === specificInternal;
|
||
const typeCode = getPerformanceSelectedType();
|
||
if (typeCode === 'all') return true;
|
||
return (performanceVehicleTypes[internal] || '?') === typeCode;
|
||
}
|
||
|
||
function aggregatePerformanceGames(rawGames) {
|
||
const sessions = new Map();
|
||
for (const game of (rawGames || [])) {
|
||
if (!gameMatchesPerformanceVehicleFilter(game)) continue;
|
||
const sessionId = String(game.session_id || '');
|
||
if (!sessionId) continue;
|
||
if (!sessions.has(sessionId)) {
|
||
sessions.set(sessionId, {
|
||
session_id: sessionId,
|
||
timestamp: Number(game.timestamp) || 0,
|
||
result: game.result || 'Unknown',
|
||
ground_kills: 0,
|
||
air_kills: 0,
|
||
assists: 0,
|
||
captures: 0,
|
||
deaths: 0
|
||
});
|
||
}
|
||
const target = sessions.get(sessionId);
|
||
target.timestamp = Math.max(target.timestamp, Number(game.timestamp) || 0);
|
||
if (target.result === 'Unknown' && game.result) target.result = game.result;
|
||
target.ground_kills += Number(game.stats && game.stats.ground_kills) || 0;
|
||
target.air_kills += Number(game.stats && game.stats.air_kills) || 0;
|
||
target.assists += Number(game.stats && game.stats.assists) || 0;
|
||
target.captures += Number(game.stats && game.stats.captures) || 0;
|
||
target.deaths += Number(game.stats && game.stats.deaths) || 0;
|
||
}
|
||
|
||
const sessionRows = Array.from(sessions.values()).sort((a, b) => a.timestamp - b.timestamp);
|
||
const totals = {
|
||
total_battles: sessionRows.length,
|
||
wins: 0,
|
||
total_kills: 0,
|
||
ground_kills: 0,
|
||
air_kills: 0,
|
||
assists: 0,
|
||
deaths: 0,
|
||
captures: 0,
|
||
win_rate: 0,
|
||
kdr: 0,
|
||
kps: 0,
|
||
performance: 0
|
||
};
|
||
const daily = new Map();
|
||
|
||
for (const row of sessionRows) {
|
||
const totalKills = row.ground_kills + row.air_kills;
|
||
totals.ground_kills += row.ground_kills;
|
||
totals.air_kills += row.air_kills;
|
||
totals.total_kills += totalKills;
|
||
totals.assists += row.assists;
|
||
totals.deaths += row.deaths;
|
||
totals.captures += row.captures;
|
||
if (String(row.result).toUpperCase() === 'WIN') totals.wins += 1;
|
||
|
||
const day = new Date((row.timestamp || 0) * 1000);
|
||
const dayKey = new Date(Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate())).toISOString().slice(0, 10);
|
||
if (!daily.has(dayKey)) {
|
||
daily.set(dayKey, {
|
||
period: dayKey,
|
||
total_battles: 0,
|
||
wins: 0,
|
||
total_kills: 0,
|
||
ground_kills: 0,
|
||
air_kills: 0,
|
||
assists: 0,
|
||
deaths: 0,
|
||
captures: 0
|
||
});
|
||
}
|
||
const bucket = daily.get(dayKey);
|
||
bucket.total_battles += 1;
|
||
if (String(row.result).toUpperCase() === 'WIN') bucket.wins += 1;
|
||
bucket.total_kills += totalKills;
|
||
bucket.ground_kills += row.ground_kills;
|
||
bucket.air_kills += row.air_kills;
|
||
bucket.assists += row.assists;
|
||
bucket.deaths += row.deaths;
|
||
bucket.captures += row.captures;
|
||
}
|
||
|
||
totals.win_rate = totals.total_battles > 0 ? (totals.wins / totals.total_battles) * 100 : 0;
|
||
totals.kdr = totals.deaths > 0 ? (totals.total_kills / totals.deaths) : totals.total_kills;
|
||
totals.kps = totals.total_battles > 0 ? (totals.total_kills / totals.total_battles) : 0;
|
||
totals.performance = totals[performanceMetric] || 0;
|
||
|
||
const series = Array.from(daily.values())
|
||
.sort((a, b) => a.period.localeCompare(b.period))
|
||
.map(day => {
|
||
const winRate = day.total_battles > 0 ? (day.wins / day.total_battles) * 100 : 0;
|
||
const kdr = day.deaths > 0 ? (day.total_kills / day.deaths) : day.total_kills;
|
||
const kps = day.total_battles > 0 ? (day.total_kills / day.total_battles) : 0;
|
||
return {
|
||
...day,
|
||
win_rate: winRate,
|
||
kdr,
|
||
kps,
|
||
x: new Date(day.period + 'T00:00:00Z').getTime()
|
||
};
|
||
});
|
||
|
||
return { sessions: sessionRows, totals, series };
|
||
}
|
||
|
||
function buildPerformancePlayerState(player, rawGames) {
|
||
const aggregate = aggregatePerformanceGames(rawGames);
|
||
return {
|
||
...player,
|
||
rawGames: rawGames || [],
|
||
...aggregate.totals,
|
||
series: aggregate.series,
|
||
performance: aggregate.totals[performanceMetric] || 0
|
||
};
|
||
}
|
||
|
||
function getPerformanceSearchPlayers() {
|
||
return currentSquadronPlayers
|
||
.filter(p => !performanceSelectedPlayers.some(sel => String(sel.uid) === String(p.uid)))
|
||
.slice()
|
||
.sort((a, b) => (Number(b.total_kills) || 0) - (Number(a.total_kills) || 0));
|
||
}
|
||
|
||
function getSelectedPerformancePlayersMeta() {
|
||
const byUid = new Map(currentSquadronPlayers.map(p => [String(p.uid), p]));
|
||
return performanceSelectedPlayers.map(sel => byUid.get(String(sel.uid)) || sel);
|
||
}
|
||
|
||
function getCustomRangeDayPair() {
|
||
const startInput = getElement('sq-start-date').value;
|
||
const endInput = getElement('sq-end-date').value;
|
||
if (!startInput && !endInput) return null;
|
||
return {
|
||
startDay: startInput || endInput,
|
||
endDay: endInput || startInput,
|
||
};
|
||
}
|
||
|
||
function updateSingleDayTimeslotVisibility() {
|
||
const wrap = getElement('sq-single-day-timeslot-filter');
|
||
const select = getElement('sq-single-day-timeslot');
|
||
const pair = getCustomRangeDayPair();
|
||
const cat = getElement('sq-filter-category').value;
|
||
const filterType = getElement('sq-date-filter-type').value;
|
||
const singleDay = !!pair && pair.startDay === pair.endDay;
|
||
const show = cat === 'date' && filterType === 'custom' && singleDay;
|
||
wrap.style.display = show ? 'flex' : 'none';
|
||
if (!show) select.value = 'all';
|
||
}
|
||
|
||
async function initSeasons() {
|
||
if (!window.seasonsFilter) return;
|
||
try {
|
||
await window.seasonsFilter.loadSeasons();
|
||
window.seasonsFilter.populateSeasonSelect(getElement('sq-season'));
|
||
window.seasonsFilter.populateSeasonSelect(getElement('sq-week-season-select'));
|
||
} catch (e) {
|
||
console.error('Failed to load seasons for squadron filter:', e);
|
||
}
|
||
}
|
||
|
||
window.updateSquadronFilterCategory = function() {
|
||
const cat = getElement('sq-filter-category').value;
|
||
getElement('sq-date-type-filter').style.display = 'none';
|
||
getElement('sq-custom-range-filters').style.display = 'none';
|
||
getElement('sq-season-select-filter').style.display = 'none';
|
||
getElement('sq-week-season-filter').style.display = 'none';
|
||
getElement('sq-week-select-filter').style.display = 'none';
|
||
|
||
if (cat === 'date') {
|
||
getElement('sq-date-type-filter').style.display = 'flex';
|
||
handleSquadronDateTypeChange();
|
||
return;
|
||
} else if (cat === 'season') {
|
||
getElement('sq-season-select-filter').style.display = 'flex';
|
||
} else if (cat === 'week') {
|
||
getElement('sq-week-season-filter').style.display = 'flex';
|
||
getElement('sq-week-select-filter').style.display = 'flex';
|
||
}
|
||
|
||
updateResetBtn();
|
||
|
||
if (cat === 'all') {
|
||
applySquadronFilters();
|
||
}
|
||
};
|
||
|
||
window.handleSquadronDateTypeChange = function() {
|
||
const filterType = getElement('sq-date-filter-type').value;
|
||
const showCustom = filterType === 'custom';
|
||
getElement('sq-custom-range-filters').style.display = showCustom ? 'flex' : 'none';
|
||
updateSingleDayTimeslotVisibility();
|
||
updateResetBtn();
|
||
if (filterType !== 'custom') {
|
||
applySquadronFilters();
|
||
}
|
||
};
|
||
|
||
window.updateSquadronWeekOptions = function() {
|
||
const seasonSelect = getElement('sq-week-season-select');
|
||
const weekSelect = getElement('sq-week');
|
||
const seasonName = seasonSelect.value;
|
||
|
||
if (seasonName && window.seasonsFilter) {
|
||
window.seasonsFilter.populateWeekSelect(weekSelect, seasonName, true);
|
||
} else {
|
||
weekSelect.innerHTML = '<option value="">' + __t('player.pleaseSelect') + '</option>';
|
||
}
|
||
};
|
||
|
||
function updateResetBtn() {
|
||
const active = getElement('sq-filter-category').value !== 'all';
|
||
getElement('sq-reset-btn-group').style.display = active ? 'flex' : 'none';
|
||
}
|
||
|
||
function syncPerformanceFilterState(startDate, endDate) {
|
||
performanceFilterStart = startDate || null;
|
||
performanceFilterEnd = endDate || null;
|
||
// Cache key already includes the filter via performanceFilterKey(), so we keep
|
||
// entries from previous filters — toggling back becomes free instead of refetching.
|
||
performanceRenderToken += 1;
|
||
if (typeof window.renderSquadronPerformanceView === 'function') {
|
||
window.renderSquadronPerformanceView();
|
||
}
|
||
}
|
||
|
||
window.resetSquadronFilters = function() {
|
||
getElement('sq-filter-category').value = 'all';
|
||
getElement('sq-date-filter-type').value = 'last7';
|
||
getElement('sq-start-date').value = '';
|
||
getElement('sq-end-date').value = '';
|
||
getElement('sq-single-day-timeslot').value = 'all';
|
||
getElement('sq-season').value = '';
|
||
getElement('sq-week-season-select').value = '';
|
||
getElement('sq-week').innerHTML = '<option value="">' + __t('player.pleaseSelect') + '</option>';
|
||
updateSquadronFilterCategory();
|
||
};
|
||
|
||
window.applySquadronFilters = function() {
|
||
const category = getElement('sq-filter-category').value;
|
||
let startDate = null;
|
||
let endDate = null;
|
||
const now = new Date();
|
||
|
||
if (category === 'all') {
|
||
// proceed with nulls
|
||
} else if (category === 'date') {
|
||
const filterType = getElement('sq-date-filter-type').value;
|
||
if (!filterType) return;
|
||
switch (filterType) {
|
||
case 'last7':
|
||
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||
endDate = now;
|
||
break;
|
||
case 'last30':
|
||
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||
endDate = now;
|
||
break;
|
||
case 'last90':
|
||
startDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
|
||
endDate = now;
|
||
break;
|
||
case 'custom':
|
||
const pair = getCustomRangeDayPair();
|
||
if (!pair) return;
|
||
const slot = pair.startDay === pair.endDay
|
||
? (getElement('sq-single-day-timeslot').value || 'all')
|
||
: 'all';
|
||
startDate = buildTimeslotBoundary(pair.startDay, slot, 'start');
|
||
endDate = buildTimeslotBoundary(pair.endDay, slot, 'end');
|
||
if (!startDate || !endDate) return;
|
||
break;
|
||
}
|
||
} else if (category === 'season') {
|
||
const seasonName = getElement('sq-season').value;
|
||
if (!seasonName) return;
|
||
if (window.seasonsFilter) {
|
||
const range = window.seasonsFilter.getSeasonDateRange(seasonName);
|
||
if (range) { startDate = range.startDate; endDate = range.endDate; }
|
||
}
|
||
} else if (category === 'week') {
|
||
const selectedSeason = getElement('sq-week-season-select').value;
|
||
const selectedWeek = getElement('sq-week').value;
|
||
if (!selectedSeason || !selectedWeek) return;
|
||
if (window.seasonsFilter) {
|
||
if (selectedWeek === 'all') {
|
||
const range = window.seasonsFilter.getSeasonDateRange(selectedSeason);
|
||
if (range) { startDate = range.startDate; endDate = range.endDate; }
|
||
} else {
|
||
const weekNum = selectedWeek === 'final' ? null : parseInt(selectedWeek);
|
||
const range = window.seasonsFilter.getWeekDateRange(selectedSeason, weekNum);
|
||
if (range) { startDate = range.startDate; endDate = range.endDate; }
|
||
}
|
||
}
|
||
}
|
||
|
||
updateResetBtn();
|
||
|
||
syncPerformanceFilterState(startDate, endDate);
|
||
|
||
if (typeof window.updateSquadronChartFilter === 'function') {
|
||
window.updateSquadronChartFilter(startDate, endDate);
|
||
}
|
||
if (typeof window.refreshSquadronGamesForFilter === 'function') {
|
||
window.refreshSquadronGamesForFilter(startDate, endDate);
|
||
}
|
||
fetchAndUpdateSquadronStats(startDate, endDate);
|
||
};
|
||
|
||
getElement('sq-start-date').addEventListener('change', updateSingleDayTimeslotVisibility);
|
||
getElement('sq-end-date').addEventListener('change', updateSingleDayTimeslotVisibility);
|
||
setCurrentPlayers(initialPlayers);
|
||
|
||
async function fetchAndUpdateSquadronStats(startDate, endDate) {
|
||
const thisRequest = ++sqRequestId;
|
||
|
||
// All-time and we have server-rendered data → reuse it
|
||
if (!startDate && !endDate && initialPlayers) {
|
||
setCurrentPlayers(initialPlayers);
|
||
updateStatCards(initialSummary);
|
||
renderMembersTable(currentSquadronPlayers);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const data = await window.apiClient.getSquadronDetails(squadronName, startDate, endDate);
|
||
if (thisRequest !== sqRequestId) return;
|
||
if (data && data.players) {
|
||
setCurrentPlayers(data.players);
|
||
}
|
||
if (data && data.squadron_summary) updateStatCards(data.squadron_summary);
|
||
if (data && data.players) {
|
||
renderMembersTable(currentSquadronPlayers);
|
||
}
|
||
} catch (err) {
|
||
if (thisRequest !== sqRequestId) return;
|
||
console.error('Failed to refresh squadron stats:', err);
|
||
}
|
||
}
|
||
|
||
function updateStatCards(s) {
|
||
const setText = (id, val) => { const el = getElement(id); if (el) el.textContent = val; };
|
||
setText('sq-stat-total-battles', s.total_battles);
|
||
setText('sq-stat-wins', s.wins);
|
||
setText('sq-stat-win-rate', (typeof s.win_rate === 'number' ? s.win_rate.toFixed(1) : s.win_rate) + '%');
|
||
setText('sq-stat-total-kills', s.total_kills);
|
||
setText('sq-stat-kdr', typeof s.kdr === 'number' ? s.kdr.toFixed(2) : s.kdr);
|
||
setText('sq-stat-kps', typeof s.kps === 'number' ? s.kps.toFixed(2) : (Number(s.kps) || 0).toFixed(2));
|
||
setText('sq-stat-ground-kills', s.ground_kills);
|
||
setText('sq-stat-air-kills', s.air_kills);
|
||
setText('sq-stat-assists', s.assists);
|
||
setText('sq-stat-deaths', s.deaths);
|
||
setText('sq-stat-captures', s.captures);
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return String(str == null ? '' : str)
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
|
||
function syncPerformanceHeaderSort() {
|
||
document.querySelectorAll('[data-performance-sort]').forEach(header => {
|
||
header.classList.remove('sorted-asc', 'sorted-desc', 'performance-focus');
|
||
});
|
||
const active = document.querySelector('[data-performance-sort="' + performanceSort.field + '"]');
|
||
if (active) active.classList.add(performanceSort.direction === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
||
const focused = document.querySelector('[data-performance-sort="' + performanceMetric + '"]');
|
||
if (focused) focused.classList.add('performance-focus');
|
||
}
|
||
|
||
function getSortedPerformancePlayers(rows) {
|
||
const { field, direction } = performanceSort;
|
||
return rows.slice().sort((a, b) => {
|
||
const aVal = a[field];
|
||
const bVal = b[field];
|
||
const cmp = field === 'nick'
|
||
? String(aVal || '').localeCompare(String(bVal || ''), undefined, { sensitivity: 'base' })
|
||
: ((Number(aVal) || 0) - (Number(bVal) || 0));
|
||
return direction === 'asc' ? cmp : -cmp;
|
||
});
|
||
}
|
||
|
||
function destroyPerformanceSparks() {
|
||
performanceSparkCharts.forEach(chart => {
|
||
try { chart.destroy(); } catch (e) {}
|
||
});
|
||
performanceSparkCharts.clear();
|
||
}
|
||
|
||
async function ensurePerformancePlayerGames(uid) {
|
||
const cacheKey = `${uid}|${performanceFilterKey()}`;
|
||
if (performancePlayerGamesCache.has(cacheKey)) return performancePlayerGamesCache.get(cacheKey);
|
||
const response = await window.apiClient.request(playerGamesEndpoint(uid));
|
||
const games = (response && response.games) || [];
|
||
performancePlayerGamesCache.set(cacheKey, games);
|
||
return games;
|
||
}
|
||
|
||
function renderPerformanceSelectedPlayers() {
|
||
const wrap = getElement('sq-performance-selected');
|
||
const hint = getElement('sq-performance-player-hint');
|
||
if (!wrap || !hint) return;
|
||
if (!performanceSelectedPlayers.length) {
|
||
wrap.innerHTML = '';
|
||
hint.textContent = __t('leaderboard.selectPlayersToCompare');
|
||
return;
|
||
}
|
||
hint.textContent = '';
|
||
wrap.innerHTML = performanceSelectedPlayers.map(player => `
|
||
<span class="performance-chip">
|
||
${escapeHtml(player.nick || player.uid || '')}
|
||
<button type="button" onclick="removePerformancePlayer('${escapeHtml(String(player.uid))}')"><i class="fas fa-times"></i></button>
|
||
</span>
|
||
`).join('');
|
||
}
|
||
|
||
function renderPerformanceVehicleSummary() {
|
||
const wrap = getElement('sq-performance-vehicle-active');
|
||
if (!wrap) return;
|
||
if (!performanceSelectedPlayers.length) {
|
||
wrap.textContent = '';
|
||
return;
|
||
}
|
||
if (performanceSpecificVehicle && performanceSpecificVehicle.internal) {
|
||
wrap.innerHTML = `
|
||
<span style="font-family:'skyquakesymbols','Inter',sans-serif;">${escapeHtml(performanceSpecificVehicle.name)}</span>
|
||
<button type="button" onclick="clearPerformanceVehicleFilter()"><i class="fas fa-times"></i></button>
|
||
`;
|
||
return;
|
||
}
|
||
const type = getPerformanceSelectedType();
|
||
wrap.textContent = type === 'all'
|
||
? __t('leaderboard.searchVehiclePlaceholder')
|
||
: __t('analytics.compRefineAny', { type: vehicleTypeLabel(type) });
|
||
}
|
||
|
||
function renderPerformanceFilterOptions(rows) {
|
||
const typeSelect = getElement('sq-performance-vehicle-type');
|
||
const vehicleInput = getElement('sq-performance-vehicle-search');
|
||
if (!typeSelect || !vehicleInput) return false;
|
||
|
||
if (!rows.length) {
|
||
performanceAvailableTypes = [];
|
||
performanceAvailableVehicles = [];
|
||
performanceSpecificVehicle = null;
|
||
typeSelect.innerHTML = `<option value="all">${escapeHtml(__t('leaderboard.any'))}</option>`;
|
||
typeSelect.value = 'all';
|
||
typeSelect.disabled = true;
|
||
vehicleInput.value = '';
|
||
vehicleInput.placeholder = __t('leaderboard.searchVehiclePlaceholder');
|
||
vehicleInput.disabled = true;
|
||
getElement('sq-performance-vehicle-results')?.classList.add('hidden');
|
||
return false;
|
||
}
|
||
|
||
const typeTotals = new Map();
|
||
const vehicleTotals = new Map();
|
||
rows.forEach(row => {
|
||
(row.rawGames || []).forEach(game => {
|
||
const internal = String(game && game.vehicle_internal || '').toLowerCase();
|
||
if (!internal || internal === 'disconnected') return;
|
||
const typeCode = performanceVehicleTypes[internal] || '?';
|
||
typeTotals.set(typeCode, (typeTotals.get(typeCode) || 0) + 1);
|
||
if (!vehicleTotals.has(internal)) {
|
||
vehicleTotals.set(internal, {
|
||
internal,
|
||
name: performanceVehicleDisplay(internal, internal),
|
||
type: typeCode,
|
||
total: 0
|
||
});
|
||
}
|
||
vehicleTotals.get(internal).total += 1;
|
||
});
|
||
});
|
||
|
||
const orderedTypes = ['F', 'B', 'H', 'L', 'T', 'AA', '?'].filter(code => typeTotals.has(code));
|
||
performanceAvailableTypes = orderedTypes;
|
||
performanceAvailableVehicles = Array.from(vehicleTotals.values()).sort((a, b) => b.total - a.total || a.name.localeCompare(b.name));
|
||
|
||
const currentType = getPerformanceSelectedType();
|
||
const nextType = currentType === 'all' || orderedTypes.includes(currentType) ? currentType : 'all';
|
||
let filterChanged = nextType !== currentType;
|
||
typeSelect.innerHTML = `<option value="all">${escapeHtml(__t('leaderboard.any'))}</option>` + orderedTypes.map(code =>
|
||
`<option value="${escapeHtml(code)}">${escapeHtml(vehicleTypeLabel(code))}</option>`
|
||
).join('');
|
||
typeSelect.value = nextType;
|
||
typeSelect.disabled = performanceAvailableTypes.length === 0;
|
||
|
||
const specificInternal = getPerformanceSelectedVehicleInternal();
|
||
if (specificInternal && !performanceAvailableVehicles.some(vehicle => vehicle.internal === specificInternal)) {
|
||
performanceSpecificVehicle = null;
|
||
vehicleInput.value = '';
|
||
filterChanged = true;
|
||
}
|
||
|
||
const activeType = typeSelect.value || 'all';
|
||
vehicleInput.disabled = performanceAvailableVehicles.length === 0;
|
||
vehicleInput.placeholder = activeType === 'all'
|
||
? __t('leaderboard.searchVehiclePlaceholder')
|
||
: `${__t('leaderboard.searchVehiclePlaceholder')} · ${vehicleTypeLabel(activeType)}`;
|
||
renderPerformanceVehicleSummary();
|
||
return filterChanged;
|
||
}
|
||
|
||
async function renderPerformancePlayerSearchResults() {
|
||
const input = getElement('sq-performance-player-search');
|
||
const results = getElement('sq-performance-player-results');
|
||
if (!input || !results) return;
|
||
const term = (input.value || '').trim().toLowerCase();
|
||
if (term.length < 1) {
|
||
results.classList.add('hidden');
|
||
return;
|
||
}
|
||
const hits = getPerformanceSearchPlayers()
|
||
.filter(player => String(player.nick || '').toLowerCase().includes(term))
|
||
.slice(0, 10);
|
||
if (!hits.length) {
|
||
results.innerHTML = `<div class="performance-search-hit">${escapeHtml(__t('common.noPlayersFound'))}</div>`;
|
||
} else {
|
||
results.innerHTML = hits.map(player => `
|
||
<div class="performance-search-hit" data-performance-pick-player="${escapeHtml(String(player.uid))}">
|
||
<div class="performance-search-name">${escapeHtml(player.nick || player.uid || '')}</div>
|
||
<div class="performance-search-meta">${formatPerformanceValue('total_kills', player.total_kills)} ${escapeHtml(__t('js.killsSuffix'))} · ${formatPerformanceValue('win_rate', player.win_rate)}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
results.classList.remove('hidden');
|
||
}
|
||
|
||
async function renderPerformanceVehicleSearchResults() {
|
||
const input = getElement('sq-performance-vehicle-search');
|
||
const results = getElement('sq-performance-vehicle-results');
|
||
if (!input || !results) return;
|
||
const term = (input.value || '').trim().toLowerCase();
|
||
if (term.length < 1 || input.disabled) {
|
||
results.classList.add('hidden');
|
||
return;
|
||
}
|
||
const activeType = getPerformanceSelectedType();
|
||
const hits = performanceAvailableVehicles
|
||
.filter(vehicle => activeType === 'all' || vehicle.type === activeType)
|
||
.filter(vehicle => vehicle.name.toLowerCase().includes(term))
|
||
.slice(0, 20);
|
||
if (!hits.length) {
|
||
results.innerHTML = `<div class="performance-search-hit">${escapeHtml(__t('common.noVehiclesFound'))}</div>`;
|
||
} else {
|
||
results.innerHTML = hits.map(vehicle => `
|
||
<div class="performance-search-hit" data-performance-pick-vehicle="${escapeHtml(vehicle.internal)}">
|
||
<div class="performance-search-name" style="font-family:'skyquakesymbols','Inter',sans-serif;">${escapeHtml(vehicle.name)}</div>
|
||
<div class="performance-search-meta">${vehicle.total} ${escapeHtml(__t('analytics.compColSpawns').toLowerCase())}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
results.classList.remove('hidden');
|
||
}
|
||
|
||
function buildPerformanceSeriesValue(point, metricKey) {
|
||
return Number(point && point[metricKey]) || 0;
|
||
}
|
||
|
||
function destroyPerformanceCharts() {
|
||
[performanceTrendChart, performanceCombatRadarChart, performanceResultsRadarChart].forEach(chart => {
|
||
if (!chart) return;
|
||
try { chart.destroy(); } catch (e) {}
|
||
});
|
||
performanceTrendChart = null;
|
||
performanceCombatRadarChart = null;
|
||
performanceResultsRadarChart = null;
|
||
}
|
||
|
||
function togglePerformanceCanvas(canvasId, emptyId, showCanvas, emptyText) {
|
||
const canvas = getElement(canvasId);
|
||
const empty = getElement(emptyId);
|
||
if (!canvas || !empty) return;
|
||
canvas.style.display = showCanvas ? '' : 'none';
|
||
empty.style.display = showCanvas ? 'none' : 'flex';
|
||
if (!showCanvas && emptyText) empty.textContent = emptyText;
|
||
}
|
||
|
||
function formatPerformanceDateLabel(value) {
|
||
return new Date(value).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
|
||
}
|
||
|
||
function buildPerformanceRadarDatasets(rows, keys) {
|
||
const maxima = {};
|
||
keys.forEach(key => {
|
||
maxima[key] = Math.max(...rows.map(row => Number(row[key]) || 0), 1);
|
||
});
|
||
return rows.map((row, index) => {
|
||
const color = performanceColor(index);
|
||
return {
|
||
label: row.nick || row.uid || '?',
|
||
data: keys.map(key => ((Number(row[key]) || 0) / maxima[key]) * 100),
|
||
actualValues: keys.map(key => Number(row[key]) || 0),
|
||
borderColor: color,
|
||
backgroundColor: color + '1f',
|
||
pointBackgroundColor: color,
|
||
pointBorderColor: color,
|
||
borderWidth: 2,
|
||
pointRadius: 2,
|
||
pointHoverRadius: 4,
|
||
fill: true
|
||
};
|
||
});
|
||
}
|
||
|
||
function renderPerformanceTrendChart(rows) {
|
||
const canvas = getElement('sq-performance-trend-chart');
|
||
const title = getElement('sq-performance-trend-title');
|
||
const meta = getElement('sq-performance-trend-meta');
|
||
const config = PERFORMANCE_METRICS[performanceMetric];
|
||
const rowsWithSeries = rows.filter(row => Array.isArray(row.series) && row.series.length > 0);
|
||
|
||
title.textContent = __t('squadrons.performance') + ' · ' + config.label;
|
||
meta.textContent = rows.length ? `${rows.length} ${__t('common.players')}` : '';
|
||
|
||
if (!rows.length) {
|
||
togglePerformanceCanvas('sq-performance-trend-chart', 'sq-performance-trend-empty', false, __t('leaderboard.selectPlayersToCompare'));
|
||
if (performanceTrendChart) {
|
||
performanceTrendChart.destroy();
|
||
performanceTrendChart = null;
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!rowsWithSeries.length) {
|
||
togglePerformanceCanvas('sq-performance-trend-chart', 'sq-performance-trend-empty', false, __t('squadrons.performanceNoData'));
|
||
if (performanceTrendChart) {
|
||
performanceTrendChart.destroy();
|
||
performanceTrendChart = null;
|
||
}
|
||
return;
|
||
}
|
||
|
||
const datasets = rowsWithSeries.map((row, index) => {
|
||
const color = performanceColor(index);
|
||
return {
|
||
label: row.nick || row.uid || '?',
|
||
data: row.series.map(point => ({ x: point.x, y: buildPerformanceSeriesValue(point, performanceMetric) })),
|
||
borderColor: color,
|
||
backgroundColor: color + '18',
|
||
pointBackgroundColor: color,
|
||
pointBorderColor: color,
|
||
pointHoverBackgroundColor: color,
|
||
pointHoverBorderColor: color,
|
||
pointRadius: 0,
|
||
pointHoverRadius: 3,
|
||
borderWidth: 2,
|
||
tension: 0.18,
|
||
fill: false
|
||
};
|
||
});
|
||
const allPoints = datasets.flatMap(dataset => dataset.data);
|
||
const xMin = Math.min(...allPoints.map(point => point.x));
|
||
const xMax = Math.max(...allPoints.map(point => point.x));
|
||
togglePerformanceCanvas('sq-performance-trend-chart', 'sq-performance-trend-empty', true);
|
||
|
||
if (performanceTrendChart) {
|
||
performanceTrendChart.data.datasets = datasets;
|
||
performanceTrendChart.options.scales.x.min = xMin;
|
||
performanceTrendChart.options.scales.x.max = xMax;
|
||
performanceTrendChart.options.plugins.tooltip.borderColor = config.color;
|
||
performanceTrendChart.options.plugins.tooltip.callbacks.label = (ctx) => `${ctx.dataset.label}: ${formatPerformanceValue(performanceMetric, ctx.parsed.y)}`;
|
||
performanceTrendChart.options.plugins.tooltip.callbacks.title = (ctx) => {
|
||
const point = ctx[0] && ctx[0].raw;
|
||
if (!point || !point.x) return '';
|
||
return formatPerformanceDateLabel(point.x);
|
||
};
|
||
performanceTrendChart.options.scales.y.ticks.callback = value => formatPerformanceValue(performanceMetric, value);
|
||
performanceTrendChart.update('none');
|
||
return;
|
||
}
|
||
|
||
performanceTrendChart = new Chart(canvas.getContext('2d'), {
|
||
type: 'line',
|
||
data: {
|
||
datasets
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
animation: false,
|
||
parsing: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false,
|
||
onClick: () => {},
|
||
labels: {
|
||
color: 'rgba(255,255,255,0.72)',
|
||
usePointStyle: true,
|
||
boxWidth: 8
|
||
}
|
||
},
|
||
tooltip: {
|
||
backgroundColor: 'rgba(15,15,15,0.92)',
|
||
borderColor: config.color,
|
||
borderWidth: 1,
|
||
titleColor: 'rgba(255,255,255,0.75)',
|
||
bodyColor: '#f5f5dc',
|
||
padding: 10,
|
||
callbacks: {
|
||
title: (ctx) => {
|
||
const point = ctx[0] && ctx[0].raw;
|
||
if (!point || !point.x) return '';
|
||
return formatPerformanceDateLabel(point.x);
|
||
},
|
||
label: (ctx) => `${ctx.dataset.label}: ${formatPerformanceValue(performanceMetric, ctx.parsed.y)}`
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
type: 'linear',
|
||
min: xMin,
|
||
max: xMax,
|
||
grid: { color: 'rgba(255,255,255,0.04)' },
|
||
ticks: {
|
||
color: 'rgba(255,255,255,0.5)',
|
||
callback: (value) => formatPerformanceDateLabel(value)
|
||
}
|
||
},
|
||
y: {
|
||
beginAtZero: true,
|
||
grid: { color: 'rgba(255,255,255,0.04)' },
|
||
ticks: {
|
||
color: 'rgba(255,255,255,0.78)',
|
||
font: { size: 12, weight: '600' },
|
||
callback: value => formatPerformanceValue(performanceMetric, value)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderPerformanceRadarChart(chartKey, canvasId, emptyId, rows, keys) {
|
||
const canvas = getElement(canvasId);
|
||
if (!canvas) return;
|
||
const hasData = rows.length && rows.some(row => keys.some(key => (Number(row[key]) || 0) > 0));
|
||
if (!rows.length) {
|
||
togglePerformanceCanvas(canvasId, emptyId, false, __t('leaderboard.selectPlayersToCompare'));
|
||
if (chartKey === 'combat' && performanceCombatRadarChart) {
|
||
performanceCombatRadarChart.destroy();
|
||
performanceCombatRadarChart = null;
|
||
}
|
||
if (chartKey === 'results' && performanceResultsRadarChart) {
|
||
performanceResultsRadarChart.destroy();
|
||
performanceResultsRadarChart = null;
|
||
}
|
||
return;
|
||
}
|
||
if (!hasData) {
|
||
togglePerformanceCanvas(canvasId, emptyId, false, __t('squadrons.performanceNoData'));
|
||
if (chartKey === 'combat' && performanceCombatRadarChart) {
|
||
performanceCombatRadarChart.destroy();
|
||
performanceCombatRadarChart = null;
|
||
}
|
||
if (chartKey === 'results' && performanceResultsRadarChart) {
|
||
performanceResultsRadarChart.destroy();
|
||
performanceResultsRadarChart = null;
|
||
}
|
||
return;
|
||
}
|
||
|
||
const labels = keys.map(key => PERFORMANCE_METRICS[key].label);
|
||
const datasets = buildPerformanceRadarDatasets(rows, keys);
|
||
togglePerformanceCanvas(canvasId, emptyId, true);
|
||
const existingChart = chartKey === 'combat' ? performanceCombatRadarChart : performanceResultsRadarChart;
|
||
|
||
if (existingChart) {
|
||
existingChart.data.labels = labels;
|
||
existingChart.data.datasets = datasets;
|
||
existingChart.update('none');
|
||
return;
|
||
}
|
||
|
||
const radarChart = new Chart(canvas.getContext('2d'), {
|
||
type: 'radar',
|
||
data: {
|
||
labels,
|
||
datasets
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
animation: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false,
|
||
onClick: () => {},
|
||
labels: {
|
||
color: 'rgba(255,255,255,0.72)',
|
||
usePointStyle: true,
|
||
boxWidth: 8
|
||
}
|
||
},
|
||
tooltip: {
|
||
backgroundColor: 'rgba(15,15,15,0.92)',
|
||
borderColor: '#90EE90',
|
||
borderWidth: 1,
|
||
titleColor: 'rgba(255,255,255,0.75)',
|
||
bodyColor: '#f5f5dc',
|
||
padding: 10,
|
||
callbacks: {
|
||
label: (ctx) => {
|
||
const actual = ctx.dataset.actualValues?.[ctx.dataIndex] ?? 0;
|
||
return `${ctx.dataset.label}: ${formatPerformanceValue(keys[ctx.dataIndex], actual)}`;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
r: {
|
||
beginAtZero: true,
|
||
min: 0,
|
||
max: 100,
|
||
angleLines: { color: 'rgba(255,255,255,0.06)' },
|
||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||
pointLabels: {
|
||
color: 'rgba(255,255,255,0.75)',
|
||
font: { size: 11, weight: '600' }
|
||
},
|
||
ticks: {
|
||
display: false,
|
||
stepSize: 20
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
if (chartKey === 'combat') performanceCombatRadarChart = radarChart;
|
||
else performanceResultsRadarChart = radarChart;
|
||
}
|
||
|
||
function renderPerformanceOverview(rows) {
|
||
renderPerformanceTrendChart(rows);
|
||
renderPerformanceRadarChart('combat', 'sq-performance-combat-radar', 'sq-performance-combat-empty', rows, PERFORMANCE_COMBAT_RADAR_KEYS);
|
||
renderPerformanceRadarChart('results', 'sq-performance-results-radar', 'sq-performance-results-empty', rows, PERFORMANCE_RESULTS_RADAR_KEYS);
|
||
}
|
||
|
||
function renderPerformanceSparks(rows) {
|
||
destroyPerformanceSparks();
|
||
if (!rows.length || typeof Chart === 'undefined') return;
|
||
const seriesWithData = rows.filter(row => row.series && row.series.length);
|
||
if (!seriesWithData.length) return;
|
||
const allX = seriesWithData.flatMap(row => row.series.map(point => point.x));
|
||
const allY = seriesWithData.flatMap(row => row.series.map(point => buildPerformanceSeriesValue(point, performanceMetric)));
|
||
const xMin = Math.min(...allX);
|
||
const xMax = Math.max(...allX);
|
||
const yMax = Math.max(...allY, 1);
|
||
|
||
rows.forEach((row, index) => {
|
||
const canvas = getElement(`sq-performance-spark-${row.uid}`);
|
||
if (!canvas || !row.series || !row.series.length) return;
|
||
const color = performanceColor(index);
|
||
const spark = new Chart(canvas.getContext('2d'), {
|
||
type: 'line',
|
||
data: {
|
||
datasets: [{
|
||
data: row.series.map(point => ({ x: point.x, y: buildPerformanceSeriesValue(point, performanceMetric) })),
|
||
borderColor: color,
|
||
backgroundColor: color + '14',
|
||
pointRadius: 0,
|
||
pointHoverRadius: 2,
|
||
borderWidth: 1.6,
|
||
tension: 0.18,
|
||
fill: true
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
animation: false,
|
||
parsing: false,
|
||
plugins: {
|
||
legend: { display: false },
|
||
tooltip: {
|
||
enabled: true,
|
||
displayColors: false,
|
||
backgroundColor: 'rgba(15,15,15,0.92)',
|
||
borderColor: color,
|
||
borderWidth: 1,
|
||
titleColor: 'rgba(255,255,255,0.75)',
|
||
bodyColor: color,
|
||
padding: 6,
|
||
callbacks: {
|
||
title: (ctx) => {
|
||
const point = ctx[0] && ctx[0].raw;
|
||
if (!point || !point.x) return '';
|
||
return formatPerformanceDateLabel(point.x);
|
||
},
|
||
label: (ctx) => formatPerformanceValue(performanceMetric, ctx.parsed.y)
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: { type: 'linear', display: false, min: xMin, max: xMax },
|
||
y: { display: false, beginAtZero: true, suggestedMax: yMax * 1.05 }
|
||
}
|
||
}
|
||
});
|
||
performanceSparkCharts.set(String(row.uid), spark);
|
||
});
|
||
}
|
||
|
||
function renderPerformanceTable(rows) {
|
||
const tbody = getElement('sq-performance-table-body');
|
||
if (!tbody) return;
|
||
const sortedRows = getSortedPerformancePlayers(rows);
|
||
syncPerformanceHeaderSort();
|
||
|
||
if (!sortedRows.length) {
|
||
tbody.innerHTML = '<tr><td colspan="13" style="text-align:center; padding:2rem; color:rgba(255,255,255,0.5);">' +
|
||
escapeHtml(__t('leaderboard.selectPlayersToCompare')) + '</td></tr>';
|
||
destroyPerformanceSparks();
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = sortedRows.map(p => {
|
||
const nickRaw = p.nick || '';
|
||
const nickEsc = escapeHtml(nickRaw);
|
||
const nickJs = nickRaw.replace(/'/g, "\\'");
|
||
return '<tr class="row-link">' +
|
||
'<td><div class="player-name"><a href="/players/' + encodeURIComponent(p.uid) + '" class="row-link-overlay" aria-label="View ' + nickEsc + '"></a>' + nickEsc +
|
||
'<button class="pdm-details-btn" onclick="event.stopPropagation(); openPlayerDetailsModal(\'' + p.uid + '\', \'' + nickJs + '\')" title="' + __t('squadrons.quickDetails') + '">' +
|
||
'<i class="fas fa-eye"></i></button></div></td>' +
|
||
'<td class="stat-cell performance-cell"><div class="performance-spark-wrap">' +
|
||
(p.series && p.series.length ? `<canvas class="performance-spark" id="sq-performance-spark-${p.uid}"></canvas>` : '') +
|
||
'</div></td>' +
|
||
'<td class="stat-cell">' + (p.total_battles || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.wins || 0) + '</td>' +
|
||
'<td class="stat-cell">' + formatPerformanceValue('win_rate', p.win_rate) + '</td>' +
|
||
'<td class="stat-cell">' + (p.total_kills || 0) + '</td>' +
|
||
'<td class="stat-cell">' + formatPerformanceValue('kps', p.kps) + '</td>' +
|
||
'<td class="stat-cell">' + formatPerformanceValue('kdr', p.kdr) + '</td>' +
|
||
'<td class="stat-cell">' + (p.ground_kills || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.air_kills || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.assists || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.deaths || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.captures || 0) + '</td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
|
||
renderPerformanceSparks(sortedRows);
|
||
}
|
||
|
||
async function renderPerformanceView() {
|
||
renderPerformanceSelectedPlayers();
|
||
renderPerformanceVehicleSummary();
|
||
|
||
const renderToken = ++performanceRenderToken;
|
||
const selectedMeta = getSelectedPerformancePlayersMeta();
|
||
if (!selectedMeta.length) {
|
||
renderPerformanceFilterOptions([]);
|
||
renderPerformanceOverview([]);
|
||
renderPerformanceTable([]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await ensurePerformanceVehicleMeta();
|
||
const rows = await Promise.all(selectedMeta.map(async player => {
|
||
const games = await ensurePerformancePlayerGames(player.uid);
|
||
return buildPerformancePlayerState(player, games);
|
||
}));
|
||
if (renderToken !== performanceRenderToken) return;
|
||
if (renderPerformanceFilterOptions(rows)) {
|
||
renderPerformanceView();
|
||
return;
|
||
}
|
||
rows.forEach(row => { row.performance = Number(row[performanceMetric]) || 0; });
|
||
renderPerformanceOverview(rows);
|
||
renderPerformanceTable(rows);
|
||
} catch (err) {
|
||
if (renderToken !== performanceRenderToken) return;
|
||
console.error('Failed to render squadron performance view:', err);
|
||
renderPerformanceFilterOptions([]);
|
||
renderPerformanceOverview([]);
|
||
renderPerformanceTable([]);
|
||
}
|
||
}
|
||
|
||
window.renderSquadronPerformanceView = renderPerformanceView;
|
||
|
||
window.sortPerformanceTable = function(field) {
|
||
if (PERFORMANCE_HEADER_METRICS.has(field)) {
|
||
performanceMetric = field;
|
||
}
|
||
if (performanceSort.field === field) {
|
||
performanceSort.direction = performanceSort.direction === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
performanceSort.field = field;
|
||
performanceSort.direction = field === 'nick' ? 'asc' : 'desc';
|
||
}
|
||
renderPerformanceView();
|
||
};
|
||
|
||
window.removePerformancePlayer = function(uid) {
|
||
performanceSelectedPlayers = performanceSelectedPlayers.filter(player => String(player.uid) !== String(uid));
|
||
renderPerformanceView();
|
||
};
|
||
|
||
window.clearPerformanceVehicleFilter = function() {
|
||
performanceSpecificVehicle = null;
|
||
const input = getElement('sq-performance-vehicle-search');
|
||
if (input) input.value = '';
|
||
renderPerformanceVehicleSummary();
|
||
renderPerformanceView();
|
||
};
|
||
|
||
function renderMembersTable(players) {
|
||
const tbody = getElement('sq-members-table-body');
|
||
if (!tbody) return;
|
||
|
||
if (!players.length) {
|
||
tbody.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = players.map(p => {
|
||
const nickRaw = p.nick || '';
|
||
const nickEsc = escapeHtml(nickRaw);
|
||
const nickJs = nickRaw.replace(/'/g, "\\'");
|
||
const winRate = (typeof p.win_rate === 'number' ? p.win_rate.toFixed(1) : (p.win_rate || 0)) + '%';
|
||
const kdr = typeof p.kdr === 'number' ? p.kdr.toFixed(2) : (p.kdr || 0);
|
||
const kps = typeof p.kps === 'number' ? p.kps.toFixed(2) : (Number(p.kps) || 0).toFixed(2);
|
||
return '<tr data-uid="' + escapeHtml(String(p.uid)) + '" class="row-link">' +
|
||
'<td class="player-name"><a href="/players/' + encodeURIComponent(p.uid) + '" class="row-link-overlay" aria-label="View ' + nickEsc + '"></a>' + nickEsc +
|
||
'<button class="pdm-details-btn" onclick="event.stopPropagation(); openPlayerDetailsModal(\'' + p.uid + '\', \'' + nickJs + '\')" title="' + __t('squadrons.quickDetails') + '">' +
|
||
'<i class="fas fa-eye"></i></button></td>' +
|
||
'<td class="stat-cell">' + (p.sqb_points || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.total_battles || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.wins || 0) + '</td>' +
|
||
'<td class="stat-cell">' + winRate + '</td>' +
|
||
'<td class="stat-cell">' + (p.total_kills || 0) + '</td>' +
|
||
'<td class="stat-cell">' + kps + '</td>' +
|
||
'<td class="stat-cell">' + kdr + '</td>' +
|
||
'<td class="stat-cell">' + (p.ground_kills || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.air_kills || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.assists || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.deaths || 0) + '</td>' +
|
||
'<td class="stat-cell">' + (p.captures || 0) + '</td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
|
||
// Reset sort state so the next call lands on desc instead of toggling
|
||
try { currentSort = { field: '', direction: 'desc' }; } catch (e) { /* no-op */ }
|
||
if (typeof sortMembersTable === 'function') {
|
||
sortMembersTable('sqb_points');
|
||
}
|
||
}
|
||
|
||
initSeasons();
|
||
renderMembersTable(currentSquadronPlayers);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|