4072 lines
163 KiB
HTML
4072 lines
163 KiB
HTML
<!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²">m²</option>
|
||
<option value="m³">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äschereitechnische 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;">×</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;">×</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>
|