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',
bakedData: {}, // Cache for standalone persistence
ownerMapping: { firstName: 'VNA', lastName: 'GNA' }, // Default for ALKIS
ownerStatuses: {}, // { "Name Vorname": "status" }
ownerStatuses: {}, // { "name vorname": { status: "...", notiz: "..." } }
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
console.log("Konfiguration geladen.");
@ -247,8 +260,7 @@ document.addEventListener('DOMContentLoaded', async () => {
function updateLegend() {
if (!legendContent) return;
// Items to always show if any turbine exists
let html = '';
let html = '<div class="legend-section-title">Anlagen-Geometrien</div>';
if (state.turbines.length > 0) {
html += `
<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: #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
Object.keys(overlays).forEach(name => {
const layer = overlays[name];
if (state.map.hasLayer(layer)) {
// Determine color (heuristic or from layer style)
let color = '#ccc';
if (name.includes('Eigentümer')) color = '#2ecc71';
if (name.includes('Hilfs')) color = '#ffcc00';
html += `<div class="legend-item"><span class="color-box" style="background: ${color}; opacity: 0.8;"></span> ${name}</div>`;
}
html += '<div class="legend-section-title" style="margin-top: 15px;">Sicherungsstand (ALKIS)</div>';
Object.keys(STATUS_MAP).forEach(status => {
const data = STATUS_MAP[status];
html += `
<div class="legend-item-status">
<span class="status-dot" style="background: ${data.color};"></span>
<div class="status-text-container">
<div class="status-label">${status}</div>
<div class="status-desc">${data.desc}</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
@ -1210,38 +1225,45 @@ document.addEventListener('DOMContentLoaded', async () => {
}
async function processALKISData(geojson, layerName) {
const style = getDynamicStyle(layerName) || { color: '#000', weight: 1, fillOpacity: 0.1 };
const layer = L.geoJSON(geojson, {
style: (feature) => {
const props = feature.properties;
const firstName = props.VNA || '';
const lastName = props.GNA || '';
const firstName = (props.VNA || '').trim();
const lastName = (props.GNA || '').trim();
const ownerName = `${firstName} ${lastName}`.trim().toLowerCase();
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 opacity = 0.1;
if (status === 'gbr' || status === 'gesichert') fillColor = '#2ecc71';
else if (status === 'external' || status === 'fremdplanung') fillColor = '#e74c3c';
else if (status === 'declined' || status === 'ablehnend' || status === 'negative') fillColor = '#e74c3c';
else if (status === 'undecided' || status === 'unentschlossen') fillColor = '#95a5a6';
else if (status === 'positive' || status === 'positiv') fillColor = '#5efd9c';
else if (status === 'in verhandlung') fillColor = '#f1c40f';
if (STATUS_MAP[status]) {
fillColor = STATUS_MAP[status].color;
opacity = 0.7;
} else if (status === 'none' || status === '') {
fillColor = 'transparent';
opacity = 0.1;
}
return {
color: '#000',
weight: 1,
fillOpacity: fillColor === 'transparent' ? 0.1 : 0.7,
fillOpacity: opacity,
fillColor: fillColor
};
},
onEachFeature: (feature, layer) => {
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;">`;
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) {
if (['VNA', 'GNA', 'status', 'notiz', 'id'].includes(key)) continue;
const val = feature.properties[key];
if (val !== null && val !== undefined) popup += `<b>${key}:</b> ${val}<br>`;
}
@ -1432,11 +1454,7 @@ document.addEventListener('DOMContentLoaded', async () => {
<td>
<select class="status-select" data-owner="${name}">
<option value="none" ${status === 'none' ? 'selected' : ''}>Kein Status</option>
<option value="gbr" ${status === 'gbr' ? 'selected' : ''}>Mitglied der GbR</option>
<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>
${Object.keys(STATUS_MAP).map(s => `<option value="${s}" ${status === s ? 'selected' : ''}>${s}</option>`).join('')}
</select>
</td>
<td>

View File

@ -228,39 +228,95 @@ body {
.floating-panel {
position: absolute;
z-index: 1000;
background: var(--panel-bg);
background: #0a2d2d; /* Dark teal background matching the image */
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;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.panel-header {
background: rgba(255, 255, 255, 0.05);
padding: 10px 15px;
background: rgba(0, 0, 0, 0.2);
padding: 12px 18px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
border-bottom: 1px solid var(--border-color);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.85rem;
font-weight: 600;
font-weight: 700;
letter-spacing: 0.5px;
color: white;
}
.panel-content {
padding: 12px;
max-height: 300px;
padding: 18px;
max-height: 500px;
overflow-y: auto;
}
/* Floating Legend Specific */
#floatingLegend {
bottom: 20px;
right: 20px;
width: 220px;
bottom: 25px;
right: 25px;
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 {
@ -270,7 +326,7 @@ body {
.toggle-btn {
background: transparent;
border: none;
color: var(--text-dim);
color: white;
cursor: pointer;
font-size: 0.7rem;
transition: transform 0.3s;