423 lines
24 KiB
HTML
423 lines
24 KiB
HTML
{% extends "base.html" %}
|
||
{% block content %}
|
||
{% set default_cols = ['fav','drag','pos_nr','text','einheit','ep','aktion'] %}
|
||
{% set col_labels = {'fav':'⭐','drag':'#','pos_nr':'Pos-Nr','text':'Kurztext / Langtext','einheit':'EH','ep':'EP (€)','aktion':'Aktion'} %}
|
||
{% set col_sortable = {'fav':False,'drag':False,'pos_nr':True,'text':True,'einheit':True,'ep':True,'aktion':False} %}
|
||
<div class="level">
|
||
<div class="level-left"><h1 class="title is-3">Leistungsverzeichnis</h1></div>
|
||
<div class="level-right">
|
||
<a class="button is-light" href="{{ url_for('admin.dashboard') }}">← Dashboard</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="lv-layout" class="lv-layout">
|
||
<!-- Linkes Panel -->
|
||
<div class="lv-panel" id="panel-left" style="width:300px;min-width:150px">
|
||
<div class="panel-header">
|
||
<span class="has-text-weight-bold is-size-7">LV-Auswahl</span>
|
||
<span class="panel-actions">
|
||
<a class="panel-btn" onclick="togglePanel('left')" title="Einklappen">▾</a>
|
||
<a class="panel-btn" onclick="resetPanel('left')" title="Standard">□</a>
|
||
</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<form method="GET" action="{{ url_for('lv.index') }}" id="lv-select-form">
|
||
<div class="select is-small is-fullwidth">
|
||
<select name="lv" onchange="this.form.submit()">
|
||
<option value="">– LV wählen –</option>
|
||
{% for name in lv_names %}
|
||
<option value="{{ name }}" {{ 'selected' if name == selected_lv }}>{{ name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</form>
|
||
{% if current_user.is_firmadmin() or current_user.darf_lv_verwalten %}
|
||
<details class="mt-1"><summary class="has-text-link is-size-7">+ Neues LV</summary>
|
||
<form method="POST" action="{{ url_for('lv.neu_lv') }}" class="mt-1">
|
||
<div class="field has-addons"><div class="control is-expanded"><input class="input is-small" type="text" name="lv_name" placeholder="LV-Name" required></div>
|
||
<div class="control"><button class="button is-small is-primary">Anlegen</button></div></div>
|
||
</form>
|
||
</details>
|
||
<details class="mt-1"><summary class="has-text-link is-size-7">📥 TXT importieren</summary>
|
||
<form method="POST" action="{{ url_for('lv.import_txt') }}" enctype="multipart/form-data" class="mt-1">
|
||
<input type="hidden" name="lv_name" value="{{ selected_lv }}">
|
||
<div class="field"><div class="control"><input class="input is-small" type="file" name="datei" accept=".txt" required></div></div>
|
||
<button class="button is-small is-info">Importieren</button>
|
||
</form>
|
||
</details>
|
||
{% endif %}
|
||
<hr class="my-2">
|
||
<form method="GET" action="{{ url_for('lv.index') }}" id="search-form">
|
||
<input type="hidden" name="lv" value="{{ selected_lv }}">
|
||
<div class="field"><div class="control has-icons-left"><input class="input is-small" name="q" id="live-search" placeholder="Live-Suche..." value="{{ search }}" autocomplete="off">
|
||
<span class="icon is-left is-small">🔍</span></div></div>
|
||
</form>
|
||
<details class="mt-1"><summary class="has-text-link is-size-7">+ Position hinzufügen</summary>
|
||
<form method="POST" action="{{ url_for('lv.position_neu') }}" class="mt-1">
|
||
<input type="hidden" name="lv_name" value="{{ selected_lv }}">
|
||
<div class="field"><input class="input is-small" name="pos_nr" placeholder="Pos-Nr *" required></div>
|
||
<div class="field"><input class="input is-small" name="kurztext" placeholder="Kurztext"></div>
|
||
<div class="field"><textarea class="textarea is-small" name="langtext" placeholder="Langtext" rows="2"></textarea></div>
|
||
<div class="columns is-mobile"><div class="column"><input class="input is-small" name="einheit" placeholder="EH" value="ST"></div>
|
||
<div class="column"><input class="input is-small" name="einzelpreis" placeholder="EP (€)" type="number" step="0.01"></div></div>
|
||
<button class="button is-small is-primary mt-1">Hinzufügen</button>
|
||
</form>
|
||
</details>
|
||
</div>
|
||
</div>
|
||
<div class="lv-divider" id="divider-1" data-prev="left" data-next="center"></div>
|
||
|
||
<!-- Mittleres Panel: Tabelle -->
|
||
<div class="lv-panel" id="panel-center" style="flex:1;min-width:300px">
|
||
<div class="panel-header">
|
||
<span class="has-text-weight-bold is-size-7">{{ selected_lv or 'Leistungsverzeichnis' }}</span>
|
||
<span class="tag is-light is-size-7 ml-1">{{ positionen|length }} Pos.</span>
|
||
<span class="panel-actions">
|
||
<a class="panel-btn" onclick="togglePanel('center')" title="Einklappen">▾</a>
|
||
<a class="panel-btn" onclick="resetPanel('center')" title="Standardgröße">□</a>
|
||
<a class="panel-btn" onclick="maxPanel('center')" title="Maximieren">⛶</a>
|
||
<a class="panel-btn" onclick="autoFitColumns()" title="Auto-Breite">⇔</a>
|
||
<span class="panel-btn-sep">|</span>
|
||
<div class="dropdown is-hoverable is-right" style="display:inline-block">
|
||
<div class="dropdown-trigger"><a class="panel-btn" title="Ansicht">👁</a></div>
|
||
<div class="dropdown-menu" style="min-width:200px">
|
||
<div class="dropdown-content" id="view-dropdown">
|
||
<div class="dropdown-item has-text-weight-bold is-size-7">Ansichten</div>
|
||
<hr class="dropdown-divider">
|
||
<div id="view-list"></div>
|
||
<hr class="dropdown-divider">
|
||
<div class="dropdown-item">
|
||
<div class="field has-addons">
|
||
<div class="control is-expanded"><input class="input is-small" id="view-name" placeholder="Name"></div>
|
||
<div class="control"><button class="button is-small is-primary" onclick="saveView()">Speichern</button></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</span>
|
||
</div>
|
||
<div class="panel-body" id="table-body">
|
||
{% if selected_lv %}
|
||
<div class="table-wrap">
|
||
<table class="table is-fullwidth is-hoverable is-striped" id="lv-table">
|
||
<thead id="thead-sortable">
|
||
<tr>
|
||
{% set col_order = view_config.column_order if view_config else default_cols %}
|
||
{% set col_visible = view_config.column_visible if view_config else {} %}
|
||
{% set col_widths = view_config.column_widths if view_config else {} %}
|
||
{% for key in col_order %}
|
||
{% if col_visible.get(key, True) %}
|
||
<th data-col="{{ key }}" style="width:{{ col_widths.get(key, '') }}px;" class="col-header {{ 'sort-'+sort_dir if sort_col == key }}">
|
||
{% if col_sortable.get(key) %}
|
||
<a class="col-label" href="{{ url_for('lv.index', lv=selected_lv, q=search, sort=key if sort_col != key or sort_dir == 'desc' else '', dir='asc' if sort_col != key or sort_dir == 'desc' else 'desc') }}">{{ col_labels.get(key, key) }}</a>
|
||
<span class="sort-icon">{{ '▲' if sort_col == key and sort_dir == 'asc' else '▼' if sort_col == key else '⇅' }}</span>
|
||
{% else %}
|
||
<span class="col-label">{{ col_labels.get(key, key) }}</span>
|
||
{% endif %}
|
||
<div class="col-resize-handle"></div>
|
||
</th>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</tr>
|
||
</thead>
|
||
<tbody id="lv-sortable">
|
||
{% for pos in positionen %}
|
||
<tr data-id="{{ pos.id }}" data-order="{{ pos.order_index }}"
|
||
data-posnr="{{ pos.pos_nr }}" data-kurztext="{{ pos.kurztext or '' }}"
|
||
data-langtext="{{ pos.langtext or '' }}" data-einheit="{{ pos.einheit }}"
|
||
data-ep="{{ pos.einzelpreis }}" data-gruppe="{{ pos.gruppe or '' }}"
|
||
class="{{ 'has-background-warning-light' if pos.favorite }}">
|
||
{% for key in col_order %}
|
||
{% if col_visible.get(key, True) %}
|
||
<td class="col-{{ key }}">
|
||
{% if key == 'fav' %}
|
||
<span style="cursor:pointer" onclick="toggleFav({{ pos.id }})">{{ '⭐' if pos.favorite else '☆' }}</span>
|
||
{% elif key == 'drag' %}
|
||
<span class="drag-handle has-text-grey-light">⠿</span>
|
||
{% elif key == 'pos_nr' %}
|
||
<code>{{ pos.pos_nr }}</code>
|
||
{% elif key == 'text' %}
|
||
<a href="#" onclick="showLang({{ pos.id }});return false" class="has-text-dark">
|
||
<strong>{{ pos.kurztext or '' }}</strong>
|
||
</a>
|
||
{% if pos.langtext %}<div class="langtext-preview">{{ pos.langtext[:200] }}</div>{% endif %}
|
||
{% elif key == 'einheit' %}
|
||
{{ pos.einheit }}
|
||
{% elif key == 'ep' %}
|
||
{% if preise_sichtbar %}{{ pos.einzelpreis|german_number }}{% else %}–{% endif %}
|
||
{% elif key == 'aktion' %}
|
||
<button class="button is-small btn-edit" onclick="editLV({{ pos.id }})">✎</button>
|
||
<form method="POST" action="{{ url_for('lv.position_loeschen', pos_id=pos.id) }}"
|
||
style="display:inline" onsubmit="return confirm('Position löschen?')">
|
||
<button class="button is-small is-danger is-outlined">✕</button>
|
||
</form>
|
||
{% endif %}
|
||
</td>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% if not positionen %}
|
||
<p class="has-text-grey has-text-centered is-size-7">Keine Positionen{{ ' für "' ~ search ~ '"' if search }}.</p>
|
||
{% endif %}
|
||
</div>
|
||
{% else %}
|
||
<p class="has-text-grey has-text-centered is-size-7">Bitte links ein LV auswählen.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="lv-divider" id="divider-2" data-prev="center" data-next="right"></div>
|
||
|
||
<!-- Rechtes Panel: Langtext -->
|
||
<div class="lv-panel" id="panel-right" style="width:380px;min-width:200px;display:none">
|
||
<div class="panel-header">
|
||
<span class="has-text-weight-bold is-size-7">Langtext</span>
|
||
<span class="panel-actions">
|
||
<a class="panel-btn" onclick="togglePanel('right')" title="Einklappen">▾</a>
|
||
<a class="panel-btn" onclick="maxPanel('right')" title="Maximieren">⛶</a>
|
||
<a class="panel-btn" onclick="hidePanel('right')" title="Schließen">✕</a>
|
||
</span>
|
||
</div>
|
||
<div class="panel-body" id="lang-content" style="font-size:0.85rem;white-space:pre-wrap;overflow-y:auto;max-height:calc(100vh - 200px)"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit-Modal -->
|
||
<div id="lv-edit-modal" class="modal">
|
||
<div class="modal-background"></div>
|
||
<div class="modal-card"><header class="modal-card-head"><p class="modal-card-title">Position bearbeiten</p><button class="delete" onclick="closeEditLV()"></button></header>
|
||
<section class="modal-card-body" id="lv-edit-content"></section></div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
const ALL_COLS = ['fav','drag','pos_nr','text','einheit','ep','aktion'];
|
||
const COL_LABELS = {'fav':'⭐','drag':'#','pos_nr':'Pos-Nr','text':'Kurztext/Langtext','einheit':'EH','ep':'EP (€)','aktion':'Aktion'};
|
||
|
||
/* === View Profiles === */
|
||
function getCurrentConfig() {
|
||
const order = [], visible = {}, widths = {};
|
||
document.querySelectorAll('#thead-sortable th').forEach(function(th){
|
||
const k=th.dataset.col; order.push(k); visible[k]=true;
|
||
const w=th.style.width; widths[k]=w ? parseInt(w) : 0;
|
||
});
|
||
ALL_COLS.forEach(function(k){if(!order.includes(k))visible[k]=false;});
|
||
const pw={};
|
||
['left','center','right'].forEach(function(n){
|
||
const el=document.getElementById('panel-'+n);
|
||
pw[n]=el.style.display==='none'?0:el.offsetWidth;
|
||
});
|
||
return {column_order:order,column_visible:visible,column_widths:widths,pane_widths:pw};
|
||
}
|
||
|
||
function loadViews() {
|
||
fetch('/views/api/profiles?view_type=lv').then(function(r){return r.json()}).then(function(profiles){
|
||
const list=document.getElementById('view-list'); list.innerHTML='';
|
||
if(!profiles.length){list.innerHTML='<div class="dropdown-item is-size-7 has-text-grey">Keine gespeicherten Ansichten</div>';return;}
|
||
profiles.forEach(function(p){
|
||
const d=document.createElement('div'); d.className='dropdown-item'; d.style.cssText='font-size:0.8rem;cursor:pointer';
|
||
d.innerHTML=(p.is_default?'⭐ ':'')+esc(p.name)+'<a class="is-pulled-right has-text-danger" style="font-size:0.7rem;cursor:pointer" onclick="event.stopPropagation();deleteView('+p.id+')">✕</a>';
|
||
d.onclick=function(){const url=new URL(window.location.href);url.searchParams.set('view_id',p.id);window.location.href=url.toString();};
|
||
list.appendChild(d);
|
||
});
|
||
});
|
||
}
|
||
function saveView(){
|
||
const name=document.getElementById('view-name').value.trim();
|
||
if(!name){alert('Bitte Namen eingeben');return;}
|
||
fetch('/views/api/profiles',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,view_type:'lv',config:getCurrentConfig()})})
|
||
.then(function(r){return r.json()}).then(function(){document.getElementById('view-name').value='';loadViews();});
|
||
}
|
||
function deleteView(id){
|
||
if(!confirm('Ansicht löschen?'))return;
|
||
fetch('/views/api/profiles/'+id,{method:'DELETE'}).then(function(){loadViews();});
|
||
}
|
||
|
||
/* === Resizable Panels === */
|
||
document.addEventListener('DOMContentLoaded',function(){
|
||
document.querySelectorAll('.lv-divider').forEach(function(div){
|
||
let drag=false,sx=0,prev=null,next=null;
|
||
div.addEventListener('mousedown',function(e){
|
||
drag=true;sx=e.clientX;
|
||
prev=document.getElementById('panel-'+div.dataset.prev);
|
||
next=document.getElementById('panel-'+div.dataset.next);
|
||
document.body.style.cursor='col-resize';document.body.style.userSelect='none';
|
||
});
|
||
document.addEventListener('mousemove',function(e){
|
||
if(!drag||!prev||!next)return;
|
||
const dx=e.clientX-sx;
|
||
if(div.id==='divider-1'){const nw=prev.offsetWidth+dx;if(nw>120){prev.style.width=nw+'px';prev.style.flex='none';sx=e.clientX;}}
|
||
else{const nw=next.offsetWidth-dx;if(nw>150){next.style.width=nw+'px';next.style.flex='none';sx=e.clientX;}}
|
||
});
|
||
document.addEventListener('mouseup',function(){if(drag){drag=false;document.body.style.cursor='';document.body.style.userSelect='';}});
|
||
});
|
||
});
|
||
|
||
/* === Panel Collapse / Maximize === */
|
||
function togglePanel(name){
|
||
const p=document.getElementById('panel-'+name);
|
||
p.classList.toggle('collapsed');
|
||
p.querySelector('.panel-btn:first-child').textContent=p.classList.contains('collapsed')?'▸':'▾';
|
||
}
|
||
function maxPanel(name){
|
||
document.querySelectorAll('.lv-panel').forEach(function(p){
|
||
if(p.id==='panel-'+name){p.style.flex='1';p.style.width='';}
|
||
else if(p.style.display!=='none'){p.style.flex='none';if(p.id!=='panel-right')p.style.width='40px';}
|
||
});
|
||
}
|
||
function resetPanel(name){
|
||
const def={left:300,center:null,right:380};
|
||
document.querySelectorAll('.lv-panel').forEach(function(p){
|
||
p.style.flex='';p.classList.remove('collapsed');
|
||
if(p.id==='panel-left')p.style.width=def.left+'px';
|
||
else if(p.id==='panel-right')p.style.width=def.right+'px';
|
||
else p.style.width='';
|
||
const b=p.querySelector('.panel-body');if(b)b.style.display='';
|
||
const btn=p.querySelector('.panel-btn:first-child');if(btn)btn.textContent='▾';
|
||
});
|
||
}
|
||
function hidePanel(name){
|
||
document.getElementById('panel-'+name).style.display='none';
|
||
}
|
||
|
||
/* === Column Resize (drag th handle) + Double-click auto-fit === */
|
||
document.addEventListener('DOMContentLoaded',function(){
|
||
document.querySelectorAll('.col-resize-handle').forEach(function(h){
|
||
let drag=false,startX=0,th=null,startW=0;
|
||
h.addEventListener('mousedown',function(e){
|
||
drag=true;startX=e.clientX;th=e.target.closest('th');
|
||
startW=th.offsetWidth-20; // -20px padding (10 left + 10 right)
|
||
window._colW={};document.querySelectorAll('#lv-table thead th').forEach(function(t){window._colW[t.dataset.col]=t.offsetWidth-20;});
|
||
e.stopPropagation();document.body.style.cursor='col-resize';document.body.style.userSelect='none';
|
||
});
|
||
document.addEventListener('mousemove',function(e){
|
||
if(!drag||!th)return;
|
||
const nw=startW+(e.clientX-startX);
|
||
if(nw>14){
|
||
Object.keys(window._colW).forEach(function(k){if(k!==th.dataset.col)setColWidth(k,window._colW[k]);});
|
||
setColWidth(th.dataset.col,nw);
|
||
}
|
||
});
|
||
document.addEventListener('mouseup',function(){if(drag){drag=false;document.body.style.cursor='';document.body.style.userSelect='';}});
|
||
h.addEventListener('dblclick',function(e){
|
||
e.stopPropagation();
|
||
const thEl=e.target.closest('th');
|
||
const col=thEl&&thEl.dataset.col;
|
||
if(col)autoFitColumn(col);
|
||
});
|
||
});
|
||
/* Auch Doppelklick auf die rechten 15px jeder Th (wenn man den Griff verfehlt) */
|
||
document.querySelectorAll('#lv-table thead th').forEach(function(th){
|
||
th.addEventListener('dblclick',function(e){
|
||
if(e.target.closest('.sort-icon')||e.target.closest('.col-resize-handle'))return;
|
||
const rect=th.getBoundingClientRect();
|
||
if(rect.right-e.clientX<15){const col=th.dataset.col;if(col)autoFitColumn(col);}
|
||
});
|
||
});
|
||
});
|
||
|
||
/* === Auto-Fit Columns === */
|
||
function autoFitColumn(col){
|
||
var fixed={fav:32,drag:28,aktion:70};
|
||
if(fixed[col]!==undefined){setColWidth(col,fixed[col]);return;}
|
||
const table=document.getElementById('lv-table');
|
||
if(!table)return;
|
||
const th=table.querySelector('thead th[data-col="'+col+'"]');
|
||
if(!th)return;
|
||
/* style.width ist Content-Breite, d.h. rendered = style.width + 20px Padding.
|
||
Wir messen Textbreite + 20px Padding + 10px Atem = 30px Zuschlag insgesamt,
|
||
setzen aber nur Textbreite + 10px als style.width. Browser addiert 20px Padding. */
|
||
let maxW=0;
|
||
if(th.querySelector('.col-label'))maxW=th.querySelector('.col-label').offsetWidth+10;
|
||
const rows=table.querySelectorAll('tbody tr');
|
||
rows.forEach(function(row){
|
||
const cell=row.querySelector('.col-'+col);
|
||
if(cell){
|
||
const tmp=document.createElement('span');
|
||
tmp.style.cssText='position:absolute;visibility:hidden;white-space:nowrap;font-size:0.85rem';
|
||
tmp.textContent=cell.textContent;
|
||
document.body.appendChild(tmp);
|
||
const w=tmp.offsetWidth+10;
|
||
document.body.removeChild(tmp);
|
||
if(w>maxW)maxW=w;
|
||
}
|
||
});
|
||
setColWidth(col,Math.min(maxW,800));
|
||
}
|
||
function setColWidth(col,w){
|
||
const th=document.querySelector('#lv-table thead th[data-col="'+col+'"]');
|
||
if(th)th.style.width=w+'px';
|
||
}
|
||
function autoFitColumns(){
|
||
const table=document.getElementById('lv-table');
|
||
if(!table)return;
|
||
table.querySelectorAll('thead th').forEach(function(th){
|
||
const col=th.dataset.col;
|
||
if(col)autoFitColumn(col);
|
||
});
|
||
}
|
||
|
||
/* === Standard Functions === */
|
||
function setupSortable(){
|
||
const el=document.getElementById('lv-sortable');
|
||
if(!el||el._sortable)return;
|
||
el._sortable=new Sortable(el,{handle:'.drag-handle',animation:150,
|
||
onEnd:function(){
|
||
const rows=el.querySelectorAll('tr');const r=[];
|
||
rows.forEach(function(row,i){r.push({id:parseInt(row.dataset.id),order_index:i+1});});
|
||
fetch('{{ url_for("lv.positionen_reihenfolge") }}',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({reihenfolge:r})});
|
||
}
|
||
});
|
||
}
|
||
function toggleFav(id){
|
||
fetch('/lv/position/'+id+'/favorite',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})
|
||
.then(function(r){return r.json()}).then(function(){location.reload();});
|
||
}
|
||
function showLang(id){
|
||
fetch('/lv/position/'+id+'/langtext').then(function(r){return r.text()}).then(function(html){
|
||
document.getElementById('lang-content').innerHTML=html;
|
||
const p=document.getElementById('panel-right');
|
||
p.style.display='';p.classList.remove('collapsed');
|
||
const b=p.querySelector('.panel-body');if(b)b.style.display='';
|
||
p.querySelector('.panel-btn:first-child').textContent='▾';
|
||
});
|
||
}
|
||
function editLV(id){
|
||
const row=document.querySelector('tr[data-id="'+id+'"]');if(!row)return;
|
||
const d=row.dataset;
|
||
document.getElementById('lv-edit-content').innerHTML=
|
||
'<form method="POST" action="/lv/position/'+id+'/bearbeiten">'+
|
||
'<div class="field"><label class="label">Pos-Nr</label><input class="input" name="pos_nr" value="'+esc(d.posnr)+'" required></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">Langtext</label><textarea class="textarea" name="langtext" rows="3">'+esc(d.langtext)+'</textarea></div>'+
|
||
'<div class="columns"><div class="column"><label class="label">Einheit</label><input class="input" name="einheit" value="'+esc(d.einheit)+'"></div>'+
|
||
'<div class="column"><label class="label">EP (€)</label><input class="input" name="einzelpreis" type="number" step="0.01" value="'+d.ep+'"></div></div>'+
|
||
'<div class="field"><label class="label">Gruppe</label><input class="input" name="gruppe" value="'+esc(d.gruppe)+'"></div>'+
|
||
'<button class="button is-primary" type="submit">Speichern</button>'+
|
||
'<button class="button is-light" type="button" onclick="closeEditLV()">Abbrechen</button></form>';
|
||
document.getElementById('lv-edit-modal').classList.add('is-active');
|
||
}
|
||
function closeEditLV(){document.getElementById('lv-edit-modal').classList.remove('is-active');}
|
||
function esc(s){if(!s)return '';return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}
|
||
|
||
/* === Live Search === */
|
||
(function(){
|
||
const inp=document.getElementById('live-search');
|
||
if(!inp)return;let t=null;
|
||
inp.addEventListener('input',function(){clearTimeout(t);t=setTimeout(function(){document.getElementById('search-form').submit();},350);});
|
||
})();
|
||
|
||
/* === Init === */
|
||
document.addEventListener('DOMContentLoaded',function(){
|
||
setupSortable();document.body.addEventListener('htmx:load',setupSortable);loadViews();
|
||
// Auto-fit on initial load if no custom view
|
||
const urlParams=new URLSearchParams(window.location.search);
|
||
if(!urlParams.get('view_id')){setTimeout(autoFitColumns,200);}
|
||
});
|
||
</script>
|
||
{% endblock %}
|