Files
SPA-landing/angebotsdatenbank/index.html

4072 lines
163 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Angebotsdatenbank - Intelligente Positionsverwaltung</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1800px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.toolbar {
padding: 20px 40px;
background: #f8f9fa;
border-bottom: 2px solid #e9ecef;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary { background: #667eea; color: white; }
.btn-success { background: #28a745; color: white; }
.btn-danger { background: #dc3545; color: white; }
.btn-info { background: #17a2b8; color: white; }
.btn-warning { background: #ffc107; color: #000; }
.btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.2); }
.controls { padding: 30px 40px; background: white; }
.search-box { position: relative; margin-bottom: 20px; }
.search-box input {
width: 100%;
padding: 15px 50px 15px 20px;
font-size: 16px;
border: 2px solid #dee2e6;
border-radius: 10px;
}
.filter-section { margin-bottom: 20px; }
.filter-label { font-weight: bold; margin-bottom: 10px; display: block; }
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-height: 200px;
overflow-y: auto;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
}
.filter-btn {
padding: 6px 14px;
border: 2px solid #667eea;
background: white;
color: #667eea;
border-radius: 20px;
cursor: pointer;
font-size: 12px;
}
.filter-btn.active { background: #667eea; color: white; }
.filter-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
border-color: #ccc;
color: #ccc;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
padding: 20px 40px;
}
.stat-item { text-align: center; padding: 15px; background: #f8f9fa; border-radius: 10px; }
.stat-number { font-size: 2em; font-weight: bold; color: #667eea; }
.content { padding: 40px; }
.positions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 20px;
}
.position-card {
background: white;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 20px;
position: relative;
transition: all 0.3s;
}
.position-card:hover {
border-color: #667eea;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
transform: translateY(-5px);
}
.position-card.duplicate {
border-color: #ffc107;
background: #fff9e6;
}
.position-badges { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
.badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: bold; }
.badge-stlb { background: #667eea; color: white; }
.badge-din { background: #28a745; color: white; }
.badge-duplicate { background: #ffc107; color: #000; }
.position-term { font-size: 1.1em; font-weight: bold; margin-bottom: 10px; }
.position-description { color: #6c757d; font-size: 0.95em; margin-bottom: 15px; }
.position-footer {
display: flex;
justify-content: space-between;
padding-top: 15px;
border-top: 1px solid #e9ecef;
}
.position-price { font-size: 1.3em; font-weight: bold; color: #28a745; }
.position-actions {
position: absolute;
top: 15px;
right: 15px;
display: flex;
gap: 5px;
}
.action-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
cursor: pointer;
}
.action-btn-edit { background: #17a2b8; color: white; }
.action-btn-delete { background: #dc3545; color: white; }
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active { display: flex; }
.modal-content {
background: white;
border-radius: 15px;
padding: 30px;
max-width: 1000px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.form-group { margin-bottom: 20px; }
.form-control {
width: 100%;
padding: 12px;
border: 2px solid #dee2e6;
border-radius: 8px;
}
textarea.form-control { min-height: 100px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.modal-footer { display: flex; gap: 10px; justify-content: flex-end; margin-top: 30px; }
.gewerk-list {
max-height: 400px;
overflow-y: auto;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
background: #f8f9fa;
}
.gewerk-item {
display: flex;
align-items: center;
padding: 8px;
margin-bottom: 5px;
background: white;
border-radius: 5px;
}
.gewerk-item input { margin-right: 10px; }
/* Zwei-Spalten Layout für Datenbank */
.database-layout {
display: grid;
grid-template-columns: 350px 1fr;
gap: 20px;
height: calc(100vh - 300px);
}
.search-sidebar {
background: white;
border-radius: 15px;
padding: 20px;
overflow-y: auto;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
position: relative;
}
.results-area {
overflow-y: auto;
}
.search-header {
position: sticky;
top: 0;
background: white;
z-index: 10;
padding-bottom: 15px;
margin: -20px -20px 15px -20px;
padding: 20px 20px 15px 20px;
border-bottom: 2px solid #e9ecef;
border-radius: 15px 15px 0 0;
}
.search-sidebar h4 {
color: #667eea;
margin-bottom: 15px;
font-size: 1.1em;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.wish-position-box {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 10px;
padding: 15px;
margin-top: 20px;
text-align: center;
}
.wish-position-box h4 {
color: #856404;
border: none;
margin-bottom: 10px;
font-size: 1em;
}
.wish-position-list {
background: white;
border-radius: 5px;
padding: 10px;
margin: 10px 0;
max-height: 150px;
overflow-y: auto;
}
.wish-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px;
border-bottom: 1px solid #e9ecef;
}
.wish-item:last-child {
border-bottom: none;
}
.wish-remove {
color: #dc3545;
cursor: pointer;
font-weight: bold;
padding: 2px 8px;
}
.wish-remove:hover {
background: #dc3545;
color: white;
border-radius: 3px;
}
.fuzzy-match {
background: #fff9e6;
border-left: 3px solid #ffc107;
}
.search-mode-toggle {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.search-mode-btn {
flex: 1;
padding: 8px;
border: 2px solid #dee2e6;
background: white;
border-radius: 8px;
cursor: pointer;
font-size: 0.9em;
}
.search-mode-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.gewerk-item label { cursor: pointer; flex: 1; }
.import-summary {
background: #e7f3ff;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
}
.no-results {
text-align: center;
padding: 60px 20px;
color: #6c757d;
}
.info-box {
background: #e7f3ff;
border-left: 4px solid #17a2b8;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
}
.duplicate-warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
}
.suggestions-box {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin-top: 10px;
display: none;
}
.suggestion-item {
padding: 8px;
margin: 5px 0;
background: white;
border-radius: 5px;
cursor: pointer;
}
.suggestion-item:hover {
background: #e7f3ff;
}
/* Tab Navigation */
.tab-navigation {
display: flex;
background: #f8f9fa;
border-bottom: 3px solid #dee2e6;
}
.tab-button {
flex: 1;
padding: 20px;
background: #f8f9fa;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-size: 16px;
font-weight: 600;
color: #495057;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.tab-button:hover {
background: #e9ecef;
color: #667eea;
}
.tab-button.active {
background: white;
color: #667eea;
border-bottom-color: #667eea;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* AI Assistant Styles */
.ai-container {
padding: 40px;
}
.ai-section {
background: white;
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
border: 2px solid #e9ecef;
}
.ai-section h3 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.5em;
display: flex;
align-items: center;
gap: 10px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.room-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.room-table th,
.room-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.room-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
.room-table tr:hover {
background: #f8f9fa;
}
.room-table input {
width: 100%;
padding: 8px;
border: 1px solid #dee2e6;
border-radius: 5px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.summary-item {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
text-align: center;
}
.summary-label {
font-size: 0.9em;
color: #6c757d;
margin-bottom: 5px;
}
.summary-value {
font-size: 1.8em;
font-weight: bold;
color: #667eea;
}
.prompt-output {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
margin-top: 20px;
border: 2px solid #dee2e6;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📋 Intelligente Angebotsdatenbank</h1>
<p>STLB-Bau Gewerke | DIN 276 (3 Ebenen) | Automatische Sortierung & Duplikat-Prüfung</p>
</div>
<!-- Tab Navigation -->
<div class="tab-navigation">
<button class="tab-button active" onclick="switchTab('database')">
<span>📊</span>
<span>Datenbank</span>
</button>
<button class="tab-button" onclick="switchTab('ai-assistant')">
<span>🤖</span>
<span>AI-Assistent</span>
</button>
</div>
<!-- Database Tab Content -->
<div id="database-tab" class="tab-content active">
<div class="toolbar">
<button class="btn btn-primary" onclick="openAddModal()"> Neue Position</button>
<button class="btn btn-success" onclick="exportData()">💾 JSON Export</button>
<button class="btn btn-info" onclick="openImportModal()">📂 JSON Import</button>
<button class="btn btn-warning" onclick="showHelp()">❓ Sortier-Hilfe</button>
<button class="btn btn-danger" onclick="clearDatabase()">🗑️ Datenbank leeren</button>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-number" id="totalPositions">0</div>
<div class="stat-label">Positionen</div>
</div>
<div class="stat-item">
<div class="stat-number" id="visiblePositions">0</div>
<div class="stat-label">Angezeigt</div>
</div>
<div class="stat-item">
<div class="stat-number" id="stlbCount">0</div>
<div class="stat-label">STLB Gewerke</div>
</div>
<div class="stat-item">
<div class="stat-number" id="din1Count">0</div>
<div class="stat-label">DIN Gruppen</div>
</div>
</div>
<div class="database-layout">
<!-- Linke Sidebar: Suchfilter -->
<div class="search-sidebar">
<!-- Sticky Header mit Suche -->
<div class="search-header">
<h4 style="margin: 0 0 15px 0; border: none; padding: 0;">🔍 Suche & Filter</h4>
<!-- Suchmodus-Umschalter -->
<div class="search-mode-toggle">
<button class="search-mode-btn active" onclick="setSearchMode('exact')" id="searchModeExact">
Exakt
</button>
<button class="search-mode-btn" onclick="setSearchMode('fuzzy')" id="searchModeFuzzy">
Ähnlich
</button>
</div>
<!-- Suchfeld -->
<div class="search-box" style="margin-bottom: 0;">
<input type="text" id="searchInput" placeholder="Volltextsuche..." style="width: 100%;">
</div>
</div>
<!-- Scrollbarer Bereich -->
<!-- Wunschpositionen-Box (wird angezeigt wenn keine Ergebnisse) -->
<div class="wish-position-box" id="wishPositionBox" style="display: none;">
<h4>💡 Nicht gefunden?</h4>
<p style="font-size: 0.9em; margin-bottom: 10px;">Position vormerken für AI-Manager:</p>
<!-- Positionstyp Dropdown -->
<div style="margin-bottom: 10px;">
<label style="font-size: 0.85em; font-weight: bold; display: block; margin-bottom: 4px;">Positionstyp:</label>
<select id="positionTypeSelect" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 5px; font-size: 0.9em;">
<option value="Neueinbau">Neueinbau</option>
<option value="Sanierung">Sanierung</option>
<option value="Demontage">Demontage</option>
<option value="Wiedereinbau">Wiedereinbau</option>
<option value="Ersatz">Ersatz</option>
</select>
</div>
<!-- Bemerkungen -->
<div style="margin-bottom: 10px;">
<label style="font-size: 0.85em; font-weight: bold; display: block; margin-bottom: 4px;">Bemerkungen (optional):</label>
<input type="text" id="positionRemarksInput" placeholder="z.B. wandhängend, mit Untergestell, ..." style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 5px; font-size: 0.9em;">
</div>
<button class="btn btn-warning" onclick="addToWishList()" style="width: 100%; margin-bottom: 10px;">
Zur Wunschliste
</button>
<div style="font-size: 0.8em; color: #666; margin-bottom: 8px; padding: 0 5px;">
<strong>Checkbox:</strong> Position ist ein Paket (wird in Einzelpositionen aufgeteilt)
</div>
<!-- SchadenProf Kontext-Bereich (nur sichtbar wenn Import vorhanden) -->
<div id="wishlistSchadenprofContext" style="display: none; background: #e8f5e9; border-left: 4px solid #4caf50; padding: 10px; margin-bottom: 15px; border-radius: 4px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<strong style="font-size: 0.9em; color: #2e7d32;">🏠 SchadenProf-Kontext verfügbar</strong>
<button onclick="toggleWishlistSchadenprofDetails()" id="toggleWishlistSchadenprofBtn" style="padding: 2px 8px; font-size: 0.8em; background: #4caf50; color: white; border: none; border-radius: 3px; cursor: pointer;">
Details ▼
</button>
</div>
<div id="wishlistSchadenprofDetails" style="display: none; font-size: 0.85em; color: #2e7d32;">
<!-- Wird dynamisch befüllt -->
</div>
</div>
<div id="wishList" class="wish-position-list" style="display: none;"></div>
<button class="btn btn-success" onclick="sendWishListToAI()" id="sendWishBtn" style="display: none; width: 100%; font-size: 0.9em;">
🤖 An AI-Manager senden
</button>
<button class="btn btn-primary" onclick="createTemplateFromWishList()" id="saveTemplateBtn" style="display: none; width: 100%; font-size: 0.9em; margin-top: 8px;">
💾 Als Template speichern
</button>
</div>
<!-- STLB Filter -->
<div class="filter-section">
<label class="filter-label">STLB-Bau Gewerk:</label>
<div class="filter-buttons" id="stlbFilters"></div>
</div>
<!-- DIN 276 Filter -->
<div class="filter-section">
<label class="filter-label">DIN 276 Ebene 1:</label>
<div class="filter-buttons" id="din1Filters"></div>
</div>
<div class="filter-section">
<label class="filter-label">DIN 276 Ebene 2:</label>
<div class="filter-buttons" id="din2Filters"></div>
</div>
<div class="filter-section">
<label class="filter-label">DIN 276 Ebene 3:</label>
<div class="filter-buttons" id="din3Filters"></div>
</div>
</div>
<!-- Rechte Seite: Ergebnisse -->
<div class="results-area">
<div class="content" id="content">
<div class="no-results">
<div style="font-size: 4em; margin-bottom: 20px;">📦</div>
<h2>Datenbank ist leer</h2>
<p>Klicken Sie auf "JSON Import" um Positionen zu laden oder erstellen Sie neue Positionen.</p>
</div>
</div>
</div>
</div>
</div>
<!-- End Database Tab -->
<!-- AI Assistant Tab Content -->
<div id="ai-assistant-tab" class="tab-content">
<div class="ai-container">
<!-- Projekt-Metadaten -->
<div class="ai-section">
<h3><span>📋</span> Projekt-Metadaten</h3>
<!-- SchadenProv Import Button -->
<div style="margin-bottom: 1rem; padding: 1rem; background: #e8f5e9; border-left: 4px solid #4caf50; border-radius: 4px;">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem;">
<button class="btn btn-success" onclick="document.getElementById('schadenprov-import-input').click()">
📥 SchadenProv Export importieren
</button>
<input type="file" id="schadenprov-import-input" accept=".json" style="display: none;" onchange="handleSchadenProvImport(event)">
<span style="font-size: 0.9rem; color: #2e7d32;">
<strong>Tipp:</strong> Exportieren Sie einen Schadenfall aus SchadenProv v2 und importieren Sie ihn hier für automatische Datenbefüllung.
</span>
</div>
</div>
<!-- Import-Analyse Bereich (initially hidden) -->
<div id="import-analysis-section" style="display: none; margin-bottom: 1.5rem;">
<!-- Importierte Daten Anzeige -->
<div style="margin-bottom: 1rem; padding: 1rem; background: #f5f5f5; border-radius: 4px; border-left: 4px solid #2196f3;">
<div style="display: flex; justify-content: space-between; align-items: center; cursor: pointer;" onclick="toggleSection('imported-data-content')">
<h4 style="margin: 0; color: #1976d2;">📋 Importierte Daten</h4>
<span id="imported-data-toggle"></span>
</div>
<div id="imported-data-content" style="display: none; margin-top: 1rem;">
<div id="imported-data-display" style="font-size: 0.9rem; line-height: 1.6;"></div>
</div>
</div>
<!-- Intelligente Analyse -->
<div id="intelligent-analysis" style="margin-bottom: 1rem; padding: 1rem; background: #fff3e0; border-radius: 4px; border-left: 4px solid #ff9800;">
<h4 style="margin: 0 0 0.5rem 0; color: #e65100;">🧠 Intelligente Analyse</h4>
<div id="analysis-results" style="font-size: 0.9rem;"></div>
</div>
<!-- Zusätzliche Informationen (dynamisch) -->
<div id="additional-info-section" style="margin-bottom: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px; border-left: 4px solid #1976d2;">
<h4 style="margin: 0 0 1rem 0; color: #0d47a1;">📝 Zusätzliche Informationen</h4>
<div id="additional-fields-container"></div>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label class="filter-label">Projektname *</label>
<input type="text" id="ai-projektname" class="form-control" placeholder="z.B. Sanierung MFH Musterstraße 123">
</div>
<div class="form-group">
<label class="filter-label">Schadensart *</label>
<select id="ai-schadensart" class="form-control">
<option value="">-- Bitte wählen --</option>
<option value="Fäkalwasserschaden">Fäkalwasserschaden</option>
<option value="Leitungswasserschaden">Leitungswasserschaden</option>
<option value="Brandschaden">Brandschaden</option>
<option value="Sturmschaden">Sturmschaden</option>
<option value="Schimmelschaden">Schimmelschaden</option>
<option value="Vandalismus">Vandalismus</option>
<option value="Modernisierung">Modernisierung</option>
<option value="Sonstiges">Sonstiges</option>
</select>
</div>
</div>
</div>
<!-- Raum-Manager -->
<div class="ai-section">
<h3><span>🏠</span> Raum-Manager</h3>
<table class="room-table" id="roomTable">
<thead>
<tr>
<th>Raum-Bezeichnung</th>
<th>Fläche (m²)</th>
<th>Höhe (m)</th>
<th>Umfang (m)</th>
<th>Anteil (%)</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="roomTableBody">
<tr class="no-results">
<td colspan="6" style="text-align: center; padding: 30px;">
Noch keine Räume hinzugefügt. Klicken Sie auf "Raum hinzufügen" um zu starten.
</td>
</tr>
</tbody>
</table>
<div class="btn-group">
<button class="btn btn-primary" onclick="addRoom()"> Raum hinzufügen</button>
<button class="btn btn-info" onclick="loadTemplate()">📁 Template laden</button>
<button class="btn btn-success" onclick="saveTemplate()">💾 Template speichern</button>
<button class="btn btn-warning" onclick="clearRooms()">🗑️ Alle Räume löschen</button>
</div>
</div>
<!-- Mengenübersicht -->
<div class="ai-section">
<h3><span>📊</span> Automatische Mengenberechnung</h3>
<div class="summary-grid">
<div class="summary-item">
<div class="summary-label">Bodenfläche gesamt</div>
<div class="summary-value" id="sum-boden">0.00 m²</div>
</div>
<div class="summary-item">
<div class="summary-label">Wandfläche gesamt</div>
<div class="summary-value" id="sum-wand">0.00 m²</div>
</div>
<div class="summary-item">
<div class="summary-label">Deckenfläche gesamt</div>
<div class="summary-value" id="sum-decke">0.00 m²</div>
</div>
<div class="summary-item">
<div class="summary-label">Sockelleiste gesamt</div>
<div class="summary-value" id="sum-sockel">0.00 m</div>
</div>
<div class="summary-item">
<div class="summary-label">Schutt/Entsorgung</div>
<div class="summary-value" id="sum-schutt">0.00 m³</div>
</div>
<div class="summary-item">
<div class="summary-label">Anzahl Räume</div>
<div class="summary-value" id="sum-rooms">0</div>
</div>
</div>
</div>
<!-- Prompt Generator -->
<div class="ai-section">
<h3><span>🤖</span> Claude-Prompt generieren</h3>
<p style="color: #6c757d; margin-bottom: 20px;">
Generieren Sie einen strukturierten Prompt für Claude, um automatisch passende Positionen
aus der Datenbank zu finden und ein vollständiges Leistungsverzeichnis zu erstellen.
</p>
<div class="btn-group">
<button class="btn btn-primary" onclick="generatePrompt()" style="font-size: 16px; padding: 15px 30px;">
🤖 Prompt generieren & in Zwischenablage kopieren
</button>
</div>
<div id="promptPreview" class="prompt-output" style="display: none;"></div>
</div>
</div>
</div>
<!-- End AI Assistant Tab -->
</div>
<!-- Modal für Position hinzufügen/bearbeiten -->
<div class="modal" id="positionModal">
<div class="modal-content">
<h2 id="modalTitle">Neue Position</h2>
<div id="duplicateWarning" style="display:none;" class="duplicate-warning">
<strong>⚠️ Mögliche Duplikate gefunden:</strong>
<div id="duplicateList"></div>
</div>
<form id="positionForm" onsubmit="savePosition(event)">
<input type="hidden" id="positionId">
<div class="form-group">
<label>Begriff *</label>
<input type="text" class="form-control" id="begriff" required oninput="checkDuplicates()">
</div>
<div class="form-group">
<label>Erklärung *</label>
<textarea class="form-control" id="erklaerung" required></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Einheit *</label>
<select class="form-control" id="einheit" required>
<option value="Stück">Stück</option>
<option value="m²"></option>
<option value="m³"></option>
<option value="lfm">lfm</option>
<option value="m">m</option>
<option value="pauschal">pauschal</option>
<option value="kg">kg</option>
<option value="t">t</option>
<option value="Std.">Std.</option>
</select>
</div>
<div class="form-group">
<label>Preis (€) *</label>
<input type="number" class="form-control" id="preis" step="0.01" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>STLB-Bau Gewerk *</label>
<select class="form-control" id="stlb" required onchange="updateLogicalFilters()"></select>
</div>
<div class="form-group">
<label>DIN 276 Ebene 1 (Kostengruppe) *</label>
<select class="form-control" id="din1" required onchange="updateDin2Options()"></select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>DIN 276 Ebene 2 *</label>
<select class="form-control" id="din2" required onchange="updateDin3Options()"></select>
</div>
<div class="form-group">
<label>DIN 276 Ebene 3 (optional)</label>
<select class="form-control" id="din3"></select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" onclick="closeModal()">Abbrechen</button>
<button type="submit" class="btn btn-success">Speichern</button>
</div>
</form>
</div>
</div>
<!-- Modal für Import mit Gewerk-Auswahl -->
<div class="modal" id="importModal">
<div class="modal-content">
<h2>JSON Import mit Gewerk-Auswahl</h2>
<p style="margin-bottom: 20px; color: #6c757d;">
Wählen Sie zuerst eine JSON-Datei aus, dann können Sie die zu importierenden Gewerke auswählen.
</p>
<div class="form-group">
<label>JSON-Datei auswählen:</label>
<input type="file" class="form-control" id="importFileSelect" accept=".json" onchange="analyzeImportFile(event)">
</div>
<div id="gewerkSelection" style="display:none;">
<h3 style="margin: 20px 0 10px 0;">Verfügbare Gewerke:</h3>
<div class="gewerk-list" id="gewerkList"></div>
<div class="import-summary" id="importSummary"></div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" onclick="closeImportModal()">Abbrechen</button>
<button type="button" class="btn btn-success" onclick="executeImport()">Ausgewählte Gewerke importieren</button>
</div>
</div>
</div>
</div>
<!-- Modal für Sortier-Hilfe -->
<div class="modal" id="helpModal">
<div class="modal-content">
<h2>📚 Sortier-Hilfe: Wie ordne ich Positionen richtig ein?</h2>
<div id="helpContent"></div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="closeHelpModal()">Schließen</button>
</div>
</div>
</div>
<script>
// ============================================================================
// DATENBANK-STRUKTUR: STLB-BAU & DIN 276
// ============================================================================
/**
* STLB-Bau Gewerke - Vollständige Liste
* Quelle: Standardleistungsbuch für das Bauwesen
*
* Verwendung: Kategorisierung nach Handwerks-/Fachgewerken
* Beispiel: 150 = Mauerarbeiten, 310 = Malerarbeiten
*/
const STLB_GEWERKE = {
'000': 'Baustelleneinrichtung',
'001': 'Gerüstarbeiten',
'002': 'Erdarbeiten',
'003': 'Landschaftsbauarbeiten',
'004': 'Pflanzarbeiten',
'005': 'Brunnen- und Aufschlussbohrungen',
'006': 'Verbau-, Ramm- und Einpressarbeiten',
'008': 'Wasserhaltungsarbeiten',
'009': 'Entwässerungsarbeiten',
'010': 'Dränarbeiten',
'011': 'Abscheider, Kläranlagen',
'012': 'Mauerarbeiten',
'013': 'Betonarbeiten',
'014': 'Naturstein-, Betonwerksteinarbeiten',
'016': 'Zimmer-, Holzbauarbeiten',
'017': 'Stahlbauarbeiten',
'018': 'Abdichtungsarbeiten gegen Wasser',
'019': 'Abbrucharbeiten',
'020': 'Dachdeckungsarbeiten',
'021': 'Dachabdichtungsarbeiten',
'022': 'Klempnerarbeiten',
'023': 'Putz-, Stuckarbeiten',
'024': 'Fliesen- und Plattenarbeiten',
'025': 'Estricharbeiten',
'027': 'Tischlerarbeiten',
'028': 'Parkett-, Holzpflasterarbeiten',
'029': 'Beschlagarbeiten',
'033': 'Gebäudereinigung',
'034': 'Maler-, Lackierarbeiten',
'035': 'Korrosionsschutzarbeiten',
'036': 'Bodenbelagarbeiten',
'037': 'Tapezierarbeiten',
'039': 'Trockenbauarbeiten',
'040': 'Wärmeerzeuger',
'041': 'Heizflächen und Rohrleitungen',
'042': 'Gas- und Wasserinstallation',
'044': 'Abwasserinstallation',
'045': 'Sanitäre Einrichtungsgegenstände',
'046': 'Betriebseinrichtungen Gas/Wasser',
'047': 'Wärme- und Kältedämmarbeiten',
'049': 'Feuerlöschanlagen und -geräte',
'053': 'Niederspannungsanlagen',
'058': 'Leuchten und Lampen',
'060': 'Sprechanlagen, Elektroakustik',
'061': 'Fernmeldeleitungsanlagen',
'063': 'Meldeanlagen',
'065': 'Empfangsantennenanlagen',
'069': 'Aufzüge, Parksysteme, Rolltreppen',
'080': 'Straßen, Wege, Plätze',
'084': 'Abbrucharbeiten Verkehrsanlagen',
'096': 'Wintergartenanlagen'
};
/**
* DIN 276 - Kosten im Bauwesen - Vollständige 3-Ebenen-Struktur
* Quelle: DIN 276-1:2018-12
*
* STRUKTUR & LOGIK:
* - Ebene 1: Hauptkostengruppen (100-700)
* - Ebene 2: Kostengruppen
* - Ebene 3: Detaillierte Einzelkosten
*
* WICHTIG: Die DIN 276 kategorisiert nach FUNKTIONALEN Aspekten des Bauwerks,
* während STLB-Bau nach GEWERKEN kategorisiert!
*
* Beispiel: Ein Elektriker (STLB 440) kann in verschiedenen DIN-Gruppen arbeiten:
* - DIN 440 (Starkstrom)
* - DIN 450 (Fernmelde-/IT-Anlagen)
* - DIN 480 (Gebäudeautomation)
*/
const DIN_276 = {
'100': {
name: 'Grundstück',
description: 'Kosten für Grunderwerb und Grundstücksnebenkosten',
level2: {
'110': {
name: 'Grundstückswert',
description: 'Kaufpreis des unbebauten Grundstücks',
level3: {}
},
'120': {
name: 'Grundstücksnebenkosten',
description: 'Notarkosten, Grunderwerbsteuer, Maklergebühren',
level3: {
'121': 'Vermessung',
'122': 'Grunderwerbsteuer',
'123': 'Gerichts- und Notarkosten'
}
},
'130': {
name: 'Freimachen',
description: 'Abriss, Rodung, Altlastenbeseitigung',
level3: {
'131': 'Gebäudeabriss',
'132': 'Rodung und Vegetation',
'133': 'Altlastenbeseitigung'
}
}
}
},
'200': {
name: 'Herrichten und Erschließen',
description: 'Vorbereitung des Baugrunds und Erschließung',
level2: {
'210': {
name: 'Herrichten',
description: 'Vorbereitung des Baugrundstücks',
level3: {
'211': 'Sicherungsmaßnahmen',
'212': 'Abbruchmaßnahmen',
'213': 'Altlastenbeseitigung',
'214': 'Herrichten der Geländeoberfläche'
}
},
'220': {
name: 'Öffentliche Erschließung',
description: 'Anschluss an öffentliche Infrastruktur',
level3: {
'221': 'Abwasserentsorgung',
'222': 'Wasserversorgung',
'223': 'Gasversorgung',
'224': 'Wärmeversorgung',
'225': 'Elektrizitätsversorgung',
'226': 'Telekommunikation'
}
},
'230': {
name: 'Nichtöffentliche Erschließung',
description: 'Private Erschließungsanlagen',
level3: {
'231': 'Abwasserentsorgung',
'232': 'Wasserversorgung',
'233': 'Gasversorgung',
'234': 'Wärmeversorgung',
'235': 'Elektrizitätsversorgung',
'236': 'Telekommunikation'
}
}
}
},
'300': {
name: 'Bauwerk - Baukonstruktionen',
description: 'Alle konstruktiven Bauteile des Gebäudes',
level2: {
'310': {
name: 'Baugrube, Erdbau',
description: 'Erdarbeiten und Baugrubenherstellung',
level3: {
'311': 'Baugrube / Verbau',
'312': 'Erdarbeiten / Aushub',
'313': 'Wasserhaltung',
'314': 'Verbauten',
'315': 'Bodenverbesserung'
}
},
'320': {
name: 'Gründung, Unterbau',
description: 'Fundamente und tragende Untergeschosse',
level3: {
'321': 'Baugrundverbesserung',
'322': 'Flachgründungen / Fundamente',
'323': 'Tiefgründungen / Pfähle',
'324': 'Bodenplatten',
'325': 'Abdichtung gegen Bodenfeuchtigkeit und Wasser',
'326': 'Dränagen'
}
},
'330': {
name: 'Außenwände',
description: 'Alle Außenwandkonstruktionen',
level3: {
'331': 'Tragende Außenwände',
'332': 'Nichttragende Außenwände / Fassaden',
'333': 'Außenwandöffnungen / Fenster',
'334': 'Außentüren und -tore',
'335': 'Außenwandbekleidungen außen',
'336': 'Außenwandbekleidungen innen',
'337': 'Elementierte Außenwände / Vorhangfassaden',
'338': 'Sonnenschutz'
}
},
'340': {
name: 'Innenwände',
description: 'Alle Innenwandkonstruktionen',
level3: {
'341': 'Tragende Innenwände',
'342': 'Nichttragende Innenwände / Trennwände',
'343': 'Innenwandbekleidungen',
'344': 'Innentüren und -fenster',
'345': 'Innenwandöffnungen'
}
},
'350': {
name: 'Decken',
description: 'Alle Deckenkonstruktionen',
level3: {
'351': 'Deckenkonstruktionen',
'352': 'Deckenbeläge',
'353': 'Deckenbekleidungen',
'354': 'Deckenöffnungen'
}
},
'360': {
name: 'Dächer',
description: 'Dachkonstruktionen und Dachbeläge',
level3: {
'361': 'Dachkonstruktionen',
'362': 'Dachfenster, Dachöffnungen',
'363': 'Dachbeläge / Abdichtungen',
'364': 'Dachbekleidungen außen',
'365': 'Dachbekleidungen innen'
}
},
'370': {
name: 'Baukonstruktive Einbauten',
description: 'Fest eingebaute Konstruktionen',
level3: {
'371': 'Allgemeine Einbauten',
'372': 'Besondere Einbauten',
'373': 'Lichtschutz, Sichtschutz',
'374': 'Lüftungseinbauten',
'375': 'Schmutzfänger'
}
},
'390': {
name: 'Sonstige Maßnahmen für Baukonstruktionen',
description: 'Zusätzliche bauliche Maßnahmen',
level3: {
'391': 'Baustelleneinrichtung',
'392': 'Gerüste',
'393': 'Sicherungsmaßnahmen',
'394': 'Abbruchmaßnahmen'
}
}
}
},
'400': {
name: 'Bauwerk - Technische Anlagen',
description: 'Alle gebäudetechnischen Installationen',
level2: {
'410': {
name: 'Abwasser-, Wasser-, Gasanlagen',
description: 'Sanitäre Grundinstallationen',
level3: {
'411': 'Abwasseranlagen',
'412': 'Wasseranlagen',
'413': 'Gasanlagen',
'414': 'Feuerlöschanlagen (Wasser)'
}
},
'420': {
name: 'Wärmeversorgungsanlagen',
description: 'Heizung und Wärmeverteilung',
level3: {
'421': 'Wärmeerzeugungsanlagen',
'422': 'Wärmeverteilnetze',
'423': 'Raumheizflächen',
'424': 'Verkehrsheizflächen'
}
},
'430': {
name: 'Lufttechnische Anlagen',
description: 'Lüftung und Klimatisierung',
level3: {
'431': 'Lüftungsanlagen',
'432': 'Teilklimaanlagen',
'433': 'Klimaanlagen',
'434': 'Kälteanlagen',
'435': 'Prozesskälteanlagen'
}
},
'440': {
name: 'Starkstromanlagen',
description: 'Elektrische Energieversorgung',
level3: {
'441': 'Hoch-/Mittelspannungsanlagen',
'442': 'Eigenstromversorgungsanlagen',
'443': 'Niederspannungsschaltanlagen',
'444': 'Niederspannungsinstallationsanlagen',
'445': 'Beleuchtungsanlagen',
'446': 'Blitzschutz- und Erdungsanlagen'
}
},
'450': {
name: 'Fernmelde- und informationstechnische Anlagen',
description: 'Kommunikation und Datentechnik',
level3: {
'451': 'Telekommunikationsanlagen',
'452': 'Such- und Signalanlagen',
'453': 'Zeitdienstanlagen',
'454': 'Elektroakustische Anlagen',
'455': 'Fernseh- und Antennenanlagen',
'456': 'Gefahrenmelde- und Alarmanlagen',
'457': 'Übertragungsnetze',
'458': 'Verkehrsbeeinflussungsanlagen'
}
},
'460': {
name: 'Förderanlagen',
description: 'Aufzüge und Transporteinrichtungen',
level3: {
'461': 'Aufzugsanlagen',
'462': 'Fahrtreppen, Fahrsteige',
'463': 'Befahranlagen',
'464': 'Transportanlagen',
'465': 'Krananlagen'
}
},
'470': {
name: 'Nutzungsspezifische Anlagen',
description: 'Spezielle technische Anlagen je Nutzung',
level3: {
'471': 'Küchentechnische Anlagen',
'472': 'Wäscherei­technische Anlagen',
'473': 'Medienversorgungsanlagen',
'474': 'Feuerlöschanlagen (nicht Wasser)',
'475': 'Prozesswärme- und -kälteanlagen',
'476': 'Weitere nutzungsspezifische Anlagen'
}
},
'480': {
name: 'Gebäudeautomation',
description: 'Automatisierung und Steuerung',
level3: {
'481': 'Automationseinrichtungen',
'482': 'Schaltschränke, Automationsschwerpunkte',
'483': 'Automationsmanagement'
}
},
'490': {
name: 'Sonstige Maßnahmen für technische Anlagen',
description: 'Zusätzliche technische Maßnahmen',
level3: {
'491': 'Abnahmen, Prüfungen',
'492': 'Einregulierung, Inbetriebsetzung'
}
}
}
},
'500': {
name: 'Außenanlagen und Freiflächen',
description: 'Alle Flächen außerhalb des Gebäudes',
level2: {
'510': {
name: 'Geländeflächen',
description: 'Unversiegelte Außenflächen',
level3: {
'511': 'Geländemodellierung',
'512': 'Oberbodenabtrag und -auftrag',
'513': 'Vegetationsflächen',
'514': 'Pflanzungen'
}
},
'520': {
name: 'Befestigte Flächen',
description: 'Versiegelte Außenflächen',
level3: {
'521': 'Wege',
'522': 'Straßen',
'523': 'Plätze',
'524': 'Stellplätze',
'525': 'Sportplätze'
}
},
'530': {
name: 'Baukonstruktionen in Außenanlagen',
description: 'Bauliche Anlagen im Außenbereich',
level3: {
'531': 'Einfriedungen',
'532': 'Stützmauern',
'533': 'Lärmschutzwände',
'534': 'Rampen, Treppen',
'535': 'Überdachungen'
}
},
'540': {
name: 'Technische Anlagen in Außenanlagen',
description: 'Technische Installationen im Außenbereich',
level3: {
'541': 'Abwasseranlagen',
'542': 'Wasseranlagen',
'543': 'Gasanlagen',
'544': 'Wärmeversorgungsanlagen',
'545': 'Lufttechnische Anlagen',
'546': 'Starkstromanlagen',
'547': 'Fernmelde- und IT-Anlagen',
'548': 'Verkehrsanlagen'
}
},
'550': {
name: 'Einbauten in Außenanlagen',
description: 'Fest installierte Außeneinrichtungen',
level3: {
'551': 'Allgemeine Einbauten',
'552': 'Besondere Einbauten',
'553': 'Wasserbecken',
'554': 'Spiel- und Sportgeräte',
'555': 'Stadtmöblierung'
}
},
'590': {
name: 'Sonstige Außenanlagen',
description: 'Weitere Außenanlagen',
level3: {}
}
}
},
'600': {
name: 'Ausstattung und Kunstwerke',
description: 'Einrichtung und künstlerische Gestaltung',
level2: {
'610': {
name: 'Ausstattung',
description: 'Nutzungsspezifische Einrichtung',
level3: {
'611': 'Allgemeine Ausstattung',
'612': 'Besondere Ausstattung',
'613': 'Informations- und Leitsysteme'
}
},
'620': {
name: 'Kunstwerke',
description: 'Kunst am Bau',
level3: {
'621': 'Kunstwerke',
'622': 'Künstlerisch gestaltete Bauteile'
}
},
'690': {
name: 'Sonstige Ausstattung',
description: 'Weitere Ausstattungen',
level3: {}
}
}
},
'700': {
name: 'Baunebenkosten',
description: 'Planungs-, Genehmigungs- und Verwaltungskosten',
level2: {
'710': {
name: 'Bauherrenaufgaben',
description: 'Kosten für Projektsteuerung und Management',
level3: {
'711': 'Projektleitung',
'712': 'Projektsteuerung',
'713': 'Fachliche Bauherrenberatung'
}
},
'720': {
name: 'Vorbereitung der Objektplanung',
description: 'Vorplanungsleistungen',
level3: {
'721': 'Untersuchungen',
'722': 'Wertermittlungen',
'723': 'Städtebauliche Leistungen',
'724': 'Landschaftsplanung'
}
},
'730': {
name: 'Architekten- und Ingenieurleistungen',
description: 'Honorare für Planer',
level3: {
'731': 'Gebäude und Innenräume',
'732': 'Freianlagen',
'733': 'Ingenieurbauwerke',
'734': 'Verkehrsanlagen',
'735': 'Tragwerksplanung',
'736': 'Technische Ausrüstung'
}
},
'740': {
name: 'Gutachten und Beratung',
description: 'Sachverständigenkosten',
level3: {
'741': 'Geotechnische Untersuchungen',
'742': 'Bauphysikalische Beratung',
'743': 'Schadstoffgutachten',
'744': 'Vermessung',
'745': 'Sonstige Fachgutachten'
}
},
'750': {
name: 'Künstlerische Leistungen',
description: 'Honorare für Künstler',
level3: {}
},
'760': {
name: 'Finanzierungskosten',
description: 'Zinsen und Finanzierungsnebenkosten',
level3: {
'761': 'Beschaffungsnebenkosten',
'762': 'Finanzierungskosten'
}
},
'770': {
name: 'Allgemeine Baunebenkosten',
description: 'Verwaltungs- und Genehmigungskosten',
level3: {
'771': 'Gebrauchsabnahme, Prüfungen',
'772': 'Genehmigungsgebühren',
'773': 'Versicherungen, Kautionen',
'774': 'Allgemeine Verwaltungskosten'
}
},
'790': {
name: 'Sonstige Baunebenkosten',
description: 'Weitere Nebenkosten',
level3: {}
}
}
}
};
/**
* MAPPING: STLB zu DIN 276
* Definiert logische Zusammenhänge zwischen Gewerken und Kostengruppen
*
* WICHTIG: Ein STLB-Gewerk kann MEHRERE DIN-Gruppen bedienen!
* Beispiel: Elektriker (STLB 440) arbeitet in DIN 440, 450, 480
*
* Diese Mappings werden für intelligente Vorschläge verwendet
*/
const STLB_TO_DIN_MAPPING = {
// ============================================================================
// VOLLSTÄNDIGES MAPPING: ALLE 55 STLB-Gewerke → DIN 276 Hauptkostengruppen (Ebene 1)
// ============================================================================
// WICHTIG: Mapping verwendet DIN 276 EBENE 1 Codes (100, 200, 300, 400, 500, 600, 700)
// Diese werden in fillDropdowns() als option.value gesetzt!
// ============================================================================
// Allgemeine / Organisatorische Gewerke (010-130)
'010': ['700', '300'], // Allgemeine Einrichtungen → Baunebenkosten, Baukonstruktionen
'020': ['200'], // Herrichten und Erschließen → Herrichten und Erschließen
'030': ['300'], // Baustelleneinrichtung → Baukonstruktionen
'040': ['100', '200'], // Rückbau und Entsorgung → Grundstück, Herrichten
'050': ['700'], // Überwachung → Baunebenkosten
'130': ['100', '200'], // Abbruch und Rückbau → Grundstück, Herrichten
// Technische Anlagen - Sanitär/Heizung/Klima (060-070, 510-550)
'060': ['400'], // Wasser/Abwasser → Bauwerk - Technische Anlagen
'070': ['400'], // Wärmeversorgung → Bauwerk - Technische Anlagen
'510': ['400'], // Heizungsanlagen → Bauwerk - Technische Anlagen
'520': ['400'], // Gas, Wasser, Abwasser → Bauwerk - Technische Anlagen
'530': ['400'], // Lufttechnische Anlagen → Bauwerk - Technische Anlagen
'540': ['400'], // Kälteanlagen → Bauwerk - Technische Anlagen
'550': ['400'], // Prozesswärme/-kälte → Bauwerk - Technische Anlagen
// Technische Anlagen - Elektro/IT (080-120, 440-500)
'080': ['400'], // Nieder-/Mittelspannung → Bauwerk - Technische Anlagen
'090': ['400'], // Fernmelde-/IT-Anlagen → Bauwerk - Technische Anlagen
'100': ['400'], // Förderanlagen → Bauwerk - Technische Anlagen
'110': ['400'], // Nutzungsspez. Anlagen → Bauwerk - Technische Anlagen
'120': ['400'], // Gebäudeautomation → Bauwerk - Technische Anlagen
'440': ['400'], // Elektroinstallation → Bauwerk - Technische Anlagen
'450': ['400'], // Kommunikations-/Sicherheitsanlagen → Bauwerk - Technische Anlagen
'460': ['400'], // Aufzüge → Bauwerk - Technische Anlagen
'470': ['400'], // Sicherheitstechnik → Bauwerk - Technische Anlagen
'480': ['400'], // MSR-Technik / Gebäudeautomation → Bauwerk - Technische Anlagen
'490': ['400'], // Elektromobilität → Bauwerk - Technische Anlagen
'500': ['400'], // Photovoltaik → Bauwerk - Technische Anlagen
// Rohbau & Gründung (140-190)
'140': ['300'], // Erd-/Grundbau → Bauwerk - Baukonstruktionen
'150': ['300'], // Mauerarbeiten → Bauwerk - Baukonstruktionen
'160': ['300'], // Putzarbeiten → Bauwerk - Baukonstruktionen
'170': ['300'], // Betonarbeiten → Bauwerk - Baukonstruktionen
'180': ['300'], // Stahlbetonarbeiten → Bauwerk - Baukonstruktionen
'190': ['300'], // Abdichtungsarbeiten → Bauwerk - Baukonstruktionen
// Dach & Fassade (200-240)
'200': ['300'], // Zimmer-/Holzbau → Bauwerk - Baukonstruktionen
'210': ['300'], // Dachdeckung/Dachabdichtung → Bauwerk - Baukonstruktionen
'220': ['300'], // Klempnerarbeiten → Bauwerk - Baukonstruktionen
'230': ['300'], // Metallbau → Bauwerk - Baukonstruktionen
'240': ['300'], // Schlosserarbeiten → Bauwerk - Baukonstruktionen
// Ausbau (250-390)
'250': ['300'], // Trockenbau → Bauwerk - Baukonstruktionen
'260': ['300'], // Estrich → Bauwerk - Baukonstruktionen
'270': ['300'], // Bodenbeläge → Bauwerk - Baukonstruktionen
'280': ['300'], // Fliesen/Platten → Bauwerk - Baukonstruktionen
'290': ['300'], // Naturwerkstein → Bauwerk - Baukonstruktionen
'300': ['300'], // Betonwerkstein → Bauwerk - Baukonstruktionen
'310': ['300'], // Malerarbeiten → Bauwerk - Baukonstruktionen
'320': ['300'], // Tapezierarbeiten → Bauwerk - Baukonstruktionen
'330': ['300', '600'], // Tischlerarbeiten → Baukonstruktionen, Ausstattung
'340': ['300'], // Glaserarbeiten → Bauwerk - Baukonstruktionen
'350': ['300'], // Estrich / Heizestrich → Bauwerk - Baukonstruktionen
'360': ['300'], // Parkett/Holzpflaster → Bauwerk - Baukonstruktionen
'370': ['300'], // Bodenbeläge (Textile) → Bauwerk - Baukonstruktionen
'380': ['300'], // Rollladen/Sonnenschutz → Bauwerk - Baukonstruktionen
'390': ['700'], // Gebäudereinigung → Baunebenkosten
// Spezialarbeiten (400-430)
'400': ['300'], // Gerüstarbeiten → Bauwerk - Baukonstruktionen
'410': ['300'], // Baugrubenverbau → Bauwerk - Baukonstruktionen
'420': ['300'], // Brunnenbohrarbeiten → Bauwerk - Baukonstruktionen
'430': ['500'], // Landschaftsbau → Außenanlagen
};
// ============================================================================
// INDEXEDDB DATENBANK
// ============================================================================
let db;
const DB_NAME = 'AngebotsdatenbankDB';
const DB_VERSION = 2;
const STORE_NAME = 'positionen';
let currentFilters = { stlb: 'alle', din1: 'alle', din2: 'alle', din3: 'alle', search: '', searchMode: 'exact' };
let wishList = []; // Sammlung nicht gefundener Suchbegriffe: [{term: string, isPackage: boolean, type: string, remarks: string}]
let importFileData = null;
let allPositions = []; // Cache für Performance
let templates = []; // Template-Sammlung: [{id, name, description, category, categoryName, positionTerms, schadenprofData, created, lastUsed}]
// IndexedDB initialisieren
function initDB() {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => console.error('DB Fehler');
request.onsuccess = (event) => {
db = event.target.result;
console.log('DB geöffnet');
loadTemplates(); // Templates aus LocalStorage laden
renderFilters();
loadPositions();
};
request.onupgradeneeded = (event) => {
db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
const objectStore = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
objectStore.createIndex('begriff', 'begriff', { unique: false });
objectStore.createIndex('stlb', 'stlb', { unique: false });
objectStore.createIndex('din1', 'din1', { unique: false });
objectStore.createIndex('din2', 'din2', { unique: false });
objectStore.createIndex('din3', 'din3', { unique: false });
}
};
}
// ============================================================================
// CRUD-OPERATIONEN
// ============================================================================
function addPosition(position) {
const transaction = db.transaction([STORE_NAME], 'readwrite');
transaction.objectStore(STORE_NAME).add(position);
}
function getAllPositions() {
return new Promise((resolve) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const request = transaction.objectStore(STORE_NAME).getAll();
request.onsuccess = () => {
allPositions = request.result;
resolve(allPositions);
};
});
}
function updatePosition(position) {
const transaction = db.transaction([STORE_NAME], 'readwrite');
transaction.objectStore(STORE_NAME).put(position);
setTimeout(() => loadPositions(), 100);
}
function deletePosition(id) {
if (!confirm('Diese Position wirklich löschen?')) return;
const transaction = db.transaction([STORE_NAME], 'readwrite');
transaction.objectStore(STORE_NAME).delete(id);
setTimeout(() => loadPositions(), 100);
}
// ============================================================================
// TEMPLATE-MANAGEMENT
// ============================================================================
// Templates aus LocalStorage laden
function loadTemplates() {
const stored = localStorage.getItem('angebotsdatenbank_templates');
if (stored) {
try {
templates = JSON.parse(stored);
console.log(`${templates.length} Templates geladen`);
} catch(e) {
console.error('Fehler beim Laden der Templates:', e);
templates = [];
}
}
// renderTemplates(); // DEAKTIVIERT - Template-Browser entfernt
}
// Templates in LocalStorage speichern
function saveTemplates() {
try {
localStorage.setItem('angebotsdatenbank_templates', JSON.stringify(templates));
console.log(`${templates.length} Templates gespeichert`);
} catch(e) {
console.error('Fehler beim Speichern der Templates:', e);
alert('Templates konnten nicht gespeichert werden!');
}
}
// Template aus Wunschliste erstellen
function createTemplateFromWishList() {
if (wishList.length === 0) {
alert('Wunschliste ist leer. Fügen Sie erst Positionen hinzu.');
return;
}
// Dialog für Name und Beschreibung
const name = prompt('Template-Name:', 'Heizungsanlage Wiedereinbau');
if (!name || name.trim() === '') {
return; // Abgebrochen
}
const description = prompt('Beschreibung (optional):', 'Typischer Wiedereinbau nach Wasserschaden');
// STLB-Kategorie aus den Positionen ableiten
// Wir nehmen den häufigsten STLB-Code aus der Wunschliste
const stlbCounts = {};
wishList.forEach(item => {
// Versuche STLB aus dem Begriff zu erraten (später können wir die DB abfragen)
// Vorerst: Einfache Keyword-Analyse
const term = item.term.toLowerCase();
let stlb = '000'; // Default
// Intelligente STLB-Zuordnung basierend auf Keywords
if (term.includes('heiz') || term.includes('heizkörper') || term.includes('wärmepumpe')) {
stlb = term.includes('wärmepumpe') ? '040' : '041';
} else if (term.includes('sanitär') || term.includes('waschbecken') || term.includes('wc') || term.includes('dusche') || term.includes('badewanne')) {
stlb = '045';
} else if (term.includes('elektro') || term.includes('steckdose') || term.includes('schalter') || term.includes('leitung')) {
stlb = '053';
} else if (term.includes('fliese') || term.includes('platte')) {
stlb = '024';
} else if (term.includes('estrich')) {
stlb = '025';
} else if (term.includes('maler') || term.includes('anstrich') || term.includes('tapete')) {
stlb = '034';
} else if (term.includes('bodenbelag') || term.includes('parkett') || term.includes('laminat') || term.includes('vinyl')) {
stlb = '036';
} else if (term.includes('trockenbau') || term.includes('rigips') || term.includes('gipskarton')) {
stlb = '039';
}
stlbCounts[stlb] = (stlbCounts[stlb] || 0) + 1;
});
// Häufigster STLB-Code
let category = '000';
let maxCount = 0;
for (const [stlb, count] of Object.entries(stlbCounts)) {
if (count > maxCount) {
maxCount = count;
category = stlb;
}
}
// STLB-Name ermitteln
const categoryName = STLB_GEWERKE[category] || 'Sonstiges';
// Position-Terms aus Wunschliste extrahieren
const positionTerms = wishList.map(item => {
let term = item.term;
if (item.type && item.type !== 'Neueinbau') {
term += ` (${item.type})`;
}
if (item.remarks) {
term += ` - ${item.remarks}`;
}
return term;
});
// SchadenProf-Daten optional hinzufügen
let schadenprofData = null;
if (window.importedSchadenProvData) {
const ask = confirm(
'SchadenProf-Daten gefunden!\n\n' +
'Sollen Gebäudeinformationen (Typ, Baujahr, Bauart) im Template gespeichert werden?\n\n' +
'Dies ermöglicht später bessere Template-Vorschläge.'
);
if (ask) {
// Extrahiere relevante Gebäudedaten
const analysisResults = window.lastAnalysisResults || {};
schadenprofData = {
gebaeudeart: analysisResults.gebaeudeart || null,
baujahr: analysisResults.baujahr || null,
bauart: analysisResults.bauart || null,
deckenkonstruktion: analysisResults.deckenkonstruktion || null
};
}
}
// Template erstellen
const template = {
id: 'template-' + Date.now(),
name: name.trim(),
description: description ? description.trim() : '',
category: category,
categoryName: categoryName,
positionTerms: positionTerms,
schadenprofData: schadenprofData,
created: new Date().toISOString(),
lastUsed: new Date().toISOString()
};
templates.push(template);
saveTemplates();
// renderTemplates(); // DEAKTIVIERT - Template-Browser entfernt
alert(
`✅ Template "${name}" erstellt!\n\n` +
`Kategorie: ${categoryName} (${category})\n` +
`Positionen: ${positionTerms.length}\n` +
`${schadenprofData ? 'Mit Gebäudedaten' : 'Ohne Gebäudedaten'}`
);
// Wunschliste leeren nach Template-Erstellung
if (confirm('Wunschliste jetzt leeren?')) {
wishList = [];
updateWishListUI();
}
}
// Template anwenden: Lade Positionen in Wunschliste
function applyTemplate(templateId) {
const template = templates.find(t => t.id === templateId);
if (!template) {
alert('Template nicht gefunden!');
return;
}
// Bestätigung
if (!confirm(
`Template "${template.name}" anwenden?\n\n` +
`${template.positionTerms.length} Position(en) werden zur Wunschliste hinzugefügt.\n\n` +
`Kategorie: ${template.categoryName}`
)) {
return;
}
// Lade Positionen in Wunschliste
template.positionTerms.forEach(term => {
// Extrahiere Type und Remarks falls vorhanden
let cleanTerm = term;
let type = 'Neueinbau';
let remarks = '';
// Parse "(Type)" aus Term
const typeMatch = term.match(/\(([^)]+)\)$/);
if (typeMatch) {
type = typeMatch[1];
cleanTerm = term.replace(/\s*\([^)]+\)$/, '');
}
// Parse "- Remarks" aus Term
const remarksMatch = cleanTerm.match(/\s*-\s*(.+)$/);
if (remarksMatch) {
remarks = remarksMatch[1];
cleanTerm = cleanTerm.replace(/\s*-\s*.+$/, '');
}
// Prüfe ob bereits in Wunschliste
const duplicate = wishList.find(item =>
item.term === cleanTerm && item.remarks === remarks
);
if (!duplicate) {
wishList.push({
term: cleanTerm,
isPackage: false,
type: type,
remarks: remarks
});
}
});
// Update lastUsed
template.lastUsed = new Date().toISOString();
saveTemplates();
updateWishListUI();
alert(`${template.positionTerms.length} Position(en) zur Wunschliste hinzugefügt!`);
// Wechsle zum Wunschliste-Tab
showView('wishlist');
}
// Template löschen
function deleteTemplate(templateId) {
const template = templates.find(t => t.id === templateId);
if (!template) return;
if (!confirm(`Template "${template.name}" wirklich löschen?`)) {
return;
}
templates = templates.filter(t => t.id !== templateId);
saveTemplates();
// renderTemplates(); // DEAKTIVIERT - Template-Browser entfernt
}
// Template-Export als JSON
function exportTemplates() {
if (templates.length === 0) {
alert('Keine Templates zum Exportieren vorhanden.');
return;
}
const data = {
version: '1.0',
exportDate: new Date().toISOString(),
templates: templates
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `templates_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
alert(`${templates.length} Template(s) exportiert!`);
}
// Template-Import aus JSON
function importTemplates(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (!data.templates || !Array.isArray(data.templates)) {
throw new Error('Ungültiges Template-Format');
}
const importCount = data.templates.length;
// Merge (verhindere Duplikate basierend auf Name)
data.templates.forEach(importedTemplate => {
const exists = templates.find(t => t.name === importedTemplate.name);
if (!exists) {
templates.push(importedTemplate);
}
});
saveTemplates();
// renderTemplates(); // DEAKTIVIERT - Template-Browser entfernt
alert(`${importCount} Template(s) importiert!`);
} catch (err) {
alert(`❌ Import fehlgeschlagen:\n${err.message}`);
}
};
reader.readAsText(file);
}
// ============================================================================
// WISHLIST SCHADENPROF-KONTEXT
// ============================================================================
// Toggle SchadenProf Details in Wunschliste
function toggleWishlistSchadenprofDetails() {
const detailsDiv = document.getElementById('wishlistSchadenprofDetails');
const btn = document.getElementById('toggleWishlistSchadenprofBtn');
if (detailsDiv.style.display === 'none') {
detailsDiv.style.display = 'block';
btn.textContent = 'Details ▲';
} else {
detailsDiv.style.display = 'none';
btn.textContent = 'Details ▼';
}
}
// Update SchadenProf-Kontext Anzeige in Wunschliste
function updateWishlistSchadenprofContext() {
const contextDiv = document.getElementById('wishlistSchadenprofContext');
const detailsDiv = document.getElementById('wishlistSchadenprofDetails');
if (!window.importedSchadenProvData) {
contextDiv.style.display = 'none';
return;
}
contextDiv.style.display = 'block';
const data = window.importedSchadenProvData;
const analysis = window.lastAnalysisResults || {};
let html = '<div style="margin-top: 8px;">';
// Schadenfall-Info
if (data.schadenfall) {
html += '<div style="margin-bottom: 8px;">';
html += `<strong>Schadenfall:</strong> ${data.schadenfall.schadennummer || 'k.A.'}<br>`;
html += `<strong>Schadensart:</strong> ${data.schadenfall.schadenart || 'k.A.'}<br>`;
if (analysis.gebaeudeart) html += `<strong>Gebäudeart:</strong> ${analysis.gebaeudeart}<br>`;
if (analysis.baujahr) html += `<strong>Baujahr:</strong> ${analysis.baujahr}<br>`;
if (analysis.bauart) html += `<strong>Bauart:</strong> ${analysis.bauart}<br>`;
html += '</div>';
}
// Räume
if (data.raeume && data.raeume.length > 0) {
html += '<div style="margin-bottom: 8px;">';
html += `<strong>Räume (${data.raeume.length}):</strong><br>`;
html += '<div style="max-height: 100px; overflow-y: auto; font-size: 0.8em; padding-left: 10px;">';
data.raeume.forEach(raum => {
html += `${raum.name} (${raum.flaeche} m²)<br>`;
});
html += '</div></div>';
}
// Vermerke mit Checkboxen
if (data.vermerke && data.vermerke.length > 0) {
html += '<div style="margin-bottom: 8px;">';
html += `<strong>Vermerke (${data.vermerke.length}):</strong> `;
html += '<span style="font-size: 0.75em; color: #666;">(für Prompt auswählen)</span><br>';
html += '<div style="max-height: 150px; overflow-y: auto; background: white; padding: 8px; border-radius: 4px; margin-top: 4px;">';
data.vermerke.forEach((vermerk, index) => {
const id = `wishlist-vermerk-${index}`;
// Default: Alle ausgewählt
html += `
<div style="margin-bottom: 6px; display: flex; align-items: start; gap: 6px;">
<input type="checkbox" id="${id}" class="wishlist-vermerk-checkbox" checked style="margin-top: 3px; flex-shrink: 0;">
<label for="${id}" style="font-size: 0.8em; cursor: pointer; flex: 1;">
<strong>${vermerk.datum || 'k.A.'}:</strong> ${vermerk.beschreibung || ''}
${vermerk.aufwandsart ? `<span style="color: #666;">(${vermerk.aufwandsart})</span>` : ''}
</label>
</div>
`;
});
html += '</div>';
html += '<div style="margin-top: 6px; display: flex; gap: 6px;">';
html += '<button onclick="selectAllWishlistVermerke(true)" style="padding: 2px 8px; font-size: 0.75em; background: #4caf50; color: white; border: none; border-radius: 3px; cursor: pointer;">Alle</button>';
html += '<button onclick="selectAllWishlistVermerke(false)" style="padding: 2px 8px; font-size: 0.75em; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer;">Keine</button>';
html += '</div></div>';
}
// Aufgaben
if (data.aufgaben && data.aufgaben.length > 0) {
html += '<div style="margin-bottom: 8px;">';
html += `<strong>Aufgaben (${data.aufgaben.length}):</strong><br>`;
html += '<div style="max-height: 100px; overflow-y: auto; font-size: 0.8em; padding-left: 10px;">';
data.aufgaben.forEach(aufgabe => {
html += `${aufgabe.beschreibung || aufgabe.titel || ''}<br>`;
});
html += '</div></div>';
}
html += '<div style="font-size: 0.75em; color: #2e7d32; margin-top: 8px; padding: 6px; background: white; border-radius: 3px;">';
html += '💡 Diese Informationen werden in den Prompt für die Position-Recherche einbezogen.';
html += '</div>';
html += '</div>';
detailsDiv.innerHTML = html;
}
// Alle Vermerke auswählen/abwählen
function selectAllWishlistVermerke(checked) {
document.querySelectorAll('.wishlist-vermerk-checkbox').forEach(cb => {
cb.checked = checked;
});
}
// Hole ausgewählte SchadenProf-Daten für Prompt
function getSelectedSchadenprofDataForWishlist() {
if (!window.importedSchadenProvData) return null;
const data = window.importedSchadenProvData;
const analysis = window.lastAnalysisResults || {};
// Sammle ausgewählte Vermerke
const selectedVermerke = [];
if (data.vermerke) {
data.vermerke.forEach((vermerk, index) => {
const checkbox = document.getElementById(`wishlist-vermerk-${index}`);
if (checkbox && checkbox.checked) {
selectedVermerke.push(vermerk);
}
});
}
return {
schadenfall: data.schadenfall,
raeume: data.raeume || [],
vermerke: selectedVermerke,
aufgaben: data.aufgaben || [],
analysis: analysis
};
}
// Template-Browser UI rendern (DEAKTIVIERT - Templates werden jetzt von Claude bei Bedarf erstellt)
/*
function renderTemplates() {
const browserDiv = document.getElementById('templateBrowser');
if (!browserDiv) return;
if (templates.length === 0) {
browserDiv.innerHTML = '';
return;
}
// Gruppiere Templates nach Kategorie
const byCategory = {};
templates.forEach(t => {
const cat = t.categoryName || 'Sonstiges';
if (!byCategory[cat]) {
byCategory[cat] = [];
}
byCategory[cat].push(t);
});
let html = `
<div style="border-top: 2px solid #667eea; padding-top: 15px; margin-top: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3 style="margin: 0; font-size: 1em; color: #667eea;">📚 Gespeicherte Templates (${templates.length})</h3>
<div style="display: flex; gap: 8px;">
<button onclick="exportTemplates()" style="padding: 4px 12px; font-size: 0.85em; background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer;">
💾 Export
</button>
<button onclick="document.getElementById('template-import-input').click()" style="padding: 4px 12px; font-size: 0.85em; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;">
📥 Import
</button>
</div>
</div>
<input type="file" id="template-import-input" accept=".json" style="display: none;" onchange="importTemplates(this.files[0])">
`;
// Zeige Templates nach Kategorie
for (const [category, temps] of Object.entries(byCategory)) {
html += `
<div style="margin-bottom: 15px;">
<div style="font-weight: bold; font-size: 0.9em; color: #495057; margin-bottom: 8px; padding: 5px 10px; background: #f8f9fa; border-radius: 5px;">
${category} (${temps.length})
</div>
`;
temps.forEach(template => {
const createdDate = new Date(template.created).toLocaleDateString('de-DE');
const hasSchadenProf = template.schadenprofData && Object.values(template.schadenprofData).some(v => v !== null);
html += `
<div style="background: white; border: 1px solid #dee2e6; border-radius: 5px; padding: 10px; margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<div style="font-weight: bold; font-size: 0.95em; margin-bottom: 4px;">
${template.name}
${hasSchadenProf ? '<span style="font-size: 0.75em; background: #e3f2fd; color: #1976d2; padding: 2px 6px; border-radius: 3px; margin-left: 6px;">🏠 Gebäudedaten</span>' : ''}
</div>
${template.description ? `<div style="font-size: 0.85em; color: #6c757d; margin-bottom: 4px;">${template.description}</div>` : ''}
<div style="font-size: 0.8em; color: #6c757d;">
${template.positionTerms.length} Position(en) • ${createdDate}
</div>
</div>
<div style="display: flex; gap: 6px; flex-shrink: 0; margin-left: 10px;">
<button onclick="applyTemplate('${template.id}')" style="padding: 4px 10px; font-size: 0.8em; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; white-space: nowrap;">
▶ Anwenden
</button>
<button onclick="deleteTemplate('${template.id}')" style="padding: 4px 10px; font-size: 0.8em; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer;">
🗑
</button>
</div>
</div>
</div>
`;
});
html += `</div>`;
}
html += `</div>`;
browserDiv.innerHTML = html;
}
*/
// ============================================================================
// DUPLIKAT-PRÜFUNG
// ============================================================================
function checkDuplicates() {
const begriff = document.getElementById('begriff').value.toLowerCase().trim();
if (begriff.length < 3) {
document.getElementById('duplicateWarning').style.display = 'none';
return;
}
const duplicates = allPositions.filter(pos => {
return pos.begriff.toLowerCase().includes(begriff) ||
begriff.includes(pos.begriff.toLowerCase());
});
if (duplicates.length > 0) {
const list = duplicates.map(d =>
`<div style="padding: 8px; margin: 5px 0; background: white; border-radius: 5px;">
<strong>${d.begriff}</strong> - ${d.erklaerung.substring(0, 60)}...
<br><small>STLB ${d.stlb} | DIN ${d.din1}/${d.din2}</small>
</div>`
).join('');
document.getElementById('duplicateList').innerHTML = list;
document.getElementById('duplicateWarning').style.display = 'block';
} else {
document.getElementById('duplicateWarning').style.display = 'none';
}
}
// ============================================================================
// FUZZY SEARCH (LEVENSHTEIN-DISTANZ)
// ============================================================================
/**
* Berechnet Levenshtein-Distanz zwischen zwei Strings
* (Minimale Anzahl Einfügungen/Löschungen/Ersetzungen um s1 in s2 zu transformieren)
*/
function levenshteinDistance(s1, s2) {
s1 = s1.toLowerCase();
s2 = s2.toLowerCase();
const len1 = s1.length;
const len2 = s2.length;
const matrix = [];
// Initialisiere Matrix
for (let i = 0; i <= len1; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= len2; j++) {
matrix[0][j] = j;
}
// Berechne Distanz
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // Löschung
matrix[i][j - 1] + 1, // Einfügung
matrix[i - 1][j - 1] + cost // Ersetzung
);
}
}
return matrix[len1][len2];
}
/**
* Berechnet Ähnlichkeits-Score (0-1, wobei 1 = identisch)
*/
function similarityScore(s1, s2) {
const distance = levenshteinDistance(s1, s2);
const maxLen = Math.max(s1.length, s2.length);
return 1 - (distance / maxLen);
}
/**
* Prüft ob String ähnlich genug ist (Threshold: 0.6 = 60% ähnlich)
*/
function isFuzzyMatch(searchTerm, text, threshold = 0.6) {
const words = text.toLowerCase().split(/\s+/);
const searchLower = searchTerm.toLowerCase();
// Prüfe exakte Teilstring-Übereinstimmung
if (text.toLowerCase().includes(searchLower)) {
return { match: true, score: 1.0, type: 'exact' };
}
// Prüfe Fuzzy-Match für jedes Wort
for (const word of words) {
const score = similarityScore(searchLower, word);
if (score >= threshold) {
return { match: true, score, type: 'fuzzy' };
}
// Prüfe auch Teilwort-Matches (z.B. "estrich" in "estricharbeiten")
if (word.includes(searchLower) || searchLower.includes(word)) {
const partialScore = Math.max(
searchLower.length / word.length,
word.length / searchLower.length
);
if (partialScore >= threshold) {
return { match: true, score: partialScore, type: 'partial' };
}
}
}
return { match: false, score: 0, type: 'none' };
}
// ============================================================================
// WUNSCHPOSITIONEN-FEATURE
// ============================================================================
function setSearchMode(mode) {
currentFilters.searchMode = mode;
// Update UI
document.getElementById('searchModeExact').classList.toggle('active', mode === 'exact');
document.getElementById('searchModeFuzzy').classList.toggle('active', mode === 'fuzzy');
// Suche neu ausführen
loadPositions();
}
function addToWishList() {
const searchTerm = currentFilters.search.trim();
const positionType = document.getElementById('positionTypeSelect').value;
const remarks = document.getElementById('positionRemarksInput').value.trim();
if (!searchTerm) {
alert('Bitte geben Sie zuerst einen Suchbegriff ein.');
return;
}
// Prüfe ob bereits in Liste (gleicher Begriff UND gleiche Bemerkung)
const duplicate = wishList.find(item =>
item.term === searchTerm && item.remarks === remarks
);
if (duplicate) {
alert('Dieser Begriff mit diesen Bemerkungen ist bereits in der Wunschliste.');
return;
}
wishList.push({
term: searchTerm,
isPackage: false,
type: positionType,
remarks: remarks
});
updateWishListUI();
// Leere Bemerkungsfeld für nächste Eingabe
document.getElementById('positionRemarksInput').value = '';
// Zeige Erfolgs-Feedback
const btn = document.querySelector('.wish-position-box button');
const originalText = btn.textContent;
btn.textContent = '✓ Hinzugefügt!';
btn.style.background = '#28a745';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
}, 1500);
}
function removeFromWishList(index) {
wishList.splice(index, 1);
updateWishListUI();
}
function togglePackage(index) {
wishList[index].isPackage = !wishList[index].isPackage;
updateWishListUI();
}
function updateWishListUI() {
const wishListDiv = document.getElementById('wishList');
const sendBtn = document.getElementById('sendWishBtn');
const saveTemplateBtn = document.getElementById('saveTemplateBtn');
if (wishList.length === 0) {
wishListDiv.style.display = 'none';
sendBtn.style.display = 'none';
saveTemplateBtn.style.display = 'none';
return;
}
wishListDiv.style.display = 'block';
sendBtn.style.display = 'block';
saveTemplateBtn.style.display = 'block';
wishListDiv.innerHTML = wishList.map((item, index) => `
<div class="wish-item" style="display: flex; gap: 8px; align-items: center; padding: 8px; border-bottom: 1px solid #e9ecef;">
<input type="checkbox"
${item.isPackage ? 'checked' : ''}
onchange="togglePackage(${index})"
title="Position ist ein Paket (mehrere Einzelpositionen)"
style="cursor: pointer;">
<div style="flex: 1;">
<div style="font-size: 0.9em; font-weight: 500;">${item.term}</div>
<div style="font-size: 0.75em; color: #666; margin-top: 2px;">${item.type}</div>
${item.remarks ? `<div style="font-size: 0.75em; color: #007bff; margin-top: 2px; font-style: italic;">💬 ${item.remarks}</div>` : ''}
</div>
<span class="wish-remove" onclick="removeFromWishList(${index})" style="cursor: pointer; color: #dc3545; font-weight: bold; padding: 2px 8px;">✕</span>
</div>
`).join('');
}
function sendWishListToAI() {
if (wishList.length === 0) {
alert('Wunschliste ist leer.');
return;
}
// Standard: Websuche immer erlaubt (nur kostenlose Portale)
// User kann mit "Abbrechen" auf "Nur offizielle Portale" einschränken
const allowWebSearch = confirm(
`📋 ${wishList.length} Wunschposition(en) werden übergeben.\n\n` +
`Standard: Freie Websuche + offizielle Portale (alle kostenlos)\n\n` +
`✅ OK: Weiter (Websuche erlaubt)\n` +
`❌ Abbrechen: Nur offizielle Ausschreibungsportale`
);
// Generiere Prompt
const prompt = generatePositionCreationPrompt(allowWebSearch);
// Versuche Clipboard API (funktioniert nur bei User-Interaktion)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(prompt).then(() => {
alert(
`✅ Positions-Erstellungsauftrag in Zwischenablage kopiert!\n\n` +
`${wishList.length} Position(en) übergeben\n` +
`Websuche: ${allowWebSearch ? 'Erlaubt' : 'Nur offizielle Portale'}\n\n` +
`Fügen Sie den Prompt in ein neues Claude-Chat-Fenster ein.`
);
// Lösche Wunschliste nach erfolgreicher Übergabe
wishList = [];
updateWishListUI();
}).catch(err => {
// Fallback: Zeige Prompt in Textarea zum manuellen Kopieren
showPromptForManualCopy(prompt, allowWebSearch);
});
} else {
// Fallback für Browser ohne Clipboard API
showPromptForManualCopy(prompt, allowWebSearch);
}
}
function showPromptForManualCopy(prompt, allowWebSearch) {
// Erstelle Modal mit Textarea
const modal = document.createElement('div');
modal.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 10000; display: flex; align-items: center; justify-content: center;';
const content = document.createElement('div');
content.style.cssText = 'background: white; padding: 20px; border-radius: 10px; max-width: 800px; width: 90%; max-height: 80vh; overflow: auto;';
content.innerHTML = `
<h3 style="margin-top: 0;">📋 Positions-Erstellungsauftrag</h3>
<p style="margin-bottom: 15px;">
${wishList.length} Position(en) | Websuche: ${allowWebSearch ? 'Erlaubt' : 'Nur offizielle Portale'}<br>
<strong>Bitte kopieren Sie den Text unten (Strg+A, dann Strg+C):</strong>
</p>
<textarea id="promptTextarea" readonly style="width: 100%; height: 400px; font-family: monospace; font-size: 0.9em; padding: 10px; border: 1px solid #ddd; border-radius: 5px;">${prompt}</textarea>
<div style="margin-top: 15px; display: flex; gap: 10px;">
<button onclick="document.getElementById('promptTextarea').select(); document.execCommand('copy'); alert('Text kopiert!');" style="flex: 1; padding: 10px; background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; font-weight: bold;">
📋 Text kopieren
</button>
<button onclick="this.closest('div').parentElement.parentElement.remove(); wishList = []; updateWishListUI();" style="flex: 1; padding: 10px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-weight: bold;">
✅ Fertig & Wunschliste leeren
</button>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
// Auto-select Textarea
setTimeout(() => {
document.getElementById('promptTextarea').select();
}, 100);
}
function generatePositionCreationPrompt(allowWebSearch) {
const timestamp = new Date().toLocaleString('de-DE');
// Gruppiere nach Paket/Einzelposition
const packages = wishList.filter(item => item.isPackage);
const singles = wishList.filter(item => !item.isPackage);
// Hole SchadenProf-Daten falls vorhanden
const schadenprofData = getSelectedSchadenprofDataForWishlist();
let prompt = `📋 POSITIONS-ERSTELLUNGSAUFTRAG - SOFORT AUSFÜHREN!
Datum: ${timestamp}
Anzahl Positionen: ${wishList.length}
Websuche: ${allowWebSearch ? '✅ Erlaubt (nur kostenlose Portale)' : '❌ Nur offizielle Portale'}
${schadenprofData ? '🏠 SchadenProf-Kontext: ✅ Verfügbar' : ''}
⚠️ WICHTIG: Dies ist ein AUSFÜHRUNGSAUFTRAG!
Nach dem Einfügen dieses Prompts SOFORT beginnen:
1. ✅ POSITION_CREATION_PROTOCOL.md lesen
2. ✅ Positionen recherchieren (WebSearch/WebFetch)
3. ✅ Duplikat-Prüfung (check_duplicate.py)
4. ✅ Positionen als JSON erstellen
5. ✅ Backup erstellen (backup_pos.py)
6. ✅ Zu Pos.json mergen (Python-Script)
7. ✅ DIN 276 validieren (validate_din_simple.py)
8. ✅ Abschlussbericht erstellen
KEINE Rückfragen - direkt ausführen!
════════════════════════════════════════════════════════════════
${schadenprofData ? `
## 🏠 SCHADENFALL-KONTEXT
**Schadenfall:** ${schadenprofData.schadenfall?.schadennummer || 'k.A.'}
**Schadensart:** ${schadenprofData.schadenfall?.schadenart || 'k.A.'}
${schadenprofData.analysis.gebaeudeart ? `**Gebäudeart:** ${schadenprofData.analysis.gebaeudeart}` : ''}
${schadenprofData.analysis.baujahr ? `**Baujahr:** ${schadenprofData.analysis.baujahr}` : ''}
${schadenprofData.analysis.bauart ? `**Bauart:** ${schadenprofData.analysis.bauart}` : ''}
${schadenprofData.analysis.deckenkonstruktion ? `**Deckenkonstruktion:** ${schadenprofData.analysis.deckenkonstruktion}` : ''}
${schadenprofData.raeume && schadenprofData.raeume.length > 0 ? `**Betroffene Räume:**
${schadenprofData.raeume.map(r => `${r.name} (${r.flaeche} m², Höhe: ${r.hoehe} m)`).join('\n')}
` : ''}
${schadenprofData.vermerke && schadenprofData.vermerke.length > 0 ? `**Relevante Vermerke:**
${schadenprofData.vermerke.map(v => `${v.datum}: ${v.beschreibung}`).join('\n')}
` : ''}
${schadenprofData.aufgaben && schadenprofData.aufgaben.length > 0 ? `**Aufgaben:**
${schadenprofData.aufgaben.map(a => `${a.beschreibung || a.titel}`).join('\n')}
` : ''}
⚠️ **Berücksichtige diese Informationen bei der Position-Erstellung:**
- Gebäudeart und Baujahr beeinflussen Materialwahl und Technik
- Vermerke enthalten wichtige Hinweise zu Besonderheiten
- Raumgrößen für Mengen- und Zeitplanung relevant
════════════════════════════════════════════════════════════════
` : ''}
## BENÖTIGTE POSITIONEN
`;
if (singles.length > 0) {
prompt += `### Einzelpositionen (${singles.length})\n\n`;
singles.forEach((item, i) => {
prompt += `${i + 1}. **${item.term}** (${item.type})\n`;
if (item.remarks) {
prompt += ` 💬 Bemerkung: ${item.remarks}\n`;
}
});
prompt += `\n`;
}
if (packages.length > 0) {
prompt += `### Pakete - in Einzelpositionen aufteilen! (${packages.length})\n`;
prompt += `⚠️ Diese Positionen sind Pakete und müssen in Einzelpositionen zerlegt werden:\n\n`;
packages.forEach((item, i) => {
prompt += `${i + 1}. **${item.term}** (${item.type})\n`;
if (item.remarks) {
prompt += ` 💬 Bemerkung: ${item.remarks}\n`;
}
prompt += ` → Analysieren und in sinnvolle Einzelpositionen aufteilen\n`;
prompt += ` → Jede Einzelposition separat recherchieren und erstellen\n`;
prompt += ` → Kontext beachten: ${item.type}\n\n`;
});
}
prompt += `════════════════════════════════════════════════════════════════
## ARBEITSANWEISUNGEN
**PFLICHTLEKTÜRE:**
Lese ZUERST vollständig: POSITION_CREATION_PROTOCOL.md
**WORKFLOW FÜR JEDE POSITION:**
1. **Recherche** (STLB-konforme Ausschreibungstexte):
Primäre Quellen (immer prüfen):
• ausschreiben.de - Öffentliche Ausschreibungen
• STLB-Bau Online (www.stlb-bau-online.de) - Standardleistungsbuch
• Heinze (www.heinze.de/tools/ausschreibungstexte/) - VOB-konforme Texte
${allowWebSearch ? `Zusätzlich erlaubt:
• Google Websuche nach "STLB [Positionsname]"
• Fachportale und Branchenseiten
• Herstellerunterlagen (für technische Details)
` : ''}
2. **Duplikat-Prüfung**:
\`\`\`bash
python check_duplicate.py "Positionsname"
\`\`\`
→ Falls Duplikat: Existierende Position verwenden!
3. **Position erstellen**:
- **Positionstyp beachten** (Neueinbau/Sanierung/Demontage/Wiedereinbau/Ersatz)
→ Beeinflusst Formulierung und Leistungsumfang!
- Begriff: Kurz, STLB-konform, angepasst an Positionstyp
- Erklärung: Hauptleistung + Nebenarbeiten + "Nach Fertigstellung [funktionsfähig/nutzbar]"
- STLB-Code: Richtiges Gewerk
- DIN 276: Korrekte 3-stufige Hierarchie
- Preis: Marktgerecht, realistisch (Sanierung/Ersatz oft teurer als Neueinbau!)
4. **BACKUP erstellen** (KRITISCH!):
\`\`\`bash
python backup_pos.py
\`\`\`
5. **Speichern & Validieren**:
- Neue Position(en) zu temporärer JSON erstellen
- Merge zu Pos.json (Python-Script mit UTF-8 encoding)
- Validierung: \`python validate_din_simple.py\`
════════════════════════════════════════════════════════════════
## REGELN
✅ IMMER:
- Recherchieren (echte Ausschreibungstexte verwenden)
- Duplikat-Prüfung durchführen
- STLB-konforme Formulierung
- DIN 276 Hierarchie validieren
- Backup vor Änderungen
❌ NIEMALS:
- Positionen "aus dem Bauch" erstellen
- Preise raten
- STLB/DIN-Codes erraten
- Duplikate erzeugen
- Pos.json ohne Backup ändern
════════════════════════════════════════════════════════════════
## AUSGABEFORMAT
Für jede erstellte Position:
\`\`\`json
{
"begriff": "...",
"erklaerung": "...",
"einheit": "Stück|m²|lfm|pauschal",
"preis": 0.00,
"stlb": "XXX",
"din1": "XXX",
"din2": "XXX",
"din3": "XXX"
}
\`\`\`
**Abschlussbericht:**
- Anzahl erstellter Positionen
- Verwendete Quellen
- Besonderheiten/Hinweise
════════════════════════════════════════════════════════════════
Beginne jetzt mit der Position-Erstellung nach POSITION_CREATION_PROTOCOL.md!
`;
return prompt;
}
// ============================================================================
// INTELLIGENTE FILTER-LOGIK
// ============================================================================
/**
* Aktualisiert die verfügbaren Optionen basierend auf getroffenen Auswahlen
* Logik: STLB-Auswahl schränkt sinnvolle DIN-Gruppen ein
*/
function updateLogicalFilters() {
const stlb = document.getElementById('stlb').value;
const din1Select = document.getElementById('din1');
if (!stlb) return;
// Hole passende DIN-Gruppen für gewähltes STLB-Gewerk
const suggestedDins = STLB_TO_DIN_MAPPING[stlb] || [];
// Markiere vorgeschlagene Optionen
Array.from(din1Select.options).forEach(option => {
if (option.value === '') return;
const din1Code = option.value;
const isSuggested = suggestedDins.some(d => din1Code.startsWith(d));
if (isSuggested) {
option.style.fontWeight = 'bold';
option.style.color = '#28a745';
} else {
option.style.fontWeight = 'normal';
option.style.color = '#6c757d';
}
});
// Auto-Auswahl wenn nur eine sinnvolle Option
if (suggestedDins.length === 1) {
const matchingOption = Array.from(din1Select.options).find(opt =>
opt.value.startsWith(suggestedDins[0])
);
if (matchingOption) {
din1Select.value = matchingOption.value;
updateDin2Options();
}
}
}
// ============================================================================
// FILTER RENDERING
// ============================================================================
function renderFilters() {
const stlbContainer = document.getElementById('stlbFilters');
stlbContainer.innerHTML = '<button class="filter-btn active" onclick="setSTLBFilter(\'alle\')">Alle</button>';
Object.entries(STLB_GEWERKE).forEach(([code, name]) => {
stlbContainer.innerHTML += `<button class="filter-btn" onclick="setSTLBFilter('${code}')">${code} - ${name}</button>`;
});
const din1Container = document.getElementById('din1Filters');
din1Container.innerHTML = '<button class="filter-btn active" onclick="setDIN1Filter(\'alle\')">Alle</button>';
Object.entries(DIN_276).forEach(([code, data]) => {
din1Container.innerHTML += `<button class="filter-btn" onclick="setDIN1Filter('${code}')">${code} - ${data.name}</button>`;
});
updateDIN2Filters();
updateDIN3Filters();
}
function setSTLBFilter(code) {
currentFilters.stlb = code;
document.querySelectorAll('#stlbFilters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent.startsWith(code) || (code === 'alle' && btn.textContent === 'Alle'));
});
loadPositions();
}
function setDIN1Filter(code) {
currentFilters.din1 = code;
currentFilters.din2 = 'alle';
currentFilters.din3 = 'alle';
document.querySelectorAll('#din1Filters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent.startsWith(code) || (code === 'alle' && btn.textContent === 'Alle'));
});
updateDIN2Filters();
updateDIN3Filters();
loadPositions();
}
function setDIN2Filter(code) {
currentFilters.din2 = code;
currentFilters.din3 = 'alle';
document.querySelectorAll('#din2Filters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent.startsWith(code) || (code === 'alle' && btn.textContent === 'Alle'));
});
updateDIN3Filters();
loadPositions();
}
function setDIN3Filter(code) {
currentFilters.din3 = code;
document.querySelectorAll('#din3Filters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent.startsWith(code) || (code === 'alle' && btn.textContent === 'Alle'));
});
loadPositions();
}
function updateDIN2Filters() {
const din2Container = document.getElementById('din2Filters');
din2Container.innerHTML = '<button class="filter-btn active" onclick="setDIN2Filter(\'alle\')">Alle</button>';
if (currentFilters.din1 !== 'alle' && DIN_276[currentFilters.din1]) {
Object.entries(DIN_276[currentFilters.din1].level2).forEach(([code, data]) => {
din2Container.innerHTML += `<button class="filter-btn" onclick="setDIN2Filter('${code}')">${code} - ${data.name}</button>`;
});
} else {
// Zeige alle Level2 wenn kein Level1 Filter gesetzt
Object.values(DIN_276).forEach(l1 => {
Object.entries(l1.level2).forEach(([code, data]) => {
if (!din2Container.innerHTML.includes(`onclick="setDIN2Filter('${code}')`)) {
din2Container.innerHTML += `<button class="filter-btn" onclick="setDIN2Filter('${code}')">${code} - ${data.name}</button>`;
}
});
});
}
}
function updateDIN3Filters() {
const din3Container = document.getElementById('din3Filters');
din3Container.innerHTML = '<button class="filter-btn active" onclick="setDIN3Filter(\'alle\')">Alle</button>';
if (currentFilters.din1 !== 'alle' && currentFilters.din2 !== 'alle' &&
DIN_276[currentFilters.din1]?.level2[currentFilters.din2]) {
Object.entries(DIN_276[currentFilters.din1].level2[currentFilters.din2].level3).forEach(([code, name]) => {
din3Container.innerHTML += `<button class="filter-btn" onclick="setDIN3Filter('${code}')">${code} - ${name}</button>`;
});
} else if (currentFilters.din2 !== 'alle') {
// Zeige alle Level3 für gewähltes Level2
Object.values(DIN_276).forEach(l1 => {
const l2Data = l1.level2[currentFilters.din2];
if (l2Data) {
Object.entries(l2Data.level3).forEach(([code, name]) => {
if (!din3Container.innerHTML.includes(`onclick="setDIN3Filter('${code}')`)) {
din3Container.innerHTML += `<button class="filter-btn" onclick="setDIN3Filter('${code}')">${code} - ${name}</button>`;
}
});
}
});
}
}
// ============================================================================
// POSITIONEN LADEN UND ANZEIGEN
// ============================================================================
function loadPositions() {
getAllPositions().then(positions => {
const content = document.getElementById('content');
const wishBox = document.getElementById('wishPositionBox');
// Filtere mit oder ohne Fuzzy-Search
let filtered = positions.filter(pos => {
const matchesSTLB = currentFilters.stlb === 'alle' || pos.stlb === currentFilters.stlb;
const matchesDIN1 = currentFilters.din1 === 'alle' || pos.din1 === currentFilters.din1;
const matchesDIN2 = currentFilters.din2 === 'alle' || pos.din2 === currentFilters.din2;
const matchesDIN3 = currentFilters.din3 === 'alle' || pos.din3 === currentFilters.din3;
const searchTerm = currentFilters.search;
let matchesSearch = true;
if (searchTerm !== '') {
if (currentFilters.searchMode === 'exact') {
// Exakte Suche (alte Logik)
const searchLower = searchTerm.toLowerCase();
matchesSearch = pos.begriff.toLowerCase().includes(searchLower) ||
pos.erklaerung.toLowerCase().includes(searchLower) ||
pos.einheit.toLowerCase().includes(searchLower) ||
pos.preis.toString().includes(searchLower);
} else {
// Fuzzy-Suche
const begriffMatch = isFuzzyMatch(searchTerm, pos.begriff);
const erklaerungMatch = isFuzzyMatch(searchTerm, pos.erklaerung);
matchesSearch = begriffMatch.match || erklaerungMatch.match;
// Speichere Match-Score für Sortierung
pos._matchScore = Math.max(begriffMatch.score, erklaerungMatch.score);
pos._matchType = begriffMatch.score > erklaerungMatch.score ? begriffMatch.type : erklaerungMatch.type;
}
}
return matchesSTLB && matchesDIN1 && matchesDIN2 && matchesDIN3 && matchesSearch;
});
// Bei Fuzzy-Search: Sortiere nach Relevanz (höchster Score zuerst)
if (currentFilters.searchMode === 'fuzzy' && currentFilters.search !== '') {
filtered.sort((a, b) => (b._matchScore || 0) - (a._matchScore || 0));
}
document.getElementById('totalPositions').textContent = positions.length;
document.getElementById('visiblePositions').textContent = filtered.length;
document.getElementById('stlbCount').textContent = new Set(positions.map(p => p.stlb)).size;
document.getElementById('din1Count').textContent = new Set(positions.map(p => p.din1)).size;
// Zeige/Verstecke Wunschpositions-Box
if (filtered.length === 0 && currentFilters.search !== '' && positions.length > 0) {
wishBox.style.display = 'block';
} else {
wishBox.style.display = 'none';
}
if (filtered.length === 0) {
content.innerHTML = `
<div class="no-results">
<div style="font-size: 4em; margin-bottom: 20px;">🔍</div>
<h2>${positions.length === 0 ? 'Datenbank ist leer' : 'Keine Positionen gefunden'}</h2>
<p>${positions.length === 0 ? 'Klicken Sie auf "JSON Import" um Positionen zu laden.' : 'Passen Sie die Filter an oder nutzen Sie die Suche.'}</p>
</div>
`;
return;
}
content.innerHTML = '<div class="positions-grid" id="positionsGrid"></div>';
const grid = document.getElementById('positionsGrid');
filtered.forEach(pos => {
const card = document.createElement('div');
card.className = 'position-card';
// Füge Fuzzy-Match-Klasse hinzu wenn Fuzzy-Modus aktiv
if (pos._matchType === 'fuzzy' || pos._matchType === 'partial') {
card.classList.add('fuzzy-match');
}
const stlbName = STLB_GEWERKE[pos.stlb] || pos.stlb;
const din1Name = DIN_276[pos.din1]?.name || pos.din1;
// Fuzzy-Match Indikator Badge
let fuzzyBadge = '';
if (pos._matchType === 'fuzzy') {
fuzzyBadge = `<span class="badge" style="background: #ffc107; color: #000;" title="Ähnlichkeits-Treffer (${Math.round(pos._matchScore * 100)}% Übereinstimmung)">≈ ${Math.round(pos._matchScore * 100)}%</span>`;
} else if (pos._matchType === 'partial') {
fuzzyBadge = `<span class="badge" style="background: #17a2b8; color: white;" title="Teil-Übereinstimmung">Teilmatch</span>`;
}
card.innerHTML = `
<div class="position-actions">
<button class="action-btn action-btn-edit" onclick="editPosition(${pos.id})">✏️</button>
<button class="action-btn action-btn-delete" onclick="deletePosition(${pos.id})">🗑️</button>
</div>
<div class="position-badges">
<span class="badge badge-stlb" title="${stlbName}">STLB ${pos.stlb}</span>
<span class="badge badge-din" title="${din1Name}">${pos.din1}</span>
${pos.din2 ? `<span class="badge badge-din">${pos.din2}</span>` : ''}
${pos.din3 ? `<span class="badge badge-din">${pos.din3}</span>` : ''}
${fuzzyBadge}
</div>
<div class="position-term">${pos.begriff}</div>
<div class="position-description">${pos.erklaerung}</div>
<div class="position-footer">
<span style="color: #6c757d;">${pos.einheit}</span>
<span class="position-price">€ ${pos.preis.toFixed(2).replace('.', ',')}</span>
</div>
`;
grid.appendChild(card);
});
});
}
// ============================================================================
// MODAL-FUNKTIONEN
// ============================================================================
function openAddModal() {
document.getElementById('modalTitle').textContent = 'Neue Position hinzufügen';
document.getElementById('positionForm').reset();
document.getElementById('positionId').value = '';
document.getElementById('duplicateWarning').style.display = 'none';
fillDropdowns();
document.getElementById('positionModal').classList.add('active');
}
function closeModal() {
document.getElementById('positionModal').classList.remove('active');
}
function fillDropdowns() {
const stlbSelect = document.getElementById('stlb');
stlbSelect.innerHTML = '<option value="">-- Bitte wählen --</option>';
Object.entries(STLB_GEWERKE).forEach(([code, name]) => {
stlbSelect.innerHTML += `<option value="${code}">${code} - ${name}</option>`;
});
const din1Select = document.getElementById('din1');
din1Select.innerHTML = '<option value="">-- Bitte wählen --</option>';
Object.entries(DIN_276).forEach(([code, data]) => {
din1Select.innerHTML += `<option value="${code}">${code} - ${data.name}</option>`;
});
}
function updateDin2Options() {
const din1 = document.getElementById('din1').value;
const din2Select = document.getElementById('din2');
const din3Select = document.getElementById('din3');
din2Select.innerHTML = '<option value="">-- Bitte wählen --</option>';
din3Select.innerHTML = '<option value="">-- Optional --</option>';
if (din1 && DIN_276[din1]) {
Object.entries(DIN_276[din1].level2).forEach(([code, data]) => {
din2Select.innerHTML += `<option value="${code}">${code} - ${data.name}</option>`;
});
}
}
function updateDin3Options() {
const din1 = document.getElementById('din1').value;
const din2 = document.getElementById('din2').value;
const din3Select = document.getElementById('din3');
din3Select.innerHTML = '<option value="">-- Optional --</option>';
if (din1 && din2 && DIN_276[din1]?.level2[din2]) {
Object.entries(DIN_276[din1].level2[din2].level3).forEach(([code, name]) => {
din3Select.innerHTML += `<option value="${code}">${code} - ${name}</option>`;
});
}
}
function savePosition(event) {
event.preventDefault();
const position = {
begriff: document.getElementById('begriff').value,
erklaerung: document.getElementById('erklaerung').value,
einheit: document.getElementById('einheit').value,
preis: parseFloat(document.getElementById('preis').value),
stlb: document.getElementById('stlb').value,
din1: document.getElementById('din1').value,
din2: document.getElementById('din2').value,
din3: document.getElementById('din3').value
};
const id = document.getElementById('positionId').value;
if (id) {
position.id = parseInt(id);
updatePosition(position);
} else {
addPosition(position);
setTimeout(() => loadPositions(), 100);
}
closeModal();
}
function editPosition(id) {
getAllPositions().then(positions => {
const position = positions.find(p => p.id === id);
if (!position) return;
document.getElementById('modalTitle').textContent = 'Position bearbeiten';
document.getElementById('positionId').value = position.id;
document.getElementById('begriff').value = position.begriff;
document.getElementById('erklaerung').value = position.erklaerung;
document.getElementById('einheit').value = position.einheit;
document.getElementById('preis').value = position.preis;
fillDropdowns();
document.getElementById('stlb').value = position.stlb;
document.getElementById('din1').value = position.din1;
updateDin2Options();
document.getElementById('din2').value = position.din2;
updateDin3Options();
document.getElementById('din3').value = position.din3;
document.getElementById('positionModal').classList.add('active');
});
}
// ============================================================================
// IMPORT/EXPORT
// ============================================================================
function openImportModal() {
document.getElementById('importModal').classList.add('active');
document.getElementById('gewerkSelection').style.display = 'none';
document.getElementById('importFileSelect').value = '';
importFileData = null;
}
function closeImportModal() {
document.getElementById('importModal').classList.remove('active');
}
function analyzeImportFile(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (!Array.isArray(data)) {
alert('Ungültiges JSON-Format (kein Array)');
return;
}
importFileData = data;
const gewerke = {};
data.forEach(pos => {
if (!gewerke[pos.stlb]) {
gewerke[pos.stlb] = {
name: STLB_GEWERKE[pos.stlb] || `Gewerk ${pos.stlb}`,
count: 0
};
}
gewerke[pos.stlb].count++;
});
const gewerkList = document.getElementById('gewerkList');
gewerkList.innerHTML = '';
const allDiv = document.createElement('div');
allDiv.className = 'gewerk-item';
allDiv.style.fontWeight = 'bold';
allDiv.style.background = '#e7f3ff';
allDiv.innerHTML = `
<input type="checkbox" id="selectAll" onchange="toggleAllGewerke(this)">
<label for="selectAll">Alle Gewerke auswählen/abwählen</label>
`;
gewerkList.appendChild(allDiv);
Object.entries(gewerke).sort((a,b) => a[0].localeCompare(b[0])).forEach(([code, info]) => {
const div = document.createElement('div');
div.className = 'gewerk-item';
div.innerHTML = `
<input type="checkbox" id="gewerk_${code}" value="${code}" class="gewerk-checkbox" checked>
<label for="gewerk_${code}">
<strong>${code}</strong> - ${info.name}
<span style="color: #6c757d;">(${info.count} Positionen)</span>
</label>
`;
gewerkList.appendChild(div);
});
document.getElementById('gewerkSelection').style.display = 'block';
updateImportSummary();
} catch (error) {
alert('Fehler beim Lesen der JSON-Datei: ' + error.message);
}
};
reader.readAsText(file);
}
function toggleAllGewerke(checkbox) {
document.querySelectorAll('.gewerk-checkbox').forEach(cb => {
cb.checked = checkbox.checked;
});
updateImportSummary();
}
function updateImportSummary() {
const checked = document.querySelectorAll('.gewerk-checkbox:checked');
const totalPositions = importFileData.filter(pos => {
return Array.from(checked).some(cb => cb.value === pos.stlb);
}).length;
document.getElementById('importSummary').innerHTML = `
<strong>Import-Zusammenfassung:</strong><br>
Ausgewählte Gewerke: ${checked.length}<br>
Positionen zum Import: ${totalPositions}
`;
}
document.addEventListener('change', (e) => {
if (e.target.classList.contains('gewerk-checkbox')) {
updateImportSummary();
}
});
function executeImport() {
const selectedGewerke = Array.from(document.querySelectorAll('.gewerk-checkbox:checked'))
.map(cb => cb.value);
if (selectedGewerke.length === 0) {
alert('Bitte wählen Sie mindestens ein Gewerk aus.');
return;
}
const filteredData = importFileData.filter(pos => selectedGewerke.includes(pos.stlb));
if (!confirm(`${filteredData.length} Positionen aus ${selectedGewerke.length} Gewerken importieren?`)) {
return;
}
clearDatabase(false).then(() => {
filteredData.forEach(pos => {
delete pos.id;
addPosition(pos);
});
setTimeout(() => {
loadPositions();
closeImportModal();
alert(`Import erfolgreich! ${filteredData.length} Positionen importiert.`);
}, 500);
});
}
function exportData() {
getAllPositions().then(positions => {
const dataStr = JSON.stringify(positions, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `angebotspositionen_${new Date().toISOString().split('T')[0]}.json`;
link.click();
URL.revokeObjectURL(url);
});
}
function clearDatabase(askConfirm = true) {
return new Promise((resolve) => {
if (askConfirm && !confirm('Alle Positionen wirklich löschen?')) {
resolve();
return;
}
const transaction = db.transaction([STORE_NAME], 'readwrite');
transaction.objectStore(STORE_NAME).clear();
transaction.oncomplete = () => {
loadPositions();
if (askConfirm) alert('Datenbank wurde geleert');
resolve();
};
});
}
// ============================================================================
// HILFE-MODAL
// ============================================================================
function showHelp() {
const helpContent = `
<div class="info-box">
<h3>🎯 Grundprinzip der Sortierung</h3>
<p><strong>STLB-Bau</strong> = Wer macht die Arbeit? (Gewerk/Handwerk)</p>
<p><strong>DIN 276</strong> = Wo gehört es im Gebäude hin? (Funktion/Kostengruppe)</p>
</div>
<h3 style="margin-top: 20px;">📐 STLB-Bau Gewerke - Sortierung nach Handwerk</h3>
<table style="width: 100%; margin-bottom: 20px; border-collapse: collapse;">
<tr style="background: #f8f9fa;">
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6;">Gewerk</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6;">Beispiele</th>
</tr>
<tr><td style="padding: 8px;"><strong>140 - Erdbau</strong></td><td style="padding: 8px;">Aushub, Baugrube</td></tr>
<tr><td style="padding: 8px;"><strong>150 - Mauerarbeiten</strong></td><td style="padding: 8px;">Maurerarbeiten, Kalksandstein setzen</td></tr>
<tr><td style="padding: 8px;"><strong>170 - Betonarbeiten</strong></td><td style="padding: 8px;">Beton gießen, Fundamente</td></tr>
<tr><td style="padding: 8px;"><strong>210 - Dachdeckung</strong></td><td style="padding: 8px;">Dachziegel, Dachsteine</td></tr>
<tr><td style="padding: 8px;"><strong>250 - Trockenbau</strong></td><td style="padding: 8px;">Rigips, Trennwände</td></tr>
<tr><td style="padding: 8px;"><strong>260 - Estrich</strong></td><td style="padding: 8px;">Estrich verlegen</td></tr>
<tr><td style="padding: 8px;"><strong>310 - Malerarbeiten</strong></td><td style="padding: 8px;">Streichen, Tapezieren</td></tr>
<tr><td style="padding: 8px;"><strong>440 - Elektro</strong></td><td style="padding: 8px;">Steckdosen, Beleuchtung</td></tr>
<tr><td style="padding: 8px;"><strong>520 - Sanitär</strong></td><td style="padding: 8px;">Wasser, Abwasser, Heizung</td></tr>
</table>
<h3>🏗️ DIN 276 - Sortierung nach Funktion/Kostengruppe</h3>
<div style="margin-bottom: 10px; padding: 10px; background: #e7f3ff; border-radius: 5px;">
<strong>Ebene 1</strong> - Hauptkostengruppe (z.B. 300 = Bauwerk)
</div>
<div style="margin-bottom: 10px; padding: 10px; background: #e7f3ff; border-radius: 5px;">
<strong>Ebene 2</strong> - Kostengruppe (z.B. 330 = Außenwände)
</div>
<div style="margin-bottom: 20px; padding: 10px; background: #e7f3ff; border-radius: 5px;">
<strong>Ebene 3</strong> - Einzelkosten (z.B. 331 = Tragende Außenwände)
</div>
<table style="width: 100%; margin-bottom: 20px; border-collapse: collapse;">
<tr style="background: #f8f9fa;">
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6;">DIN 276</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6;">Beschreibung</th>
</tr>
<tr><td style="padding: 8px;"><strong>300</strong></td><td style="padding: 8px;">Baukonstruktionen (alles was das Gebäude trägt)</td></tr>
<tr><td style="padding: 8px;"><strong>310</strong></td><td style="padding: 8px;">Baugrube, Erdbau</td></tr>
<tr><td style="padding: 8px;"><strong>320</strong></td><td style="padding: 8px;">Gründung, Fundamente</td></tr>
<tr><td style="padding: 8px;"><strong>330</strong></td><td style="padding: 8px;">Außenwände</td></tr>
<tr><td style="padding: 8px;"><strong>340</strong></td><td style="padding: 8px;">Innenwände</td></tr>
<tr><td style="padding: 8px;"><strong>350</strong></td><td style="padding: 8px;">Decken</td></tr>
<tr><td style="padding: 8px;"><strong>360</strong></td><td style="padding: 8px;">Dächer</td></tr>
<tr><td style="padding: 8px;"><strong>400</strong></td><td style="padding: 8px;">Technische Anlagen</td></tr>
<tr><td style="padding: 8px;"><strong>410</strong></td><td style="padding: 8px;">Wasser, Abwasser, Gas</td></tr>
<tr><td style="padding: 8px;"><strong>420</strong></td><td style="padding: 8px;">Heizung, Wärmeversorgung</td></tr>
<tr><td style="padding: 8px;"><strong>440</strong></td><td style="padding: 8px;">Elektro, Starkstrom</td></tr>
<tr><td style="padding: 8px;"><strong>450</strong></td><td style="padding: 8px;">IT, Kommunikation, Sicherheit</td></tr>
</table>
<h3>💡 Praxis-Beispiele</h3>
<div style="margin-bottom: 15px; padding: 15px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 5px;">
<strong>Beispiel 1: Rigipswand im EG</strong><br>
<strong>STLB:</strong> 250 (Trockenbau) - weil der Trockenbauer es macht<br>
<strong>DIN:</strong> 340 (Innenwände) - weil es eine Innenwand ist
</div>
<div style="margin-bottom: 15px; padding: 15px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 5px;">
<strong>Beispiel 2: Steckdose im Bad</strong><br>
<strong>STLB:</strong> 440 (Elektroinstallation) - weil der Elektriker es macht<br>
<strong>DIN:</strong> 444 (Niederspannungsinstallation) - funktionale Einordnung
</div>
<div style="margin-bottom: 15px; padding: 15px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 5px;">
<strong>Beispiel 3: Dachziegel</strong><br>
<strong>STLB:</strong> 210 (Dachdeckung) - weil der Dachdecker es macht<br>
<strong>DIN:</strong> 363 (Dachbeläge) - weil es ein Dachbelag ist
</div>
<h3>✅ Intelligente Hilfen in der Datenbank</h3>
<ul style="line-height: 1.8;">
<li><strong>Automatische Vorschläge:</strong> Bei Auswahl eines STLB-Gewerks werden passende DIN-Gruppen grün markiert</li>
<li><strong>Duplikat-Warnung:</strong> Beim Eingeben des Begriffs wird geprüft ob ähnliche Positionen existieren</li>
<li><strong>Filter-Logik:</strong> Filter schränken sich gegenseitig ein für bessere Übersicht</li>
</ul>
`;
document.getElementById('helpContent').innerHTML = helpContent;
document.getElementById('helpModal').classList.add('active');
}
function closeHelpModal() {
document.getElementById('helpModal').classList.remove('active');
}
// ============================================================================
// EVENT LISTENERS & INIT
// ============================================================================
document.getElementById('searchInput').addEventListener('input', (e) => {
currentFilters.search = e.target.value;
loadPositions();
});
document.getElementById('positionModal').addEventListener('click', (e) => {
if (e.target.id === 'positionModal') closeModal();
});
document.getElementById('importModal').addEventListener('click', (e) => {
if (e.target.id === 'importModal') closeImportModal();
});
document.getElementById('helpModal').addEventListener('click', (e) => {
if (e.target.id === 'helpModal') closeHelpModal();
});
// ========================================
// AI-ASSISTENT FUNKTIONEN
// ========================================
// Tab-Switching
function switchTab(tabName) {
// Deactivate all tabs
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Activate selected tab
if (tabName === 'database') {
document.querySelector('.tab-button:nth-child(1)').classList.add('active');
document.getElementById('database-tab').classList.add('active');
} else if (tabName === 'ai-assistant') {
document.querySelector('.tab-button:nth-child(2)').classList.add('active');
document.getElementById('ai-assistant-tab').classList.add('active');
}
}
// Room Data Structure
let rooms = [];
// Add Room to Table
function addRoom() {
const roomId = Date.now();
const room = {
id: roomId,
name: '',
fläche: 0,
höhe: 0,
umfang: 0,
anteil: 100
};
rooms.push(room);
renderRoomTable();
updateSummary();
}
// Render Room Table
function renderRoomTable() {
const tbody = document.getElementById('roomTableBody');
if (rooms.length === 0) {
tbody.innerHTML = `
<tr class="no-results">
<td colspan="6" style="text-align: center; padding: 30px;">
Noch keine Räume hinzugefügt. Klicken Sie auf "Raum hinzufügen" um zu starten.
</td>
</tr>
`;
return;
}
tbody.innerHTML = rooms.map(room => `
<tr>
<td>
<input type="text" value="${room.name}"
onchange="updateRoom(${room.id}, 'name', this.value)"
placeholder="z.B. Bad 1.OG rechts">
</td>
<td>
<input type="number" value="${room.fläche}" step="0.01" min="0"
onchange="updateRoom(${room.id}, 'fläche', parseFloat(this.value))">
</td>
<td>
<input type="number" value="${room.höhe}" step="0.01" min="0"
onchange="updateRoom(${room.id}, 'höhe', parseFloat(this.value))">
</td>
<td>
<input type="number" value="${room.umfang}" step="0.01" min="0"
onchange="updateRoom(${room.id}, 'umfang', parseFloat(this.value))">
</td>
<td>
<input type="number" value="${room.anteil}" step="1" min="0" max="100"
onchange="updateRoom(${room.id}, 'anteil', parseFloat(this.value))">
</td>
<td>
<button class="btn btn-danger" onclick="deleteRoom(${room.id})" style="padding: 8px 12px;">🗑️</button>
</td>
</tr>
`).join('');
}
// Update Room Data
function updateRoom(roomId, field, value) {
const room = rooms.find(r => r.id === roomId);
if (room) {
room[field] = value;
updateSummary();
}
}
// Delete Room
function deleteRoom(roomId) {
if (confirm('Raum wirklich löschen?')) {
rooms = rooms.filter(r => r.id !== roomId);
renderRoomTable();
updateSummary();
}
}
// Clear All Rooms
function clearRooms() {
if (confirm('Wirklich alle Räume löschen?')) {
rooms = [];
renderRoomTable();
updateSummary();
}
}
// Update Summary Calculations
function updateSummary() {
let totalBoden = 0;
let totalWand = 0;
let totalDecke = 0;
let totalSockel = 0;
let totalSchutt = 0;
rooms.forEach(room => {
const anteilFaktor = room.anteil / 100;
// Bodenfläche = Fläche × Anteil
totalBoden += room.fläche * anteilFaktor;
// Wandfläche = Umfang × Höhe × Anteil
totalWand += room.umfang * room.höhe * anteilFaktor;
// Deckenfläche = Fläche × Anteil
totalDecke += room.fläche * anteilFaktor;
// Sockelleiste = Umfang × Anteil
totalSockel += room.umfang * anteilFaktor;
// Schutt/Entsorgung = Fläche × Höhe × 0.1 (Faktor für Schuttvolumen)
totalSchutt += room.fläche * room.höhe * 0.1;
});
document.getElementById('sum-boden').textContent = totalBoden.toFixed(2) + ' m²';
document.getElementById('sum-wand').textContent = totalWand.toFixed(2) + ' m²';
document.getElementById('sum-decke').textContent = totalDecke.toFixed(2) + ' m²';
document.getElementById('sum-sockel').textContent = totalSockel.toFixed(2) + ' m';
document.getElementById('sum-schutt').textContent = totalSchutt.toFixed(2) + ' m³';
document.getElementById('sum-rooms').textContent = rooms.length;
}
// Save Template to localStorage
function saveTemplate() {
const projektname = document.getElementById('ai-projektname').value;
if (!projektname) {
alert('Bitte geben Sie einen Projektnamen ein, bevor Sie ein Template speichern.');
return;
}
if (rooms.length === 0) {
alert('Bitte fügen Sie mindestens einen Raum hinzu.');
return;
}
const templateName = prompt('Template-Name:', projektname);
if (!templateName) return;
const template = {
name: templateName,
schadensart: document.getElementById('ai-schadensart').value,
rooms: rooms,
timestamp: new Date().toISOString()
};
// Get existing templates
const templates = JSON.parse(localStorage.getItem('aiTemplates') || '[]');
// Check if template with same name exists
const existingIndex = templates.findIndex(t => t.name === templateName);
if (existingIndex >= 0) {
if (!confirm(`Template "${templateName}" existiert bereits. Überschreiben?`)) {
return;
}
templates[existingIndex] = template;
} else {
templates.push(template);
}
localStorage.setItem('aiTemplates', JSON.stringify(templates));
alert(`Template "${templateName}" erfolgreich gespeichert!`);
}
// Load Template from localStorage
function loadTemplate() {
const templates = JSON.parse(localStorage.getItem('aiTemplates') || '[]');
if (templates.length === 0) {
alert('Keine Templates vorhanden. Erstellen Sie zuerst ein Template mit "Template speichern".');
return;
}
// Create selection dialog
const templateNames = templates.map((t, i) => `${i + 1}. ${t.name} (${new Date(t.timestamp).toLocaleDateString()})`).join('\n');
const selection = prompt(`Verfügbare Templates:\n\n${templateNames}\n\nGeben Sie die Nummer ein:`, '1');
if (!selection) return;
const index = parseInt(selection) - 1;
if (index < 0 || index >= templates.length) {
alert('Ungültige Auswahl.');
return;
}
const template = templates[index];
// Load template data
document.getElementById('ai-schadensart').value = template.schadensart || '';
rooms = JSON.parse(JSON.stringify(template.rooms)); // Deep copy
// Regenerate IDs to avoid conflicts
rooms.forEach(room => {
room.id = Date.now() + Math.random();
});
renderRoomTable();
updateSummary();
alert(`Template "${template.name}" erfolgreich geladen!`);
}
// Generate Claude Prompt
function generatePrompt() {
const projektname = document.getElementById('ai-projektname').value;
const schadensart = document.getElementById('ai-schadensart').value;
if (!projektname || !schadensart) {
alert('Bitte füllen Sie Projektname und Schadensart aus.');
return;
}
if (rooms.length === 0) {
alert('Bitte fügen Sie mindestens einen Raum hinzu.');
return;
}
// Calculate totals
let totalBoden = 0;
let totalWand = 0;
let totalDecke = 0;
let totalSockel = 0;
let totalSchutt = 0;
rooms.forEach(room => {
const anteilFaktor = room.anteil / 100;
totalBoden += room.fläche * anteilFaktor;
totalWand += room.umfang * room.höhe * anteilFaktor;
totalDecke += room.fläche * anteilFaktor;
totalSockel += room.umfang * anteilFaktor;
totalSchutt += room.fläche * room.höhe * 0.1;
});
// Hole zusätzliche Felder aus Import-Analyse (falls vorhanden)
const extraGebaeude = document.getElementById('extra-gebaeude-typ')?.value || '';
const extraBauart = document.getElementById('extra-bauart')?.value || '';
const extraBaujahr = document.getElementById('extra-baujahr')?.value || '';
const extraDeckenkonstruktion = document.getElementById('extra-deckenkonstruktion')?.value || '';
const extraBesonderheiten = document.getElementById('extra-besonderheiten')?.value || '';
// Generate JSON prompt
const promptData = {
projekt: {
name: projektname,
schadensart: schadensart,
datum: new Date().toLocaleDateString('de-DE')
},
gebäudeinformationen: {
typ: extraGebaeude,
bauart: extraBauart,
baujahr: extraBaujahr,
deckenkonstruktion: extraDeckenkonstruktion,
besonderheiten: extraBesonderheiten
},
räume: rooms.map(r => ({
name: r.name,
fläche: r.fläche,
höhe: r.höhe,
umfang: r.umfang,
anteil: r.anteil
})),
gesamtmengen: {
bodenfläche: parseFloat(totalBoden.toFixed(2)),
wandfläche: parseFloat(totalWand.toFixed(2)),
deckenfläche: parseFloat(totalDecke.toFixed(2)),
sockelleiste: parseFloat(totalSockel.toFixed(2)),
schutt: parseFloat(totalSchutt.toFixed(2))
}
};
// Füge nur AUSGEWÄHLTE Vermerke hinzu
if (window.importedSchadenProvData?.vermerke && window.importedSchadenProvData.vermerke.length > 0) {
const selectedVermerke = [];
const checkboxes = document.querySelectorAll('.vermerk-checkbox');
checkboxes.forEach((checkbox, index) => {
if (checkbox.checked && window.importedSchadenProvData.vermerke[index]) {
const v = window.importedSchadenProvData.vermerke[index];
selectedVermerke.push({
datum: v.datum || '',
beschreibung: v.beschreibung || '',
aufwandsart: v.aufwandsart || ''
});
}
});
if (selectedVermerke.length > 0) {
promptData.vermerke = selectedVermerke;
}
}
const prompt = `Ich benötige ein vollständiges Leistungsverzeichnis (LV) für folgendes Projekt:
PROJEKT-DATEN:
${JSON.stringify(promptData, null, 2)}
AUFGABE:
1. Analysiere die Räume und benötigten Mengen
2. Berücksichtige die Gebäudeinformationen (Typ, Bauart, Baujahr, Deckenkonstruktion)
3. Beachte alle Vermerke - sie enthalten wichtige Details zum Schadenfall!
4. Suche in der angebotsdatenbank-vollstaendig.json nach passenden Positionen für "${schadensart}"
5. Ergänze fehlende Positionen falls nötig
6. Erstelle ein vollständiges LV nach folgendem Format:
═══════════════════════════════════════════
LEISTUNGSVERZEICHNIS
Projekt: ${projektname}
Schadensart: ${schadensart}
═══════════════════════════════════════════
───────────────────────────────────────────
STLB XXX - [Gewerk-Bezeichnung]
───────────────────────────────────────────
Pos. X.X [Positions-Begriff]
Gesamt: XX.XX [Einheit]
${rooms.map(r => ` - ${r.name}: [Menge] [Einheit]`).join('\n')}
EP: XX.XX € | GP: XXX.XX €
WICHTIG:
- Gruppiere nach STLB-Gewerken
- Zeige bei jeder Position die Einzelmengen pro Raum
- Berücksichtige den Anteil-Prozentsatz bei der Mengenberechnung
- Verwende realistische Preise aus der Datenbank
- Sortiere Positionen logisch nach Arbeitsablauf`;
// Show preview
const preview = document.getElementById('promptPreview');
preview.textContent = prompt;
preview.style.display = 'block';
// Copy to clipboard
navigator.clipboard.writeText(prompt).then(() => {
alert('✅ Prompt wurde in die Zwischenablage kopiert!\n\nSie können ihn jetzt in Claude Code einfügen.');
}).catch(err => {
alert('Fehler beim Kopieren: ' + err);
});
}
// ========================================
// SCHADENPROV v2 INTEGRATION
// ========================================
/**
* Importiert einen SchadenProv v2 Schadenfall-Export und befüllt automatisch alle Felder
* @param {Event} event - File input change event
*/
async function handleSchadenProvImport(event) {
const file = event.target.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
// Validiere Export-Format
if (data.exportType !== 'einzelner-schadenfall') {
alert('❌ Ungültiges Format!\n\nBitte exportieren Sie einen einzelnen Schadenfall aus SchadenProv v2.');
return;
}
if (!data.schadenfall) {
alert('❌ Keine Schadenfall-Daten gefunden!');
return;
}
const schadenfall = data.schadenfall;
// 1. Projektname befüllen
let projektname = '';
if (schadenfall.versicherungsnehmer) {
projektname = `${schadenfall.versicherungsnehmer}`;
}
if (schadenfall.schadenort) {
projektname += projektname ? ` - ${schadenfall.schadenort}` : schadenfall.schadenort;
}
if (schadenfall.schadennummer) {
projektname = `${schadenfall.schadennummer}${projektname ? ' - ' + projektname : ''}`;
}
if (!projektname) {
projektname = schadenfall.schadennummer || 'Schadenfall';
}
document.getElementById('ai-projektname').value = projektname;
// 2. Schadensart befüllen
if (schadenfall.schadenart) {
const schadensartSelect = document.getElementById('ai-schadensart');
const schadensart = schadenfall.schadenart;
// Versuche exakte Übereinstimmung zu finden
let matched = false;
for (let option of schadensartSelect.options) {
if (option.value.toLowerCase().includes(schadensart.toLowerCase()) ||
schadensart.toLowerCase().includes(option.value.toLowerCase())) {
schadensartSelect.value = option.value;
matched = true;
break;
}
}
// Falls keine Übereinstimmung: Wähle "Sonstiges"
if (!matched) {
schadensartSelect.value = 'Sonstiges';
}
}
// 3. Räume importieren
const importedRooms = data.raeume || schadenfall.raeume || [];
if (importedRooms.length > 0) {
// Lösche bestehende Räume
rooms = [];
// Importiere Räume aus SchadenProv
importedRooms.forEach(raum => {
rooms.push({
id: Date.now() + Math.random(), // Eindeutige ID
name: raum.name || '',
fläche: parseFloat(raum.flaeche) || 0,
höhe: parseFloat(raum.hoehe) || 0,
umfang: parseFloat(raum.umfang) || 0,
anteil: parseInt(raum.anteil) || 100
});
});
renderRoomTable();
updateSummary();
}
// Zeige Import-Analyse Bereich
document.getElementById('import-analysis-section').style.display = 'block';
// 4. Importierte Daten anzeigen
displayImportedData(data);
// 5. Intelligente Analyse durchführen
const analysisResults = analyzeImportedData(data);
displayAnalysisResults(analysisResults);
// 6. Zusätzliche Felder generieren basierend auf Analyse
generateAdditionalFields(analysisResults);
// Speichere importierte Daten global für Prompt-Generierung
window.importedSchadenProvData = data;
window.lastAnalysisResults = analysisResults; // Speichere auch Analyse-Ergebnisse
// Zeige SchadenProf-Kontext in Wunschliste an
updateWishlistSchadenprofContext();
// Erfolgs-Nachricht
alert(`✅ SchadenProv-Import erfolgreich!\n\nProjektname: ${projektname}\nSchadensart: ${schadenfall.schadenart || 'k.A.'}\nRäume: ${importedRooms.length}\nVermerke: ${data.vermerke?.length || 0}\n\n💡 Siehe Import-Analyse für Details und zusätzliche Informationen.`);
// Wechsle zum AI-Assistent Tab falls noch nicht aktiv
switchTab('ai-assistant');
// Reset file input für erneuten Import
event.target.value = '';
} catch (error) {
console.error('Fehler beim Import:', error);
alert(`❌ Fehler beim Importieren:\n\n${error.message}\n\nBitte prüfen Sie, ob die Datei ein gültiger SchadenProv v2 Export ist.`);
}
}
/**
* Zeigt die vollständigen importierten Daten an
*/
function displayImportedData(data) {
const display = document.getElementById('imported-data-display');
let html = '';
const schadenfall = data.schadenfall;
// Grunddaten
html += '<div style="margin-bottom: 1rem;"><strong>📄 Schadenfall-Daten:</strong><br>';
html += `Schadennummer: ${schadenfall.schadennummer || 'k.A.'}<br>`;
html += `Versicherungsnehmer: ${schadenfall.versicherungsnehmer || 'k.A.'}<br>`;
html += `Schadenort: ${schadenfall.schadenort || 'k.A.'}<br>`;
html += `Schadenart: ${schadenfall.schadenart || 'k.A.'}<br>`;
html += `Schadendatum: ${schadenfall.schadendatum || 'k.A.'}</div>`;
// Räume
const raeume = data.raeume || [];
if (raeume.length > 0) {
html += '<div style="margin-bottom: 1rem;"><strong>🏠 Räume:</strong><br>';
raeume.forEach(raum => {
html += `${raum.name} (${raum.flaeche}m², ${raum.hoehe}m Höhe, ${raum.anteil}% Anteil)<br>`;
});
html += '</div>';
}
// Vermerke (WICHTIG!)
const vermerke = data.vermerke || [];
if (vermerke.length > 0) {
html += `<div style="margin-bottom: 1rem;">`;
html += `<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">`;
html += `<strong>📝 Vermerke (${vermerke.length}):</strong>`;
html += `<div style="display: flex; gap: 0.5rem;">`;
html += `<button onclick="selectAllVermerke(true)" style="padding: 0.25rem 0.5rem; font-size: 0.85rem; cursor: pointer; background: #4caf50; color: white; border: none; border-radius: 3px;">✓ Alle</button>`;
html += `<button onclick="selectAllVermerke(false)" style="padding: 0.25rem 0.5rem; font-size: 0.85rem; cursor: pointer; background: #f44336; color: white; border: none; border-radius: 3px;">✗ Keine</button>`;
html += `</div></div>`;
html += '<div style="max-height: 300px; overflow-y: auto; padding: 0.5rem; background: #fff; border-radius: 4px; margin-top: 0.5rem;">';
vermerke.forEach((vermerk, index) => {
const datum = vermerk.datum ? new Date(vermerk.datum).toLocaleDateString('de-DE') : 'k.A.';
html += `<div style="margin-bottom: 0.5rem; padding: 0.5rem; border-left: 3px solid #ff9800; background: #fff3e0; display: flex; gap: 0.5rem;">`;
html += `<input type="checkbox" id="vermerk-${index}" class="vermerk-checkbox" checked style="cursor: pointer; flex-shrink: 0; margin-top: 0.25rem;">`;
html += `<label for="vermerk-${index}" style="cursor: pointer; flex-grow: 1;">`;
html += `<strong>${datum}</strong> | ${vermerk.aufwandsart || 'Vermerk'}<br>`;
html += `${vermerk.beschreibung || 'Keine Beschreibung'}`;
html += `</label>`;
html += `</div>`;
});
html += '</div></div>';
}
// Aufgaben
const aufgaben = data.aufgaben || [];
if (aufgaben.length > 0) {
html += `<div style="margin-bottom: 1rem;"><strong>✓ Aufgaben (${aufgaben.length}):</strong><br>`;
aufgaben.forEach(aufgabe => {
const status = aufgabe.erledigt ? '✅' : '⏳';
html += `${status} ${aufgabe.beschreibung || 'k.A.'}<br>`;
});
html += '</div>';
}
// Statistiken
const stats = data.stats;
if (stats) {
html += '<div><strong>📊 Statistiken:</strong><br>';
if (stats.gesamtFlaeche) html += `Gesamtfläche: ${stats.gesamtFlaeche}m²<br>`;
if (stats.anzahlRaeume) html += `Anzahl Räume: ${stats.anzahlRaeume}<br>`;
if (stats.anzahlVermerke) html += `Anzahl Vermerke: ${stats.anzahlVermerke}<br>`;
if (stats.anzahlAufgaben) html += `Anzahl Aufgaben: ${stats.anzahlAufgaben}</div>`;
}
display.innerHTML = html;
}
/**
* Analysiert die importierten Daten intelligent und extrahiert relevante Informationen
*/
function analyzeImportedData(data) {
const analysis = {
detected: [],
missing: [],
suggestions: [],
keywords: {
gebaeude: null,
bauart: null,
baujahr: null,
deckenkonstruktion: null,
besonderheiten: []
}
};
// Kombiniere alle Textquellen für Analyse
let allText = '';
const schadenfall = data.schadenfall;
if (schadenfall.schadenort) allText += schadenfall.schadenort + ' ';
if (schadenfall.schadenart) allText += schadenfall.schadenart + ' ';
const vermerke = data.vermerke || [];
vermerke.forEach(v => {
if (v.beschreibung) allText += v.beschreibung + ' ';
});
const textLower = allText.toLowerCase();
// 1. Gebäudetyp erkennen
if (textLower.includes('mehrfamilienhaus') || textLower.includes('mfh')) {
analysis.detected.push('✓ Gebäudetyp: Mehrfamilienhaus (MFH)');
analysis.keywords.gebaeude = 'Mehrfamilienhaus';
} else if (textLower.includes('einfamilienhaus') || textLower.includes('efh')) {
analysis.detected.push('✓ Gebäudetyp: Einfamilienhaus (EFH)');
analysis.keywords.gebaeude = 'Einfamilienhaus';
} else if (textLower.includes('reihenhaus') || textLower.includes('rh')) {
analysis.detected.push('✓ Gebäudetyp: Reihenhaus');
analysis.keywords.gebaeude = 'Reihenhaus';
} else if (textLower.includes('gewerbe') || textLower.includes('büro')) {
analysis.detected.push('✓ Gebäudetyp: Gewerbeimmobilie');
analysis.keywords.gebaeude = 'Gewerbeimmobilie';
} else {
analysis.missing.push('❓ Gebäudetyp nicht erkannt');
}
// 2. Bauart erkennen
if (textLower.includes('altbau')) {
analysis.detected.push('✓ Bauart: Altbau');
analysis.keywords.bauart = 'Altbau';
} else if (textLower.includes('neubau')) {
analysis.detected.push('✓ Bauart: Neubau');
analysis.keywords.bauart = 'Neubau';
} else {
analysis.missing.push('❓ Bauart (Altbau/Neubau) nicht erkannt');
}
// 3. Baujahr extrahieren (4-stellige Zahl zwischen 1800-2030)
const jahrMatch = allText.match(/\b(18\d{2}|19\d{2}|20[0-2]\d|2030)\b/);
if (jahrMatch) {
analysis.detected.push(`✓ Baujahr: ${jahrMatch[0]}`);
analysis.keywords.baujahr = jahrMatch[0];
} else {
analysis.missing.push('❓ Baujahr nicht gefunden');
}
// 4. Deckenkonstruktion
if (textLower.includes('holzbalkendecke') || textLower.includes('holzbalken')) {
analysis.detected.push('✓ Deckenkonstruktion: Holzbalkendecke');
analysis.keywords.deckenkonstruktion = 'Holzbalkendecke';
} else if (textLower.includes('betondecke') || textLower.includes('massivdecke') || textLower.includes('stahlbeton')) {
analysis.detected.push('✓ Deckenkonstruktion: Betondecke');
analysis.keywords.deckenkonstruktion = 'Betondecke';
} else {
analysis.missing.push('❓ Deckenkonstruktion nicht erkannt');
}
// 5. Besonderheiten erkennen
if (textLower.includes('fußbodenheizung') || textLower.includes('fbh')) {
analysis.detected.push('✓ Besonderheit: Fußbodenheizung vorhanden');
analysis.keywords.besonderheiten.push('Fußbodenheizung');
}
if (textLower.includes('denkmalschutz')) {
analysis.detected.push('✓ Besonderheit: Denkmalschutz');
analysis.keywords.besonderheiten.push('Denkmalschutz');
}
if (textLower.includes('schimmel')) {
analysis.detected.push('✓ Besonderheit: Schimmelbefall erkannt');
analysis.keywords.besonderheiten.push('Schimmelbefall');
}
if (textLower.includes('asbest')) {
analysis.detected.push('⚠️ WICHTIG: Asbest erwähnt - Spezialentsorgung erforderlich!');
analysis.keywords.besonderheiten.push('Asbest - Spezialentsorgung!');
}
// 6. Schadendetails
const wasserhoeheMatch = allText.match(/(\d+)\s*(cm|centimeter)/i);
if (wasserhoeheMatch) {
analysis.detected.push(`✓ Wasserhöhe: ${wasserhoeheMatch[1]}cm`);
analysis.keywords.besonderheiten.push(`Wasserhöhe ${wasserhoeheMatch[1]}cm`);
}
const einwirkdauerMatch = allText.match(/(\d+)\s*(stunde|std|h)/i);
if (einwirkdauerMatch) {
analysis.detected.push(`✓ Einwirkdauer: ca. ${einwirkdauerMatch[1]} Stunden`);
analysis.keywords.besonderheiten.push(`Einwirkdauer ${einwirkdauerMatch[1]}h`);
}
// 7. Intelligente Vorschläge
if (analysis.keywords.bauart === 'Altbau' && !analysis.keywords.baujahr) {
analysis.suggestions.push('💡 Bei Altbauten ist das Baujahr wichtig (z.B. für Statik, Materialien)');
}
if (analysis.keywords.deckenkonstruktion === 'Holzbalkendecke') {
analysis.suggestions.push('💡 Holzbalkendecken erfordern oft längere Trocknungszeiten');
if (!textLower.includes('trocknung')) {
analysis.suggestions.push('💡 Technische Trocknung wahrscheinlich erforderlich');
}
}
if (schadenfall.schadenart?.toLowerCase().includes('wasser') && !textLower.includes('durchfeucht')) {
analysis.suggestions.push('💡 Durchfeuchtungsgrad (in %) wäre hilfreich für Kosteneinschätzung');
}
return analysis;
}
/**
* Zeigt die Analyseergebnisse an
*/
function displayAnalysisResults(analysis) {
const resultsDiv = document.getElementById('analysis-results');
let html = '';
// Erkannte Informationen
if (analysis.detected.length > 0) {
html += '<div style="margin-bottom: 1rem;"><strong>✅ Erkannte Informationen:</strong><br>';
analysis.detected.forEach(item => {
html += `${item}<br>`;
});
html += '</div>';
}
// Fehlende Informationen
if (analysis.missing.length > 0) {
html += '<div style="margin-bottom: 1rem; color: #e65100;"><strong>❓ Fehlende Informationen:</strong><br>';
analysis.missing.forEach(item => {
html += `${item}<br>`;
});
html += '</div>';
}
// Intelligente Vorschläge
if (analysis.suggestions.length > 0) {
html += '<div style="color: #1976d2;"><strong>💡 Empfehlungen:</strong><br>';
analysis.suggestions.forEach(item => {
html += `${item}<br>`;
});
html += '</div>';
}
resultsDiv.innerHTML = html;
}
/**
* Generiert zusätzliche Eingabefelder basierend auf der Analyse
*/
function generateAdditionalFields(analysis) {
const container = document.getElementById('additional-fields-container');
let html = '';
html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">';
// Gebäudetyp
html += '<div class="form-group">';
html += '<label class="filter-label">Gebäudetyp</label>';
html += '<select id="extra-gebaeude-typ" class="form-control">';
html += '<option value="">-- Bitte wählen --</option>';
const gebaeudeTpyen = ['Einfamilienhaus', 'Mehrfamilienhaus', 'Reihenhaus', 'Doppelhaushälfte', 'Gewerbeimmobilie', 'Sonstiges'];
gebaeudeTpyen.forEach(typ => {
const selected = analysis.keywords.gebaeude === typ ? ' selected' : '';
html += `<option value="${typ}"${selected}>${typ}</option>`;
});
html += '</select></div>';
// Bauart
html += '<div class="form-group">';
html += '<label class="filter-label">Bauart</label>';
html += '<select id="extra-bauart" class="form-control">';
html += '<option value="">-- Bitte wählen --</option>';
const bauarten = ['Altbau', 'Neubau', 'Saniert'];
bauarten.forEach(art => {
const selected = analysis.keywords.bauart === art ? ' selected' : '';
html += `<option value="${art}"${selected}>${art}</option>`;
});
html += '</select></div>';
// Baujahr
html += '<div class="form-group">';
html += '<label class="filter-label">Baujahr</label>';
const baujahr = analysis.keywords.baujahr || '';
html += `<input type="number" id="extra-baujahr" class="form-control" placeholder="z.B. 1952" value="${baujahr}">`;
html += '</div>';
// Deckenkonstruktion
html += '<div class="form-group">';
html += '<label class="filter-label">Deckenkonstruktion</label>';
html += '<select id="extra-deckenkonstruktion" class="form-control">';
html += '<option value="">-- Bitte wählen --</option>';
const deckenarten = ['Holzbalkendecke', 'Betondecke (Stahlbeton)', 'Massivdecke', 'Leichtbaudecke'];
deckenarten.forEach(art => {
const selected = analysis.keywords.deckenkonstruktion === art ? ' selected' : '';
html += `<option value="${art}"${selected}>${art}</option>`;
});
html += '</select></div>';
html += '</div>'; // End grid
// Besonderheiten Textarea
html += '<div class="form-group" style="margin-top: 1rem;">';
html += '<label class="filter-label">Besonderheiten / Zusätzliche Informationen</label>';
const besonderheiten = analysis.keywords.besonderheiten.join(', ');
html += `<textarea id="extra-besonderheiten" class="form-control" rows="3" placeholder="z.B. Fußbodenheizung, Denkmalschutz, spezielle Materialien...">${besonderheiten}</textarea>`;
html += '</div>';
container.innerHTML = html;
}
/**
* Toggle für Collapsible Sections
*/
function toggleSection(sectionId) {
const section = document.getElementById(sectionId);
const toggle = document.getElementById(sectionId + '-toggle');
if (section.style.display === 'none') {
section.style.display = 'block';
toggle.textContent = '▲';
} else {
section.style.display = 'none';
toggle.textContent = '▼';
}
}
/**
* Alle/Keine Vermerke auswählen
* @param {boolean} select - true = alle auswählen, false = alle abwählen
*/
function selectAllVermerke(select) {
const checkboxes = document.querySelectorAll('.vermerk-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = select;
});
}
initDB();
</script>
<footer style="text-align:center;padding:1rem;margin-top:2rem;border-top:1px solid #e5e7eb;font-size:0.85rem;color:#6b7280;">
<a href="#" onclick="openImpressum();return false;" style="color:#6b7280;text-decoration:none;">Impressum</a>
<span style="color:#d1d5db;margin:0 0.5rem;">|</span>
<a href="#" onclick="openDatenschutz();return false;" style="color:#6b7280;text-decoration:none;">Datenschutz</a>
</footer>
<!-- IMPRESSUM MODAL -->
<div class="legal-modal" id="impressumModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Impressum</h2>
<button onclick="closeImpressum()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/impressum.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<!-- DATENSCHUTZ MODAL -->
<div class="legal-modal" id="datenschutzModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Datenschutz</h2>
<button onclick="closeDatenschutz()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/datenschutz.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<script>
function openImpressum(){document.getElementById("impressumModal").style.display="flex";}
function closeImpressum(){document.getElementById("impressumModal").style.display="none";}
function openDatenschutz(){document.getElementById("datenschutzModal").style.display="flex";}
function closeDatenschutz(){document.getElementById("datenschutzModal").style.display="none";}
document.addEventListener("keydown",function(e){if(e.key==="Escape"){closeImpressum();closeDatenschutz();}});
["impressumModal","datenschutzModal"].forEach(function(id){
var el=document.getElementById(id);
if(el)el.addEventListener("click",function(e){if(e.target===this)this.style.display="none";});
});
</script>
</body>
</html>