@@ -307,7 +307,7 @@ <h3>Gildenrat-Login</h3>
307307 import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js" ;
308308 import { getFirestore , doc , setDoc , onSnapshot , collection , deleteDoc , getDoc , serverTimestamp , query , orderBy , addDoc , updateDoc , where , getDocs } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js" ;
309309 import { getAuth , signInAnonymously , onAuthStateChanged , signInWithEmailAndPassword , signOut } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js" ;
310-
310+
311311 // ============== INITIALISIERUNG & GLOBALE VARIABLEN ==============
312312 const firebaseConfig = {
313313 apiKey : "AIzaSyBmqCCIOKq0OQOTEgJJ7Lj8CYlLihVBVSU" ,
@@ -329,6 +329,9 @@ <h3>Gildenrat-Login</h3>
329329 let lootDatesUnsubscribe = null ;
330330 let selectedLootDateUnsubscribe = null ;
331331 let heartbeatIntervalId = null ;
332+ let allLootDocuments = [ ] ;
333+ let playerSummaryState = { } ;
334+ let summarySortState = { column : 'total' , direction : 'desc' } ;
332335
333336 const DATA_COLLECTION = "raid-tool-data" ;
334337 const HISTORY_COLLECTION = "raid-tool-history" ;
@@ -734,7 +737,6 @@ <h3>Nachricht</h3>
734737 } ) ;
735738 }
736739
737- // --- Logik für loot.html ---
738740 function initLootPage ( ) {
739741 const importSection = document . getElementById ( 'loot-import-section' ) ;
740742 const importBtn = document . getElementById ( 'import-loot-btn' ) ;
@@ -744,6 +746,15 @@ <h3>Nachricht</h3>
744746 importBtn ?. addEventListener ( 'click' , handleLootImport ) ;
745747 }
746748
749+ // NEU: Event listener für den Zusammenfassungs-Button
750+ document . getElementById ( 'show-player-summary-btn' ) ?. addEventListener ( 'click' , showPlayerSummaryView ) ;
751+
752+ // Alle Loot-Daten einmalig beim Laden der Seite abrufen
753+ const allLootQuery = query ( lootCollectionRef , orderBy ( "raidDate" , "desc" ) ) ;
754+ getDocs ( allLootQuery ) . then ( snapshot => {
755+ allLootDocuments = snapshot . docs . map ( doc => ( { id : doc . id , data : doc . data ( ) . lootData } ) ) ;
756+ } ) ;
757+
747758 const q = query ( lootCollectionRef , orderBy ( "raidDate" , "desc" ) ) ;
748759 lootDatesUnsubscribe = onSnapshot ( q , ( snapshot ) => {
749760 const datesList = document . getElementById ( 'loot-dates-list' ) ;
@@ -764,6 +775,7 @@ <h3>Nachricht</h3>
764775 dateButton . onclick = ( ) => {
765776 document . querySelectorAll ( '#loot-dates-list button' ) . forEach ( btn => btn . classList . remove ( 'active-tab' ) ) ;
766777 dateButton . classList . add ( 'active-tab' ) ;
778+ // Diese Funktion schaltet automatisch zurück zur Einzelansicht
767779 displayLootForDate ( date ) ;
768780 } ;
769781 datesList . appendChild ( dateButton ) ;
@@ -805,6 +817,8 @@ <h3>Nachricht</h3>
805817 if ( selectedLootDateUnsubscribe ) {
806818 selectedLootDateUnsubscribe ( ) ;
807819 }
820+ const viewTitle = document . getElementById ( 'loot-view-title' ) ;
821+ if ( viewTitle ) viewTitle . textContent = "Loot-Details" ;
808822
809823 const lootDocRef = doc ( db , LOOT_COLLECTION , dateId ) ;
810824 const displayContainer = document . getElementById ( 'loot-details-display' ) ;
@@ -1239,6 +1253,297 @@ <h4 class="text-lg font-bold" style="color: var(--color-gold);">${itemLinkHtml}<
12391253 } ;
12401254 } ) ;
12411255 } ;
1256+ function showPlayerSummaryView ( ) {
1257+ const displayContainer = document . getElementById ( 'loot-details-display' ) ;
1258+ const viewTitle = document . getElementById ( 'loot-view-title' ) ;
1259+ if ( ! displayContainer || ! viewTitle ) return ;
1260+
1261+ viewTitle . textContent = "Spieler-Zusammenfassung" ;
1262+ playerSummaryState = { } ; // Zustand zurücksetzen
1263+ summarySortState = { column : 'total' , direction : 'desc' } ;
1264+
1265+
1266+ if ( allLootDocuments . length === 0 ) {
1267+ displayContainer . innerHTML = '<p class="text-gray-500">Keine Loot-Daten zum Auswerten vorhanden.</p>' ;
1268+ return ;
1269+ }
1270+
1271+ const datesHtml = allLootDocuments . map ( doc => `
1272+ <label class="flex items-center space-x-2 p-2 rounded-md hover:bg-slate-700 cursor-pointer transition-colors duration-150">
1273+ <input type="checkbox" data-date-id="${ doc . id } " class="summary-date-checkbox form-checkbox h-5 w-5 rounded bg-slate-900 border-slate-600 text-jade-400 focus:ring-jade-400">
1274+ <span>${ new Date ( doc . id + 'T12:00:00Z' ) . toLocaleDateString ( 'de-DE' , { month : 'long' , day : 'numeric' } ) } </span>
1275+ </label>
1276+ ` ) . join ( '' ) ;
1277+
1278+ displayContainer . innerHTML = `
1279+ <div class="flex justify-between items-center mb-4">
1280+ <p class="text-gray-400">Wähle die Daten aus, die einbezogen werden sollen.</p>
1281+ <div class="flex gap-2">
1282+ <button id="summary-select-all-btn" class="text-sm py-1 px-3 rounded-md bg-blue-600 hover:bg-blue-700">Alle</button>
1283+ <button id="summary-deselect-all-btn" class="text-sm py-1 px-3 rounded-md bg-slate-600 hover:bg-slate-500">Keine</button>
1284+ </div>
1285+ </div>
1286+ <div id="summary-dates-grid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 mb-6 p-4 bg-slate-900/50 rounded-lg border border-slate-700">
1287+ ${ datesHtml }
1288+ </div>
1289+ <div id="player-summary-results">
1290+ <p class="text-gray-500 text-center py-4">Bitte mindestens ein Datum auswählen.</p>
1291+ </div>
1292+ ` ;
1293+
1294+ // KORREKTUR: Der Listener wird präziser gesetzt, um das Bubbling-Problem zu beheben.
1295+ document . getElementById ( 'summary-dates-grid' ) . addEventListener ( 'change' , ( event ) => {
1296+ if ( event . target . classList . contains ( 'summary-date-checkbox' ) ) {
1297+ handleSummaryDateSelection ( ) ;
1298+ }
1299+ } ) ;
1300+ document . getElementById ( 'summary-select-all-btn' ) ?. addEventListener ( 'click' , ( ) => toggleAllSummaryCheckboxes ( true ) ) ;
1301+ document . getElementById ( 'summary-deselect-all-btn' ) ?. addEventListener ( 'click' , ( ) => toggleAllSummaryCheckboxes ( false ) ) ;
1302+ }
1303+
1304+ function toggleAllSummaryCheckboxes ( select ) {
1305+ document . querySelectorAll ( '.summary-date-checkbox' ) . forEach ( cb => cb . checked = select ) ;
1306+ handleSummaryDateSelection ( ) ;
1307+ }
1308+
1309+ function handleSummaryDateSelection ( ) {
1310+ const selectedDates = Array . from ( document . querySelectorAll ( '.summary-date-checkbox:checked' ) )
1311+ . map ( cb => cb . dataset . dateId ) ;
1312+
1313+ const oldState = { ...playerSummaryState } ; // Den alten Zustand für die offenen Menüs sichern
1314+ playerSummaryState = { } ;
1315+
1316+ if ( selectedDates . length > 0 ) {
1317+ const filteredLootItems = allLootDocuments
1318+ . filter ( doc => selectedDates . includes ( doc . id ) )
1319+ . flatMap ( doc => doc . data ) ;
1320+
1321+ filteredLootItems . forEach ( item => {
1322+ if ( ! item . received || ! item . checksum ) return ; // Sicherstellen, dass ein Checksum vorhanden ist
1323+
1324+ const winner = item . awardedTo . split ( '-' ) [ 0 ] ;
1325+ let rollTypeSource = '' ;
1326+
1327+ if ( item . winningRollType ) {
1328+ rollTypeSource = item . winningRollType ;
1329+ } else {
1330+ const winningRoll = item . Rolls . find ( roll => roll . player === winner ) ;
1331+ if ( winningRoll && winningRoll . classification ) {
1332+ rollTypeSource = winningRoll . classification ;
1333+ } else { return ; }
1334+ }
1335+
1336+ const rollType = rollTypeSource . toUpperCase ( ) ;
1337+ let category ;
1338+ if ( rollType === 'MS' ) category = 'ms' ;
1339+ else if ( rollType === 'OS' ) category = 'os' ;
1340+ else if ( [ 'T-MOG' , 'TRANSMOG' , 'STYLE' ] . includes ( rollType ) ) category = 'transmog' ;
1341+ else return ;
1342+
1343+ if ( ! playerSummaryState [ winner ] ) {
1344+ playerSummaryState [ winner ] = { items : [ ] , isDetailsOpen : oldState [ winner ] ?. isDetailsOpen || false } ;
1345+ }
1346+
1347+ playerSummaryState [ winner ] . items . push ( {
1348+ name : item . itemLink . replace ( / [ \[ \] ] / g, '' ) ,
1349+ id : item . itemID ,
1350+ // KORREKTUR: Wir verwenden item.checksum statt item.itemGUID
1351+ uniqueId : item . checksum ,
1352+ category : category ,
1353+ included : true
1354+ } ) ;
1355+ } ) ;
1356+ }
1357+
1358+ drawPlayerSummaryTable ( ) ;
1359+ }
1360+
1361+ function renderPlayerSummary ( selectedDates ) {
1362+ const resultsContainer = document . getElementById ( 'player-summary-results' ) ;
1363+ if ( selectedDates . length === 0 ) {
1364+ resultsContainer . innerHTML = '<p class="text-gray-500 text-center py-4">Bitte mindestens ein Datum auswählen.</p>' ;
1365+ return ;
1366+ }
1367+
1368+ const playerStats = { } ;
1369+ const filteredLootItems = allLootDocuments
1370+ . filter ( doc => selectedDates . includes ( doc . id ) )
1371+ . flatMap ( doc => doc . data ) ;
1372+
1373+ filteredLootItems . forEach ( item => {
1374+ if ( ! item . received ) return ;
1375+
1376+ const winner = item . awardedTo . split ( '-' ) [ 0 ] ;
1377+ let rollTypeSource = '' ;
1378+
1379+ // --- NEUE, INTELLIGENTERE LOGIK ---
1380+ // 1. Versuche, den direkten Typ zu verwenden
1381+ if ( item . winningRollType ) {
1382+ rollTypeSource = item . winningRollType ;
1383+ }
1384+ // 2. Wenn das Feld fehlt, finde den Wurf des Gewinners und nutze dessen Klassifizierung
1385+ else {
1386+ const winningRoll = item . Rolls . find ( roll => roll . player === winner ) ;
1387+ if ( winningRoll && winningRoll . classification ) {
1388+ rollTypeSource = winningRoll . classification ;
1389+ } else {
1390+ // Wenn wir den Typ nicht bestimmen können, überspringe das Item.
1391+ return ;
1392+ }
1393+ }
1394+ // --- ENDE DER NEUEN LOGIK ---
1395+
1396+ const rollType = rollTypeSource . toUpperCase ( ) ;
1397+ let category ;
1398+ if ( rollType === 'MS' ) {
1399+ category = 'ms' ;
1400+ } else if ( rollType === 'OS' ) {
1401+ category = 'os' ;
1402+ } else if ( [ 'T-MOG' , 'TRANSMOG' , 'STYLE' ] . includes ( rollType ) ) {
1403+ category = 'transmog' ;
1404+ } else {
1405+ return ;
1406+ }
1407+
1408+ if ( ! playerStats [ winner ] ) {
1409+ playerStats [ winner ] = { ms : 0 , os : 0 , transmog : 0 , total : 0 } ;
1410+ }
1411+
1412+ playerStats [ winner ] [ category ] ++ ;
1413+ playerStats [ winner ] . total ++ ;
1414+ } ) ;
1415+
1416+ const statsArray = Object . entries ( playerStats ) . map ( ( [ name , stats ] ) => ( { name, ...stats } ) ) ;
1417+ statsArray . sort ( ( a , b ) => b . total - a . total || a . name . localeCompare ( b . name ) ) ;
1418+
1419+ const tableRows = statsArray . map ( player => `
1420+ <tr class="border-b border-slate-700 hover:bg-slate-750/50">
1421+ <td class="px-4 py-3 font-medium">${ player . name } </td>
1422+ <td class="px-4 py-3 text-center">${ player . ms } </td>
1423+ <td class="px-4 py-3 text-center">${ player . os } </td>
1424+ <td class="px-4 py-3 text-center">${ player . transmog } </td>
1425+ <td class="px-4 py-3 text-center font-bold text-gold">${ player . total } </td>
1426+ </tr>
1427+ ` ) . join ( '' ) ;
1428+
1429+ resultsContainer . innerHTML = `
1430+ <div class="overflow-x-auto rounded-lg border border-slate-700">
1431+ <table class="min-w-full text-sm text-left text-gray-300">
1432+ <thead class="text-xs text-gray-400 uppercase bg-slate-700">
1433+ <tr>
1434+ <th scope="col" class="px-4 py-3">Spieler</th>
1435+ <th scope="col" class="px-4 py-3 text-center">MS</th>
1436+ <th scope="col" class="px-4 py-3 text-center">OS</th>
1437+ <th scope="col" class="px-4 py-3 text-center">Transmog</th>
1438+ <th scope="col" class="px-4 py-3 text-center">Total</th>
1439+ </tr>
1440+ </thead>
1441+ <tbody class="divide-y divide-slate-700">
1442+ ${ tableRows . length > 0 ? tableRows : '<tr><td colspan="5" class="text-center p-4 text-gray-500">Keine zählbaren Loot-Daten für die Auswahl gefunden.</td></tr>' }
1443+ </tbody>
1444+ </table>
1445+ </div>
1446+ ` ;
1447+ }
1448+ function drawPlayerSummaryTable ( ) {
1449+ const resultsContainer = document . getElementById ( 'player-summary-results' ) ;
1450+ const players = Object . keys ( playerSummaryState ) ;
1451+
1452+ if ( players . length === 0 ) {
1453+ resultsContainer . innerHTML = '<p class="text-gray-500 text-center py-4">Bitte mindestens ein Datum auswählen.</p>' ;
1454+ return ;
1455+ }
1456+
1457+ const statsArray = players . map ( name => {
1458+ const includedItems = playerSummaryState [ name ] . items . filter ( item => item . included ) ;
1459+ return {
1460+ name : name ,
1461+ ms : includedItems . filter ( item => item . category === 'ms' ) . length ,
1462+ os : includedItems . filter ( item => item . category === 'os' ) . length ,
1463+ transmog : includedItems . filter ( item => item . category === 'transmog' ) . length ,
1464+ total : includedItems . length
1465+ } ;
1466+ } ) ;
1467+
1468+ // --- NEU: Sortierlogik ---
1469+ const { column, direction } = summarySortState ;
1470+ statsArray . sort ( ( a , b ) => {
1471+ if ( a [ column ] !== b [ column ] ) {
1472+ return direction === 'desc' ? b [ column ] - a [ column ] : a [ column ] - b [ column ] ;
1473+ }
1474+ // Sekundäre Sortierung nach Total, dann nach Name
1475+ if ( b . total !== a . total ) return b . total - a . total ;
1476+ return a . name . localeCompare ( b . name ) ;
1477+ } ) ;
1478+ // --- ENDE Sortierlogik ---
1479+
1480+ const tableRows = statsArray . map ( player => {
1481+ const itemsHtml = playerSummaryState [ player . name ] . items . map ( item => {
1482+ const wowheadUrl = `https://www.wowhead.com/mop-classic/item=${ item . id } ` ;
1483+ return `<div class="flex items-center gap-2 px-2 py-1"><input type="checkbox" class="item-inclusion-checkbox" data-player-name="${ player . name } " data-item-id="${ item . uniqueId } " ${ item . included ? 'checked' : '' } ><a href="${ wowheadUrl } " target="_blank" rel="noopener noreferrer" class="hover:underline text-sm">[${ item . name } ]</a><span class="text-xs uppercase bg-slate-600 px-1.5 py-0.5 rounded-full">${ item . category } </span></div>` ;
1484+ } ) . join ( '' ) ;
1485+ const isHidden = playerSummaryState [ player . name ] . isDetailsOpen ? '' : 'hidden' ;
1486+ const rotation = playerSummaryState [ player . name ] . isDetailsOpen ? 'rotate-90' : '' ;
1487+ return `<tr class="border-b border-slate-700 hover:bg-slate-750/50"><td class="px-4 py-3 font-medium"><button class="toggle-details-btn flex items-center gap-2" data-player-name="${ player . name } ">${ player . name } <span class="text-xs text-gray-400 transition-transform ${ rotation } ">▶</span></button></td><td class="px-4 py-3 text-center">${ player . ms } </td><td class="px-4 py-3 text-center">${ player . os } </td><td class="px-4 py-3 text-center">${ player . transmog } </td><td class="px-4 py-3 text-center font-bold text-gold">${ player . total } </td></tr><tr class="item-details-row ${ isHidden } bg-slate-800"><td colspan="5" class="p-2"><div class="flex flex-col gap-1">${ itemsHtml } </div></td></tr>` ;
1488+ } ) . join ( '' ) ;
1489+
1490+ // --- NEU: Dynamische Kopfzeile mit Sortier-Buttons ---
1491+ const sortableColumns = { 'MS' : 'ms' , 'OS' : 'os' , 'Transmog' : 'transmog' , 'Total' : 'total' } ;
1492+ const headerHtml = Object . entries ( sortableColumns ) . map ( ( [ title , key ] ) => {
1493+ const isActive = summarySortState . column === key ;
1494+ const indicator = isActive ? ( summarySortState . direction === 'desc' ? '▼' : '▲' ) : '' ;
1495+ return `<th scope="col" class="px-4 py-3 text-center"><button class="sort-btn font-bold uppercase ${ isActive ? 'text-gold' : 'text-gray-400' } " data-sort-column="${ key } ">${ title } <span class="indicator w-4 inline-block">${ indicator } </span></button></th>` ;
1496+ } ) . join ( '' ) ;
1497+
1498+ resultsContainer . innerHTML = `<div class="overflow-x-auto rounded-lg border border-slate-700"><table class="min-w-full text-sm text-left text-gray-300"><thead class="text-xs uppercase bg-slate-700"><tr><th scope="col" class="px-4 py-3">Spieler</th>${ headerHtml } </tr></thead><tbody class="divide-y divide-slate-700">${ tableRows } </tbody></table></div>` ;
1499+
1500+ // Event Listener hinzufügen
1501+ resultsContainer . querySelectorAll ( '.toggle-details-btn' ) . forEach ( btn => btn . addEventListener ( 'click' , handleToggleDetails ) ) ;
1502+ resultsContainer . querySelectorAll ( '.item-inclusion-checkbox' ) . forEach ( cb => cb . addEventListener ( 'change' , handleItemInclusionChange ) ) ;
1503+ resultsContainer . querySelectorAll ( '.sort-btn' ) . forEach ( btn => btn . addEventListener ( 'click' , handleSummarySort ) ) ;
1504+ }
1505+ function handleSummarySort ( event ) {
1506+ const newColumn = event . currentTarget . dataset . sortColumn ;
1507+ if ( summarySortState . column === newColumn ) {
1508+ // Wenn die selbe Spalte geklickt wird, Richtung umkehren
1509+ summarySortState . direction = summarySortState . direction === 'desc' ? 'asc' : 'desc' ;
1510+ } else {
1511+ // Bei Klick auf eine neue Spalte, diese als aktiv setzen und Standardrichtung (desc) wählen
1512+ summarySortState . column = newColumn ;
1513+ summarySortState . direction = 'desc' ;
1514+ }
1515+ // Tabelle mit neuer Sortierung neu zeichnen
1516+ drawPlayerSummaryTable ( ) ;
1517+ }
1518+
1519+ // ZWEI WEITERE NEUE FUNKTIONEN
1520+ function handleToggleDetails ( event ) {
1521+ const button = event . currentTarget ;
1522+ // KORREKTUR: Spielername wird aus data-Attribut gelesen (sicherer)
1523+ const playerName = button . dataset . playerName ;
1524+
1525+ if ( playerSummaryState [ playerName ] ) {
1526+ // Den Zustand im State-Objekt umschalten
1527+ playerSummaryState [ playerName ] . isDetailsOpen = ! playerSummaryState [ playerName ] . isDetailsOpen ;
1528+ }
1529+
1530+ // Tabelle neu zeichnen, um die Änderung (offen/geschlossen) anzuzeigen
1531+ drawPlayerSummaryTable ( ) ;
1532+ }
1533+
1534+ function handleItemInclusionChange ( event ) {
1535+ const checkbox = event . target ;
1536+ const playerName = checkbox . dataset . playerName ;
1537+ // KORREKTUR: Wir lesen das data-item-id Attribut
1538+ const itemId = checkbox . dataset . itemId ;
1539+
1540+ // KORREKTUR: Wir finden das Item anhand der neuen uniqueId Eigenschaft
1541+ const item = playerSummaryState [ playerName ] ?. items . find ( i => i . uniqueId === itemId ) ;
1542+ if ( item ) {
1543+ item . included = checkbox . checked ;
1544+ }
1545+ drawPlayerSummaryTable ( ) ;
1546+ }
12421547 </ script >
12431548</ body >
12441549</ html >
0 commit comments