Files

3044 lines
159 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block content %}
{% if locked_by_name %}
<div style="background:#e74c3c;color:#fff;padding:8px 16px;text-align:center;font-weight:600;font-size:0.85rem;">🔒 {{ locked_by_name }} bearbeitet dieses Aufmaß gerade Sie haben nur Lesezugriff.</div>
{% endif %}
<div class="level mb-2">
<div class="level-left">
<h1 class="title is-4 mb-0">{{ aufmass.name }}</h1>
{% if aufmass.typ %}<span class="tag is-info is-light ml-2">{{ aufmass.typ }}</span>{% endif %}
<span class="tag is-medium {{ 'is-success' if aufmass.status == 'aktiv' else ('is-warning' if aufmass.status == 'abgeschlossen' else 'is-light') }} ml-1">{{ aufmass.status }}</span>
<span class="ml-3 has-text-grey is-size-6">{{ project.bezeichnung or project.sm_nr }}</span>
</div>
<div class="level-right" style="display:flex;align-items:center;gap:4px">
<button class="button is-small is-dark" id="btn-export-menu" onclick="toggleExportMenu()">📤 Export ▾</button>
<div id="export-dropdown" style="display:none;position:absolute;z-index:1000;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);padding:8px;min-width:220px;margin-top:120px">
<div style="padding:4px 6px;font-size:0.75rem;color:#888;border-bottom:1px solid #eee;margin-bottom:4px">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
<input type="checkbox" id="chk-aktuelle-ansicht"> Nur aktuelle Ansicht
</label>
</div>
<div style="display:flex;flex-direction:column;gap:6px">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 6px;border-radius:4px" onmouseover="this.style.background='#f5f5f5'" onmouseout="this.style.background=''">
<input type="checkbox" id="export-excel" onchange="updateExportZipButton()"> 📊 Excel
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 6px;border-radius:4px" onmouseover="this.style.background='#f5f5f5'" onmouseout="this.style.background=''">
<input type="checkbox" id="export-pdf" onchange="updateExportZipButton()"> 📄 PDF
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 6px;border-radius:4px" onmouseover="this.style.background='#f5f5f5'" onmouseout="this.style.background=''">
<input type="checkbox" id="export-txt" onchange="updateExportZipButton()"> 📄 TXT
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 6px;border-radius:4px" onmouseover="this.style.background='#f5f5f5'" onmouseout="this.style.background=''">
<input type="checkbox" id="export-x31" onchange="updateExportZipButton()"> 📋 X31
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 6px;border-radius:4px" onmouseover="this.style.background='#f5f5f5'" onmouseout="this.style.background=''">
<input type="checkbox" id="export-x31ca" onchange="updateExportZipButton()"> 📋 X31 California
</label>
</div>
<div id="export-zip-section" style="display:none;margin-top:10px;padding-top:10px;border-top:1px solid #eee">
<button class="button is-small is-success" style="width:100%" onclick="downloadZipExport()">📦 ZIP Download</button>
</div>
</div>
<button class="button is-small is-info" id="btn-history-log" onclick="showHistoryLog()">📜 History</button>
<a class="button is-small is-light" href="{{ url_for('aufmass.aufmass_list', project_id=project.id) }}">← Aufmaß-Übersicht</a>
</div>
</div>
<div class="box mb-2" style="padding:0.4rem 0.75rem">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
{% set ns_vis = namespace(val=0) %}
{% for mod in modules %}{% if ('module', mod.id|string) not in hidden_set and ('module', mod.id) not in hidden_set %}{% set ns_vis.val = ns_vis.val + 1 %}{% endif %}{% endfor %}
{% for cm in custom_modules %}{% if ('custom', cm.id|string) not in hidden_set and ('custom', cm.id) not in hidden_set %}{% set ns_vis.val = ns_vis.val + 1 %}{% endif %}{% endfor %}
<details id="zusatz-details" style="font-size:0.95rem">
<summary style="cursor:pointer;font-weight:600;color:#2F5496;user-select:none">⚡ Zusatzmodule ({{ ns_vis.val }})</summary>
<div id="modul-sortable" style="display:flex;flex-wrap:wrap;gap:4px;margin:4px 0 0 0">
{% for mod in modules %}
{% if ('module', mod.id|string) not in hidden_set and ('module', mod.id) not in hidden_set %}
<a class="button is-small is-outlined js-mod-btn" style="color:#2F5496;cursor:grab" data-type="module" data-id="{{ mod.id }}" hx-get="{{ url_for('modules.formular', module_name=mod.name, aufmass_id=aufmass.id) }}"
hx-target="#modul-modal-body" hx-trigger="click[!event.ctrlKey]" hx-on::after-request="document.getElementById('modul-modal').classList.add('is-active');document.getElementById('modul-modal-title').textContent='{{ mod.icon }} {{ mod.titel }}';setTimeout(function(){restoreModulFormState()},0)">{{ mod.icon }} {{ mod.titel }}</a>
{% endif %}
{% endfor %}
{% for cm in custom_modules %}
{% if ('custom', cm.id|string) not in hidden_set and ('custom', cm.id) not in hidden_set %}
<a class="button is-small is-outlined js-mod-btn" style="color:#7C3AED;background:rgba(124,58,237,0.06);border-color:#7C3AED;cursor:grab" data-type="custom" data-id="{{ cm.id }}" hx-get="{{ url_for('custom_modules.formular', module_id=cm.id, aufmass_id=aufmass.id) }}"
hx-target="#modul-modal-body" hx-trigger="click[!event.ctrlKey]" hx-on::after-request="document.getElementById('modul-modal').classList.add('is-active');document.getElementById('modul-modal-title').textContent='{{ cm.icon }} {{ cm.name }}';setTimeout(function(){restoreModulFormState()},0)">{{ cm.icon }} {{ cm.name }}</a>
{% endif %}
{% endfor %}
</div>
</details>
<span id="batch-info" style="display:none;font-size:0.75rem;color:#555">
<span id="sel-count">0</span> ausgewählt
<button class="button is-small is-danger" id="btn-hide-selected" style="font-size:0.7rem;padding:2px 8px">Ausblenden</button>
<button class="button is-small is-info" id="btn-show-selected" style="font-size:0.7rem;padding:2px 8px">Einblenden</button>
<button class="button is-small is-light" id="btn-clear-selection" style="font-size:0.7rem;padding:2px 8px"></button>
</span>
</div>
{% set ns = namespace(hc=0) %}
{% for mod in modules %}{% if ('module', mod.id|string) in hidden_set or ('module', mod.id) in hidden_set %}{% set ns.hc = ns.hc + 1 %}{% endif %}{% endfor %}
{% for cm in custom_modules %}{% if ('custom', cm.id|string) in hidden_set or ('custom', cm.id) in hidden_set %}{% set ns.hc = ns.hc + 1 %}{% endif %}{% endfor %}
{% if ns.hc > 0 %}
<details id="hidden-details" style="font-size:0.8rem;margin-top:4px">
<summary style="cursor:pointer;color:#888;font-weight:600">👁 Ausgeblendete ({{ ns.hc }})</summary>
<div id="modul-sortable-hidden" style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">
{% for mod in modules %}{% if ('module', mod.id|string) in hidden_set or ('module', mod.id) in hidden_set %}
<a class="button is-small is-light is-outlined js-mod-btn" style="font-size:0.7rem;color:#2F5496;opacity:0.45;border-color:#2F5496" data-type="module" data-id="{{ mod.id }}">{{ mod.icon }} {{ mod.titel }}</a>
{% endif %}{% endfor %}
{% for cm in custom_modules %}{% if ('custom', cm.id|string) in hidden_set or ('custom', cm.id) in hidden_set %}
<a class="button is-small is-light is-outlined js-mod-btn" style="font-size:0.7rem;color:#7C3AED;opacity:0.7;border-color:#7C3AED;background:rgba(124,58,237,0.08)" data-type="custom" data-id="{{ cm.id }}">{{ cm.icon }} {{ cm.name }}</a>
{% endif %}{% endfor %}
</div>
</details>
{% endif %}
</div>
<style>
#modul-sortable .js-mod-btn.selected,#modul-sortable-hidden .js-mod-btn.selected{outline:2px solid #3273dc;outline-offset:1px}
</style>
<details class="box mb-2" style="padding:0.4rem 0.75rem">
<summary class="has-text-weight-bold has-text-link" style="cursor:pointer">📋 Kopfdaten</summary>
<form method="POST" action="{{ url_for('aufmass.project_kopfdaten_save', project_id=project.id) }}" class="mt-2">
<input type="hidden" name="aufmass_id" value="{{ aufmass.id }}">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;gap:4px 8px;font-size:0.8rem">
<div style="grid-column:1/3"><label class="label is-small">Vertrag</label><input class="input is-small" name="vertrag" value="{{ project.vertrag or '' }}"></div>
<div style="grid-column:3/5"><label class="label is-small">LV-Name</label><input class="input is-small" name="lv_name" value="{{ project.lv_name or '' }}"></div>
<div style="grid-column:5/7"><label class="label is-small">Typ</label><input class="input is-small" name="typ" value="{{ aufmass.typ or '' }}"></div>
<div style="grid-column:7/9"><label class="label is-small">Aufmaß-Datum</label><input class="input is-small" name="datum" type="date" value="{{ project.datum.strftime('%Y-%m-%d') if project.datum else '' }}"></div>
<div style="grid-column:1/5"><label class="label is-small">Projekt</label><input class="input is-small" name="bezeichnung" value="{{ project.bezeichnung or '' }}"></div>
<div style="grid-column:5/9"><label class="label is-small">Baustelle</label><input class="input is-small" name="baustelle" value="{{ project.baustelle or '' }}"></div>
<div style="grid-column:9/13"><label class="label is-small">Bauabschnitt</label><input class="input is-small" name="bauabschnitt" value="{{ project.bauabschnitt or '' }}"></div>
<div style="grid-column:1/4"><label class="label is-small">SM-Nr.</label><input class="input is-small" name="sm_nr" value="{{ project.sm_nr or '' }}"></div>
<div style="grid-column:4/7"><label class="label is-small">Abruf-Nr.</label><input class="input is-small" name="abruf_nr" value="{{ project.abruf_nr or '' }}"></div>
<div style="grid-column:7/10"><label class="label is-small">Startdatum</label><input class="input is-small" name="datum_start" type="date" value="{{ project.datum_start.strftime('%Y-%m-%d') if project.datum_start else '' }}"></div>
<div style="grid-column:10/13"><label class="label is-small">Enddatum</label><input class="input is-small" name="datum_ende" type="date" value="{{ project.datum_ende.strftime('%Y-%m-%d') if project.datum_ende else '' }}"></div>
<div style="grid-column:1/3"><label class="label is-small">Ansprechpartner Name</label><input class="input is-small" name="ansprechpartner_vorname" placeholder="Vorname" value="{{ project.ansprechpartner_vorname or '' }}"></div>
<div style="grid-column:3/5"><input class="input is-small" name="ansprechpartner_nachname" placeholder="Nachname" value="{{ project.ansprechpartner_nachname or '' }}" style="margin-top:1.6rem"></div>
<div style="grid-column:5/8"><label class="label is-small">Telefon</label><input class="input is-small" name="ansprechpartner_tel" value="{{ project.ansprechpartner_tel or '' }}"></div>
<div style="grid-column:8/13"><label class="label is-small">Email</label><input class="input is-small" name="ansprechpartner_email" value="{{ project.ansprechpartner_email or '' }}"></div>
</div>
<div class="mt-2">
<button class="button is-small is-primary" type="submit">Kopfdaten speichern</button>
</div>
</form>
</details>
<style>
/* === LV-Tabelle links === */
#lv-table{width:100%;table-layout:fixed;font-size:0.75rem;border-collapse:collapse}
#lv-table thead{position:sticky;top:0;z-index:2}
#lv-table thead tr{background:#fff;color:#333}
#lv-table thead th{padding:5px 6px;font-weight:600;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.5px;border:1px solid #ccc}
#lv-table tbody td{border:1px solid #ddd;padding:4px 6px}
#lv-table .lv-item{cursor:pointer}
#lv-table .lv-item:active{cursor:grabbing}
#lv-table .lv-item:hover{background:#f0f4ff}
#lv-table .lv-item.is-selected{background:#cfe2ff;outline:2px solid #2F5496;outline-offset:-2px}
#lv-table .lv-item.is-selected-multi{background:#e8f5e9}
/* === Positionen-Tabelle rechts === */
#pos-table{width:100%;font-size:0.75rem;border-collapse:collapse}
#pos-table thead{position:sticky;top:0;z-index:2}
#pos-table thead tr{background:#fff;color:#333}
#pos-table thead th{padding:5px 6px;font-weight:600;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.5px;border:1px solid #ccc}
#pos-table tbody td{border:1px solid #ddd;padding:4px 6px}
#pos-table tbody tr.is-selected-right{background:#cfe2ff;outline:2px solid #2F5496;outline-offset:-2px}
#pos-table tbody tr:hover{background:#f0f4ff}
/* === Formelart-Buttons (schwarze Schrift) === */
.btn-formel{padding:2px 8px;font-size:0.7rem;border:1px solid #aaa;cursor:pointer;background:#fff;color:#000}
.btn-formel.is-active{background:#cfe2ff;border-color:#2F5496;color:#000;font-weight:600}
/* === Drag-Klon-Stil === */
.sortable-ghost{opacity:0.4}
.sortable-chosen{background:#d4e3ff}
/* === Drag & Drop === */
.lv-item.dragging{opacity:0.3}
#pos-sortable.drop-active{outline:2px dashed #2F5496;outline-offset:-2px;background:#f0f7ff}
/* === Leere Zeile (Trenner) keine Inhalte === */
#pos-sortable tr.is-trenner td{border-top:2px dashed #ccc;border-bottom:2px dashed #ccc;background:#f9f9f9;color:#bbb;height:20px}
/* === Drop-Indicator (dicke rote Linie, keine Zeilen-Markierung) === */
#drop-indicator{position:fixed;left:0;top:0;height:6px;background:#FF1744;pointer-events:none;z-index:10000;display:none;border-radius:3px;box-shadow:0 0 12px rgba(255,23,68,0.8),0 2px 4px rgba(0,0,0,0.3)}
#drop-indicator::before{content:'';position:absolute;left:0;top:-7px;width:14px;height:20px;background:#FF1744;clip-path:polygon(0 50%,100% 0,100% 100%);border-radius:1px}
#drop-indicator::after{content:'';position:absolute;right:0;top:-7px;width:14px;height:20px;background:#FF1744;clip-path:polygon(100% 50%,0 0,0 100%);border-radius:1px}
/* === Number-Spinner ausblenden === */
input[type=number].hide-spinner::-webkit-outer-spin-button,
input[type=number].hide-spinner::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
input[type=number].hide-spinner{-moz-appearance:textfield}
/* === Spalten-Resize === */
.resize-handle{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;z-index:3;opacity:0;pointer-events:none}
.resize-hit{position:absolute;top:0;right:-5px;width:10px;height:100%;cursor:col-resize;z-index:3;opacity:0}
.resize-hit:hover ~ .resize-handle,.resize-handle.dragging{opacity:1;background:#2F5496}
#pos-table thead th,#lv-table thead th{position:relative}
/* === LV-Filter (wie POS-Filter) === */
#lv-table thead th .fbtn{margin-left:2px;font-size:0.6rem;opacity:0.4;cursor:pointer}
#lv-table thead th:hover .fbtn{opacity:0.8}
#lv-table thead th .fbtn.active{opacity:1;color:#2F5496;font-weight:bold}
#lv-table tbody tr.filter-hidden{display:none}
/* === Spalten-Filter === */
#pos-table thead th .fbtn{margin-left:2px;font-size:0.6rem;opacity:0.4;cursor:pointer}
#pos-table thead th:hover .fbtn{opacity:0.8}
#pos-table thead th .fbtn.active{opacity:1;color:#2F5496;font-weight:bold}
#filter-checkboxes label{display:flex;align-items:center;gap:4px;padding:2px 4px;cursor:pointer;border-radius:3px}
#filter-checkboxes label:hover{background:#f0f4ff}
#filter-checkboxes input[type=checkbox]{margin:0}
#pos-table tbody tr.filter-hidden{display:none}
</style>
<div id="am-layout" style="display:flex;gap:0;height:calc(100vh - 240px);overflow:hidden">
<!-- === LINKS: LV-Auswahl + Eingabe-Group + Langtext === -->
<div id="am-left" style="width:40%;min-width:360px;display:flex;flex-direction:column;background:#fff;border:1px solid #ddd;border-radius:4px;overflow:hidden;flex-shrink:0">
<!-- Suche -->
<div style="padding:5px 6px;background:#f0f2f5;border-bottom:1px solid #ddd;flex-shrink:0">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:2px">
{% if project.lv_name %}
<span style="margin-left:auto;font-size:0.65rem;color:#888">LV: {{ project.lv_name }}</span>
{% elif lv_names %}
<form method="POST" action="{{ url_for('aufmass.project_lv_set', project_id=project.id) }}" style="margin-left:auto;display:flex;gap:3px;align-items:center">
<span style="font-size:0.65rem;color:#888">LV:</span>
<select name="lv_name" class="is-small" style="font-size:0.65rem;padding:1px 4px;max-width:150px">
<option value=""> wählen </option>
{% for n in lv_names %}
<option value="{{ n }}">{{ n }}</option>
{% endfor %}
</select>
<button class="button is-small is-link" style="font-size:0.6rem;padding:1px 6px">Laden</button>
</form>
{% endif %}
</div>
<input class="input is-small" id="lv-search" placeholder="Suche in LV-Positionen..." autocomplete="off" style="width:100%">
</div>
<!-- Multi-Select Toolbar -->
<div style="display:flex;align-items:center;gap:4px;padding:2px 6px;background:#f5f5f5;border-bottom:1px solid #ddd;flex-shrink:0;font-size:0.65rem">
<span id="lv-count" style="color:#888;cursor:pointer" onclick="toggleAllLV()">0 ausgewählt</span>
<span style="color:#bbb">|</span>
<span style="color:#888">{{ lv_positionen|length }} Pos.</span>
<span style="color:#bbb;margin:0 4px;font-size:0.6rem;color:#999">Ctrl+Klick → Mehrfach | Shift+Klick → Bereich</span>
<button class="button is-small is-link" style="font-size:0.6rem;padding:1px 6px;margin-left:auto" onclick="addSelectedLV()">+ Auswahl hinzufügen</button>
</div>
<!-- LV-Liste als Tabelle (höhenverstellbar per Divider) -->
<div id="lv-table-wrap" style="flex-shrink:0;height:400px;min-height:80px;overflow-y:auto;padding:0">
<table id="lv-table">
<thead>
<tr>
<th style="width:80px;cursor:pointer" data-field="posnr" onclick="showLVFilterMenu('posnr',this)">Pos-Nr <span class="fbtn"></span></th>
<th style="cursor:pointer" data-field="text" onclick="showLVFilterMenu('text',this)">Kurztext <span class="fbtn"></span></th>
<th style="width:50px;cursor:pointer" data-field="eh" onclick="showLVFilterMenu('eh',this)">EH <span class="fbtn"></span></th>
<th style="width:75px;cursor:pointer" data-field="ep" onclick="showLVFilterMenu('ep',this)">EP (€) <span class="fbtn"></span></th>
</tr>
</thead>
<tbody id="lv-list">
{% if lv_positionen %}
{% for lp in lv_positionen %}
<tr class="lv-item" data-id="{{ lp.id }}" data-posnr="{{ lp.pos_nr }}" data-text="{{ lp.kurztext or '' }}"
data-lang="{{ lp.langtext or '' }}" data-eh="{{ lp.einheit }}" data-ep="{{ lp.einzelpreis }}"
data-rsa="{{ lp.rsa or '' }}" data-abschnitt="{{ lp.abschnitt or '' }}"
draggable="true">
<td><code style="font-size:0.7rem">{{ lp.pos_nr }}</code></td>
<td style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ lp.kurztext or '' }}</td>
<td style="text-align:right;color:#666">{{ lp.einheit }}</td>
<td style="text-align:right;white-space:nowrap">{{ lp.einzelpreis|german_number }} €</td>
</tr>
{% endfor %}
{% else %}
<tr><td colspan="4" style="text-align:center;padding:20px;color:#999;font-size:0.75rem">
{% if project.lv_name %}
Keine Positionen für LV "{{ project.lv_name }}" gefunden.
{% else %}
Kein LV zugewiesen. Wählen Sie oben ein LV aus.
{% endif %}
</td></tr>
{% endif %}
</tbody>
</table>
</div>
<!-- Horizontaler Divider zwischen LV-Tabelle und Eingabe -->
<div id="lv-divider" style="height:5px;cursor:row-resize;background:#e8e8e8;flex-shrink:0"></div>
<!-- Eingabe-Group + Langtext (füllt Resthöhe, Langtext wächst mit) -->
<div style="flex:1;display:flex;flex-direction:column;border-top:2px solid #2F5496;background:#fafafa;min-height:0">
<div style="flex-shrink:0;padding:5px 6px 4px">
<div style="font-size:0.65rem;font-weight:700;color:#2F5496;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px">Eingabe</div>
<input type="hidden" id="form-lv-id">
<div class="columns is-mobile is-variable is-1 mb-1" style="margin-bottom:2px">
<div class="column is-6" style="padding:0 2px"><label class="is-size-7 has-text-grey">PosNr</label><input class="input is-small" id="form-posnr" placeholder="PosNr"></div>
<div class="column is-6" style="padding:0 2px"><label class="is-size-7 has-text-grey">Kurztext</label><input class="input is-small" id="form-kurztext" placeholder="Kurztext"></div>
</div>
<div class="field mb-1" style="margin-bottom:2px"><label class="is-size-7 has-text-grey">RSA-Abschnitt</label>
<input class="input is-small" id="form-abschnitt" placeholder="z.B. 2.1.3">
</div>
<div class="field mb-1" style="margin-bottom:2px"><label class="is-size-7 has-text-grey">Formelart</label>
<div style="display:flex;gap:0">
<a class="btn-formel is-active" id="btn-std" onclick="setFormelTyp('standard')">Standard</a>
<a class="btn-formel" id="btn-frei" onclick="setFormelTyp('frei')">Freie Formel (Z91)</a>
</div>
</div>
<div id="form-std">
<div class="columns is-mobile is-variable is-1 mb-0" style="margin-bottom:1px">
<div class="column" style="padding:0 2px"><label class="is-size-7 has-text-grey">Faktor</label><input class="input is-small" id="form-faktor" type="text" inputmode="decimal" value="1,0" oninput="sanitizeNum(this);berechneStd()"></div>
<div class="column" style="padding:0 2px" id="field-laenge"><label class="is-size-7 has-text-grey">Länge (m)</label><input class="input is-small" id="form-laenge" type="text" inputmode="decimal" value="" oninput="sanitizeNum(this);berechneStd()"></div>
<div class="column" style="padding:0 2px" id="field-breite"><label class="is-size-7 has-text-grey">Breite (m)</label><input class="input is-small" id="form-breite" type="text" inputmode="decimal" value="" oninput="sanitizeNum(this);berechneStd()"></div>
<div class="column" style="padding:0 2px" id="field-tiefe"><label class="is-size-7 has-text-grey">Tiefe (m)</label><input class="input is-small" id="form-tiefe" type="text" inputmode="decimal" value="" oninput="sanitizeNum(this);berechneStd()"></div>
<div class="column" style="padding:0 2px"><label class="is-size-7 has-text-grey">Menge</label><input class="input is-small" id="form-menge" type="text" inputmode="decimal" value="" readonly></div>
</div>
</div>
<div id="form-frei" style="display:none">
<div class="field mb-0" style="margin-bottom:1px"><label class="is-size-7 has-text-grey">Formel (z.B. 2*1+1)</label>
<input class="input is-small" id="form-formel" placeholder="2*1+1" oninput="berechneFrei()">
</div>
<div class="field mb-0" style="margin-bottom:1px"><label class="is-size-7 has-text-grey">Menge (berechnet)</label>
<input class="input is-small" id="form-menge-frei" type="text" inputmode="decimal" value="" readonly style="background:#eee">
</div>
</div>
<div class="field mb-0" style="margin-bottom:2px"><label class="is-size-7 has-text-grey">Bemerkung</label>
<input class="input is-small" id="form-bemerkung" placeholder="Bemerkung">
</div>
<div style="display:flex;gap:4px;margin-top:3px">
<button class="button is-small is-primary" onclick="addMitFormular()">+ Hinzufügen</button>
<label style="font-size:0.7rem;cursor:pointer;display:flex;align-items:center;gap:3px;white-space:nowrap">
<input type="checkbox" id="chk-empty-end" checked>
Leere Zeile am Ende anhängen
</label>
<button class="button is-small is-light" draggable="true" id="btn-leere-zeile" ondragstart="event.dataTransfer.setData('text/plain','__LEERE_ZEILE__');event.dataTransfer.effectAllowed='all'" title="Ziehen zum Einfügen in die Positionstabelle">+ Leere Zeile</button>
</div>
</div>
<div style="flex:1;border-top:1px solid #ddd;padding:3px 6px 4px;overflow-y:scroll;background:#fff;height:0">
<div style="font-size:0.65rem;font-weight:600;color:#888;margin-bottom:1px;display:flex;align-items:center;justify-content:space-between">
<span>Langtext</span>
<a id="btn-lang-panel" style="cursor:pointer;font-size:0.7rem;color:#2F5496;display:none;white-space:nowrap" onclick="toggleLangPanel()" title="Langtext im rechten Sidepanel öffnen/schließen">≫ Langtext</a>
</div>
<div id="form-langtext" style="font-size:0.75rem;color:#555;white-space:pre-wrap;line-height:1.3"></div>
</div>
</div>
</div>
<!-- Resizable Divider -->
<div id="am-divider" style="width:5px;cursor:col-resize;background:#e8e8e8;flex-shrink:0"></div>
<!-- === RECHTS: Aufmaß-Tabelle === -->
<div id="am-right" style="flex:1;min-width:500px;display:flex;flex-direction:column;background:#fff;border:1px solid #ddd;border-radius:4px;overflow:hidden">
<div style="padding:4px 8px;background:#f0f2f5;border-bottom:1px solid #ddd;flex-shrink:0;display:flex;align-items:center;justify-content:space-between">
<div style="display:flex;align-items:center;gap:6px">
<span class="has-text-weight-bold is-size-7">Positionen ({{ positionen|length }})</span>
<button class="button is-small is-warning" id="btn-undo" onclick="doUndo()" title="Rückgängig (Strg+Z)" disabled style="padding:2px 6px;font-size:0.75rem"></button>
<button class="button is-small is-warning" id="btn-redo" onclick="doRedo()" title="Wiederholen (Strg+Y)" disabled style="padding:2px 6px;font-size:0.75rem"></button>
</div>
<span id="right-count" style="font-size:0.6rem;color:#888;min-width:80px;text-align:center"></span>
<span class="has-text-weight-bold is-size-7">Gesamt: <span id="summe-gp">{{ positionen|sum(attribute='gesamtpreis')|german_number }}</span></span>
</div>
<div style="flex:1;overflow-y:auto;padding:2px;min-height:490px">
<table id="pos-table">
<thead>
<tr>
<th style="width:45px" data-field="sort">Zeile</th>
<th style="width:40px">Z-Art</th>
<th style="width:80px;cursor:pointer" data-field="abschnitt" onclick="showFilterMenu('abschnitt',this)">Abschnitt <span class="fbtn"></span></th>
<th style="width:65px;cursor:pointer" data-field="posnr" onclick="showFilterMenu('posnr',this)">Pos-Nr <span class="fbtn"></span></th>
<th style="width:45px;cursor:pointer" data-field="faktor" onclick="showFilterMenu('faktor',this)">Faktor <span class="fbtn"></span></th>
<th style="width:50px;cursor:pointer" data-field="laenge" onclick="showFilterMenu('laenge',this)">Länge <span class="fbtn"></span></th>
<th style="width:50px;cursor:pointer" data-field="breite" onclick="showFilterMenu('breite',this)">Breite <span class="fbtn"></span></th>
<th style="width:50px;cursor:pointer" data-field="tiefe" onclick="showFilterMenu('tiefe',this)">Tiefe <span class="fbtn"></span></th>
<th style="width:55px;cursor:pointer" data-field="menge" onclick="showFilterMenu('menge',this)">Menge <span class="fbtn"></span></th>
<th style="width:30px;cursor:pointer" data-field="einheit" onclick="showFilterMenu('einheit',this)">EH <span class="fbtn"></span></th>
<th style="min-width:80px;cursor:pointer" data-field="kurztext" onclick="showFilterMenu('kurztext',this)">Kurztext <span class="fbtn"></span></th>
<th style="width:60px;cursor:pointer" data-field="bemerkung" onclick="showFilterMenu('bemerkung',this)">Bemerkung <span class="fbtn"></span></th>
<th style="width:55px;cursor:pointer" data-field="menge-hinten" onclick="showFilterMenu('menge-hinten',this)">Menge <span class="fbtn"></span></th>
<th style="width:55px;cursor:pointer" data-field="ep" onclick="showFilterMenu('ep',this)">EP (€) <span class="fbtn"></span></th>
<th style="width:60px;cursor:pointer" data-field="gp" onclick="showFilterMenu('gp',this)">GP (€) <span class="fbtn"></span></th>
<th style="width:40px"></th>
</tr>
</thead>
<tbody id="pos-sortable">
{% for pos in positionen %}
{% set empty = pos.pos_nr == '' and pos.faktor == 0 and pos.laenge == 0 and pos.breite == 0 and pos.tiefe == 0 and pos.menge == 0 and pos.einzelpreis == 0 and pos.gesamtpreis == 0 %}
<tr data-id="{{ pos.id }}" data-sort="{{ pos.sortierung }}"
data-posnr="{{ pos.pos_nr }}" data-kurztext="{{ pos.kurztext or '' }}"
data-langtext="{{ pos.langtext or '' }}" data-einheit="{{ pos.einheit }}"
data-ep="{{ pos.einzelpreis }}" data-faktor="{{ pos.faktor }}"
data-laenge="{{ pos.laenge }}" data-breite="{{ pos.breite }}" data-tiefe="{{ pos.tiefe }}"
data-menge="{{ pos.menge }}" data-gp="{{ pos.gesamtpreis }}"
data-menge-hinten="{{ pos.menge_hinten or 0 }}"
data-bemerkung="{{ pos.bemerkung or '' }}" data-rsa="{{ pos.rsa or '' }}"
data-abschnitt="{{ pos.abschnitt or '' }}"
data-formel-typ="{{ pos.formel_typ or 'standard' }}" data-formel="{{ pos.formel or '' }}"
draggable="true"{% if empty %} class="is-trenner"{% endif %}>
<td>{{ pos.sortierung }}</td>
<td data-field="formel_typ"><span class="tag is-light is-small" style="font-size:0.65rem">{{ 'Z91' if pos.formel_typ=='frei' else 'Std' if not empty }}</span></td>
<td style="font-size:0.7rem" data-field="abschnitt">{{ pos.abschnitt or '' }}</td>
<td><code style="font-size:0.7rem">{{ pos.pos_nr }}</code></td>
{% if empty %}
<td colspan="11" style="text-align:center;color:#ccc;font-style:italic;padding:4px 0"> Leere Zeile </td>
<td style="text-align:center">
<form method="POST" action="{{ url_for('aufmass.position_loeschen', project_id=project.id, aufmass_id=aufmass.id, pos_id=pos.id) }}"
style="display:inline" onsubmit="return confirm('Löschen?')">
<button class="button is-small is-danger is-outlined" style="font-size:0.65rem;padding:0 4px;border:none"></button>
</form>
</td>
{% else %}
<td style="text-align:right" data-field="faktor">{{ '' if pos.formel_typ=='frei' else pos.faktor|german_number }}</td>
{% if pos.formel_typ=='frei' %}
<td colspan="3" style="text-align:center;font-family:monospace;font-size:0.75rem;background:#fafafa" data-field="formel" title="Formel: {{ pos.formel }}">{{ pos.formel or '' }}</td>
{% else %}
<td style="text-align:right" data-field="laenge" class="{{ 'has-background-warning-light' if (pos.einheit in ('M','M2','M3') and pos.laenge == 0) }}">{{ pos.laenge|german_number(zero_dash=True) }}</td>
<td style="text-align:right" data-field="breite" class="{{ 'has-background-warning-light' if (pos.einheit in ('M2','M3') and pos.breite == 0) }}">{{ pos.breite|german_number(zero_dash=True) }}</td>
<td style="text-align:right" data-field="tiefe" class="{{ 'has-background-warning-light' if (pos.einheit == 'M3' and pos.tiefe == 0) }}">{{ pos.tiefe|german_number(zero_dash=True) }}</td>
{% endif %}
<td style="text-align:right" data-field="menge"><strong data-cell="menge">{{ pos.menge|german_number(3, zero_dash=True) }}</strong></td>
<td data-field="einheit">{{ pos.einheit }}</td>
<td style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:120px" data-field="kurztext" title="{{ pos.kurztext or '' }}">{{ pos.kurztext or '' }}</td>
<td style="font-size:0.7rem;max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" data-field="bemerkung">{{ pos.bemerkung or '' }}</td>
<td style="text-align:right" data-field="menge_hinten">{{ pos.menge_hinten|german_number(zero_dash=True) }}</td>
<td style="text-align:right;white-space:nowrap">{{ pos.einzelpreis|german_number }}</td>
<td style="text-align:right;white-space:nowrap"><strong data-cell="gp">{{ pos.gesamtpreis|german_number }}</strong></td>
<td>
<a style="cursor:pointer;font-size:0.8rem" onclick="openEdit({{ pos.id }})"></a>
<form method="POST" action="{{ url_for('aufmass.position_loeschen', project_id=project.id, aufmass_id=aufmass.id, pos_id=pos.id) }}"
style="display:inline" onsubmit="return confirm('Löschen?')">
<button class="button is-small is-danger is-outlined" style="font-size:0.65rem;padding:0 4px;border:none"></button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% if not positionen %}
<p class="has-text-grey has-text-centered is-size-7 mt-3">Keine Positionen. Wählen Sie links aus dem LV und klicken Hinzufügen.</p>
{% endif %}
</div>
<!-- Filter-Dropdown für Spalten -->
<div id="filter-menu" style="position:fixed;display:none;z-index:10000;background:#fff;border:1px solid #bbb;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.2);padding:8px;min-width:220px;max-width:300px;font-size:0.8rem">
<div style="display:flex;gap:2px;margin-bottom:6px">
<button class="button is-small is-light" onclick="filterSort('asc')" style="font-size:0.75rem;flex:1" title="Aufsteigend sortieren">↑ Aufst.</button>
<button class="button is-small is-light" onclick="filterSort('desc')" style="font-size:0.75rem;flex:1" title="Absteigend sortieren">↓ Abst.</button>
<button class="button is-small is-light" onclick="filterSort(null)" style="font-size:0.75rem;flex:0.6" title="Sortierung zurücksetzen"></button>
</div>
<input id="filter-search" type="text" placeholder="Suchen…" style="width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid #ccc;border-radius:4px;font-size:0.8rem;margin-bottom:4px" oninput="filterSearch(this.value)">
<div id="filter-values" style="max-height:180px;overflow-y:auto;border-top:1px solid #eee;margin-top:4px;padding-top:4px">
<div style="display:flex;gap:4px;margin-bottom:4px">
<button class="button is-small is-light" onclick="filterSelectAll()" style="font-size:0.7rem;flex:1;padding:0 4px">Alles</button>
<button class="button is-small is-light" onclick="filterSelectNone()" style="font-size:0.7rem;flex:1;padding:0 4px">Nichts</button>
</div>
<div id="filter-checkboxes"></div>
</div>
</div>
<!-- Context-Menu für rechte Tabelle -->
<div id="pos-context-menu" style="position:fixed;display:none;z-index:9999;background:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);padding:4px 0;min-width:190px;font-size:0.85rem">
<div class="ctx-item" onclick="openMultiEdit();hideContextMenu()" style="padding:7px 14px;cursor:pointer;display:flex;align-items:center;gap:8px" onmouseenter="this.style.background='#f0f2f5'" onmouseleave="this.style.background=''">
<span style="font-size:1rem">✏️</span><span>Markierte bearbeiten</span>
</div>
<div class="ctx-item" onclick="copySelectedPos();hideContextMenu()" style="padding:7px 14px;cursor:pointer;display:flex;align-items:center;gap:8px" onmouseenter="this.style.background='#f0f2f5'" onmouseleave="this.style.background=''">
<span style="font-size:1rem">📋</span><span>Markierte kopieren</span>
</div>
<div class="ctx-item" onclick="deleteSelectedPos();hideContextMenu()" style="padding:7px 14px;cursor:pointer;display:flex;align-items:center;gap:8px" onmouseenter="this.style.background='#f0f2f5'" onmouseleave="this.style.background=''">
<span style="font-size:1rem"></span><span>Markierte löschen</span>
</div>
<div style="border-top:1px solid #eee;margin:2px 0"></div>
<div class="ctx-item" onclick="addLeereZeile();hideContextMenu()" style="padding:7px 14px;cursor:pointer;display:flex;align-items:center;gap:8px" onmouseenter="this.style.background='#f0f2f5'" onmouseleave="this.style.background=''">
<span style="font-size:1rem">📄</span><span>Leere Zeile einfügen</span>
</div>
</div>
<!-- Untere Leiste: Checkbox + Buttons -->
<div style="flex-shrink:0;padding:4px 8px;background:#f0f2f5;border-top:1px solid #ddd;display:flex;align-items:center;gap:6px">
<button class="button is-danger is-outlined" style="height:36px;font-size:0.8rem" onclick="deleteSelectedPos()" title="Markierte Einträge löschen">✕ Markierte löschen</button>
<button class="button is-link is-outlined" style="height:36px;font-size:0.8rem" onclick="copySelectedPos()" title="Markierte Einträge kopieren">📋 Markierte kopieren</button>
<button class="button is-link is-outlined" style="height:36px;font-size:0.8rem" onclick="document.getElementById('pos-import-file').click()" title="Positionen aus TXT-Datei importieren">📥 Import TXT</button>
<form method="POST" action="{{ url_for('aufmass.positionen_import_txt', project_id=project.id, aufmass_id=aufmass.id) }}" enctype="multipart/form-data" style="display:none">
<input type="file" name="file" accept=".txt" id="pos-import-file" onchange="this.form.submit()">
</form>
<span style="flex:1"></span>
<label style="font-size:0.75rem;cursor:pointer;display:flex;align-items:center;gap:4px">
<input type="checkbox" id="chk-anfuegen" checked>
Am Ende anfügen
</label>
<button class="button is-light is-small" id="btn-reset-filter" onclick="filterReset()" style="height:36px;font-size:0.75rem" title="Filter zurücksetzen">↺ Filter zurücksetzen</button>
<span style="border-left:1px solid #ccc;height:22px"></span>
<button class="button is-danger" style="height:36px;font-size:0.8rem" onclick="deleteAllPos()">🗑 Tabelle löschen</button>
</div>
</div>
<!-- Langtext Sidepanel Divider -->
<div id="lang-divider" style="width:5px;cursor:col-resize;background:#e8e8e8;flex-shrink:0;display:none"></div>
<!-- Langtext Sidepanel (rechts) -->
<div id="am-lang-panel" style="width:380px;min-width:28px;display:none;background:#fff;border:1px solid #ddd;border-radius:4px;overflow:hidden;flex-shrink:0;position:relative">
<!-- Vertical Langtext label + <<-Button (sichtbar im collapsed Zustand) -->
<div id="lang-collapsed" style="display:flex;flex-direction:column;align-items:center;padding-top:12px;width:28px;background:#f0f2f5;border-right:1px solid #ddd;border-radius:4px 0 0 4px;cursor:pointer;position:absolute;left:0;top:0;bottom:0;z-index:5" onclick="toggleLangPanel()">
<span style="writing-mode:vertical-rl;text-orientation:mixed;font-size:0.65rem;font-weight:700;color:#2F5496;letter-spacing:2px;transform:rotate(180deg)">Langtext</span>
<span style="font-size:0.75rem;color:#2F5496;margin-top:6px"></span>
</div>
<!-- Expanded content -->
<div id="lang-expanded" style="display:none;flex-direction:column;height:100%">
<div style="display:flex;align-items:center;padding:4px 8px;background:#f0f2f5;border-bottom:1px solid #ddd;flex-shrink:0">
<span style="font-size:0.65rem;font-weight:700;color:#2F5496;text-transform:uppercase;letter-spacing:0.5px;cursor:pointer" onclick="toggleLangPanel()" title="Einklappen">Langtext</span>
<span style="flex:1"></span>
<a style="cursor:pointer;font-size:0.75rem;color:#888;padding:2px 4px" onclick="toggleLangPanel()" title="Einklappen"></a>
<a style="cursor:pointer;font-size:0.75rem;color:#888;padding:2px 4px;margin-left:2px" onclick="closeLangPanel()" title="Schließen"></a>
</div>
<div id="lang-panel-content" style="flex:1;padding:6px 8px;overflow-y:scroll;font-size:0.85rem;white-space:pre-wrap;line-height:1.4"></div>
</div>
</div>
</div>
<div id="pos-edit-modal" class="modal">
<div class="modal-background" onclick="closeEdit()"></div>
<div class="modal-card">
<header class="modal-card-head"><p class="modal-card-title">Position bearbeiten</p><button class="delete" onclick="closeEdit()"></button></header>
<section class="modal-card-body" id="pos-edit-content"></section>
</div>
</div>
<!-- Multi-Edit Modal -->
<div id="multi-edit-modal" class="modal">
<div class="modal-background" onclick="closeMultiEdit()"></div>
<div class="modal-card" style="width:98vw;max-width:none">
<header class="modal-card-head"><p class="modal-card-title">✏️ Markierte Positionen bearbeiten</p><button class="delete" onclick="closeMultiEdit()"></button></header>
<section class="modal-card-body" id="multi-edit-body" style="max-height:70vh;overflow:auto;padding:0">
<div id="multi-edit-content">Lade…</div>
</section>
<footer class="modal-card-foot" style="justify-content:flex-end;gap:8px">
<button class="button is-light" onclick="closeMultiEdit()">Abbrechen</button>
<button class="button is-primary" id="btn-multi-save" onclick="saveMultiEdit()">Speichern</button>
</footer>
</div>
</div>
<!-- History Log Modal -->
<div class="modal" id="history-log-modal">
<div class="modal-background" onclick="document.getElementById('history-log-modal').classList.remove('is-active')"></div>
<div class="modal-card" style="width:95%;max-width:1100px">
<header class="modal-card-head">
<p class="modal-card-title">📜 Änderungshistory</p>
<button class="delete" onclick="document.getElementById('history-log-modal').classList.remove('is-active')"></button>
</header>
<section class="modal-card-body" style="max-height:75vh;overflow-y:auto">
<div id="history-log-list" style="font-size:0.8rem"></div>
</section>
<footer class="modal-card-foot">
<button class="button" onclick="document.getElementById('history-log-modal').classList.remove('is-active')">Schließen</button>
<button class="button is-info" onclick="fetchHistoryLog()">↻ Aktualisieren</button>
</footer>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const PROJECT_ID = {{ project.id }};
const AUFMASS_ID = {{ aufmass.id }};
let currentLVId = null;
let currentEinheit = '';
let formelTyp = 'standard';
/* === History/Undo/Redo - muss VOR saveMultiEdit definiert sein === */
var _undoStack = [];
var _redoStack = [];
var MAX_UNDO = 10;
// Call loadHistoryCache when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
loadHistoryCache();
});
function saveHistoryCache() {
var key = 'aufmass_history_' + AUFMASS_ID;
try {
localStorage.setItem(key, JSON.stringify({undo: _undoStack, redo: _redoStack}));
} catch(e) {}
}
function loadHistoryCache() {
var key = 'aufmass_history_' + AUFMASS_ID;
try {
var cached = localStorage.getItem(key);
if (cached) {
var data = JSON.parse(cached);
_undoStack = data.undo || [];
_redoStack = data.redo || [];
}
} catch(e) {}
updateUndoRedoButtons();
}
function updateUndoRedoButtons() {
var undoBtn = document.getElementById('btn-undo');
var redoBtn = document.getElementById('btn-redo');
if (undoBtn) undoBtn.disabled = _undoStack.length === 0;
if (redoBtn) redoBtn.disabled = _redoStack.length === 0;
}
function pushUndoState(description, diff) {
_undoStack.push({description: description, diff: diff, time: new Date().toISOString()});
if (_undoStack.length > MAX_UNDO) {
_undoStack.shift();
}
_redoStack = [];
saveHistoryCache();
updateUndoRedoButtons();
}
function doUndo() {
if (_undoStack.length === 0) {
alert('Nichts zum Rückgängig machen.');
return;
}
var state = _undoStack.pop();
_redoStack.push(state);
saveHistoryCache();
var btn = document.getElementById('btn-undo');
if (btn) btn.classList.add('is-loading');
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/undo', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({})
}).then(function(r){ return r.json(); }).then(function(data){
if (data.error) {
alert('Fehler: ' + data.error);
_undoStack.push(state);
_redoStack.pop();
saveHistoryCache();
} else {
location.href = location.pathname + '?t=' + Date.now();
}
}).catch(function(e){
alert('Fehler: ' + e.message);
_undoStack.push(state);
_redoStack.pop();
saveHistoryCache();
}).finally(function(){
var btn = document.getElementById('btn-undo');
if (btn) btn.classList.remove('is-loading');
});
updateUndoRedoButtons();
}
function doRedo() {
if (_redoStack.length === 0) {
alert('Nichts zum Wiederholen.');
return;
}
var state = _redoStack.pop();
_undoStack.push(state);
saveHistoryCache();
var btn = document.getElementById('btn-redo');
if (btn) btn.classList.add('is-loading');
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/redo', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({})
}).then(function(r){ return r.json(); }).then(function(data){
if (data.error) {
alert('Fehler: ' + data.error);
_undoStack.pop();
_redoStack.push(state);
saveHistoryCache();
} else {
location.href = location.pathname + '?t=' + Date.now();
}
}).catch(function(e){
alert('Fehler: ' + e.message);
_undoStack.pop();
_redoStack.push(state);
saveHistoryCache();
}).finally(function(){
var btn = document.getElementById('btn-redo');
if (btn) btn.classList.remove('is-loading');
});
updateUndoRedoButtons();
}
function showHistoryLog() {
var modal = document.getElementById('history-log-modal');
if (!modal) return;
modal.classList.add('is-active');
fetchHistoryLog();
}
function fetchHistoryLog() {
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/history?limit=50')
.then(function(r){ return r.json(); })
.then(function(data){
var container = document.getElementById('history-log-list');
if (!container) return;
container.innerHTML = '';
data.forEach(function(entry){
var actionClass = entry.action === 'undo' ? 'is-warning' : (entry.action === 'redo' ? 'is-info' : 'is-success');
var actionIcon = entry.action === 'undo' ? '↩' : (entry.action === 'redo' ? '↪' : '•');
var canRevert = entry.action === 'change';
var html = '<div class="history-entry" style="border:1px solid #ddd;border-radius:4px;margin-bottom:8px;overflow:hidden">';
html += '<div class="history-header" style="background:#f5f5f5;padding:8px 12px;display:flex;align-items:center;gap:10px;cursor:pointer" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display===\'none\'?\'block\':\'none\'">';
html += '<span class="tag ' + actionClass + '" style="min-width:60px;text-align:center">' + actionIcon + ' ' + entry.action + '</span>';
html += '<span style="color:#666;font-size:0.75rem;flex:1">' + (entry.description || 'no description') + '</span>';
html += '<span style="color:#888;font-size:0.7rem">' + entry.time + '</span>';
html += '<span style="color:#555;font-size:0.75rem;font-weight:500">' + entry.user_name + '</span>';
if (canRevert) {
html += '<button class="button is-small is-warning" style="padding:2px 8px;font-size:0.7rem" onclick="event.stopPropagation();revertHistoryEntry('+entry.id+')">↩ Rückgängig</button>';
}
html += '</div>';
html += '<div class="history-details" style="padding:10px 12px;background:#fff;white-space:pre-wrap;font-family:monospace;font-size:0.75rem;line-height:1.5;color:#333;display:none">';
html += entry.long_description || entry.description || 'Keine Details';
html += '</div></div>';
container.insertAdjacentHTML('beforeend', html);
});
if (data.length === 0) {
container.innerHTML = '<p style="text-align:center;color:#888;padding:30px">Keine History-Einträge vorhanden</p>';
}
}).catch(function(e){
console.error('History load failed', e);
});
}
function revertHistoryEntry(historyId) {
if (!confirm('Diese Änderung rückgängig machen?')) return;
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/history/'+historyId+'/undo', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(function(r){ return r.json(); })
.then(function(data){
if (data.ok) {
alert(data.message);
fetchHistoryLog();
updateUndoRedoButtons();
} else {
alert('Fehler: ' + (data.error || 'Unbekannt'));
}
}).catch(function(e){
alert('Fehler: ' + e);
});
}
document.addEventListener('keydown', function(e){
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
e.preventDefault();
doUndo();
}
if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key === 'z'))) {
e.preventDefault();
doRedo();
}
});
function formatGerman(n){
if(n===null||n===undefined||isNaN(n)||n===0)return'';
var parts=n.toFixed(2).split('.');
parts[0]=parts[0].replace(/\B(?=(\d{3})+(?!\d))/g,'.');
return parts.join(',');
}
function formatGerman3(n){
if(n===null||n===undefined||isNaN(n)||n===0)return'';
var parts=n.toFixed(3).split('.');
parts[0]=parts[0].replace(/\B(?=(\d{3})+(?!\d))/g,'.');
return parts.join(',');
}
function germanNum(v, prec){
if(v===null||v===undefined||v==='')return'';
var n=parseFloat(String(v).replace(/,/g,'.'));
if(isNaN(n)||n===0)return'';
if(prec!==undefined)return n.toFixed(prec).replace('.',',');
return String(n).replace('.',',');
}
function sanitizeNum(el){
el.value=el.value.replace(/[^0-9,\-]/g,'');
validateNumField(el);
}
function validateNumField(el){
var v=el.value.trim();
if(v===''||v==='-'||v===','||v==='-,'){
el.style.borderColor='';
return true;
}
var valid=/^-?\d{1,3}(\.\d{3})*(,\d*)?$/.test(v)||/^-?\d+(,\d*)?$/.test(v);
el.style.borderColor=valid?'':'#f14668';
return valid;
}
/* === Export mit Aktueller Ansicht === */
function exportWithFilter(type){
var chk=document.getElementById('chk-aktuelle-ansicht');
var base='{{ url_for("export.excel", project_id=project.id) }}'.replace('/excel','');
var urls={excel:base+'/excel', pdf:base+'/pdf', txt:base+'/txt'};
var sep='?';
var params='aufmass_id={{ aufmass.id }}';
if(chk&&chk.checked){
var ids=[];
document.querySelectorAll('#pos-sortable tr[data-id]').forEach(function(r){
if(r.style.display!=='none')ids.push(r.dataset.id);
});
if(ids.length)params+='&visible_ids='+ids.join(',');
}
window.location.href=urls[type]+sep+params;
}
function toggleExportMenu() {
var dropdown = document.getElementById('export-dropdown');
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
}
document.addEventListener('click', function(e) {
var btn = document.getElementById('btn-export-menu');
var dropdown = document.getElementById('export-dropdown');
if (btn && dropdown && !btn.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.style.display = 'none';
}
});
function updateExportZipButton() {
var checks = ['excel', 'pdf', 'txt', 'x31', 'x31ca'];
var anyChecked = checks.some(function(id) {
var chk = document.getElementById('export-' + id);
return chk && chk.checked;
});
var zipSection = document.getElementById('export-zip-section');
if (zipSection) {
zipSection.style.display = anyChecked ? 'block' : 'none';
}
}
function getVisibleIdsParam() {
var chk = document.getElementById('chk-aktuelle-ansicht');
if (chk && chk.checked) {
var ids = [];
document.querySelectorAll('#pos-sortable tr[data-id]').forEach(function(r) {
if (r.style.display !== 'none') ids.push(r.dataset.id);
});
if (ids.length) return '&visible_ids=' + ids.join(',');
}
return '';
}
function downloadZipExport() {
var checks = [
{ id: 'excel', suffix: '.xlsx', url: '{{ url_for("export.excel", project_id=project.id) }}' },
{ id: 'pdf', suffix: '.pdf', url: '{{ url_for("export.pdf", project_id=project.id) }}' },
{ id: 'txt', suffix: '.txt', url: '{{ url_for("export.txt", project_id=project.id) }}' },
{ id: 'x31', suffix: '.x31', url: '{{ url_for("export.x31", project_id=project.id) }}' },
{ id: 'x31ca', suffix: '.x31ca', url: '{{ url_for("export.x31_california", project_id=project.id) }}' }
];
var selected = checks.filter(function(c) {
var chk = document.getElementById('export-' + c.id);
return chk && chk.checked;
});
if (selected.length === 0) {
alert('Bitte mindestens einen Export auswählen.');
return;
}
var baseParams = 'aufmass_id={{ aufmass.id }}' + getVisibleIdsParam();
var projectName = '{{ project.bezeichnung or project.sm_nr or "Projekt" }}'.replace(/[^a-zA-Z0-9äöüÄÖÜß_-]/g, '_');
var aufmassName = '{{ aufmass.name }}'.replace(/[^a-zA-Z0-9äöüÄÖÜß_-]/g, '_');
var date = new Date().toISOString().split('T')[0].replace(/-/g, '.');
var zipName = projectName + ' - ' + aufmassName + ' - ' + date + '.zip';
var form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("export.zip_download", project_id=project.id) }}';
form.style.display = 'none';
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'exports';
input.value = JSON.stringify(selected.map(function(s) { return { id: s.id, suffix: s.suffix }; }));
form.appendChild(input);
var paramsInput = document.createElement('input');
paramsInput.type = 'hidden';
paramsInput.name = 'base_params';
paramsInput.value = baseParams;
form.appendChild(paramsInput);
var zipNameInput = document.createElement('input');
zipNameInput.type = 'hidden';
zipNameInput.name = 'zip_name';
zipNameInput.value = zipName;
form.appendChild(zipNameInput);
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
document.getElementById('export-dropdown').style.display = 'none';
}
/* === Nach Reload: markierte Positionen wiederherstellen + hinscrollen === */
(function(){
// Checkbox-Zustand wiederherstellen
var chk=document.getElementById('chk-anfuegen');
if(chk){
var saved=localStorage.getItem('chk_anfuegen');
if(saved!==null)chk.checked=saved==='true';
chk.addEventListener('change',function(){localStorage.setItem('chk_anfuegen',chk.checked);});
}
var raw=sessionStorage.getItem('markedPositions');
if(raw){
sessionStorage.removeItem('markedPositions');
var ids=JSON.parse(raw);
setTimeout(function(){
var firstRow=null;
ids.forEach(function(id){
var row=document.querySelector('#pos-sortable tr[data-id="'+id+'"]');
if(row){
rightSelectedSet.add(row.dataset.id);
row.classList.add('is-selected-right');
if(!firstRow)firstRow=row;
}
});
updateRightCount();
// Zur ersten markierten Zeile scrollen
if(firstRow){
var cont=firstRow.closest('#am-right')||firstRow.closest('[style*="overflow"]');
if(cont)firstRow.scrollIntoView({block:'nearest',behavior:'smooth'});
}
},100);
}
})();
function reloadWithMarked(ids){
if(ids&&ids.length)sessionStorage.setItem('markedPositions',JSON.stringify(ids));
location.reload();
}
/* === Resizable Divider (Width in localStorage) === */
(function(){
const div=document.getElementById('am-divider');
const left=document.getElementById('am-left');
if(!div||!left)return;
// Gespeicherte Breite wiederherstellen
const saved=localStorage.getItem('am_left_width');
if(saved&&parseInt(saved)>280)left.style.width=saved+'px';
let drag=false,sx=0;
div.addEventListener('mousedown',function(e){drag=true;sx=e.clientX;document.body.style.cursor='col-resize';document.body.style.userSelect='none';});
document.addEventListener('mousemove',function(e){
if(!drag)return;
const nw=left.offsetWidth+(e.clientX-sx);
if(nw>280)left.style.width=nw+'px';
sx=e.clientX;
});
document.addEventListener('mouseup',function(){
if(drag)localStorage.setItem('am_left_width',left.offsetWidth);
drag=false;document.body.style.cursor='';document.body.style.userSelect='';
});
})();
/* === Horizontaler Divider (LV-Tabelle ↔ Eingabe) === */
(function(){
const div=document.getElementById('lv-divider');
const wrap=document.getElementById('lv-table-wrap');
if(!div||!wrap)return;
if(location.search.includes('reset_lv=1')){
localStorage.removeItem('lv_table_height');
localStorage.removeItem('am_left_width');
history.replaceState(null,'',location.pathname+location.hash);
}
const saved=localStorage.getItem('lv_table_height');
if(saved&&parseInt(saved)>80)wrap.style.height=saved+'px';
let drag=false,sy=0;
div.addEventListener('mousedown',function(e){drag=true;sy=e.clientY;document.body.style.cursor='row-resize';document.body.style.userSelect='none';});
div.addEventListener('dblclick',function(){wrap.style.height='400px';localStorage.removeItem('lv_table_height');});
document.addEventListener('mousemove',function(e){
if(!drag)return;
const nh=wrap.offsetHeight+(e.clientY-sy);
if(nh>80){
const parent=div.parentElement;
const maxNh=parent.clientHeight - wrap.offsetTop - 5 - 50;
wrap.style.height=Math.min(nh,maxNh)+'px';
}
sy=e.clientY;
});
document.addEventListener('mouseup',function(){
if(drag)localStorage.setItem('lv_table_height',wrap.offsetHeight);
drag=false;document.body.style.cursor='';document.body.style.userSelect='';
});
})();
/* === Column-Resize für LV- und Pos-Tabelle === */
(function(){
function makeResizable(tableId,storageKey){
var table=document.getElementById(tableId);
if(!table)return;
var thead=table.querySelector('thead');
if(!thead)return;
var cols=thead.querySelectorAll('th');
// Gespeicherte Breiten wiederherstellen
var saved=localStorage.getItem(storageKey);
if(saved){
try{
var widths=JSON.parse(saved);
cols.forEach(function(th,i){
if(widths[i])th.style.width=widths[i];
});
}catch(e){}
}
// Resize-Handles hinzufügen (Hit-Fläche + Handle) für alle Spalten
cols.forEach(function(th){
var txt=th.textContent.trim();
if(txt==='✕'||txt==='Z-Art')return; // Aktionsspalten überspringen
var hit=document.createElement('div');
hit.className='resize-hit';
var handle=document.createElement('div');
handle.className='resize-handle';
var dragging=false,startX=0,startW=0;
function onStart(e){
dragging=true;startX=e.clientX;startW=th.offsetWidth;
handle.classList.add('dragging');
document.body.style.cursor='col-resize';document.body.style.userSelect='none';
}
hit.addEventListener('mousedown',onStart);
// Doppelklick = Auto-Fit
hit.addEventListener('dblclick',function(e){
var maxW=0;
var idx=Array.from(cols).indexOf(th);
table.querySelectorAll('tbody tr').forEach(function(row){
var cell=row.children[idx];
if(cell){var w=cell.scrollWidth+8;if(w>maxW)maxW=w;}
});
var hdrW=th.scrollWidth;
var newW=Math.max(hdrW,maxW,30);
th.style.width=newW+'px';
saveWidths(cols,storageKey);
});
th.appendChild(hit);
th.appendChild(handle);
document.addEventListener('mousemove',function(e){
if(!dragging)return;
var diff=e.clientX-startX;
var newW=Math.max(30,startW+diff);
th.style.width=newW+'px';
// Spaltenbreite der Tabelle anpassen
table.style.tableLayout='fixed';
});
document.addEventListener('mouseup',function(){
if(dragging){
dragging=false;
handle.classList.remove('dragging');
document.body.style.cursor='';document.body.style.userSelect='';
saveWidths(cols,storageKey);
}
});
});
}
function saveWidths(cols,key){
var widths={};
cols.forEach(function(th,i){
if(th.style.width)widths[i]=th.style.width;
});
localStorage.setItem(key,JSON.stringify(widths));
}
makeResizable('lv-table','lv_col_widths');
makeResizable('pos-table','pos_col_widths');
})();
/* === Header-Kontextmenü: Ansicht speichern === */
(function(){
var hdrMenu=document.createElement('div');
hdrMenu.id='hdr-context-menu';
hdrMenu.style.cssText='position:fixed;display:none;z-index:10001;background:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);padding:4px 0;min-width:180px;font-size:0.8rem';
hdrMenu.innerHTML='<div style="padding:7px 14px;cursor:pointer;display:flex;align-items:center;gap:8px" onmouseenter="this.style.background=\'#f0f2f5\'" onmouseleave="this.style.background=\'\'" onclick="saveColumnWidths();hideHeaderMenu()"><span style="font-size:1rem">💾</span><span>Ansicht speichern</span></div>';
document.body.appendChild(hdrMenu);
window.hideHeaderMenu=function(){hdrMenu.style.display='none';};
document.addEventListener('click',function(e){
if(hdrMenu.style.display!=='none'&&!hdrMenu.contains(e.target))hideHeaderMenu();
});
document.addEventListener('keydown',function(e){if(e.key==='Escape')hideHeaderMenu();});
// Rechtsklick auf header
document.querySelectorAll('#pos-table thead th, #lv-table thead th').forEach(function(th){
th.addEventListener('contextmenu',function(e){
e.preventDefault();
hdrMenu.style.left=e.clientX+'px';
hdrMenu.style.top=e.clientY+'px';
hdrMenu.style.display='block';
});
});
})();
function saveColumnWidths(){
['lv_col_widths','pos_col_widths'].forEach(function(key){
var saved=localStorage.getItem(key);
if(saved)localStorage.setItem('saved_'+key,saved);
});
// Flash-ähnliche Rückmeldung
var btn=document.getElementById('btn-reset-filter');
if(btn){var old=btn.textContent;btn.textContent='✓ Gespeichert';btn.style.background='#d4edda';setTimeout(function(){btn.textContent=old;btn.style.background='';},1500);}
}
/* === Live Search (clientseitig, filtert die LV-Tabelle) === */
(function(){
const inp=document.getElementById('lv-search');
if(!inp)return;let t;
inp.addEventListener('input',function(){
clearTimeout(t);t=setTimeout(function(){
const q=inp.value.trim().toLowerCase();
const items=document.querySelectorAll('#lv-list .lv-item');
var found=0;
items.forEach(function(el){
var txt=(el.dataset.posnr+' '+el.dataset.text+' '+el.dataset.rsa+' '+el.dataset.abschnitt).toLowerCase();
if(!q||txt.indexOf(q)!==-1){el.style.display='';found++;}
else{el.style.display='none';}
});
var emptyMsg=document.getElementById('lv-empty-row');
if(!found&&q){
if(!emptyMsg){
var tbody=document.getElementById('lv-list');
var tr=document.createElement('tr');tr.id='lv-empty-row';
tr.innerHTML='<td colspan="4" style="padding:12px;text-align:center;color:#999;font-size:0.75rem">Keine Positionen gefunden.</td>';
tbody.appendChild(tr);
}
}else if(emptyMsg){emptyMsg.remove();}
},200);
});
})();
/* === LV Select: Windows-Stil (Ctrl/Klick, Shift/Bereich) + Formular befüllen === */
var selectedSet=new Set();
var lastClickedIdx=-1;
var currentLVRow=null;
// Event-Delegation für Klick auf der LV-Liste
document.addEventListener('DOMContentLoaded',function(){
var lvBody=document.getElementById('lv-list');
if(!lvBody)return;
// Klick
lvBody.addEventListener('click',function(e){
var row=e.target.closest('.lv-item');
if(!row)return;
var idx=Array.from(lvBody.children).indexOf(row);
if(e.ctrlKey||e.metaKey){
// Toggle in multi-selection
if(selectedSet.has(row.dataset.id)){
selectedSet.delete(row.dataset.id);
row.classList.remove('is-selected');
}else{
selectedSet.add(row.dataset.id);
row.classList.add('is-selected');
}
lastClickedIdx=idx;
updateLVCount();
return;
}
if(e.shiftKey&&lastClickedIdx>=0){
// Range select
var min=Math.min(lastClickedIdx,idx);
var max=Math.max(lastClickedIdx,idx);
for(var i=min;i<=max;i++){
var r=lvBody.children[i];
if(r&&r.classList){
selectedSet.add(r.dataset.id);
r.classList.add('is-selected');
}
}
updateLVCount();return;
}
// Einfach-Klick: Selektion zurücksetzen, nur diese Zeile auswählen + Formular füllen
clearMultiSelect();
selectedSet.add(row.dataset.id);
row.classList.add('is-selected');
updateLVCount();
fillForm(row);
lastClickedIdx=idx;
});
});
function fillForm(row){
currentLVRow=row;
currentLVId=parseInt(row.dataset.id);
var d=row.dataset;
currentEinheit=d.eh||'';
document.getElementById('form-lv-id').value=d.id;
document.getElementById('form-posnr').value=d.posnr;
document.getElementById('form-kurztext').value=d.text||'';
document.getElementById('form-abschnitt').value=d.abschnitt||'';
document.getElementById('form-bemerkung').value='';
document.getElementById('form-langtext').textContent=d.lang||'';
updateLangPanel();
setFormelTyp('standard');
var faktorEl=document.getElementById('form-faktor');
['field-laenge','field-breite','field-tiefe'].forEach(function(id){
document.getElementById(id).querySelector('label').style.fontWeight='normal';
document.getElementById(id).querySelector('input').style.background='';
document.getElementById(id).querySelector('input').value='';
});
var eh=d.eh;
faktorEl.value='1,0';
faktorEl.style.background='';
faktorEl.style.fontWeight='';
faktorEl.placeholder='';
if(eh==='M'){
document.getElementById('field-laenge').querySelector('input').style.background='#fff3cd';
}else if(eh==='M2'){
document.getElementById('field-laenge').querySelector('input').style.background='#fff3cd';
document.getElementById('field-breite').querySelector('input').style.background='#fff3cd';
}else if(eh==='M3'){
document.getElementById('field-laenge').querySelector('input').style.background='#fff3cd';
document.getElementById('field-breite').querySelector('input').style.background='#fff3cd';
document.getElementById('field-tiefe').querySelector('input').style.background='#fff3cd';
}
var mengeEl=document.getElementById('form-menge');
berechneStd();
if(eh==='ST'||eh==='LE'||eh==='STD'||eh==='h'||eh==='Psch'){
mengeEl.style.background='#fff3cd';
mengeEl.style.fontWeight='bold';
mengeEl.value='';
}else{
mengeEl.style.background='';
mengeEl.style.fontWeight='';
}
}
function clearMultiSelect(){
selectedSet.forEach(function(id){
var row=document.querySelector('#lv-list .lv-item[data-id="'+id+'"]');
if(row)row.classList.remove('is-selected');
});
selectedSet.clear();
}
/* === Multi-Select für rechte Tabelle === */
var rightSelectedSet=new Set();
var rightLastClickedIdx=-1;
function clearRightMultiSelect(){
rightSelectedSet.forEach(function(id){
var row=document.querySelector('#pos-sortable tr[data-id="'+id+'"]');
if(row)row.classList.remove('is-selected-right');
});
rightSelectedSet.clear();
var el=document.getElementById('right-count');
if(el)el.textContent='';
}
function updateRightCount(){
var n=rightSelectedSet.size;
var el=document.getElementById('right-count');
if(!el)return;
if(n===0){el.textContent='';return;}
var sum=0;
rightSelectedSet.forEach(function(id){
var row=document.querySelector('#pos-sortable tr[data-id="'+id+'"]');
if(row)sum+=parseFloat(row.dataset.gp)||0;
});
el.textContent=n+' ausgewählt, Summe='+formatGerman(sum)+' €';
}
/* === Formelart umschalten === */
function setFormelTyp(t){
formelTyp=t;
var b1=document.getElementById('btn-std'),b2=document.getElementById('btn-frei');
b1.className='btn-formel'+(t==='standard'?' is-active':'');
b2.className='btn-formel'+(t==='frei'?' is-active':'');
document.getElementById('form-std').style.display=t==='standard'?'':'none';
document.getElementById('form-frei').style.display=t==='frei'?'':'none';
}
/* === Berechnungen === */
function parseNum(v){return parseFloat(String(v).replace(/,/g,'.'))||0;}
function berechneStd(){
const f=parseNum(document.getElementById('form-faktor').value);
const l=parseNum(document.getElementById('form-laenge').value);
const b=parseNum(document.getElementById('form-breite').value);
const t=parseNum(document.getElementById('form-tiefe').value);
const eh=currentLVRow&&currentLVRow.dataset.eh?currentLVRow.dataset.eh:currentEinheit||'ST';
var menge=0;
if(eh==='ST'||eh==='LE'||eh==='STD'||eh==='h'||eh==='Psch')menge=f*1;
else if(eh==='M')menge=l;
else if(eh==='M2')menge=l*b;
else if(eh==='M3')menge=l*b*t;
else menge=l;
document.getElementById('form-menge').value=germanNum(menge,3)||'0,000';
}
function berechneFrei(){
var menge=0;
try{
const expr=document.getElementById('form-formel').value.trim();
if(expr)menge=berechneAusdruck(expr);
}catch(e){menge=0;}
document.getElementById('form-menge-frei').value=germanNum(menge,3)||'0,000';
}
/* === Einfacher Math-Parser (Punkt vor Strich) === */
function berechneAusdruck(s){
s=s.replace(/,/g,'.'); // Deutsche Dezimaltrenner
var tokens=[],i=0,num='';
function flush(){if(num!==''){tokens.push(parseFloat(num));num='';}}
while(i<s.length){
var c=s[i];
if(c>='0'&&c<='9'||c==='.'){num+=c;i++;continue;}
flush();
if(c==='+'||c==='-'||c==='*'||c==='/'||c==='('||c===')'){tokens.push(c);i++;continue;}
if(c===' '||c==='\t'){i++;continue;}
throw new Error('Ungültiges Zeichen: '+c);
}
flush();
var pos=0;
function prim(){
if(pos>=tokens.length)throw new Error('Erwarte Zahl');
if(typeof tokens[pos]==='number')return tokens[pos++];
if(tokens[pos]==='('){pos++;var v=expr();if(tokens[pos]!==')')throw new Error(') erwartet');pos++;return v;}
throw new Error('Syntaxfehler');
}
function mul(){
var v=prim();
while(pos<tokens.length&&(tokens[pos]==='*'||tokens[pos]==='/')){
var op=tokens[pos++];var r=prim();
v=op==='*'?v*r:v/r;
}
return v;
}
function expr(){
var v=mul();
while(pos<tokens.length&&(tokens[pos]==='+'||tokens[pos]==='-')){
var op=tokens[pos++];var r=mul();
v=op==='+'?v+r:v-r;
}
return v;
}
return expr();
}
/* === Hinzufügen mit Formular-Daten (overrides + insert_idx) === */
function addMitFormular(){
if(selectedSet.size===0){alert('Keine LV-Positionen ausgewählt.');return;}
var ids=[];selectedSet.forEach(function(id){ids.push(parseInt(id));});
var overrides={};
overrides.formel_typ=formelTyp;
if(formelTyp==='frei'){
overrides.formel=document.getElementById('form-formel').value;
}else{
overrides.faktor=parseNum(document.getElementById('form-faktor').value);
overrides.laenge=parseNum(document.getElementById('form-laenge').value);
overrides.breite=parseNum(document.getElementById('form-breite').value);
overrides.tiefe=parseNum(document.getElementById('form-tiefe').value);
}
// Text-Overrides nur bei Einzelauswahl (sonst überschreiben sie alle LV-spezifischen Werte)
if(ids.length===1){
var pn=document.getElementById('form-posnr').value;if(pn)overrides.pos_nr=pn;
['kurztext','abschnitt','bemerkung'].forEach(function(id){
var v=document.getElementById('form-'+id).value;
if(v)overrides[id]=v;
});
}
var body={ids:ids,overrides:overrides};
var insertIdx=getInsertIdxFromCheckbox();
if(insertIdx>=0)body.insert_idx=insertIdx;
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/positionen/batch-add',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
}).then(function(r){return r.json()}).then(function(resp){
var marked=resp.added?resp.added.map(function(p){return p.id;}):ids;
// Push to undo stack for new positions with overrides - ONE ENTRY PER POSITION
if(resp.added && resp.added.length > 0) {
resp.added.forEach(function(pos) {
var overridesDesc = Object.keys(overrides || {}).map(function(k){ return k + '=' + overrides[k]; }).join(', ') || 'keine';
var desc = 'Pos ' + (pos.posnr || pos.id) + ' aus LV hinzugefügt (' + overridesDesc + ')';
pushUndoState(desc, [{
position_id: pos.id,
pos_nr: pos.posnr || '',
old: {id: pos.id},
new: {_action: 'add_form', id: pos.id, overrides: overrides}
}]);
});
} else if(marked && marked.length > 0) {
pushUndoState(marked.length + ' Position(en) mit Formular hinzugefügt', marked.map(function(id) {
return {position_id: id, pos_nr: '', old: {id: id}, new: {_action: 'add_form', id: id, overrides: overrides}};
}));
}
var doEmpty=document.getElementById('chk-empty-end')?.checked;
if(doEmpty){
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/position/leer',{
method:'POST',headers:{'Content-Type':'application/json'}
}).then(function(r){return r.json()}).then(function(resp2){
var m2=marked.slice();
if(resp2.id)m2.push(resp2.id);
reloadWithMarked(m2);
}).catch(function(){reloadWithMarked(marked)});
}else{reloadWithMarked(marked);}
});
}
/* === Leere Zeile + insert_idx === */
function addLeereZeile(){
var body={};
var insertIdx=getInsertIdxFromCheckbox();
if(insertIdx>=0)body.insert_idx=insertIdx;
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/position/leer',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
}).then(function(r){return r.json()}).then(function(resp){
if(resp.id) {
pushUndoState('+ Leere Zeile hinzugefügt', [{
position_id: resp.id,
pos_nr: resp.pos_nr || '',
old: {id: null},
new: {_action: 'add_empty', id: resp.id}
}]);
}
reloadWithMarked(resp.id?[resp.id]:[]);});
}
/* === Multi-Select: Alle/Keine + Auswahl hinzufügen === */
function toggleAllLV(){
var anySelected=selectedSet.size>0;
document.querySelectorAll('#lv-list .lv-item').forEach(function(row){
if(anySelected){selectedSet.delete(row.dataset.id);row.classList.remove('is-selected');}
else{selectedSet.add(row.dataset.id);row.classList.add('is-selected');}
});
updateLVCount();
}
function updateLVCount(){
var all=document.querySelectorAll('#lv-list .lv-item').length;
var n=selectedSet.size;
var sum=0;
selectedSet.forEach(function(id){
var row=document.querySelector('#lv-list .lv-item[data-id="'+id+'"]');
if(row)sum+=parseFloat(row.dataset.ep)||0;
});
document.getElementById('lv-count').textContent=n+'/'+all+' ausgewählt'+(sum>0?' | ∑ EP='+formatGerman(sum)+' €':'');
}
/* === Prüft ob "Am Ende anfügen" aktiv ist === */
function shouldAppendEnd(){
var chk=document.getElementById('chk-anfuegen');
return chk&&chk.checked;
}
/* === insert_idx aus Checkbox + rightLastClickedIdx ermitteln === */
function getInsertIdxFromCheckbox(){
if(shouldAppendEnd())return -1;
if(rightLastClickedIdx>=0)return rightLastClickedIdx+1;
return -1;
}
function getInsertIdxFromDrop(){
if(shouldAppendEnd())return -1;
if(_dragInsertIdx>=0)return _dragInsertIdx;
return -1;
}
function addSelectedLV(){
var ids=[];
selectedSet.forEach(function(id){ids.push(parseInt(id));});
if(!ids.length){alert('Keine LV-Positionen ausgewählt.');return;}
var body={ids:ids};
var insertIdx=getInsertIdxFromCheckbox();
if(insertIdx>=0)body.insert_idx=insertIdx;
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/positionen/batch-add',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
}).then(function(r){return r.json()}).then(function(resp){
var ids2=resp.added?resp.added.map(function(p){return p.id;}):ids;
if(resp.added && resp.added.length > 0) {
resp.added.forEach(function(pos) {
var kt = pos.kurztext ? ' - ' + pos.kurztext.substring(0,20) : '';
var desc = '+ Pos ' + (pos.pos_nr || pos.id) + kt;
pushUndoState(desc, [{
position_id: pos.id,
pos_nr: pos.pos_nr || '',
old: {id: null},
new: {_action: 'add', id: pos.id}
}]);
});
} else if(ids2 && ids2.length > 0) {
pushUndoState(ids2.length + ' Position(en) aus LV hinzugefügt', ids2.map(function(id) {
return {position_id: id, pos_nr: '', old: {id: null}, new: {_action: 'add', id: id}};
}));
}
reloadWithMarked(ids2);});
}
/* === Markierte Einträge löschen, kopieren + Tabelle leeren === */
function deleteSelectedPos(){
if(rightSelectedSet.size===0){alert('Keine Positionen markiert.');return;}
var ids=[];rightSelectedSet.forEach(function(id){ids.push(parseInt(id));});
// Store positions data for undo before deleting
var positionsData = [];
ids.forEach(function(id) {
var row = document.querySelector('#pos-sortable tr[data-id="'+id+'"]');
if(row) {
positionsData.push({
id: id,
posnr: row.dataset.posnr || '',
faktor: row.dataset.faktor || '1',
laenge: row.dataset.laenge || '0',
breite: row.dataset.breite || '0',
tiefe: row.dataset.tiefe || '0',
menge: row.dataset.menge || '0',
menge_hinten: row.dataset.mengeHinten || '0',
einheit: row.dataset.einheit || '',
formel_typ: row.dataset.formelTyp || 'standard',
formel: row.dataset.formel || '',
abschnitt: row.dataset.abschnitt || '',
bemerkung: row.dataset.bemerkung || '',
sort_order: row.rowIndex
});
}
});
// Push to undo stack BEFORE deleting
if(positionsData.length > 0) {
pushUndoState(positionsData.length + ' Position(en) gelöscht', positionsData.map(function(p) {
return {position_id: p.id, pos_nr: p.posnr, old: p, new: {_action: 'delete', id: p.id}};
}));
}
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/positionen/batch-loeschen',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({ids:ids})
}).then(function(r){return r.json()}).then(function(){location.reload();});
}
function copySelectedPos(){
if(rightSelectedSet.size===0){alert('Keine Positionen markiert.');return;}
var ids=[];rightSelectedSet.forEach(function(id){ids.push(parseInt(id));});
var body={ids:ids.map(Number)};
if(!shouldAppendEnd()){
if(rightLastClickedIdx>=0)body.insert_idx=rightLastClickedIdx+1;
}
// Store for undo (need to capture original IDs before copy)
var originalIds = ids.slice();
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/positionen/kopieren',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
}).then(function(r){return r.json()}).then(function(resp){
// Push to undo stack with new position IDs
if(resp.added && resp.added.length > 0) {
var copyInfo = [];
for(var i=0; i<resp.added.length; i++) {
copyInfo.push({
old_id: originalIds[i],
new_id: resp.added[i]
});
}
pushUndoState(originalIds.length + ' Position(en) kopiert', copyInfo.map(function(info) {
return {position_id: info.new_id, pos_nr: '', old: {id: info.old_id}, new: {_action: 'copy', old_id: info.old_id, new_id: info.new_id}};
}));
}
if(resp.added)reloadWithMarked(resp.added);
else location.reload();
});
}
function deleteAllPos(){
if(!confirm('Wirklich ALLE Positionen dieser Tabelle löschen?'))return;
// Store ALL positions data for undo
var allRows = document.querySelectorAll('#pos-sortable tr');
var allPositionsData = [];
allRows.forEach(function(row) {
var id = parseInt(row.dataset.id);
if(id) {
allPositionsData.push({
id: id,
posnr: row.dataset.posnr || '',
faktor: row.dataset.faktor || '1',
laenge: row.dataset.laenge || '0',
breite: row.dataset.breite || '0',
tiefe: row.dataset.tiefe || '0',
menge: row.dataset.menge || '0',
menge_hinten: row.dataset.mengeHinten || '0',
einheit: row.dataset.einheit || '',
formel_typ: row.dataset.formelTyp || 'standard',
formel: row.dataset.formel || '',
abschnitt: row.dataset.abschnitt || '',
bemerkung: row.dataset.bemerkung || ''
});
}
});
// Push to undo stack BEFORE deleting all
if(allPositionsData.length > 0) {
pushUndoState('Alle ' + allPositionsData.length + ' Position(en) gelöscht', allPositionsData.map(function(p) {
return {position_id: p.id, pos_nr: p.posnr, old: p, new: {_action: 'delete_all', id: p.id}};
}));
}
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/positionen/leeren',{
method:'POST',headers:{'Content-Type':'application/json'}
}).then(function(r){return r.json()}).then(function(){location.reload();});
}
/* === Drop-Indicator: farbige Linie === */
var dropIndicator=(function(){
var el=document.createElement('div');el.id='drop-indicator';
document.body.appendChild(el);return el;
})();
var _dragInsertIdx=-1;
function getInsertIndex(y){
var rows=document.querySelectorAll('#pos-sortable tr');
for(var i=0;i<rows.length;i++){
var r=rows[i].getBoundingClientRect();
if(y<r.top+r.height/2)return i;
}
return rows.length;
}
function showDropIndicator(e){
var table=document.getElementById('pos-table');
if(!table)return;
dropIndicator.style.display='block';
var rows=document.querySelectorAll('#pos-sortable tr');
var tableRect=table.getBoundingClientRect();
if(!rows.length){
dropIndicator.style.top=(tableRect.top+2)+'px';
dropIndicator.style.left=(tableRect.left+2)+'px';
dropIndicator.style.width=(tableRect.width-4)+'px';
_dragInsertIdx=0;
return;
}
var idx=getInsertIndex(e.clientY);
_dragInsertIdx=idx;
if(idx<rows.length){
var rowRect=rows[idx].getBoundingClientRect();
dropIndicator.style.top=(rowRect.top-2)+'px';
}else{
var lastRect=rows[rows.length-1].getBoundingClientRect();
dropIndicator.style.top=(lastRect.bottom-2)+'px';
}
dropIndicator.style.left=(tableRect.left+2)+'px';
dropIndicator.style.width=(tableRect.width-4)+'px';
}
function hideDropIndicator(){
dropIndicator.style.display='none';
_dragInsertIdx=-1;
}
/* === Drag & Drop (LV→Tabelle) + Multi-Select rechts + Reordering === */
document.addEventListener('DOMContentLoaded',function(){
var lvBody=document.getElementById('lv-list');
var posBody=document.getElementById('pos-sortable');
// LV-Dragstart
if(lvBody){
lvBody.addEventListener('dragstart',function(e){
window._dragFromLV=true;
if(selectedSet.size>0){
var ids=[];selectedSet.forEach(function(id){ids.push(parseInt(id));});
e.dataTransfer.setData('text/plain',JSON.stringify(ids));
}else{
var row=e.target.closest('.lv-item');
if(row)e.dataTransfer.setData('text/plain',JSON.stringify([parseInt(row.dataset.id)]));
}
e.dataTransfer.effectAllowed='copy';
});
}
// === Multi-Select rechte Tabelle ===
if(posBody){
posBody.addEventListener('click',function(e){
var row=e.target.closest('tr');
if(!row)return;
if(e.target.tagName==='A'||e.target.tagName==='BUTTON')return;
var idx=Array.from(posBody.children).indexOf(row);
if(e.ctrlKey||e.metaKey){
if(rightSelectedSet.has(row.dataset.id)){
rightSelectedSet.delete(row.dataset.id);
row.classList.remove('is-selected-right');
}else{
rightSelectedSet.add(row.dataset.id);
row.classList.add('is-selected-right');
}
rightLastClickedIdx=idx;updateRightCount();return;
}
if(e.shiftKey&&rightLastClickedIdx>=0){
var min=Math.min(rightLastClickedIdx,idx);
var max=Math.max(rightLastClickedIdx,idx);
clearRightMultiSelect();
for(var i=min;i<=max;i++){
var r=posBody.children[i];
if(r){rightSelectedSet.add(r.dataset.id);r.classList.add('is-selected-right');}
}
rightLastClickedIdx=idx;updateRightCount();return;
}
clearRightMultiSelect();
rightSelectedSet.add(row.dataset.id);
row.classList.add('is-selected-right');
rightLastClickedIdx=idx;
updateRightCount();
});
// Klick auf Position → Eingabe füllen
posBody.addEventListener('click',function(e){
var row=e.target.closest('tr');
if(!row||row.classList.contains('is-trenner'))return;
if(e.target.tagName==='A'||e.target.tagName==='BUTTON'||e.target.tagName==='INPUT')return;
var d=row.dataset;
var eh=d.einheit||'';
currentEinheit=eh;
// LV-Auswahl nicht zurücksetzen (sonst geht "Am Ende anfügen" verloren)
document.getElementById('form-lv-id').value='';
document.getElementById('form-posnr').value=d.posnr||'';
document.getElementById('form-kurztext').value=d.kurztext||'';
document.getElementById('form-abschnitt').value=d.abschnitt||'';
document.getElementById('form-langtext').textContent=d.langtext||'';
updateLangPanel();
document.getElementById('form-bemerkung').value=d.bemerkung||'';
setFormelTyp(d.formelTyp||'standard');
if((d.formelTyp||'standard')==='frei'){
document.getElementById('form-formel').value=d.formel||'';
berechneFrei();
}else{
currentEinheit=eh;
// Hintergrund-Farben zurücksetzen
['field-laenge','field-breite','field-tiefe'].forEach(function(id){
document.getElementById(id).querySelector('label').style.fontWeight='normal';
document.getElementById(id).querySelector('input').style.background='';
document.getElementById(id).querySelector('input').value='';
});
// Faktor: default 1,0 wenn 0, sonst Wert
var fakt=parseNum(d.faktor);
var faktorEl=document.getElementById('form-faktor');
faktorEl.value=fakt>0?germanNum(d.faktor,2):'1,0';
faktorEl.style.background='';
faktorEl.style.fontWeight='';
faktorEl.placeholder='';
if(eh==='ST'||eh==='LE'||eh==='STD'||eh==='h'||eh==='Psch'){
['field-laenge','field-breite','field-tiefe'].forEach(function(id){
document.getElementById(id).querySelector('input').value='';
});
}
if(eh==='M'){
document.getElementById('field-laenge').querySelector('input').style.background='#fff3cd';
document.getElementById('form-laenge').value=d.laenge&&parseNum(d.laenge)?germanNum(d.laenge,2):'';
}else if(eh==='M2'){
document.getElementById('field-laenge').querySelector('input').style.background='#fff3cd';
document.getElementById('field-breite').querySelector('input').style.background='#fff3cd';
document.getElementById('form-laenge').value=d.laenge&&parseNum(d.laenge)?germanNum(d.laenge,2):'';
document.getElementById('form-breite').value=d.breite&&parseNum(d.breite)?germanNum(d.breite,2):'';
}else if(eh==='M3'){
document.getElementById('field-laenge').querySelector('input').style.background='#fff3cd';
document.getElementById('field-breite').querySelector('input').style.background='#fff3cd';
document.getElementById('field-tiefe').querySelector('input').style.background='#fff3cd';
document.getElementById('form-laenge').value=d.laenge&&parseNum(d.laenge)?germanNum(d.laenge,2):'';
document.getElementById('form-breite').value=d.breite&&parseNum(d.breite)?germanNum(d.breite,2):'';
document.getElementById('form-tiefe').value=d.tiefe&&parseNum(d.tiefe)?germanNum(d.tiefe,2):'';
}
// Menge-Highlight für ST/LE/STD
berechneStd();
if(eh==='ST'||eh==='LE'||eh==='STD'||eh==='h'||eh==='Psch'){
document.getElementById('form-menge').style.background='#fff3cd';
document.getElementById('form-menge').style.fontWeight='bold';
document.getElementById('form-menge').value='';
}else{
document.getElementById('form-menge').style.background='';
document.getElementById('form-menge').style.fontWeight='';
}
}
});
// Doppelklick → Inline-Edit
posBody.addEventListener('dblclick',function(e){
var td=e.target.closest('td');
if(!td)return;
var row=td.closest('tr');
if(!row||row.classList.contains('is-trenner'))return;
var field=td.dataset.field;
if(!field)return; // nur Zellen mit data-field sind editierbar
if(td.querySelector('input')||td.querySelector('select'))return;
var fTyp=row.dataset.formelTyp||'standard';
var txt=td.textContent.trim();
if(txt==='')txt='';
var textFields={'formel_typ':1,'einheit':1,'abschnitt':1,'kurztext':1,'bemerkung':1,'formel':1};
// Z-Art: Select
if(field==='formel_typ'){
var sel=document.createElement('select');
sel.style.width='100%';sel.style.boxSizing='border-box';sel.style.fontSize='inherit';
sel.className='input is-small';
['Standard','Freie Formel Z91'].forEach(function(t){
var o=document.createElement('option');
o.value=t;o.textContent=t;
if((fTyp==='frei'&&t==='Freie Formel Z91')||(fTyp!=='frei'&&t==='Standard'))o.selected=true;
sel.appendChild(o);
});
td.textContent='';td.appendChild(sel);sel.focus();
function saveZArt(){
var newTyp=sel.value==='Freie Formel Z91'?'frei':'standard';
var oldTyp=row.dataset.formelTyp||'standard';
td.innerHTML='<span class="tag is-light is-small" style="font-size:0.65rem">'+(newTyp==='frei'?'Z91':'Std')+'</span>';
row.dataset.formelTyp=newTyp;
// Push to undo stack
if (oldTyp !== newTyp) {
pushUndoState('Z-Art geändert', [{position_id: parseInt(row.dataset.id), pos_nr: '', old: {formel_typ: oldTyp}, new: {formel_typ: newTyp}}]);
}
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/position/'+row.dataset.id+'/update-cell',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({field:'formel_typ',value:newTyp})
}).then(function(r){return r.json()}).then(function(resp){
if(resp.formel_typ_changed)location.reload();
});
}
sel.addEventListener('blur',saveZArt);
sel.addEventListener('change',function(ev){ev.preventDefault();saveZArt();});
return;
}
var inp=document.createElement('input');
inp.value=(field==='formel')?(row.dataset.formel||''):txt;
inp.style.width='100%';inp.style.boxSizing='border-box';inp.style.fontSize='inherit';
inp.className='input is-small';
td.textContent='';td.appendChild(inp);inp.focus();inp.select();
var _saving=false;
function save(){
if(_saving)return;
_saving=true;
var val=inp.value.trim();
var isText=textFields[field];
var oldVal = txt;
if(isText){
if(field==='formel'&&td.colSpan){
td.colSpan=3;td.style.textAlign='center';td.style.fontFamily='monospace';td.style.fontSize='0.75rem';td.style.background='#fafafa';
}
var inner=td.querySelector('[data-cell]');
if(inner){inner.textContent=val||'';}else{td.textContent=val||'';}
}else{
var numVal=parseFloat(val.replace(',','.'))||0;
var fmt=formatGerman(numVal);
var inner=td.querySelector('[data-cell]');
if(inner){inner.textContent=fmt;}else{td.textContent=fmt;}
val=numVal;
}
td.style.borderColor='';
// Push to undo stack
if (oldVal !== val) {
var rowId = parseInt(row.dataset.id);
var undoChange = {position_id: rowId, pos_nr: '', old: {}, new: {}};
undoChange.old[field] = isText ? oldVal : (parseFloat(oldVal.replace(',','.'))||0);
undoChange.new[field] = val;
pushUndoState('Feld "'+field+'" geändert', [undoChange]);
}
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/position/'+row.dataset.id+'/update-cell',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({field:field,value:val})
}).then(function(r){return r.json()}).then(function(resp){
var row2=document.querySelector('#pos-sortable tr[data-id="'+row.dataset.id+'"]');
if(resp.menge!==undefined){row2.dataset.menge=resp.menge;
var mengeTd=row2.querySelector('[data-cell="menge"]');if(mengeTd)mengeTd.innerHTML=formatGerman3(resp.menge);}
if(resp.gesamtpreis!==undefined){row2.dataset.gp=resp.gesamtpreis;
var gpTd=row2.querySelector('[data-cell="gp"]');if(gpTd)gpTd.innerHTML=formatGerman(resp.gesamtpreis);}
if(resp.menge_hinten!==undefined){row2.dataset.mengeHinten=resp.menge_hinten;
var hintenTd=row2.querySelector('[data-field="menge_hinten"]');if(hintenTd)hintenTd.textContent=formatGerman(resp.menge_hinten);}
});
}
inp.addEventListener('blur',save);
inp.addEventListener('keydown',function(ev){
if(ev.key==='Enter'){ev.preventDefault();save();}
if(ev.key==='Escape'){td.textContent=txt;inp.blur();}
});
});
}
// === Drag innerhalb der rechten Tabelle (verschieben markierter Zeilen) ===
if(posBody){
// Dragstart: alle markierten IDs speichern
posBody.addEventListener('dragstart',function(e){
var row=e.target.closest('tr');
if(!row)return;
if(e.target.tagName==='A'||e.target.tagName==='BUTTON'){e.preventDefault();return;}
var ids=[];
if(rightSelectedSet.has(row.dataset.id)){
rightSelectedSet.forEach(function(id){ids.push(id);});
}else{
ids.push(row.dataset.id);
}
e.dataTransfer.setData('text/plain',JSON.stringify(ids));
e.dataTransfer.effectAllowed='move';
window._dragFromTable=true;
});
posBody.addEventListener('dragend',function(){window._dragFromTable=false;hideDropIndicator();});
// Shared drop/dragover handler für Container + posBody (für leere Tabelle)
var posContainer=document.querySelector('#am-right > div:not([style*="flex-shrink"])');
function handlePosDrop(e){
e.preventDefault();
e.stopPropagation();
var raw=e.dataTransfer.getData('text/plain');
var dropIdx=_dragInsertIdx;
hideDropIndicator();
if(raw==='__LEERE_ZEILE__'){
var body={};
if(!shouldAppendEnd()&&dropIdx>=0){
body.insert_idx=dropIdx;
}
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/position/leer',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
}).then(function(r){return r.json()}).then(function(resp){
if(resp.id) {
// Push to undo stack for new empty position
pushUndoState('Leere Zeile hinzugefügt', [{
position_id: resp.id,
pos_nr: '',
old: {id: resp.id},
new: {_action: 'add_empty', id: resp.id}
}]);
reloadWithMarked([resp.id]);
} else {
reloadWithMarked([]);
}
});
return;
}
var saveIdx=dropIdx;
var useEnd=shouldAppendEnd();
var ids;
try{ids=JSON.parse(raw);}catch(ex){ids=null;}
if(!ids||!Array.isArray(ids)||ids.length===0)return;
if(window._dragFromLV){
var body={ids:ids.map(Number)};
if(!useEnd&&saveIdx>=0)body.insert_idx=saveIdx;
// Push to undo stack BEFORE fetch for LV additions
var insertLine = saveIdx >= 0 ? saveIdx : (document.querySelectorAll('#pos-sortable tr').length || 1);
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/positionen/batch-add',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
}).then(function(r){return r.json()}).then(function(resp){
var ids2=resp.added?resp.added.map(function(p){return p.id;}):ids;
// Push to undo stack AFTER fetch for new positions - ONE ENTRY PER POSITION
if(resp.added && resp.added.length > 0) {
resp.added.forEach(function(pos, i) {
var desc = '+ Pos ' + (pos.pos_nr || pos.id) + ' via Drag&Drop aus LV (Zeile ' + (insertLine + i) + ')';
if(pos.kurztext) desc += ' - ' + pos.kurztext.substring(0,25);
pushUndoState(desc, [{
position_id: pos.id,
pos_nr: pos.pos_nr || '',
old: {id: null},
new: {_action: 'add', id: pos.id}
}]);
});
} else if(ids2 && ids2.length > 0) {
pushUndoState(ids2.length + ' Position(en) aus LV hinzugefügt', ids2.map(function(id) {
return {position_id: id, pos_nr: '', old: {id: null}, new: {_action: 'add', id: id}};
}));
}
// Mark ALL added positions after LV drag & drop
if(ids2 && ids2.length > 0) {
sessionStorage.setItem('marked_pos_' + AUFMASS_ID, JSON.stringify(ids2));
}
reloadWithMarked(ids2);});
}else if(window._dragFromTable){
var body={ids:ids.map(function(x){return parseInt(x,10);})};
if(!useEnd && typeof saveIdx === 'number' && saveIdx >= 0){
body.insert_idx = saveIdx;
}
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/positionen/batch-reorder',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
}).then(function(r){return r.json()}).then(function(resp){
if(resp && resp.status === 'ok') {
reloadWithMarked(ids);
}
}).catch(function(e){
console.error('batch-reorder error:', e);
});
}
window._dragFromLV=false;window._dragFromTable=false;
}
function handlePosDragOver(e){
e.preventDefault();
if(window._dragFromLV||window._dragFromTable){
e.dataTransfer.dropEffect=window._dragFromLV?'copy':'move';
}else{
e.dataTransfer.dropEffect='copy';
}
}
if(posContainer){
posContainer.addEventListener('dragover',handlePosDragOver);
posContainer.addEventListener('drop',handlePosDrop);
}
posBody.addEventListener('dragover',handlePosDragOver);
posBody.addEventListener('drop',handlePosDrop);
// === Context-Menu per Rechtsklick ===
posBody.addEventListener('contextmenu',function(e){
var row=e.target.closest('tr');
if(!row)return;
if(e.target.tagName==='A'||e.target.tagName==='BUTTON')return;
var idx=Array.from(posBody.children).indexOf(row);
if(!row.classList.contains('is-trenner')&&!e.ctrlKey&&!e.shiftKey){
if(!rightSelectedSet.has(row.dataset.id)){
clearRightMultiSelect();
rightSelectedSet.add(row.dataset.id);
row.classList.add('is-selected-right');
updateRightCount();
}
}
rightLastClickedIdx=idx;
showContextMenu(e);
});
}
// === Document-weite Handler nur für den visuellen Indicator ===
document.addEventListener('dragover',function(e){
var table=document.getElementById('pos-table');
if(!table)return;
var r=table.getBoundingClientRect();
if(e.clientX>=r.left&&e.clientX<=r.right&&e.clientY>=r.top&&e.clientY<=r.bottom){
showDropIndicator(e);
}else if(dropIndicator.style.display!=='none'){
hideDropIndicator();
}
});
document.addEventListener('dragleave',function(e){
if(!e.relatedTarget||e.relatedTarget===document.documentElement)hideDropIndicator();
});
document.addEventListener('dragend',function(){window._dragFromLV=false;window._dragFromTable=false;hideDropIndicator();});
// === Farben laden ===
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/positionen/farben').then(function(r){return r.json()}).then(function(farben){
Object.keys(farben).forEach(function(pid){
var row=document.querySelector('#pos-sortable tr[data-id="'+pid+'"]');
if(!row)return;
var cols=farben[pid];
if(cols.all){row.style.background=cols.all;return;}
if(cols.pos_nr){
var td=row.querySelector('td:nth-child(4)');
if(td)td.style.background=cols.pos_nr;td.style.color='#fff';
}
if(cols.menge){
var td=row.querySelector('td:nth-child(9)');
if(td)td.style.background=cols.menge;
}
if(cols.laenge){
var td=row.querySelector('td:nth-child(6)');
if(td)td.style.background=cols.laenge;
}
if(cols.breite){
var td=row.querySelector('td:nth-child(7)');
if(td)td.style.background=cols.breite;
}
if(cols.tiefe){
var td=row.querySelector('td:nth-child(8)');
if(td)td.style.background=cols.tiefe;
}
});
});
});
/* === Spalten-Filter (Sortieren + Filtern) === */
var filters={}; // { field: { search:'', sort:null, values:Set|null } }
var activeFilterField=null;
function showFilterMenu(field,th){
hideFilterMenu();
var menu=document.getElementById('filter-menu');
activeFilterField=field;
if(!filters[field])filters[field]={search:'',sort:null,values:null};
var rect=th.getBoundingClientRect();
menu.style.left=rect.left+'px';
menu.style.top=(rect.bottom+2)+'px';
menu.style.display='block';
document.getElementById('filter-values').style.display='';
// Search vorbelegen
var inp=document.getElementById('filter-search');
inp.value=filters[field].search||'';
// Checkboxen bauen
buildFilterCheckboxes(field);
}
function hideFilterMenu(){
document.getElementById('filter-menu').style.display='none';
activeFilterField=null;
}
function buildFilterCheckboxes(field){
var div=document.getElementById('filter-checkboxes');
div.innerHTML='';
var rows=document.querySelectorAll('#pos-sortable tr[data-id]');
var values=new Set();
rows.forEach(function(r){
var v=String(r.getAttribute('data-'+field)||'');
if(v)values.add(v);
});
var arr=Array.from(values).sort(function(a,b){
var an=parseFloat(a.replace(',','.'));var bn=parseFloat(b.replace(',','.'));
if(!isNaN(an)&&!isNaN(bn))return an-bn;
return a.localeCompare(b);
});
arr.unshift('');
values=arr;
var search=(filters[field].search||'').toLowerCase();
var currentVal=filters[field].values;
values.forEach(function(v){
if(search&&!v.toLowerCase().includes(search))return;
var lb=document.createElement('label');
var cb=document.createElement('input');
cb.type='checkbox';
cb.checked=currentVal===null||currentVal.has(v);
cb.onchange=function(){filterToggleValue(field,v);};
lb.appendChild(cb);
lb.appendChild(document.createTextNode(' '+(v||'(leer)')));
div.appendChild(lb);
});
}
function filterToggleValue(field,value){
if(!filters[field])filters[field]={search:'',sort:null,values:null};
if(!filters[field].values)filters[field].values=new Set();
if(filters[field].values.has(value)){
filters[field].values.delete(value);
if(!filters[field].values.size)filters[field].values=null;
}else{
filters[field].values.add(value);
// Prüfen ob alle Werte ausgewählt → null (kein Filter)
var all=document.querySelectorAll('#filter-checkboxes input[type=checkbox]');
var allChecked=true;
all.forEach(function(cb){if(!cb.checked)allChecked=false;});
if(allChecked)filters[field].values=null;
}
applyFilters();
}
function filterSearch(val){
if(!activeFilterField)return;
if(!filters[activeFilterField])filters[activeFilterField]={search:'',sort:null,values:null};
filters[activeFilterField].search=val;
buildFilterCheckboxes(activeFilterField);
applyFilters();
}
function filterSelectAll(){
if(!activeFilterField)return;
if(!filters[activeFilterField])filters[activeFilterField]={search:'',sort:null,values:null};
filters[activeFilterField].values=null; // null = kein Filter
applyFilters();
buildFilterCheckboxes(activeFilterField);
}
function filterSelectNone(){
if(!activeFilterField)return;
if(!filters[activeFilterField])filters[activeFilterField]={search:'',sort:null,values:null};
filters[activeFilterField].values=new Set(); // leeres Set = nichts zeigen
applyFilters();
buildFilterCheckboxes(activeFilterField);
}
function filterSort(dir){
if(!activeFilterField)return;
if(!filters[activeFilterField])filters[activeFilterField]={search:'',sort:null,values:null};
filters[activeFilterField].sort=dir;
applyFilters();
hideFilterMenu();
}
function applyFilters(){
// Sort- und Filter-Status visualisieren (fbtn active)
var all=document.querySelectorAll('#pos-table thead th[data-field]');
all.forEach(function(th){
var f=th.getAttribute('data-field');
var btn=th.querySelector('.fbtn');
if(btn){
var st=filters[f];
var active=st&&((st.sort)||(st.search)||(st.values));
btn.classList.toggle('active',!!active);
}
});
var rows=Array.from(document.querySelectorAll('#pos-sortable tr'));
// Filter anwenden
rows.forEach(function(r){
if(!r.dataset.id){r.style.display='';return;}
var show=true;
Object.keys(filters).forEach(function(f){
var st=filters[f];
if(!st)return;
// Text-Suche
if(st.search){
var val=String(r.getAttribute('data-'+f)||'');
if(val.toLowerCase().indexOf(st.search.toLowerCase())===-1){show=false;return;}
}
// Wert-Liste
if(st.values!==null){
var val2=String(r.getAttribute('data-'+f)||'');
if(!st.values.has(val2)){show=false;return;}
}
});
r.style.display=show?'':'none';
});
// Sortieren
var sortField=null,sortDir=null;
Object.keys(filters).forEach(function(f){
if(filters[f].sort){sortField=f;sortDir=filters[f].sort;}
});
if(sortField&&sortDir){
var parent=document.getElementById('pos-sortable');
var allSorted=rows.filter(function(r){return r.dataset.id;}).sort(function(a,b){
var va=String(a.getAttribute('data-'+sortField)||'');
var vb=String(b.getAttribute('data-'+sortField)||'');
var na=parseFloat(va.replace(',','.'));var nb=parseFloat(vb.replace(',','.'));
if(!isNaN(na)&&!isNaN(nb)){
return sortDir==='asc'?na-nb:nb-na;
}
return sortDir==='asc'?va.localeCompare(vb):vb.localeCompare(va);
});
// Trenner an ihren Positionen lassen, nur Datenzeilen umsortieren
var trenner=rows.filter(function(r){return r.classList.contains('is-trenner');});
// Alle Zeilen neu anordnen: Trenner bleiben, Datenzeilen dazwischen einsortiert
var newOrder=[];
var dataIdx=0;
rows.forEach(function(r){
if(r.classList.contains('is-trenner')){
newOrder.push(r);
}else{
if(dataIdx<allSorted.length)newOrder.push(allSorted[dataIdx++]);
}
});
newOrder.forEach(function(r){parent.appendChild(r);});
}
// Reset-Button-Status
var hasActive=Object.keys(filters).some(function(f){
var st=filters[f];return st&&(st.sort||st.search||st.values!==null);
});
document.getElementById('btn-reset-filter').style.display=hasActive?'':'none';
}
function filterReset(){
filters={};
activeFilterField=null;
document.getElementById('filter-search').value='';
hideFilterMenu();
// Alle Zeilen zeigen + Sortierung zurücksetzen via Reload (einfachster Weg)
location.reload();
}
// Filter-Menü schließen bei Klick außerhalb
document.addEventListener('click',function(e){
var menu=document.getElementById('filter-menu');
if(menu&&menu.style.display!=='none'&&!menu.contains(e.target)&&!e.target.closest('th[data-field]')){
hideFilterMenu();
}
});
/* === LV-Tabellen Filter === */
var lvFilters={};
var activeLVFilterField=null;
function showLVFilterMenu(field,th){
hideLVFilterMenu();
var menu=document.getElementById('filter-menu');
activeLVFilterField=field;
if(!lvFilters[field])lvFilters[field]={search:'',sort:null};
var rect=th.getBoundingClientRect();
menu.style.left=rect.left+'px';
menu.style.top=(rect.bottom+2)+'px';
menu.style.display='block';
var inp=document.getElementById('filter-search');
inp.value=lvFilters[field].search||'';
// Nur Sortierung + Suche, keine Checkboxen (LV hat zu viele unique values)
document.getElementById('filter-values').style.display='none';
}
function hideLVFilterMenu(){activeLVFilterField=null;}
function applyLVFilters(){
var all=document.querySelectorAll('#lv-table thead th[data-field]');
all.forEach(function(th){
var f=th.getAttribute('data-field');
var btn=th.querySelector('.fbtn');
if(btn){
var st=lvFilters[f];
btn.classList.toggle('active',!!(st&&(st.sort||st.search)));
}
});
var rows=Array.from(document.querySelectorAll('#lv-list .lv-item'));
rows.forEach(function(r){
var show=true;
Object.keys(lvFilters).forEach(function(f){
var st=lvFilters[f];
if(!st)return;
if(st.search){
var val=String(r.getAttribute('data-'+f)||'');
if(val.toLowerCase().indexOf(st.search.toLowerCase())===-1){show=false;return;}
}
});
r.style.display=show?'':'none';
r.classList.toggle('filter-hidden',!show);
});
// Sortieren
var sortField=null,sortDir=null;
Object.keys(lvFilters).forEach(function(f){
if(lvFilters[f].sort){sortField=f;sortDir=lvFilters[f].sort;}
});
if(sortField&&sortDir){
var parent=document.getElementById('lv-list');
var sorted=rows.filter(function(r){return r.style.display!=='none';});
sorted.sort(function(a,b){
var va=String(a.getAttribute('data-'+sortField)||'');
var vb=String(b.getAttribute('data-'+sortField)||'');
var na=parseFloat(va.replace(',','.'));var nb=parseFloat(vb.replace(',','.'));
if(!isNaN(na)&&!isNaN(nb))return sortDir==='asc'?na-nb:nb-na;
return sortDir==='asc'?va.localeCompare(vb):vb.localeCompare(va);
});
sorted.forEach(function(r){parent.appendChild(r);});
}
updateLVCount();
}
// LV filter nutzt die gleichen Buttons (Sort) aus dem filter-menu
// Überschreibe filterSort für LV
var _origFilterSort=filterSort;
filterSort=function(dir){
if(activeLVFilterField!==null&&activeLVFilterField!==undefined){
if(!lvFilters[activeLVFilterField])lvFilters[activeLVFilterField]={search:'',sort:null};
lvFilters[activeLVFilterField].sort=dir;
applyLVFilters();
hideFilterMenu();
hideLVFilterMenu();
}else{
_origFilterSort(dir);
}
};
var _origFilterSearch=filterSearch;
filterSearch=function(val){
var menu=document.getElementById('filter-menu');
if(activeLVFilterField!==null&&activeLVFilterField!==undefined){
if(!lvFilters[activeLVFilterField])lvFilters[activeLVFilterField]={search:'',sort:null};
lvFilters[activeLVFilterField].search=val;
applyLVFilters();
}else{
_origFilterSearch(val);
}
};
// LV filter zurücksetzen + Checkboxen wieder zeigen wenn POS-Filter
var _origHideFilterMenu=hideFilterMenu;
hideFilterMenu=function(){
_origHideFilterMenu();
document.getElementById('filter-values').style.display='';
activeLVFilterField=null;
};
/* === Context-Menu === */
function showContextMenu(e){
e.preventDefault();
var menu=document.getElementById('pos-context-menu');
if(!menu)return;
menu.style.left=e.clientX+'px';
menu.style.top=e.clientY+'px';
menu.style.display='block';
}
function hideContextMenu(){
var menu=document.getElementById('pos-context-menu');
if(menu)menu.style.display='none';
}
document.addEventListener('click',function(e){
var menu=document.getElementById('pos-context-menu');
if(menu&&menu.style.display!=='none'&&!menu.contains(e.target))hideContextMenu();
});
document.addEventListener('keydown',function(e){
if(e.key==='Escape')hideContextMenu();
});
/* === Edit Modal === */
function openEdit(posId){
const row=document.querySelector('#pos-sortable tr[data-id="'+posId+'"]');
if(!row)return;
const d=row.dataset;
var ft=d.formelTyp||'standard';
document.getElementById('pos-edit-content').innerHTML=
'<form method="POST" action="/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/position/'+posId+'/aktualisieren">'+
'<div class="field"><label class="label">Pos-Nr</label><input class="input" name="pos_nr" value="'+esc(d.posnr)+'"></div>'+
'<div class="field"><label class="label">Kurztext</label><input class="input" name="kurztext" value="'+esc(d.kurztext)+'"></div>'+
'<div class="field"><label class="label">Einheit</label><input class="input" name="einheit" value="'+esc(d.einheit)+'"></div>'+
'<div class="field"><label class="label">Einzelpreis (€)</label><input class="input" name="einzelpreis" type="number" step="0.01" value="'+d.ep+'"></div>'+
'<div class="field"><label class="label">RSA-Abschnitt</label><input class="input" name="abschnitt" value="'+esc(d.abschnitt||'')+'"></div>'+
'<div class="field"><label class="label">Formelart</label><div>'+
'<label class="radio"><input type="radio" name="formel_typ" value="standard" '+(ft==='standard'?'checked':'')+' onchange="editFormelTyp(this.value)"> Standard</label>'+
'<label class="radio"><input type="radio" name="formel_typ" value="frei" '+(ft==='frei'?'checked':'')+' onchange="editFormelTyp(this.value)"> Freie Formel (Z91)</label></div></div>'+
'<div id="edit-std" style="display:'+(ft==='standard'?'':'none')+'">'+
'<div class="columns"><div class="column"><label class="label">Faktor</label><input class="input" name="faktor" type="number" step="0.01" value="'+d.faktor+'"></div>'+
'<div class="column"><label class="label">Länge (m)</label><input class="input" name="laenge" type="number" step="0.01" value="'+d.laenge+'"></div>'+
'<div class="column"><label class="label">Breite (m)</label><input class="input" name="breite" type="number" step="0.01" value="'+d.breite+'"></div>'+
'<div class="column"><label class="label">Tiefe (m)</label><input class="input" name="tiefe" type="number" step="0.01" value="'+d.tiefe+'"></div></div></div>'+
'<div id="edit-frei" style="display:'+(ft==='frei'?'':'none')+'">'+
'<div class="field"><label class="label">Formel (Z91)</label><input class="input" name="formel" value="'+esc(d.formel||'')+'" placeholder="2*1+1"></div></div>'+
'<div class="field"><label class="label">Bemerkung</label><textarea class="textarea" name="bemerkung" rows="2">'+esc(d.bemerkung)+'</textarea></div>'+
'<button class="button is-primary" type="submit">Speichern & Neu berechnen</button>'+
'<button class="button is-light" type="button" onclick="closeEdit()">Abbrechen</button></form>';
document.getElementById('pos-edit-modal').classList.add('is-active');
}
window.editFormelTyp=function(v){
document.getElementById('edit-std').style.display=v==='standard'?'':'none';
document.getElementById('edit-frei').style.display=v==='frei'?'':'none';
};
function closeEdit(){document.getElementById('pos-edit-modal').classList.remove('is-active');}
function esc(s){if(!s)return '';return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
/* === Multi-Edit === */
function openMultiEdit(){
if (rightSelectedSet.size === 0) return;
// Sortiere IDs nach ihrer Position in der Tabelle (data-sort Attribut)
var ids = Array.from(rightSelectedSet).sort(function(a, b){
var rowA = document.querySelector('#pos-sortable tr[data-id="'+a+'"]');
var rowB = document.querySelector('#pos-sortable tr[data-id="'+b+'"]');
var sortA = rowA ? parseInt(rowA.dataset.sort) || 0 : 0;
var sortB = rowB ? parseInt(rowB.dataset.sort) || 0 : 0;
return sortA - sortB;
});
var rows = [];
ids.forEach(function(id){
var r = document.querySelector('#pos-sortable tr[data-id="'+id+'"]');
if (!r) return;
var d = r.dataset;
var ft = d.formelTyp || 'standard';
rows.push({
id: id,
posnr: d.posnr||'',
kurztext: d.kurztext||'',
einheit: d.einheit||'',
ep: d.ep||'0',
gp: d.gp||'0',
faktor: d.faktor||'1',
laenge: d.laenge||'0',
breite: d.breite||'0',
tiefe: d.tiefe||'0',
formel: d.formel||'',
menge: d.menge||'0',
menge_hinten: d.mengeHinten||'0',
abschnitt: d.abschnitt||'',
bemerkung: d.bemerkung||'',
formelTyp: ft
});
});
if (rows.length === 0) return;
function _gf(v,p){var s=germanNum(v,p);return s!==''?s:'0';}
window._meOrigData = {};
rows.forEach(function(r){
window._meOrigData[r.id] = {faktor: r.faktor, laenge: r.laenge, breite: r.breite, tiefe: r.tiefe, menge: r.menge, menge_hinten: r.menge_hinten, formel: r.formel, posnr: r.posnr};
});
function _rel(eh,f){
if(f==='faktor')return true;
if(f==='laenge')return !/^(ST|LE|STD|h|Psch)$/.test(eh);
if(f==='breite')return eh==='M2'||eh==='M3';
if(f==='tiefe')return eh==='M3';
return true;
}
var html = '<div style="overflow-x:auto"><table class="table is-fullwidth is-hoverable" id="me-table" style="font-size:0.85rem;table-layout:fixed;white-space:nowrap"><thead><tr>' +
'<th style="width:30px">#</th><th style="width:45px">Z-Art</th><th style="width:55px">Abschn</th>' +
'<th style="width:50px">Faktor</th><th style="width:50px">Länge</th><th style="width:50px">Breite</th><th style="width:50px">Tiefe</th>' +
'<th style="width:60px">Menge</th><th style="width:55px">EH</th><th style="width:100px">Kurztext</th><th style="width:75px">Bemerkung</th>' +
'<th style="width:60px">M-hinten</th><th style="width:60px">EP</th><th style="width:65px">GP</th><th style="width:30px">✕</th>' +
'</tr></thead><tbody>';
rows.forEach(function(r, idx){
var ft = r.formelTyp;
var isFrei = ft === 'frei';
var stUnit = /^(ST|LE|STD|h|Psch)$/.test(r.einheit);
function _inp(field,val,width,css){return'<input class="input is-small me-field me-recalc" data-id="'+r.id+'" data-field="'+field+'" value="'+val+'" style="width:'+width+';font-size:0.8rem;padding:1px 4px;height:26px'+(css?css:'')+'" inputmode="decimal" oninput="meRecalc(\''+r.id+'\')">';}
var la = _rel(r.einheit,'laenge') ? _inp('laenge',_gf(r.laenge,3),'55px') : '<span class="me-na" style="color:#888"></span>';
var br = _rel(r.einheit,'breite') ? _inp('breite',_gf(r.breite,3),'55px') : '<span class="me-na" style="color:#888"></span>';
var ti = _rel(r.einheit,'tiefe') ? _inp('tiefe',_gf(r.tiefe,3),'55px') : '<span class="me-na" style="color:#888"></span>';
html += '<tr id="me-row-'+r.id+'">' +
'<td style="text-align:center;color:#888;font-size:0.7rem">'+esc(r.posnr)+'</td>' +
'<td><select class="is-small me-field me-recalc" data-id="'+r.id+'" data-field="formel_typ" style="width:45px;font-size:0.8rem;padding:1px" onchange="meRecalc(\''+r.id+'\')">' +
'<option value="standard"'+(ft==='standard'?' selected':'')+'>Std</option>'+
'<option value="frei"'+(ft==='frei'?' selected':'')+'>Z91</option></select></td>' +
'<td><input class="input is-small me-field" data-id="'+r.id+'" data-field="abschnitt" value="'+esc(r.abschnitt)+'" style="width:60px;font-size:0.8rem;padding:1px 4px;height:26px"></td>' +
'<td>'+(isFrei?'<span class="me-na" style="color:#888"></span>':_inp('faktor',_gf(r.faktor,3),'55px'))+'</td>' +
'<td>'+(isFrei?
'<input class="input is-small me-field me-recalc" data-id="'+r.id+'" data-field="formel" value="'+esc(r.formel||'')+'" style="width:100%;font-size:0.8rem;padding:1px 4px;height:26px;font-family:monospace;background:#e8f4fd" oninput="meRecalc(\''+r.id+'\')">' :
la)+'</td>' +
'<td>'+(isFrei?'<span class="me-na" style="color:#888"></span>':br)+'</td>' +
'<td>'+(isFrei?'<span class="me-na" style="color:#888"></span>':ti)+'</td>' +
// Menge: editierbar bei ST/LE/STD/h/Psch, sonst berechnet
'<td>'+(stUnit?'<input class="input is-small me-field me-recalc" data-id="'+r.id+'" data-field="menge" value="'+_gf(r.menge,3)+'" style="width:60px;font-size:0.8rem;padding:1px 4px;height:26px;background:#fff3cd" inputmode="decimal" oninput="meRecalc(\''+r.id+'\')">':'<span class="me-menge" style="font-size:0.85rem;font-weight:600">'+_gf(r.menge,3)+'</span>')+'</td>' +
'<td><input class="input is-small me-field me-recalc" data-id="'+r.id+'" data-field="einheit" value="'+esc(r.einheit)+'" style="width:70px;font-size:0.8rem;padding:1px 4px;height:26px" onchange="meRecalc(\''+r.id+'\')"></td>' +
'<td style="overflow:hidden;font-size:0.8rem;padding:0 4px;white-space:nowrap">'+esc(r.kurztext)+'</td>' +
'<td style="overflow:hidden"><input class="input is-small me-field" data-id="'+r.id+'" data-field="bemerkung" value="'+esc(r.bemerkung)+'" style="width:100%;font-size:0.8rem;padding:1px 4px;height:26px"></td>' +
'<td><input class="input is-small me-field me-recalc" data-id="'+r.id+'" data-field="menge_hinten" value="'+_gf(r.menge_hinten,3)+'" style="width:55px;font-size:0.8rem;padding:1px 4px;height:26px" inputmode="decimal" oninput="meRecalc(\''+r.id+'\')"></td>' +
'<td class="me-ep" style="font-size:0.8rem">'+_gf(r.ep,2)+'</td>' +
'<td class="me-gp" style="font-size:0.85rem;font-weight:600">'+_gf(r.gp,2)+'</td>' +
'<td></td></tr>';
});
html += '</tbody></table></div>';
document.getElementById('multi-edit-content').innerHTML = html;
// Unit-basierte Amber-Markierung (welche Felder sind relevant)
rows.forEach(function(r){
var row = document.getElementById('me-row-'+r.id);
if (!row || r.formelTyp === 'frei') return;
var eh = r.einheit;
if (eh === 'M') {
var el = row.querySelector('.me-recalc[data-field="laenge"]');
if (el) el.style.background = '#fff3cd';
} else if (eh === 'M2') {
var els = ['laenge','breite'];
els.forEach(function(f){ var el=row.querySelector('.me-recalc[data-field="'+f+'"]'); if(el)el.style.background='#fff3cd'; });
} else if (eh === 'M3') {
['laenge','breite','tiefe'].forEach(function(f){ var el=row.querySelector('.me-recalc[data-field="'+f+'"]'); if(el)el.style.background='#fff3cd'; });
}
});
document.getElementById('multi-edit-modal').classList.add('is-active');
}
function closeMultiEdit(){
document.getElementById('multi-edit-modal').classList.remove('is-active');
}
function meRecalc(id){
var row = document.getElementById('me-row-'+id);
if (!row) return;
var inputs = row.querySelectorAll('.me-recalc');
var vals = {};
inputs.forEach(function(inp){ vals[inp.dataset.field] = inp.value; });
var ft = vals.formel_typ || 'standard';
var eh = vals.einheit || '';
var stUnit = /^(ST|LE|STD|h|Psch)$/.test(eh);
var faktor = parseFloat((vals.faktor||'1').replace(',','.')) || 1;
var laenge = parseFloat((vals.laenge||'0').replace(',','.')) || 0;
var breite = parseFloat((vals.breite||'0').replace(',','.')) || 0;
var tiefe = parseFloat((vals.tiefe||'0').replace(',','.')) || 0;
var menge_val = parseFloat((vals.menge||'0').replace(',','.')) || 0;
var menge_hinten = parseFloat((vals.menge_hinten||'0').replace(',','.')) || 0;
var ep = parseFloat((row.querySelector('.me-ep')?.textContent||'0').replace(',','.').replace('','')) || 0;
var menge;
if (ft === 'frei') {
// Clientseitige Formel-Auswertung für Live-Vorschau
var formelStr = vals.formel || '';
menge = 0;
if (formelStr.trim()) {
var san = formelStr.replace(/,/g,'.').replace(/\s/g,'');
if (/^[\d+\-*/().]+$/.test(san)) {
try { var c = Function('"use strict"; return ('+san+')')(); if(typeof c==='number'&&isFinite(c)) menge=c; } catch(e) {}
}
}
} else if (stUnit) {
menge = menge_val;
} else if (eh === 'M') {
menge = laenge;
} else if (eh === 'M2') {
menge = laenge * breite;
} else if (eh === 'M3') {
menge = laenge * breite * tiefe;
} else {
menge = laenge;
}
// Server berechnet menge_hinten = faktor * menge
var newMh = ft === 'frei' ? menge : faktor * menge;
var gp = newMh * ep;
// menge_hinten-Eingabe aktualisieren
var mhInput = row.querySelector('[data-field="menge_hinten"]');
if (mhInput) mhInput.value = newMh.toFixed(3).replace('.',',');
var mengeEl = row.querySelector('.me-menge');
if (mengeEl) {
mengeEl.textContent = menge.toFixed(3).replace('.',',');
} else {
var mengeInput = row.querySelector('.me-recalc[data-field="menge"]');
if (mengeInput) mengeInput.value = menge.toFixed(3).replace('.',',');
}
var gpEl = row.querySelector('.me-gp');
if (gpEl) gpEl.textContent = gp.toFixed(2).replace('.',',');
// Bei Einheit- oder Z-Art-Wechsel: Inputs umbauen
var needsRebuild = false;
if (eh !== row.dataset.prevUnit) { row.dataset.prevUnit = eh; needsRebuild = true; }
if (ft !== row.dataset.prevFt) { row.dataset.prevFt = ft; needsRebuild = true; }
if (needsRebuild) {
rebuildRow(id, ft, stUnit);
// Unit-basierte Amber-Markierung nach Neuaufbau
if (ft !== 'frei') {
if (eh === 'M') {
var el = row.querySelector('.me-recalc[data-field="laenge"]');
if (el) el.style.background = '#fff3cd';
} else if (eh === 'M2') {
['laenge','breite'].forEach(function(f){
var el=row.querySelector('.me-recalc[data-field="'+f+'"]');
if(el)el.style.background='#fff3cd';
});
} else if (eh === 'M3') {
['laenge','breite','tiefe'].forEach(function(f){
var el=row.querySelector('.me-recalc[data-field="'+f+'"]');
if(el)el.style.background='#fff3cd';
});
}
}
}
}
function rebuildRow(id, ft, stUnit){
var row = document.getElementById('me-row-'+id);
if (!row) return;
var isFrei = ft === 'frei';
var hideFLT = isFrei;
function _gn(v){ var s = germanNum(v,3); return s !== '' ? s : '0'; }
var ehInput = row.querySelector('.me-recalc[data-field="einheit"]');
var ehVal = ehInput ? ehInput.value : '';
function _rel(f){
if(f==='faktor')return true;
if(f==='laenge')return !/^(ST|LE|STD|h|Psch)$/.test(ehVal);
if(f==='breite')return ehVal==='M2'||ehVal==='M3';
if(f==='tiefe')return ehVal==='M3';
return true;
}
['faktor','laenge','breite','tiefe'].forEach(function(f){
var colIdx = {faktor:4, laenge:5, breite:6, tiefe:7}[f];
var td = row.querySelector('td:nth-child('+colIdx+')');
if (!td) return;
var inp = td.querySelector('.me-recalc[data-field="'+f+'"]');
var isRel = !hideFLT && _rel(f);
if (inp) {
if (!isRel) { td.innerHTML = '<span class="me-na" style="color:#888"></span>'; }
} else {
var span = td.querySelector('.me-na');
if (span && isRel) {
var orig = window._meOrigData ? window._meOrigData[id] : null;
var val = (orig && orig[f]) ? _gn(orig[f]) : '0';
td.innerHTML = '<input class="input is-small me-field me-recalc" data-id="'+id+'" data-field="'+f+'" value="'+val+'" style="width:55px;font-size:0.8rem;padding:1px 4px;height:26px" inputmode="decimal" oninput="meRecalc(\''+id+'\')">';
}
}
});
// Formel-Umschaltung: Länge ↔ Formel
if (isFrei) {
if (!row.querySelector('[data-field="formel"]')) {
var laengeTd = row.querySelector('td:nth-child(5)');
if (laengeTd) {
var orig2 = window._meOrigData ? window._meOrigData[id] : null;
var fv = (orig2 && orig2.formel !== undefined) ? esc(orig2.formel) : '';
laengeTd.innerHTML = '<input class="input is-small me-field me-recalc" data-id="'+id+'" data-field="formel" value="'+fv+'" style="width:100%;font-size:0.8rem;padding:1px 4px;height:26px;font-family:monospace;background:#e8f4fd" oninput="meRecalc(\''+id+'\')">';
}
}
} else {
var formelInp = row.querySelector('[data-field="formel"]');
if (formelInp) {
var td = formelInp.parentElement;
var orig2 = window._meOrigData ? window._meOrigData[id] : null;
var lv = (orig2 && orig2.laenge) ? _gn(orig2.laenge) : '0';
td.innerHTML = '<input class="input is-small me-field me-recalc" data-id="'+id+'" data-field="laenge" value="'+lv+'" style="width:55px;font-size:0.8rem;padding:1px 4px;height:26px" inputmode="decimal" oninput="meRecalc(\''+id+'\')">';
}
}
// Menge umbauen
var mengeCell = row.querySelector('td:nth-child(8)');
if (mengeCell) {
var orig = window._meOrigData ? window._meOrigData[id] : null;
if (stUnit) {
if (!mengeCell.querySelector('input')) {
var val = (orig && orig.menge) ? _gn(orig.menge) : '0';
mengeCell.innerHTML = '<input class="input is-small me-field me-recalc" data-id="'+id+'" data-field="menge" value="'+val+'" style="width:60px;font-size:0.8rem;padding:1px 4px;height:26px;background:#fff3cd" inputmode="decimal" oninput="meRecalc(\''+id+'\')">';
}
} else {
if (!mengeCell.querySelector('.me-menge')) {
var val = (orig && orig.menge) ? _gn(orig.menge) : '0';
mengeCell.innerHTML = '<span class="me-menge" style="font-size:0.85rem;font-weight:600">'+val+'</span>';
}
}
}
meRecalc(id);
}
function saveMultiEdit(){
try {
var fields = document.querySelectorAll('.me-field');
var updates = {};
fields.forEach(function(inp){
var id = inp.dataset.id;
var field = inp.dataset.field;
var val = inp.value.trim();
if (!updates[id]) updates[id] = {};
updates[id][field] = val;
});
// Fehlende Felder aus Originaldaten ergänzen (für "" Platzhalter)
Object.keys(window._meOrigData || {}).forEach(function(id){
if (!updates[id]) return;
var orig = window._meOrigData[id];
['faktor','laenge','breite','tiefe','menge_hinten'].forEach(function(f){
if (!(f in updates[id]) && (f in orig)) {
updates[id][f] = orig[f];
}
});
});
var numFields = {'faktor':1,'laenge':1,'breite':1,'tiefe':1,'menge_hinten':1,'menge':1};
Object.keys(updates).forEach(function(id){
Object.keys(updates[id]).forEach(function(f){
if (numFields[f] && updates[id][f] !== '') {
updates[id][f] = parseFloat(String(updates[id][f]).replace(',','.')) || 0;
}
});
});
// Push to undo stack - ONE ENTRY PER POSITION
Object.keys(updates).forEach(function(id){
if (window._meOrigData && window._meOrigData[id]) {
var oldVals = {};
var changesDesc = [];
Object.keys(updates[id]).forEach(function(f){
var oldVal = window._meOrigData[id][f] || null;
var newVal = updates[id][f];
oldVals[f] = oldVal;
if (oldVal !== newVal) {
// Create a readable description for this field
var fieldLabels = {
'faktor': 'Faktor', 'laenge': 'Länge', 'breite': 'Breite', 'tiefe': 'Tiefe',
'menge': 'Menge', 'menge_hinten': 'Menge hinten', 'einheit': 'EH',
'abschnitt': 'Abschnitt', 'bemerkung': 'Bemerkung', 'formel': 'Formel',
'formel_typ': 'Z-Art'
};
changesDesc.push((fieldLabels[f] || f) + ': ' + (oldVal || 'leer') + ' → ' + (newVal || 'leer'));
}
});
if (changesDesc.length > 0) {
var posNr = window._meOrigData[id].posnr || ('Pos #' + id);
var description = posNr + ': ' + changesDesc.join(', ');
pushUndoState(description, [{position_id: parseInt(id), pos_nr: posNr, old: oldVals, new: updates[id]}]);
}
}
});
// Schließe Modal sofort
var modal = document.getElementById('multi-edit-modal');
if (modal) modal.classList.remove('is-active');
var btn = document.getElementById('btn-multi-save');
if (btn) btn.classList.add('is-loading');
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/positionen/batch-edit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({positions: updates})
}).then(function(r){
if (!r.ok) {
throw new Error('Server responded with status ' + r.status);
}
return r.text().then(function(text){
if (!text || text.trim() === '') {
return {};
}
try {
return JSON.parse(text);
} catch(e) {
console.error('Response text:', text);
throw new Error('Invalid JSON response: ' + text.substring(0, 200));
}
});
}).then(function(data){
location.href = location.pathname + '?t=' + Date.now();
}).catch(function(e){
alert('Fehler beim Speichern: '+e.message);
if (btn) btn.classList.remove('is-loading');
});
} catch(e) {
alert('Fehler: '+e.message);
}
}
/* === Langtext Sidepanel === */
function updateLangPanel(){
var lt=document.getElementById('form-langtext');
var btn=document.getElementById('btn-lang-panel');
if(!lt||!btn)return;
var hasText=lt.textContent.trim().length>0;
btn.style.display=hasText?'inline':'none';
// Sidepanel content immer aktualisieren
var pc=document.getElementById('lang-panel-content');
if(pc)pc.textContent=lt.textContent;
}
function openLangPanel(){
var panel=document.getElementById('am-lang-panel');
var exp=document.getElementById('lang-expanded');
if(!panel||!exp)return;
// Wenn Panel bereits aufgeklappt ist → einklappen
if(panel.style.display!=='none'&&exp.style.display!=='none'){
toggleLangPanel(false);
return;
}
var lt=document.getElementById('form-langtext');
var pc=document.getElementById('lang-panel-content');
var div=document.getElementById('lang-divider');
var coll=document.getElementById('lang-collapsed');
if(!lt||!pc||!coll)return;
pc.textContent=lt.textContent;
coll.style.display='none';
exp.style.display='flex';
panel.style.display='flex';
div.style.display='';
panel.style.width=localStorage.getItem('am_lang_width')||'380px';
panel.style.minWidth='200px';
}
function toggleLangPanel(forceOpen){
var panel=document.getElementById('am-lang-panel');
var div=document.getElementById('lang-divider');
var coll=document.getElementById('lang-collapsed');
var exp=document.getElementById('lang-expanded');
if(!panel||!coll||!exp)return;
var isOpen=panel.style.display!=='none'&&exp.style.display!=='none';
if(forceOpen===true&&isOpen)return;
if(forceOpen===false&&!isOpen)return;
if(isOpen||forceOpen===false){
coll.style.display='flex';
exp.style.display='none';
panel.style.width='28px';
panel.style.minWidth='28px';
}else{
coll.style.display='none';
exp.style.display='flex';
panel.style.display='flex';
div.style.display='';
panel.style.width=localStorage.getItem('am_lang_width')||'380px';
panel.style.minWidth='200px';
}
}
function closeLangPanel(){
var panel=document.getElementById('am-lang-panel');
var div=document.getElementById('lang-divider');
if(panel)panel.style.display='none';
if(div)div.style.display='none';
}
/* Sidepanel Resizer */
(function(){
var div=document.getElementById('lang-divider');
var panel=document.getElementById('am-lang-panel');
if(!div||!panel)return;
var saved=localStorage.getItem('am_lang_width');
if(saved&&parseInt(saved)>100){panel.style.width=saved+'px';panel.style.minWidth=saved+'px';}
var drag=false,sx=0;
div.addEventListener('mousedown',function(e){
if(panel.style.display==='none')return;
var exp=document.getElementById('lang-expanded');
if(!exp||exp.style.display==='none')return;
drag=true;sx=e.clientX;
document.body.style.cursor='col-resize';document.body.style.userSelect='none';
});
document.addEventListener('mousemove',function(e){
if(!drag)return;
var nw=panel.offsetWidth-(e.clientX-sx);
if(nw>100)panel.style.width=nw+'px';
sx=e.clientX;
});
document.addEventListener('mouseup',function(){
if(drag){localStorage.setItem('am_lang_width',panel.offsetWidth);drag=false;
document.body.style.cursor='';document.body.style.userSelect='';}
});
// Init: collapsed by default
var coll=document.getElementById('lang-collapsed');
var exp=document.getElementById('lang-expanded');
var panel=document.getElementById('am-lang-panel');
if(coll&&exp&&panel){exp.style.display='none';coll.style.display='flex';panel.style.display='none';panel.style.width='28px';panel.style.minWidth='28px';}
})();
</script>
{% if positionen %}
<div class="box mt-3">
<h3 class="title is-5">Mengen- und Positions-Zusammenfassung</h3>
<div style="overflow-x:auto">
<table class="table is-fullwidth is-hoverable" style="font-size:0.8rem">
<thead>
<tr>
<th>Pos-Nr</th>
<th>Kurztext</th>
<th class="has-text-right">Menge</th>
<th class="has-text-right">EP (€)</th>
<th class="has-text-right">GP (€)</th>
</tr>
</thead>
<tbody>
{% set seen = namespace(posns=[]) %}
{% set totals = namespace(menge=0, gp=0) %}
{% for pos in positionen if pos.pos_nr %}
{% if pos.pos_nr not in seen.posns %}
{% set _ = seen.posns.append(pos.pos_nr) %}
{% set menge_sum = namespace(val=0) %}
{% for p in positionen if p.pos_nr == pos.pos_nr %}
{% set menge_sum.val = menge_sum.val + (p.menge_hinten or p.menge or 0) %}
{% endfor %}
{% set totals.menge = totals.menge + menge_sum.val %}
{% set totals.gp = totals.gp + (positionen|selectattr('pos_nr','equalto',pos.pos_nr)|sum(attribute='gesamtpreis') or 0) %}
<tr>
<td><code>{{ pos.pos_nr }}</code></td>
<td>{{ pos.kurztext or '' }}</td>
<td class="has-text-right">{{ menge_sum.val|german_number(3) }}</td>
<td class="has-text-right">{{ pos.einzelpreis|german_number }}</td>
<td class="has-text-right">{{ positionen|selectattr('pos_nr','equalto',pos.pos_nr)|sum(attribute='gesamtpreis')|german_number }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
<tfoot>
<tr class="has-text-weight-bold">
<td colspan="2">Summe</td>
<td class="has-text-right">{{ totals.menge|german_number(3) }}</td>
<td></td>
<td class="has-text-right">{{ totals.gp|german_number }} €</td>
</tr>
</tfoot>
</table>
</div>
</div>
{% endif %}
<!-- Modal für Zusatzmodule (volle Bildschirmbreite) -->
<div id="modul-modal" class="modal">
<div class="modal-background" onclick="var m=document.getElementById('modul-modal');if(m){saveModulFormState();m.classList.remove('is-active')}"></div>
<div class="modal-card" style="width:auto;max-width:95vw;max-height:90vh">
<header class="modal-card-head" style="padding:0.5rem">
<p class="modal-card-title" style="font-size:1rem" id="modul-modal-title">Zusatzmodul</p>
<button class="delete" onclick="var m=document.getElementById('modul-modal');if(m){saveModulFormState();m.classList.remove('is-active')}"></button>
</header>
<section class="modal-card-body" id="modul-modal-body"></section>
</div>
</div>
<script>
function closeModulModal(){var m=document.getElementById('modul-modal');if(m){saveModulFormState();m.classList.remove('is-active')}}
/* Formularwerte des Modul-Popups speichern/wiederherstellen */
function saveModulFormState(){
var body=document.getElementById('modul-modal-body');
if(!body)return;
var data={};
body.querySelectorAll('input,textarea,select').forEach(function(el){
var name=el.getAttribute('name');
if(!name)return;
if(el.type==='checkbox'||el.type==='radio'){data[name]=el.checked?'an':''}
else{data[name]=el.value}
});
try{localStorage.setItem('modul_form_state',JSON.stringify(data))}catch(e){}
}
function restoreModulFormState(){
var raw;
try{raw=localStorage.getItem('modul_form_state')}catch(e){}
if(!raw)return;
var data;
try{data=JSON.parse(raw)}catch(e){}
if(!data)return;
var body=document.getElementById('modul-modal-body');
if(!body)return;
body.querySelectorAll('input,textarea,select').forEach(function(el){
var name=el.getAttribute('name');
if(!name||data[name]===undefined)return;
if(el.type==='checkbox'||el.type==='radio'){el.checked=data[name]==='an'}
else{el.value=data[name]}
});
}
// ===== Zusatzmodule + Ausgeblendete Details State speichern/wiederherstellen =====
function saveToggleState(){
var hd = document.getElementById('hidden-details');
if (hd) try { localStorage.setItem('hidden_details_open', hd.hasAttribute('open') ? '1' : '0'); } catch(e) {}
}
(function(){
var d = document.getElementById('zusatz-details');
if (!d) return;
var s = localStorage.getItem('zusatz_open');
if (s === '1') d.setAttribute('open', '');
d.addEventListener('toggle', function(){
localStorage.setItem('zusatz_open', d.hasAttribute('open') ? '1' : '0');
});
})();
(function(){
var d = document.getElementById('hidden-details');
if (!d) return;
var s = localStorage.getItem('hidden_details_open');
if (s === '1') d.setAttribute('open', '');
d.addEventListener('toggle', function(){
localStorage.setItem('hidden_details_open', d.hasAttribute('open') ? '1' : '0');
});
})();
// ===== Multi-Select: Klick toggelt Auswahl (capture phase → vor HTMX) =====
document.addEventListener('click', function(ev){
var btn = ev.target.closest('.js-mod-btn');
if (!btn) return;
var hasHtmx = btn.hasAttribute('hx-get');
// Bei normalem Klick auf sichtbares Modul: Selektion löschen, HTMX öffnet Formular
if (!ev.ctrlKey && hasHtmx) {
document.querySelectorAll('.js-mod-btn.selected').forEach(function(b){ b.classList.remove('selected'); });
updateBatchUI();
return;
}
// Ctrl+Klick oder Klick auf ausgeblendetes Modul: Selektion toggeln
ev.preventDefault();
ev.stopPropagation();
btn.classList.toggle('selected');
updateBatchUI();
}, {capture: true});
function updateBatchUI(){
var sel = document.querySelectorAll('.js-mod-btn.selected');
var info = document.getElementById('batch-info');
var count = document.getElementById('sel-count');
if (!info || !count) return;
if (sel.length === 0) { info.style.display = 'none'; return; }
info.style.display = 'inline';
count.textContent = sel.length;
var vis = document.getElementById('modul-sortable');
var hid = document.getElementById('modul-sortable-hidden');
var hasVisible = vis && Array.from(vis.querySelectorAll('.js-mod-btn.selected')).length > 0;
var hasHidden = hid && Array.from(hid.querySelectorAll('.js-mod-btn.selected')).length > 0;
var btnHide = document.getElementById('btn-hide-selected');
var btnShow = document.getElementById('btn-show-selected');
if (btnHide) btnHide.style.display = hasVisible ? '' : 'none';
if (btnShow) btnShow.style.display = hasHidden ? '' : 'none';
}
document.getElementById('btn-hide-selected')?.addEventListener('click', function(){ batchToggle('hide'); });
document.getElementById('btn-show-selected')?.addEventListener('click', function(){ batchToggle('show'); });
document.getElementById('btn-clear-selection')?.addEventListener('click', function(){
document.querySelectorAll('.js-mod-btn.selected').forEach(function(b){ b.classList.remove('selected'); });
updateBatchUI();
});
// ===== Batch Hide/Show (auch für Multi-Drag) =====
function batchToggle(action, entries){
if (!entries) {
var sel = document.querySelectorAll('.js-mod-btn.selected');
if (sel.length === 0) return;
entries = [];
sel.forEach(function(btn){ entries.push({type: btn.dataset.type, id: btn.dataset.id}); });
}
if (entries.length === 0) return;
saveToggleState();
fetch('/projekt/module-toggle-hidden-batch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: action, entries: entries})
}).then(function(r){ return r.json(); }).then(function(){
location.reload();
}).catch(function(e){ console.error('Batch toggle failed', e); });
}
// ===== SortableJS mit Cross-Container Drag & Drop =====
function initSortable(){
if (typeof Sortable === 'undefined') return;
function makeSortable(el){
if (!el) return;
new Sortable(el, {
group: 'modules',
animation: 150,
handle: '.js-mod-btn',
onStart: function(ev){
// Hidden-Details automatisch öffnen beim Ziehen
var details = document.getElementById('hidden-details');
if (details && !details.hasAttribute('open')) details.setAttribute('open', '');
// Markiere alle selektierten im Quell-Container
var src = ev.from;
if (!src) return;
var selected = src.querySelectorAll('.js-mod-btn.selected');
if (selected.length > 1) {
window._dragMulti = [];
selected.forEach(function(b){ window._dragMulti.push({type: b.dataset.type, id: b.dataset.id}); });
} else {
window._dragMulti = null;
}
},
onEnd: function(ev){
if (ev.from !== ev.to) {
// Cross-container move
var entries = window._dragMulti || [{type: ev.item.dataset.type, id: ev.item.dataset.id}];
var action = ev.to.id === 'modul-sortable-hidden' ? 'hide' : 'show';
batchToggle(action, entries);
} else {
saveToggleState();
var order = [];
ev.from.querySelectorAll('.js-mod-btn').forEach(function(btn){
order.push({type: btn.dataset.type, id: btn.dataset.id});
});
fetch('/projekt/module-reorder', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({order: order})
}).catch(function(e){ console.error('Reorder failed', e); });
}
window._dragMulti = null;
}
});
}
makeSortable(document.getElementById('modul-sortable'));
makeSortable(document.getElementById('modul-sortable-hidden'));
}
document.addEventListener('DOMContentLoaded', initSortable);
function toggleModHidden(el) {
var type = el.dataset.type;
var id = el.dataset.id;
saveToggleState();
fetch('/projekt/module-toggle-hidden', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type: type, id: id})
}).then(function(r){ return r.json(); }).then(function(){
location.reload();
}).catch(function(e){ console.error('Toggle failed', e); });
}
/* ===== Lock-Management ===== */
{% if locked_by_name %}
(function(){
// Read-only mode: disable all interactive elements
function disableInputs(root){
root.querySelectorAll('button, input, textarea, select, a.button').forEach(function(el){el.disabled=true;el.style.pointerEvents='none';el.style.opacity='0.6'});
document.querySelectorAll('#pos-sortable tr').forEach(function(row){row.style.cursor='not-allowed'});
document.querySelectorAll('.js-mod-btn').forEach(function(el){el.style.pointerEvents='none';el.style.opacity='0.5'});
var dropAreas = document.querySelectorAll('#pos-sortable');
dropAreas.forEach(function(da){da.style.pointerEvents='none'});
}
disableInputs(document);
})();
{% else %}
(function(){
var PROJECT_ID = {{ project.id }};
var AUFMASS_ID = {{ aufmass.id }};
var HEARTBEAT_INTERVAL = 30000; // 30s
var hbTimer = null;
function lockHeartbeat(){
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/heartbeat',{method:'POST',headers:{'Content-Type':'application/json'}})
.catch(function(e){console.error('Heartbeat failed',e)});
}
function releaseLock(){
if(hbTimer){clearInterval(hbTimer);hbTimer=null}
navigator.sendBeacon('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/unlock','{}');
}
// Start heartbeat after acquiring lock
fetch('/projekt/'+PROJECT_ID+'/'+AUFMASS_ID+'/lock',{method:'POST',headers:{'Content-Type':'application/json'}})
.then(function(r){
if(r.status===423)return r.json().then(function(d){console.warn('Lock held by:',d.holder)});
hbTimer=setInterval(lockHeartbeat,HEARTBEAT_INTERVAL);
}).catch(function(e){console.error('Lock acquire failed',e)});
window.addEventListener('beforeunload',releaseLock);
})();
{% endif %}
{% endblock %}