Full update of owner status categories and premium legend UI based on user image
Deploy Bürgerwind / deploy (push) Successful in 16s Details

This commit is contained in:
Johannes Baumeister 2026-04-30 21:52:13 +02:00
parent b03a7b2568
commit 826fbb8973
2 changed files with 119 additions and 45 deletions

82
app.js
View File

@ -10,10 +10,23 @@ document.addEventListener('DOMContentLoaded', async () => {
activeVariant: 'A', activeVariant: 'A',
bakedData: {}, // Cache for standalone persistence bakedData: {}, // Cache for standalone persistence
ownerMapping: { firstName: 'VNA', lastName: 'GNA' }, // Default for ALKIS ownerMapping: { firstName: 'VNA', lastName: 'GNA' }, // Default for ALKIS
ownerStatuses: {}, // { "Name Vorname": "status" } ownerStatuses: {}, // { "name vorname": { status: "...", notiz: "..." } }
showAuxiliary: true showAuxiliary: true
}; };
const STATUS_MAP = {
'Ablehnung': { color: '#ff0000', desc: 'Der Eigentümer lehnt das Vorhaben strikt ab.' },
'Erwartet Negativ': { color: '#ffa500', desc: 'Erste Signale oder Tendenzen deuten auf eine Ablehnung hin.' },
'Unentschlossen': { color: '#ffff00', desc: 'Rückmeldung ist noch offen oder der Eigentümer zögert.' },
'Unbekannt': { color: '#cccccc', desc: 'Bisher kein Kontakt erfolgt; Status ist völlig offen.' },
'Erwartet Positiv': { color: '#90ee90', desc: 'Eine grundsätzliche Bereitschaft zur Zustimmung wird erwartet.' },
'Zusage (mündlich)': { color: '#008000', desc: 'Klare mündliche Zustimmung liegt vor, der schriftliche Vertrag ist noch offen.' },
'Vertraglich gesichert': { color: '#006400', desc: 'Der Vertrag liegt unterschrieben vor.' },
'In der Projektgesellschaft': { color: '#ff00ff', desc: 'Grundstückseigentümer ist in der Projektgesellschaft.' },
'Fremdplanung': { color: '#c71585', desc: 'Anderes Vorhaben (WEA), keine Kooperation.' },
'Kooperationspartner': { color: '#ffffff', desc: 'Anderes Vorhaben mit dem kooperiert wird.' }
};
// Removed fetch for config to prevent CORS errors on file:// protocol // Removed fetch for config to prevent CORS errors on file:// protocol
console.log("Konfiguration geladen."); console.log("Konfiguration geladen.");
@ -247,8 +260,7 @@ document.addEventListener('DOMContentLoaded', async () => {
function updateLegend() { function updateLegend() {
if (!legendContent) return; if (!legendContent) return;
// Items to always show if any turbine exists let html = '<div class="legend-section-title">Anlagen-Geometrien</div>';
let html = '';
if (state.turbines.length > 0) { if (state.turbines.length > 0) {
html += ` html += `
<div class="legend-item"><span class="color-box" style="background: #00c8ff;"></span> Rotorfläche</div> <div class="legend-item"><span class="color-box" style="background: #00c8ff;"></span> Rotorfläche</div>
@ -258,22 +270,25 @@ document.addEventListener('DOMContentLoaded', async () => {
<div class="legend-item"><span class="color-box" style="background: rgba(52, 152, 219, 0.3); border: 1px solid #3498db;"></span> Fundament</div> <div class="legend-item"><span class="color-box" style="background: rgba(52, 152, 219, 0.3); border: 1px solid #3498db;"></span> Fundament</div>
<div class="legend-item"><span class="color-box" style="background: #e74c3c; opacity: 0.6;"></span> Kranstellfläche (KSF)</div> <div class="legend-item"><span class="color-box" style="background: #e74c3c; opacity: 0.6;"></span> Kranstellfläche (KSF)</div>
`; `;
} else {
html += '<div style="font-size: 0.7rem; opacity: 0.6; padding-left: 20px;">Keine Anlagen gesetzt</div>';
} }
// Search for active external layers html += '<div class="legend-section-title" style="margin-top: 15px;">Sicherungsstand (ALKIS)</div>';
Object.keys(overlays).forEach(name => { Object.keys(STATUS_MAP).forEach(status => {
const layer = overlays[name]; const data = STATUS_MAP[status];
if (state.map.hasLayer(layer)) { html += `
// Determine color (heuristic or from layer style) <div class="legend-item-status">
let color = '#ccc'; <span class="status-dot" style="background: ${data.color};"></span>
if (name.includes('Eigentümer')) color = '#2ecc71'; <div class="status-text-container">
if (name.includes('Hilfs')) color = '#ffcc00'; <div class="status-label">${status}</div>
<div class="status-desc">${data.desc}</div>
html += `<div class="legend-item"><span class="color-box" style="background: ${color}; opacity: 0.8;"></span> ${name}</div>`; </div>
} </div>
`;
}); });
legendContent.innerHTML = html || '<div style="font-size: 0.75rem; color: var(--text-dim); text-align: center;">Keine aktiven Layer</div>'; legendContent.innerHTML = html;
} }
// Toggle Legend collapse // Toggle Legend collapse
@ -1210,38 +1225,45 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
async function processALKISData(geojson, layerName) { async function processALKISData(geojson, layerName) {
const style = getDynamicStyle(layerName) || { color: '#000', weight: 1, fillOpacity: 0.1 };
const layer = L.geoJSON(geojson, { const layer = L.geoJSON(geojson, {
style: (feature) => { style: (feature) => {
const props = feature.properties; const props = feature.properties;
const firstName = props.VNA || ''; const firstName = (props.VNA || '').trim();
const lastName = props.GNA || ''; const lastName = (props.GNA || '').trim();
const ownerName = `${firstName} ${lastName}`.trim().toLowerCase(); const ownerName = `${firstName} ${lastName}`.trim().toLowerCase();
const stored = state.ownerStatuses[ownerName]; const stored = state.ownerStatuses[ownerName];
const status = (typeof stored === 'string' ? stored : (stored?.status || props.status || "")).toLowerCase(); const status = typeof stored === 'object' ? (stored.status || '') : (stored || props.status || '');
let fillColor = 'transparent'; let fillColor = 'transparent';
let opacity = 0.1;
if (status === 'gbr' || status === 'gesichert') fillColor = '#2ecc71'; if (STATUS_MAP[status]) {
else if (status === 'external' || status === 'fremdplanung') fillColor = '#e74c3c'; fillColor = STATUS_MAP[status].color;
else if (status === 'declined' || status === 'ablehnend' || status === 'negative') fillColor = '#e74c3c'; opacity = 0.7;
else if (status === 'undecided' || status === 'unentschlossen') fillColor = '#95a5a6'; } else if (status === 'none' || status === '') {
else if (status === 'positive' || status === 'positiv') fillColor = '#5efd9c'; fillColor = 'transparent';
else if (status === 'in verhandlung') fillColor = '#f1c40f'; opacity = 0.1;
}
return { return {
color: '#000', color: '#000',
weight: 1, weight: 1,
fillOpacity: fillColor === 'transparent' ? 0.1 : 0.7, fillOpacity: opacity,
fillColor: fillColor fillColor: fillColor
}; };
}, },
onEachFeature: (feature, layer) => { onEachFeature: (feature, layer) => {
if (feature.properties) { if (feature.properties) {
const status = feature.properties.status || 'Kein Status';
const notiz = feature.properties.notiz || '';
let popup = `<b>${layerName}</b><br><hr style="margin: 5px 0; border: 0; border-top: 1px solid #444;">`; let popup = `<b>${layerName}</b><br><hr style="margin: 5px 0; border: 0; border-top: 1px solid #444;">`;
popup += `<b>Eigentümer:</b> ${feature.properties.VNA} ${feature.properties.GNA}<br>`;
popup += `<b>Status:</b> ${status}<br>`;
if (notiz) popup += `<b>Notiz:</b> ${notiz}<br>`;
popup += `<hr style="margin: 5px 0; border: 0; border-top: 1px solid #444;">`;
for (let key in feature.properties) { for (let key in feature.properties) {
if (['VNA', 'GNA', 'status', 'notiz', 'id'].includes(key)) continue;
const val = feature.properties[key]; const val = feature.properties[key];
if (val !== null && val !== undefined) popup += `<b>${key}:</b> ${val}<br>`; if (val !== null && val !== undefined) popup += `<b>${key}:</b> ${val}<br>`;
} }
@ -1432,11 +1454,7 @@ document.addEventListener('DOMContentLoaded', async () => {
<td> <td>
<select class="status-select" data-owner="${name}"> <select class="status-select" data-owner="${name}">
<option value="none" ${status === 'none' ? 'selected' : ''}>Kein Status</option> <option value="none" ${status === 'none' ? 'selected' : ''}>Kein Status</option>
<option value="gbr" ${status === 'gbr' ? 'selected' : ''}>Mitglied der GbR</option> ${Object.keys(STATUS_MAP).map(s => `<option value="${s}" ${status === s ? 'selected' : ''}>${s}</option>`).join('')}
<option value="external" ${status === 'external' ? 'selected' : ''}>Fremdplanung</option>
<option value="declined" ${status === 'declined' ? 'selected' : ''}>Ablehnend</option>
<option value="positive" ${status === 'positive' ? 'selected' : ''}>Positiv</option>
<option value="undecided" ${status === 'undecided' ? 'selected' : ''}>Unentschlossen</option>
</select> </select>
</td> </td>
<td> <td>

View File

@ -228,39 +228,95 @@ body {
.floating-panel { .floating-panel {
position: absolute; position: absolute;
z-index: 1000; z-index: 1000;
background: var(--panel-bg); background: #0a2d2d; /* Dark teal background matching the image */
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
border: 1px solid var(--border-color); border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
overflow: hidden; overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
} }
.panel-header { .panel-header {
background: rgba(255, 255, 255, 0.05); background: rgba(0, 0, 0, 0.2);
padding: 10px 15px; padding: 12px 18px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 700;
letter-spacing: 0.5px;
color: white;
} }
.panel-content { .panel-content {
padding: 12px; padding: 18px;
max-height: 300px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
} }
/* Floating Legend Specific */ /* Floating Legend Specific */
#floatingLegend { #floatingLegend {
bottom: 20px; bottom: 25px;
right: 20px; right: 25px;
width: 220px; width: 320px;
}
.legend-section-title {
font-size: 0.7rem;
font-weight: 800;
color: var(--primary-color);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1.2px;
opacity: 0.8;
}
.legend-item {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.85rem;
margin-bottom: 10px;
color: rgba(255, 255, 255, 0.9);
}
.legend-item-status {
display: flex;
align-items: flex-start;
gap: 15px;
margin-bottom: 18px;
}
.status-dot {
width: 20px;
height: 20px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 2px;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.status-text-container {
display: flex;
flex-direction: column;
gap: 2px;
}
.status-label {
font-weight: 800;
font-size: 0.95rem;
color: white;
}
.status-desc {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.6);
line-height: 1.4;
} }
#floatingLegend.collapsed .panel-content { #floatingLegend.collapsed .panel-content {
@ -270,7 +326,7 @@ body {
.toggle-btn { .toggle-btn {
background: transparent; background: transparent;
border: none; border: none;
color: var(--text-dim); color: white;
cursor: pointer; cursor: pointer;
font-size: 0.7rem; font-size: 0.7rem;
transition: transform 0.3s; transition: transform 0.3s;