3044 lines
159 KiB
HTML
3044 lines
159 KiB
HTML
{% 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&¤tLVRow.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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||
|
||
/* === 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 %}
|