您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Prüft Personal & Erweiterungen gegen Baupläne, bietet Zuweisungs-Assistenten und Chat-Anfragen. Finale Version mit robustem Dark-Mode-Styling und optimierter Anzeige.
// ==UserScript== // @name * Wachen-Personal- & Erweiterungs-Prüfer // @namespace leitstellenspiel-scripts // @version 6.0.0 // @description Prüft Personal & Erweiterungen gegen Baupläne, bietet Zuweisungs-Assistenten und Chat-Anfragen. Finale Version mit robustem Dark-Mode-Styling und optimierter Anzeige. // @author Masklin / Gemini // @license MIT // @match https://*.leitstellenspiel.de/ // @match https://*.leitstellenspiel.de/buildings/* // @match https://*.leitstellenspiel.de/schoolings/* // @exclude https://*.leitstellenspiel.de/buildings/*/personals // @grant none // @run-at document-idle // ==/UserScript== (async function () { 'use strict'; // ============== // 1. ALLGEMEINE KONSTANTEN & HELFER // ============== const MODAL_ID = 'blueprint-checker-modal'; const CHAT_REQUEST_KEY = 'lss_chat_request_message'; const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // ============== // 2. ROUTER: Entscheidet, welcher Skript-Teil ausgeführt wird // ============== const path = window.location.pathname; if (path.includes('/schoolings/')) { runSchoolingAssistant(); } else if (path.includes('/buildings/')) { runBuildingChecker(); } else if (path === '/') { runMainPageHelper(); } // ================================================================================= // TEIL A: LOGIK FÜR DIE HAUPTSEITE (`/`) - DER CHAT-HELFER // ================================================================================= function runMainPageHelper() { const message = localStorage.getItem(CHAT_REQUEST_KEY); if (message) { const chatInput = document.getElementById('alliance_chat_message'); if (chatInput) { chatInput.value = message; chatInput.focus(); localStorage.removeItem(CHAT_REQUEST_KEY); } } } // ================================================================================= // TEIL B: LOGIK FÜR DIE LEHRGANGSSEITE (`/schoolings/*`) - DER ZUWEISUNGS-ASSISTENT // ================================================================================= async function runSchoolingAssistant() { const params = new URLSearchParams(window.location.search); const buildingId = params.get('assign_building_id'); const needed = parseInt(params.get('needed'), 10); if (!buildingId || !needed) return; const assistantStatus = document.createElement('div'); assistantStatus.innerHTML = `<div id="assistant-status-box" style="padding: 10px; background-color: #337ab7; color: white; text-align: center; position: fixed; top: 50px; left: 50%; transform: translateX(-50%); z-index: 10000; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.5);">Personal-Prüfer Assistent: Bereite alles vor...</div>`; document.body.appendChild(assistantStatus); function updateStatus(message) { const box = document.getElementById('assistant-status-box'); if (box) box.innerHTML = message; } try { updateStatus(`Suche Wache (ID: ${buildingId}) und lade Personal...`); await ensurePanelLoadedAndReady(buildingId); updateStatus(`Wähle ${needed} Mitarbeiter ohne Ausbildung aus...`); await selectPersonnel(buildingId, needed); const buildingPanel = document.querySelector(`.panel[building_id='${buildingId}']`); if (buildingPanel) buildingPanel.scrollIntoView({ behavior: 'smooth', block: 'center' }); updateStatus(`<strong>Auswahl abgeschlossen!</strong><br>Bitte überprüfe die Liste und klicke auf "Ausbilden".`); setTimeout(() => assistantStatus.remove(), 8000); } catch (error) { console.error("Fehler im Zuweisungs-Assistent:", error); updateStatus(`Ein Fehler ist aufgetreten: ${error.message}`); setTimeout(() => assistantStatus.remove(), 8000); } } async function ensurePanelLoadedAndReady(buildingId) { const panelHeading = document.querySelector(`.personal-select-heading[building_id='${buildingId}']`); const panelBody = document.querySelector(`.panel-body[building_id='${buildingId}']`); if (!panelHeading || !panelBody) throw new Error(`Panel für Wache ${buildingId} nicht gefunden.`); if (panelBody.classList.contains("hidden") || panelBody.innerHTML.includes("ajax-loader")) { panelHeading.click(); } let attempts = 0; while (attempts < 100) { if (panelBody.querySelector(".schooling_checkbox")) { await sleep(50); return; } await sleep(50); attempts++; } throw new Error(`Personal für Wache ${buildingId} konnte nicht geladen werden (Timeout).`); } async function selectPersonnel(buildingId, capacity) { let freeSlotsInCourse = parseInt(document.getElementById('schooling_free')?.textContent || '0', 10); let selectedCount = 0; const checkboxes = document.querySelectorAll(`.schooling_checkbox[building_id='${buildingId}']`); for (const checkbox of checkboxes) { if (selectedCount >= capacity || freeSlotsInCourse <= 0) break; if (!checkbox.checked && !checkbox.disabled) { const educationCell = document.getElementById(`school_personal_education_${checkbox.value}`); const hasAnyEducation = educationCell && educationCell.textContent.trim() !== ""; if (!hasAnyEducation) { checkbox.checked = true; checkbox.dispatchEvent(new Event('change')); selectedCount++; freeSlotsInCourse--; } } } if (typeof window.update_costs === 'function') window.update_costs(); if (typeof window.update_schooling_free === 'function') window.update_schooling_free(); } // ================================================================================= // TEIL C: LOGIK FÜR DIE WACHEN-SEITE (`/buildings/*`) - DER PERSONAL- & ERWEITERUNGS-PRÜFER // ================================================================================= function runBuildingChecker() { const databaseName = "BosErnie_StationBlueprints"; const objectStoreName = "main"; const cacheKeyBlueprints = "blueprints"; const vehiclesConfigKey = "personalpruefer.vehicle-cache"; const schoolingsMapKey = "personalpruefer.schooling-cache"; const schoolingDetailsCacheKey = "personalpruefer.schooling-details-cache"; const storageTtl = 24 * 60 * 60 * 1000; const slotsCacheTtl = 5 * 60 * 1000; const vehiclesConfigurationOverride = [ { id: 134, maxStaff: 4, training: [{ key: "police_horse", number: 4 }] }, { id: 135, maxStaff: 2, training: [{ key: "police_horse", number: 2 }] }, { id: 137, maxStaff: 6, training: [{ key: "police_horse", number: 6 }] }, { id: 29, maxStaff: 1, training: [{ key: "notarzt", number: 1 }] }, { id: 122, maxStaff: 2, training: [{ key: "thw_energy_supply", number: 2 }] }, { id: 123, maxStaff: 3, training: [{ key: "water_damage_pump", number: 3 }] }, { id: 93, maxStaff: 5, training: [{ key: "thw_rescue_dogs", number: 5 }] }, { id: 53, maxStaff: 6, training: [{ key: "dekon_p", number: 6 }] }, { id: 81, maxStaff: 3, training: [{ key: "police_mek", number: 3 }] }, { id: 79, maxStaff: 3, training: [{ key: "police_sek", number: 3 }] }, { id: 74, maxStaff: 3, training: [{ key: "notarzt", number: 3 }] }, { id: 172, maxStaff: 6, training: [{ key: "disaster_response_technology", number: 6 }] }, { id: 173, maxStaff: 7, training: [{ key: "disaster_response_technology", number: 7 }] }, { id: 126, maxStaff: 5, training: [{ key: "fire_drone", number: 5 }] }, { id: 51, maxStaff: 2, training: [{ key: "police_fukw", number: 2 }] }, ]; function injectGlobalStyles() { const style = document.createElement('style'); style.textContent = ` #${MODAL_ID} .modal-pruefer-content { background-color: #fefefe; color: #333; margin: 8% auto; padding: 20px; border: 1px solid #ddd; width: 90%; max-width: 1100px; border-radius: 5px; } html.dark #${MODAL_ID} .modal-pruefer-content { background-color: #2d3748; color: #e2e8f0; border-color: #4a5568; } #${MODAL_ID} .modal-pruefer-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e5e5e5; padding-bottom: 10px; margin-bottom: 10px; } html.dark #${MODAL_ID} .modal-pruefer-header { border-bottom-color: #4a5568; } #${MODAL_ID} .modal-pruefer-header h2 { margin: 0; font-size: 1.5em; } #${MODAL_ID} .modal-pruefer-close { color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; } .pruefer-log-box { background-color: #f5f5f5; border: 1px solid #ddd; padding: 10px; margin-bottom: 15px; border-radius: 4px; font-size: 0.9em; } .pruefer-log-box .log-columns { display: flex; justify-content: space-between; } .pruefer-log-box .log-column { flex: 1; padding: 0 10px; } .pruefer-log-box .log-column ul { list-style-position: inside; padding: 0; margin:0; } html.dark .pruefer-log-box { background-color: #1a202c !important; border-color: #4a5568 !important; color: #e2e8f0 !important; } .pruefer-log-box p { margin-top:0; margin-bottom: 5px; } html.dark #${MODAL_ID} .alert { color: #e2e8f0; background-color: transparent; border: 1px solid #4a5568; } #${MODAL_ID} .alert-personnel-deficit { color: #a94442; background-color: #f2dede; border: 1px solid #ebccd1; font-weight: bold; padding: 15px; margin-bottom: 20px; border-radius: 4px; } html.dark #${MODAL_ID} .alert-personnel-deficit { color: #e53e3e; background-color: rgba(255, 0, 0, 0.1); border-color: #e53e3e; font-weight: bold; } #${MODAL_ID} table thead tr { border-bottom: 2px solid #ddd; } html.dark #${MODAL_ID} table thead tr { color: #ffffff !important; border-bottom-color: #4a5568; background-color: #1a202c; } #${MODAL_ID} .extension-label { display: inline-block; margin: 2px; padding: .2em .6em .3em; font-weight: 700; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: .25em; } #${MODAL_ID} .extension-label.label-success { background-color: #5cb85c; } #${MODAL_ID} .extension-label.label-danger { background-color: #d9534f; cursor: pointer; } #${MODAL_ID} .extension-label.label-danger:hover { background-color: #c9302c; } #${MODAL_ID} .extension-label.label-warning { background-color: #f0ad4e; } `; document.head.appendChild(style); } async function retrieveBlueprints() { try { const db = await new Promise((resolve, reject) => { const request = window.indexedDB.open(databaseName, 2); request.onerror = () => reject("IndexedDB konnte nicht geöffnet werden."); request.onsuccess = () => resolve(request.result); }); return new Promise((resolve, reject) => { const transaction = db.transaction([objectStoreName], 'readonly'); const store = transaction.objectStore(objectStoreName); const request = store.get(cacheKeyBlueprints); request.onerror = () => reject("Baupläne konnten nicht gelesen werden."); request.onsuccess = () => resolve(request.result); }); } catch (error) { console.error("Fehler beim Abrufen der Baupläne:", error); return null; } } function transformVehiclesData(data) { return Object.entries(data).filter(([,v])=>!v.isTrailer).map(([e,t])=>{const a=[];t.staff?.training&&Object.values(t.staff.training).forEach(e=>{Object.entries(e).forEach(([e,{min:n,all:r}])=>{n&&n>0?a.push({key:e,number:n}):!0===r&&a.push({key:e,number:t.maxPersonnel})})});return{id:Number(e),caption:t.caption,maxStaff:t.maxPersonnel,training:a}}) } function applyVehicleConfigurationOverride(data) { return data.map(vehicle => { const override = vehiclesConfigurationOverride.find(v => v.id === vehicle.id); if (override) { return { ...vehicle, ...override }; } return vehicle; }); } async function initVehiclesConfiguration() { const cached = localStorage.getItem(vehiclesConfigKey); if (cached) { const parsed = JSON.parse(cached); if (parsed.lastUpdate > Date.now() - storageTtl) return parsed.data; } try { const response = await fetch("https://api.lss-manager.de/de_DE/vehicles"); if (!response.ok) throw new Error("API-Fehler"); const data = await response.json(); const transformedData = transformVehiclesData(data); const finalData = applyVehicleConfigurationOverride(transformedData); localStorage.setItem(vehiclesConfigKey, JSON.stringify({ lastUpdate: Date.now(), data: finalData })); return finalData; } catch (err) { console.error("Fehler beim Laden der Fahrzeugdaten:", err); return null; } } async function initSchoolingsMap() { const cached = localStorage.getItem(schoolingsMapKey); if (cached) { const parsed = JSON.parse(cached); if (parsed.lastUpdate > Date.now() - storageTtl) return parsed.data; } try { const response = await fetch("https://api.lss-manager.de/de_DE/schoolings"); if (!response.ok) throw new Error("API-Fehler"); const apiData = await response.json(); const schoolingsMap = { 'null': 'Keine Ausbildung nötig' }; for (const org of Object.values(apiData)) { for (const schooling of org) { if (schooling.key && schooling.staffList) { schoolingsMap[schooling.key] = schooling.staffList; } } } localStorage.setItem(schoolingsMapKey, JSON.stringify({ lastUpdate: Date.now(), data: schoolingsMap })); return schoolingsMap; } catch (err) { console.error("Fehler beim Laden der Ausbildungsnamen:", err); return { 'null': 'Keine Ausbildung nötig' }; } } async function getAvailableSchoolingsDetails() { const cached = localStorage.getItem(schoolingDetailsCacheKey); if (cached) { const parsed = JSON.parse(cached); if (parsed.lastUpdate > Date.now() - slotsCacheTtl) return parsed.data; } try { const response = await fetch("/schoolings"); if (!response.ok) throw new Error("Netzwerkfehler"); const htmlString = await response.text(); const doc = new DOMParser().parseFromString(htmlString, "text/html"); const table = doc.querySelector("#schooling_opened_table"); if (!table) return {}; const schoolings = {}; const rows = table.querySelectorAll("tbody tr[data-education-key]"); rows.forEach(row => { const key = row.getAttribute("data-education-key"); const linkElement = row.querySelector("td:first-child a"); const slotsElement = row.querySelector("td:nth-child(2)"); const costElement = row.querySelector("td:nth-child(3)"); if (key && linkElement && costElement && slotsElement) { const id = linkElement.href.split("/").pop(); const slots = parseInt(slotsElement.innerText.trim(), 10) || 0; const costText = costElement.innerText.trim(); const cost = parseInt(costText, 10) || 0; const currency = costText.includes("Coins") ? "Co" : "Cr"; if (!schoolings[key]) schoolings[key] = []; schoolings[key].push({ id, cost, currency, slots }); } }); for (const key in schoolings) { schoolings[key].sort((a, b) => a.cost - b.cost); } localStorage.setItem(schoolingDetailsCacheKey, JSON.stringify({ lastUpdate: Date.now(), data: schoolings })); return schoolings; } catch (err) { console.error("Fehler beim Laden der Lehrgangsdetails:", err); return {}; } } async function getPersonnelData(buildingId) { try { const response = await fetch(`/buildings/${buildingId}/personals`); if (!response.ok) throw new Error("Netzwerkfehler beim Abruf der Personalseite"); const htmlString = await response.text(); const doc = new DOMParser().parseFromString(htmlString, "text/html"); const personnelRows = doc.querySelectorAll("#personal_table tbody tr"); const totalPersonnelCount = personnelRows.length; const personnel = {}; const inTraining = {}; personnelRows.forEach(row => { const name = row.querySelector("td:first-child")?.innerText.trim(); if (!name) return; const uniqueNameKey = `${name}_${Math.random()}`; personnel[uniqueNameKey] = new Set(); const filterable = row.getAttribute("data-filterable-by"); if (filterable) { try { const keys = JSON.parse(filterable.replace(/"/g, '"')); keys.forEach(key => key && personnel[uniqueNameKey].add(key)); } catch (e) {} } const inTrainingSpan = row.querySelector('span[data-education-key]'); if (inTrainingSpan?.textContent.includes('Im Unterricht')) { const key = inTrainingSpan.getAttribute('data-education-key'); if (key) { inTraining[key] = (inTraining[key] || 0) + 1; } } }); const available = { 'null': 0 }; Object.values(personnel).forEach(trainings => { if (trainings.size === 0) available['null']++; else trainings.forEach(key => { available[key] = (available[key] || 0) + 1; }); }); return { count: totalPersonnelCount, available, inTraining }; } catch (e) { console.error("Fehler bei der Analyse der Personaldaten:", e); return null; } } function createFallbackName(key) { if (!key) return ''; return key.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase()); } function calculateRequirements(blueprint, vehicleDatabase, trainingMap) { const required = { 'null': 0 }; let totalRequiredPersonnel = 0; const log = []; const vehicleMap = Object.fromEntries(vehicleDatabase.map(v => [v.id, v])); blueprint.vehicles.forEach(vehicle => { const dbEntry = vehicleMap[vehicle.id]; if (!dbEntry) return; totalRequiredPersonnel += dbEntry.maxStaff * vehicle.quantity; const vehicleRequirements = {}; if (dbEntry.training && dbEntry.training.length > 0) { dbEntry.training.forEach(t => { const translatedName = trainingMap[t.key] || createFallbackName(t.key); if (vehicleRequirements[translatedName]) { if (t.number > vehicleRequirements[translatedName].number) { vehicleRequirements[translatedName] = { key: t.key, number: t.number }; } } else { vehicleRequirements[translatedName] = { key: t.key, number: t.number }; } }); } let specialStaff = 0; let requirementsStrings = []; for (const translatedName in vehicleRequirements) { const req = vehicleRequirements[translatedName]; const amount = req.number * vehicle.quantity; required[req.key] = (required[req.key] || 0) + amount; specialStaff += amount; requirementsStrings.push(`${amount}x ${translatedName}`); } const generalStaff = (dbEntry.maxStaff * vehicle.quantity) - specialStaff; if (generalStaff > 0) { required['null'] += generalStaff; requirementsStrings.push(`${generalStaff}x ${trainingMap['null']}`); } if (requirementsStrings.length > 0) { log.push(`<strong>${vehicle.quantity}x ${dbEntry.caption || 'Unbekanntes Fahrzeug'}:</strong> ${requirementsStrings.join(', ')}`); } }); log.sort(); return { required, log, totalRequiredPersonnel }; } function getExtensionsStatus() { const extensionsTab = document.querySelector('#ausbauten'); if (!extensionsTab) return []; const statuses = []; const rows = extensionsTab.querySelectorAll('tbody tr'); rows.forEach(row => { const nameElement = row.querySelector('b'); if (!nameElement) return; const name = nameElement.innerText.trim(); const buyButton = row.querySelector('a.btn-success[href*="/extension/credits/"]'); const timer = row.querySelector('.extension-timer'); if (timer) { const endTime = timer.dataset.endTime; statuses.push({ name, status: 'constructing', endTime }); } else if (buyButton) { const cost = parseInt(buyButton.textContent.replace(/\D/g, ''), 10) || 0; statuses.push({ name, status: 'unbuilt', cost }); } else { statuses.push({ name, status: 'built' }); } }); return statuses; } function showResults(buildingId, required, totalRequiredPersonnel, available, inTraining, personnelCount, trainingMap, log, availableSchoolingsDetails, blueprintExtensions, extensionsStatus) { const modalContent = document.getElementById(`${MODAL_ID}-content`); let isDarkMode = false; try { const bgColor = window.getComputedStyle(document.body).backgroundColor; const colorValues = bgColor.match(/\d+/g).map(Number); const brightness = (colorValues[0] + colorValues[1] + colorValues[2]) / 3; if (brightness < 128) isDarkMode = true; } catch(e) { isDarkMode = document.documentElement.classList.contains('dark'); } const theme = isDarkMode ? { headerText: '#ffffff', bodyText: '#333333', border: '#4a5568', bgOdd: 'rgba(0,0,0,0.2)', bgEven: 'transparent', success: 'rgba(46, 125, 50, 0.4)', warning: 'rgba(245, 127, 23, 0.4)', danger: 'rgba(198, 40, 40, 0.4)' } : { headerText: '#333333', bodyText: '#333333', border: '#ddd', bgOdd: '#f9f9f9', bgEven: '#fff', success: '#dff0d8', warning: '#fcf8e3', danger: '#f2dede' }; let finalHTML = ''; const personnelDeficit = totalRequiredPersonnel - personnelCount; if (personnelDeficit > 0) { finalHTML += `<div class="alert alert-personnel-deficit"><strong>Achtung:</strong> Es sind nicht genügend Mitarbeiter auf der Wache, um den Bauplan zu erfüllen. Es fehlen <strong>${personnelDeficit}</strong> Mitarbeiter. Voraussichtliche Erfüllung in <strong>${personnelDeficit} Tagen</strong>.</div>`; } if (blueprintExtensions && blueprintExtensions.length > 0) { let missingExtensions = []; let totalCost = 0; let extensionsContent = `<h4>Erweiterungs-Prüfung</h4><div>`; blueprintExtensions.forEach(reqExt => { const extName = reqExt.name; const statusInfo = extensionsStatus.find(s => s.name === extName); if (statusInfo?.status === 'built') { extensionsContent += `<span class="extension-label label-success" title="Bereits gebaut"><span class="glyphicon glyphicon-ok"></span> ${extName}</span>`; } else if (statusInfo?.status === 'constructing') { const endDate = new Date(parseInt(statusInfo.endTime, 10)); const title = `Im Bau, Fertigstellung am ${endDate.toLocaleString()}`; extensionsContent += `<span class="extension-label label-warning" title="${title}">🚧 ${extName}</span>`; } else { missingExtensions.push(extName); if (statusInfo?.cost) totalCost += statusInfo.cost; extensionsContent += `<span class="extension-label label-danger build-extension-button" data-extension-name="${extName}" title="Erweiterung '${extName}' für ${statusInfo?.cost?.toLocaleString() || 'unbekannte'} Credits bauen"><span class="glyphicon glyphicon-remove"></span> ${extName}</span>`; } }); extensionsContent += `</div>`; if (missingExtensions.length > 0) { extensionsContent += `<div style="margin-top: 10px;"><button id="build-all-extensions-btn" class="btn btn-danger btn-sm" data-count="${missingExtensions.length}" data-cost="${totalCost}">Alle ${missingExtensions.length} fehlenden bauen (${totalCost.toLocaleString()} Credits)</button></div>`; } finalHTML += extensionsContent + '<hr>'; } if (log.length > 0) { let logHTML = `<div class="pruefer-log-box"><p><strong>Anforderungs-Details (Personal):</strong></p>`; const itemsPerColumn = Math.ceil(log.length / 3); const col1 = log.slice(0, itemsPerColumn); const col2 = log.slice(itemsPerColumn, itemsPerColumn * 2); const col3 = log.slice(itemsPerColumn * 2); logHTML += `<div class="log-columns"> <div class="log-column"><ul><li>${col1.join('</li><li>')}</li></ul></div> <div class="log-column"><ul><li>${col2.join('</li><li>')}</li></ul></div> <div class="log-column"><ul><li>${col3.join('</li><li>')}</li></ul></div> </div></div>`; finalHTML += logHTML; } const allKeys = new Set([...Object.keys(required), ...Object.keys(available), ...Object.keys(inTraining)]); let hasMissing = false; allKeys.forEach(key => { if ((required[key] || 0) > (available[key] || 0)) hasMissing = true; }); const showInTrainingColumn = Array.from(allKeys).some(key => (inTraining[key] || 0) > 0 && (required[key] || 0) > 0); const showMissingColumn = hasMissing; const showSchoolingColumn = hasMissing; let headerHTML = `<th>Ausbildung</th><th>Benötigt</th><th>Vorhanden</th>`; if (showInTrainingColumn) headerHTML += `<th>In Ausbildung</th>`; if (showMissingColumn) headerHTML += `<th>Fehlend</th>`; if (showSchoolingColumn) headerHTML += `<th>Verbandslehrgänge</th>`; let tableHTML = `<h4>Personal-Prüfung</h4><p>${personnelCount} Mitarbeiter gefunden:</p><table style="width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 0.9em;"><thead><tr style="color: ${theme.headerText}; text-align: left;">${headerHTML.replace(/<th>/g, '<th style="padding: 8px;">')}</tr></thead><tbody>`; const sortedKeys = Array.from(allKeys).sort((a, b) => (a === 'null') ? 1 : (b === 'null') ? -1 : (trainingMap[a] || a).localeCompare(trainingMap[b] || b)); sortedKeys.forEach((key, index) => { const requiredAmount = required[key] || 0; if (requiredAmount === 0) return; const availableAmount = available[key] || 0; const inTrainingAmount = inTraining[key] || 0; const missing = Math.max(0, requiredAmount - availableAmount); const trainingName = trainingMap[key] || createFallbackName(key); let rowBgColor = (index % 2 === 0) ? theme.bgEven : theme.bgOdd; if (missing > 0) { rowBgColor = requiredAmount <= availableAmount + inTrainingAmount ? theme.warning : theme.danger; } else if (requiredAmount > 0) { rowBgColor = theme.success; } const cellStyle = `padding: 8px; border-bottom: 1px solid ${theme.border}; vertical-align: middle;`; let rowContent = `<td style="${cellStyle}">${trainingName}</td><td style="${cellStyle}">${requiredAmount}</td><td style="${cellStyle}">${availableAmount}</td>`; if (showInTrainingColumn) rowContent += `<td style="${cellStyle}">${inTrainingAmount > 0 ? `<strong>+${inTrainingAmount}</strong>` : '0'}</td>`; if (showMissingColumn) rowContent += `<td style="${cellStyle}"><strong>${missing}</strong></td>`; if (showSchoolingColumn) { let cellContent = '-'; const uncoveredNeed = Math.max(0, requiredAmount - availableAmount - inTrainingAmount); if (uncoveredNeed > 0) { const courses = (availableSchoolingsDetails[key] || []).filter(c => c.slots > 0); const totalSlots = courses.reduce((sum, course) => sum + course.slots, 0); if (totalSlots >= uncoveredNeed) { let neededSlots = uncoveredNeed; const links = []; for (const course of courses) { if (neededSlots <= 0) break; const assignCount = Math.min(neededSlots, course.slots); const url = `/schoolings/${course.id}?assign_building_id=${buildingId}&needed=${assignCount}`; const buttonText = `Zuweisen (${assignCount})`; const titleText = `Weise ${assignCount} von ${course.slots} Plätzen in diesem Kurs zu. Kosten: ${course.cost} ${course.currency}.`; links.push(`<a href="${url}" target="_blank" class="btn btn-primary btn-xs" style="margin: 2px;" title="${titleText}">${buttonText}</a>`); neededSlots -= assignCount; } cellContent = `<div style="display: flex; flex-wrap: wrap;">${links.join('')}</div>`; } else { const neededToRequest = uncoveredNeed - totalSlots; let postButton = ''; if (key !== 'null') { postButton = `<button class="btn btn-warning btn-xs post-request-button" style="margin-left: 5px;" data-training-name="${trainingName}" data-missing-count="${neededToRequest}">Anfrage posten</button>`; } cellContent = `<strong style="color: #D32F2F;">Nein</strong> (${totalSlots} Plätze) ${postButton}`; } } rowContent += `<td style="${cellStyle}">${cellContent}</td>`; } tableHTML += `<tr style="background-color: ${rowBgColor}; color: ${theme.bodyText};">${rowContent}</tr>`; }); tableHTML += `</tbody></table>`; if (hasMissing) { if (showInTrainingColumn) tableHTML += `<p class="alert alert-info" style="margin-top:10px;"><strong>Hinweis:</strong> Gelbe Zeilen markieren Anforderungen, die durch Personal in Ausbildung gedeckt werden.</p>`; if (sortedKeys.some(k => (required[k] || 0) > (available[k] || 0) + (inTraining[k] || 0))) tableHTML += `<p class="alert alert-danger"><strong>Es fehlen Ausbildungen, die auch mit Personal in Ausbildung nicht erfüllt werden können.</strong></p>`; } else if (sortedKeys.some(k => (required[k] || 0) > 0)) { tableHTML += `<p class="alert alert-success" style="margin-top:10px;"><strong>Perfekt! Alle für den Bauplan benötigten Ausbildungen sind vorhanden.</strong></p>`; } else { tableHTML += `<p class="alert alert-info" style="margin-top:10px;"><strong>Für diesen Bauplan werden keine speziellen Ausbildungen benötigt.</strong></p>`; } modalContent.innerHTML = finalHTML + tableHTML; } async function startCheckProcess(h1, selectedBlueprint, vehicleDb, schoolingsMap, availableSchoolingsDetails) { const modal = document.getElementById(MODAL_ID); const modalContent = document.getElementById(`${MODAL_ID}-content`); document.getElementById(`${MODAL_ID}-title`).innerText = `Prüfung für: ${h1.innerText.trim()} (Bauplan: ${selectedBlueprint.name})`; modal.style.display = 'block'; modalContent.innerHTML = `Daten werden analysiert...`; const buildingId = window.location.pathname.split('/')[2]; const [personnelData, extensionsStatus] = await Promise.all([ getPersonnelData(buildingId), getExtensionsStatus() ]); if (personnelData === null) { modalContent.innerHTML = `<div class="alert alert-danger">FEHLER: Personaldaten konnten nicht geladen werden.</div>`; return; } const { required, log, totalRequiredPersonnel } = calculateRequirements(selectedBlueprint, vehicleDb, schoolingsMap); showResults(buildingId, required, totalRequiredPersonnel, personnelData.available, personnelData.inTraining, personnelData.count, schoolingsMap, log, availableSchoolingsDetails, selectedBlueprint.extensions, extensionsStatus); } async function buildExtension(extensionName, buttonElement) { const extensionsTab = document.querySelector('#ausbauten'); if (!extensionsTab) return false; const rows = extensionsTab.querySelectorAll('tbody tr'); for (const row of rows) { const nameElement = row.querySelector('b'); if (nameElement && nameElement.innerText.trim() === extensionName) { const creditsButton = row.querySelector('a.btn-success[href*="/extension/credits/"]'); if (creditsButton) { const url = creditsButton.href; const csrfToken = document.querySelector('meta[name="csrf-token"]').content; if(buttonElement) { buttonElement.innerHTML = `Baue...`; buttonElement.disabled = true; } try { const response = await fetch(url, { method: 'POST', headers: { 'X-CSRF-Token': csrfToken } }); if (!response.ok) throw new Error(`Server-Fehler: ${response.statusText}`); if(buttonElement) { buttonElement.classList.remove('label-danger', 'build-extension-button'); buttonElement.classList.add('label-warning'); buttonElement.innerHTML = `🚧 ${extensionName}`; buttonElement.title = 'Wird gebaut (Seite neu laden für Details)'; buttonElement.style.cursor = 'default'; } } catch (error) { console.error('Fehler beim Bauen der Erweiterung:', error); alert(`Fehler beim Bauen von "${extensionName}".`); if(buttonElement) { buttonElement.innerHTML = `❌ ${extensionName}`; buttonElement.disabled = false; } } return true; } } } return false; } new MutationObserver(async (_, observer) => { const h1 = document.querySelector("h1[building_type]"); if (h1) { const personalDt = Array.from(document.querySelectorAll('dl.dl-horizontal dt')).find(dt => dt.textContent.includes('Personal:')); const personalGroup = personalDt?.nextElementSibling?.querySelector('.btn-group'); if (personalGroup) { observer.disconnect(); injectGlobalStyles(); let buttonContainer = document.getElementById('lss-personal-pruefer-container'); if (!buttonContainer) { buttonContainer = document.createElement('div'); buttonContainer.className = 'btn-group'; buttonContainer.id = 'lss-personal-pruefer-container'; buttonContainer.style.marginLeft = '5px'; personalGroup.parentNode.insertBefore(buttonContainer, personalGroup.nextSibling); } buttonContainer.innerHTML = '<em>Lade Daten...</em>'; const [stationBlueprints, vehicleDb, schoolingsMap, availableSchoolingsDetails] = await Promise.all([retrieveBlueprints(), initVehiclesConfiguration(), initSchoolingsMap(), getAvailableSchoolingsDetails()]); if (!stationBlueprints || !vehicleDb || !schoolingsMap) { buttonContainer.innerHTML = '<em>Fehler bei Daten</em>'; return; } const buildingTypeId = parseInt(h1.getAttribute("building_type")); const matchingBlueprints = Object.values(stationBlueprints).filter(bp => bp.enabled && bp.buildingTypeId === buildingTypeId); buttonContainer.innerHTML = ''; if (matchingBlueprints.length > 0) { matchingBlueprints.forEach(blueprint => { const checkButton = document.createElement('a'); checkButton.className = 'btn btn-primary btn-xs'; checkButton.innerHTML = `<span class="glyphicon glyphicon-list-alt"></span> Prüfen: ${blueprint.name}`; checkButton.addEventListener('click', (e) => { e.preventDefault(); startCheckProcess(h1, blueprint, vehicleDb, schoolingsMap, availableSchoolingsDetails); }); buttonContainer.appendChild(checkButton); }); } else { buttonContainer.innerHTML = '<em>Keine Baupläne</em>'; } if (!document.getElementById(MODAL_ID)) { const modalHTML = `<div id="${MODAL_ID}" style="display:none; position:fixed; z-index:9999; left:0; top:0; width:100%; height:100%; overflow:auto; background-color:rgba(0,0,0,0.5);"><div class="modal-pruefer-content"><div class="modal-pruefer-header"><h2 id="${MODAL_ID}-title">Prüfung</h2><span id="${MODAL_ID}-close" class="modal-pruefer-close">×</span></div><div id="${MODAL_ID}-content"></div></div></div>`; document.body.insertAdjacentHTML('beforeend', modalHTML); const modal = document.getElementById(MODAL_ID); document.getElementById(`${MODAL_ID}-close`).addEventListener('click', () => modal.style.display = 'none'); window.addEventListener('click', e => { if (e.target == modal) modal.style.display = 'none'; }); modal.addEventListener('click', async function(event) { const target = event.target.closest('.post-request-button, .build-extension-button, #build-all-extensions-btn'); if (!target) return; event.preventDefault(); event.stopPropagation(); if (target.classList.contains('post-request-button')) { const trainingName = target.dataset.trainingName; const missingCount = target.dataset.missingCount; const message = `Hallo zusammen, könnte jemand bitte einen Lehrgang für "${trainingName}" starten? Es werden ${missingCount} Plätze benötigt. Vielen Dank!`; localStorage.setItem(CHAT_REQUEST_KEY, message); window.open('/','_blank'); target.textContent = 'Weitergeleitet...'; target.disabled = true; } else if (target.classList.contains('build-extension-button')) { await buildExtension(target.dataset.extensionName, target); } else if (target.id === 'build-all-extensions-btn') { const count = target.dataset.count; const cost = parseInt(target.dataset.cost, 10); if (confirm(`Möchtest du wirklich ${count} fehlende Erweiterungen für insgesamt ${cost.toLocaleString()} Credits bauen?`)) { target.textContent = 'Arbeite...'; target.disabled = true; const missingLabels = modal.querySelectorAll('.build-extension-button'); for (const label of missingLabels) { await buildExtension(label.dataset.extensionName, label); await sleep(500); } target.textContent = 'Alle Aufträge gesendet'; } } }); } } } }).observe(document.body, { childList: true, subtree: true }); } })();