Initial commit – AufmaßCreater v2.35
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<h1 class="title is-3">Dashboard</h1>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select id="profile-select" onchange="loadProfile(this.value)">
|
||||
{% for p in profile_list or [] %}
|
||||
<option value="{{ p.id }}" {{ 'selected' if active_profile and active_profile.id == p.id }}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-small is-info" onclick="saveProfile()">Speichern</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-small is-light" onclick="newProfile()">+ Neu</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-cards" class="columns is-multiline mt-3">
|
||||
<div class="column is-one-third" data-card="projekte">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Projekte (aktiv)</p>
|
||||
<p class="title">{{ projekte_anzahl or 0 }}</p>
|
||||
<a href="{{ url_for('aufmass.index') }}" class="button is-small is-link is-outlined mt-2">Alle Projekte</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third" data-card="module">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Module verfügbar</p>
|
||||
<p class="title">{{ modules|length }}</p>
|
||||
<a href="{{ url_for('admin.firma') }}" class="button is-small is-link is-outlined mt-2">Module verwalten</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third" data-card="mitarbeiter">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Mitarbeiter</p>
|
||||
<p class="title">{{ mitarbeiter_anzahl }}</p>
|
||||
<a href="{{ url_for('admin.firma') }}" class="button is-small is-link is-outlined mt-2">Mitarbeiter verwalten</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third" data-card="projektwerte">
|
||||
<div class="box">
|
||||
<p class="heading">Projektwerte (€)</p>
|
||||
{% if gesamt_summe > 0 %}
|
||||
<p class="is-size-4 has-text-weight-bold has-text-primary">Gesamt: {{ gesamt_summe|german_number }} €</p>
|
||||
<table class="table is-fullwidth is-hoverable mt-2" style="font-size:0.85rem">
|
||||
<thead><tr><th>Bezeichnung</th><th style="text-align:right">Wert (€)</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p, summe in projekte_mit_summe %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('aufmass.aufmass_list', project_id=p.id) }}">{{ p.bezeichnung or p.sm_nr }}</a></td>
|
||||
<td style="text-align:right">{{ summe|german_number }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-grey is-size-7">Keine Projekte mit Werten.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" data-card="lizenzen">
|
||||
<h2 class="title is-5">Lizenzen</h2>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-one-third">
|
||||
<div class="notification is-light has-text-centered" style="margin-bottom:0">
|
||||
<p class="heading">Mitarbeiter</p>
|
||||
<p class="title is-4">{{ mitarbeiter_anzahl }}/{{ license_max_ma }}</p>
|
||||
<p class="is-size-7 has-text-grey">belegt/verfügbar</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="notification is-light has-text-centered" style="margin-bottom:0">
|
||||
<p class="heading">Module</p>
|
||||
<p class="title is-4">{{ license_module_used }}/{{ license_module_count }}</p>
|
||||
<p class="is-size-7 has-text-grey">belegt/verfügbar</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="notification is-light has-text-centered" style="margin-bottom:0">
|
||||
<p class="heading">Lizenzen</p>
|
||||
<p class="title is-4">{{ license_count }}</p>
|
||||
<p class="is-size-7 has-text-grey">vorhanden</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
|
||||
<script>
|
||||
var cardContainer = document.getElementById('dashboard-cards');
|
||||
|
||||
new Sortable(cardContainer, {
|
||||
animation: 150,
|
||||
handle: '.box',
|
||||
onEnd: function() {
|
||||
var order = [];
|
||||
cardContainer.querySelectorAll('.column[data-card]').forEach(function(col) {
|
||||
order.push(col.dataset.card);
|
||||
});
|
||||
localStorage.setItem('dash_order_' + {{ current_user.id }}, JSON.stringify(order));
|
||||
}
|
||||
});
|
||||
|
||||
function loadOrder() {
|
||||
var saved = localStorage.getItem('dash_order_' + {{ current_user.id }});
|
||||
if (!saved) return;
|
||||
var order = JSON.parse(saved);
|
||||
var cards = {};
|
||||
cardContainer.querySelectorAll('.column[data-card]').forEach(function(col) {
|
||||
cards[col.dataset.card] = col;
|
||||
});
|
||||
order.forEach(function(key) {
|
||||
if (cards[key]) cardContainer.appendChild(cards[key]);
|
||||
});
|
||||
}
|
||||
loadOrder();
|
||||
|
||||
function saveProfile() {
|
||||
var order = [];
|
||||
cardContainer.querySelectorAll('.column[data-card]').forEach(function(col) {
|
||||
order.push(col.dataset.card);
|
||||
});
|
||||
var lizOrder = document.querySelector('[data-card="lizenzen"]');
|
||||
var sel = document.getElementById('profile-select');
|
||||
var profileId = sel.value;
|
||||
fetch('/api/profiles/' + (profileId || ''), {
|
||||
method: profileId ? 'PUT' : 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
name: 'Dashboard',
|
||||
view_type: 'dashboard',
|
||||
config_json: {card_order: order}
|
||||
})
|
||||
}).then(function(r){return r.json()}).then(function(data){
|
||||
if (data.id) {
|
||||
var opt = sel.querySelector('option[value="'+profileId+'"]');
|
||||
if (!opt) {
|
||||
opt = document.createElement('option');
|
||||
opt.value = data.id;
|
||||
opt.textContent = data.name || 'Dashboard';
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
sel.value = data.id;
|
||||
}
|
||||
alert('Gespeichert');
|
||||
});
|
||||
}
|
||||
|
||||
function loadProfile(profileId) {
|
||||
if (!profileId) return;
|
||||
fetch('/api/profiles/' + profileId)
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(data){
|
||||
var cfg = data.config_json || {};
|
||||
if (cfg.card_order) {
|
||||
var cards = {};
|
||||
cardContainer.querySelectorAll('.column[data-card]').forEach(function(col) {
|
||||
cards[col.dataset.card] = col;
|
||||
});
|
||||
cfg.card_order.forEach(function(key) {
|
||||
if (cards[key]) cardContainer.appendChild(cards[key]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function newProfile() {
|
||||
var name = prompt('Profil-Name:');
|
||||
if (!name) return;
|
||||
fetch('/api/profiles', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
view_type: 'dashboard',
|
||||
config_json: {card_order: []}
|
||||
})
|
||||
}).then(function(r){return r.json()}).then(function(data){
|
||||
if (data.id) {
|
||||
var sel = document.getElementById('profile-select');
|
||||
var opt = document.createElement('option');
|
||||
opt.value = data.id;
|
||||
opt.textContent = data.name;
|
||||
opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,203 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">{{ company.name }}</h1></div>
|
||||
<div class="level-right">
|
||||
<a class="button is-light" href="{{ url_for('admin.dashboard') }}">← Zurück</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Mitarbeiter</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>E-Mail</th><th>Rolle</th><th>Status</th><th>Rechte</th><th>Aktion</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.full_name }}</td>
|
||||
<td>{{ u.email }}</td>
|
||||
<td>
|
||||
<span class="tag {{ 'is-warning' if u.is_firmadmin() else 'is-light' }}">
|
||||
{{ u.rolle }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="tag {{ 'is-success' if u.aktiv else 'is-danger' }}">
|
||||
{{ 'Aktiv' if u.aktiv else 'Deaktiviert' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if u.rolle == 'mitarbeiter' %}
|
||||
<a class="button is-small is-link is-outlined" href="{{ url_for('admin.mitarbeiter_rechte', user_id=u.id) }}">Rechte</a>
|
||||
{% else %}
|
||||
<span class="has-text-grey is-size-7">–</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if current_user.is_firmadmin() and u.id != current_user.id %}
|
||||
<a class="button is-small is-info is-outlined" href="{{ url_for('admin.mitarbeiter_bearbeiten', user_id=u.id) }}">Bearbeiten</a>
|
||||
<a class="button is-small {{ 'is-warning' if u.aktiv else 'is-success' }}"
|
||||
href="{{ url_for('admin.mitarbeiter_toggle', user_id=u.id) }}">
|
||||
{{ 'Deaktivieren' if u.aktiv else 'Aktivieren' }}
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('admin.mitarbeiter_loeschen', user_id=u.id) }}"
|
||||
style="display:inline" onsubmit="return confirm('Benutzer {{ u.email }} wirklich löschen?')">
|
||||
<button class="button is-small is-danger">Löschen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if current_user.is_firmadmin() %}
|
||||
<details class="mt-3">
|
||||
<summary class="has-text-link">+ Mitarbeiter hinzufügen</summary>
|
||||
<form method="POST" action="{{ url_for('admin.mitarbeiter_neu') }}" class="mt-2 box">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-4">
|
||||
<input class="input" type="email" name="email" placeholder="E-Mail *" required>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<input class="input" type="text" name="vorname" placeholder="Vorname">
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<input class="input" type="text" name="nachname" placeholder="Nachname">
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="rolle">
|
||||
<option value="mitarbeiter">Mitarbeiter</option>
|
||||
<option value="firmadmin">Firmadmin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<input class="input" type="password" name="password" placeholder="Start-Passwort" minlength="6" required>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_projekte_anlegen" value="1"> Projekte anlegen</label>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_lv_verwalten" value="1"> LV verwalten</label>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_preise_sehen" value="1"> Preise sehen</label>
|
||||
<label class="checkbox is-size-7"><input type="checkbox" name="darf_aufmass_verwalten" value="1"> Aufmaße verwalten</label>
|
||||
{% if company.evergabe_aktiviert %}
|
||||
<br>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_evergabe_nutzen" value="1"> E-Vergabe Addon nutzen</label>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_kopfdaten_holen" value="1"> Kopfdaten holen</label>
|
||||
<label class="checkbox is-size-7"><input type="checkbox" name="darf_aufmass_uebertragen" value="1"> Aufmaße übertragen</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<button class="button is-primary" type="submit">Anlegen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Firmenlogo</h2>
|
||||
{% if company.logo %}
|
||||
<div style="margin-bottom:12px">
|
||||
<img src="{{ url_for('admin.firma_logo') }}" alt="Logo" style="max-width:200px;max-height:80px;border:1px solid #ddd;border-radius:6px;padding:8px">
|
||||
<p class="is-size-7 has-text-grey mt-1">Aktuelles Logo</p>
|
||||
<form method="POST" action="{{ url_for('admin.firma_logo_upload') }}" style="display:inline">
|
||||
<input type="hidden" name="delete" value="1">
|
||||
<button class="button is-small is-danger is-outlined" type="submit">Entfernen</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="margin-bottom:12px;display:flex;align-items:center;gap:12px;border:2px dashed #ccc;border-radius:8px;padding:16px;background:#fafafa">
|
||||
<div style="width:60px;height:60px;background:#e0e0e0;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:24px;color:#999">🏢</div>
|
||||
<div>
|
||||
<p style="font-weight:600;font-size:1rem;color:#555">{{ company.name }}</p>
|
||||
<p class="is-size-7 has-text-grey">Kein Logo hochgeladen</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('admin.firma_logo_upload') }}" enctype="multipart/form-data">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input class="input is-small" type="file" name="logo" accept="image/png,image/jpeg,image/gif,image/webp" required>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-small is-primary" type="submit">Hochladen</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="is-size-7 has-text-grey">Erlaubte Formate: PNG, JPG, GIF, WebP</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Verfügbare Module ({{ modules|length }})</h2>
|
||||
<p class="is-size-7 has-text-grey mb-2">Diese Module stehen Ihrer Firma zur Verfügung.</p>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Modul</th><th>Kategorie</th></tr></thead>
|
||||
<tbody>
|
||||
{% for m in modules %}
|
||||
<tr>
|
||||
<td>{{ m.icon }} {{ m.titel }}</td>
|
||||
<td>{{ m.kategorie or '–' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Lizenz</h2>
|
||||
{% if licenses %}
|
||||
<table class="table is-fullwidth">
|
||||
<thead><tr><th>UID</th><th>Mitarbeiter-Plätze</th><th>Modul-Plätze</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for lic in licenses %}
|
||||
<tr>
|
||||
<td><code>{{ lic.uid }}</code></td>
|
||||
<td>{{ lic.user_slots_display() }}</td>
|
||||
<td>{{ lic.module_slots_display() }}</td>
|
||||
<td><span class="tag {{ 'is-success' if lic.aktiv else 'is-danger' }}">{{ 'Aktiv' if lic.aktiv else 'Inaktiv' }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Keine Lizenz hinterlegt. Standard-Module sind aktiv.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if company.evergabe_aktiviert %}
|
||||
<div class="box">
|
||||
<h2 class="title is-5">E-Vergabe</h2>
|
||||
<p class="is-size-7 has-text-grey mb-2">Logindaten für die E-Vergabe-Plattform.</p>
|
||||
<form method="POST" action="{{ url_for('admin.firma_evergabe_save') }}">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label is-small">Benutzer</label>
|
||||
<div class="control"><input class="input is-small" name="evergabe_benutzer" value="{{ company.evergabe_benutzer or '' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label is-small">Passwort</label>
|
||||
<div class="control"><input class="input is-small" name="evergabe_passwort" type="password" value="{{ company.evergabe_passwort or '' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label is-small">Name</label>
|
||||
<div class="control"><input class="input is-small" name="evergabe_name" value="{{ company.evergabe_name or '' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-3">
|
||||
<button class="button is-small is-primary" type="submit">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,90 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">Benutzer bearbeiten: {{ user.full_name }}</h1></div>
|
||||
<div class="level-right">
|
||||
{% if current_user.is_superadmin() %}
|
||||
<a class="button is-small" href="{{ url_for('superadmin.firma_detail', company_id=user.company_id) }}">← Firma</a>
|
||||
{% else %}
|
||||
<a class="button is-small" href="{{ url_for('admin.firma') }}">← Firma</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<form method="POST">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">E-Mail</label>
|
||||
<div class="control">
|
||||
<input class="input" type="email" name="email" value="{{ user.email }}" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Vorname</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="vorname" value="{{ user.vorname or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Nachname</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="nachname" value="{{ user.nachname or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<div class="field">
|
||||
<label class="label">Rolle</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="rolle">
|
||||
<option value="mitarbeiter" {{ 'selected' if user.rolle == 'mitarbeiter' }}>Mitarbeiter</option>
|
||||
<option value="firmadmin" {{ 'selected' if user.rolle == 'firmadmin' }}>Firmadmin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Neues Passwort (leer lassen für keine Änderung)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="password" minlength="6" placeholder="mind. 6 Zeichen">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<div class="field">
|
||||
<label class="label">Berechtigungen</label>
|
||||
<div class="control">
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_projekte_anlegen" value="1" {{ 'checked' if user.darf_projekte_anlegen }}> Projekte anlegen</label>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_lv_verwalten" value="1" {{ 'checked' if user.darf_lv_verwalten }}> LV verwalten</label>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_preise_sehen" value="1" {{ 'checked' if user.darf_preise_sehen }}> Preise sehen</label>
|
||||
<label class="checkbox is-size-7"><input type="checkbox" name="darf_aufmass_verwalten" value="1" {{ 'checked' if user.darf_aufmass_verwalten }}> Aufmaße verwalten</label>
|
||||
{% if company and company.evergabe_aktiviert %}
|
||||
<br>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_evergabe_nutzen" value="1" {{ 'checked' if user.darf_evergabe_nutzen }}> E-Vergabe Addon nutzen</label>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_kopfdaten_holen" value="1" {{ 'checked' if user.darf_kopfdaten_holen }}> Kopfdaten holen</label>
|
||||
<label class="checkbox is-size-7"><input type="checkbox" name="darf_aufmass_uebertragen" value="1" {{ 'checked' if user.darf_aufmass_uebertragen }}> Aufmaße übertragen</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<button class="button is-primary" type="submit">Speichern</button>
|
||||
{% if current_user.is_superadmin() %}
|
||||
<a class="button is-light" href="{{ url_for('superadmin.firma_detail', company_id=user.company_id) }}">Abbrechen</a>
|
||||
{% else %}
|
||||
<a class="button is-light" href="{{ url_for('admin.firma') }}">Abbrechen</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,101 @@
|
||||
{% extends "base.html" %}
|
||||
{% block head %}
|
||||
<style>.buttons .button.is-outlined { color:#000 !important; }</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
<div class="box">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<h1 class="title is-4 has-text-dark">Profil</h1>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<a class="button is-light is-small" href="{{ url_for('admin.dashboard') }}">← Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="has-text-centered mb-4">
|
||||
{% if current_user.profile_image %}
|
||||
<figure class="image is-96x96" style="margin:0 auto;border-radius:50%;overflow:hidden">
|
||||
<img src="{{ url_for('static', filename='avatars/'+current_user.profile_image) }}" alt="Avatar">
|
||||
</figure>
|
||||
{% else %}
|
||||
<div style="width:96px;height:96px;border-radius:50%;background:#2F5496;display:inline-flex;align-items:center;justify-content:center;font-size:2.5rem;font-weight:bold;color:#fff">
|
||||
{{ (current_user.vorname[0] if current_user.vorname else '') + (current_user.nachname[0] if current_user.nachname else '') or '?' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('admin.avatar_upload') }}" enctype="multipart/form-data" class="mt-2">
|
||||
<div class="field has-addons" style="justify-content:center">
|
||||
<div class="control"><input class="input is-small" type="file" name="avatar" accept="image/*"></div>
|
||||
<div class="control"><button class="button is-small is-primary">Hochladen</button></div>
|
||||
</div>
|
||||
</form>
|
||||
{% if current_user.profile_image %}
|
||||
<form method="POST" action="{{ url_for('admin.avatar_entfernen') }}" class="mt-1">
|
||||
<button class="button is-small is-danger is-outlined">Bild entfernen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<div class="field">
|
||||
<label class="label has-text-dark">E-Mail</label>
|
||||
<div class="control">
|
||||
<input class="input" type="email" value="{{ current_user.email }}" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label has-text-dark">Vorname</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="vorname" value="{{ current_user.vorname or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label has-text-dark">Nachname</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="nachname" value="{{ current_user.nachname or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label has-text-dark">Neues Passwort (leer lassen für keine Änderung)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="password" minlength="6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h2 class="title is-5 has-text-dark">Einstellungen – Schriftgröße</h2>
|
||||
<div class="field">
|
||||
<label class="label has-text-dark">Schriftgröße für die gesamte Anwendung</label>
|
||||
<div class="control">
|
||||
<div class="buttons">
|
||||
<a class="button {{ 'is-info' if session.get('font_size','1') == '0.8' else 'is-outlined' }}"
|
||||
href="{{ url_for('set_font_size', size='0.8') }}">A− Klein</a>
|
||||
<a class="button {{ 'is-info' if session.get('font_size','1') == '1' else 'is-outlined' }}"
|
||||
href="{{ url_for('set_font_size', size='1') }}">A Normal</a>
|
||||
<a class="button {{ 'is-info' if session.get('font_size','1') == '1.1' else 'is-outlined' }}"
|
||||
href="{{ url_for('set_font_size', size='1.1') }}">A+ Größer</a>
|
||||
<a class="button {{ 'is-info' if session.get('font_size','1') == '1.25' else 'is-outlined' }}"
|
||||
href="{{ url_for('set_font_size', size='1.25') }}">A++ Groß</a>
|
||||
<a class="button {{ 'is-info' if session.get('font_size','1') == '1.5' else 'is-outlined' }}"
|
||||
href="{{ url_for('set_font_size', size='1.5') }}">A+++ Sehr groß</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field mt-4">
|
||||
<button class="button is-primary" type="submit">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,116 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">Rechte: {{ user.full_name }}</h1></div>
|
||||
<div class="level-right">
|
||||
<a class="button is-small" href="{{ url_for('admin.firma') }}">← Firma</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Firmenweite Berechtigungen</h2>
|
||||
<form method="POST">
|
||||
<table class="table is-fullwidth">
|
||||
<tr>
|
||||
<td>Projekte anlegen</td>
|
||||
<td><input type="checkbox" name="darf_projekte_anlegen" value="1" {{ 'checked' if user.darf_projekte_anlegen }}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LV verwalten</td>
|
||||
<td><input type="checkbox" name="darf_lv_verwalten" value="1" {{ 'checked' if user.darf_lv_verwalten }}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Preise sehen</td>
|
||||
<td><input type="checkbox" name="darf_preise_sehen" value="1" {{ 'checked' if user.darf_preise_sehen }}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aufmaße verwalten</td>
|
||||
<td><input type="checkbox" name="darf_aufmass_verwalten" value="1" {{ 'checked' if user.darf_aufmass_verwalten }}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>E-Vergabe Addon nutzen</td>
|
||||
<td><input type="checkbox" name="darf_evergabe_nutzen" value="1" {{ 'checked' if user.darf_evergabe_nutzen }}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kopfdaten holen erlauben</td>
|
||||
<td><input type="checkbox" name="darf_kopfdaten_holen" value="1" {{ 'checked' if user.darf_kopfdaten_holen }}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aufmaße in E-Vergabe übertragen</td>
|
||||
<td><input type="checkbox" name="darf_aufmass_uebertragen" value="1" {{ 'checked' if user.darf_aufmass_uebertragen }}></td>
|
||||
</tr>
|
||||
</table>
|
||||
<button class="button is-primary">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if modules %}
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Modul-Freigaben</h2>
|
||||
<p class="is-size-7 has-text-grey mb-2">Legen Sie fest, welche Module dieser Mitarbeiter nutzen darf.</p>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Modul</th><th>Kategorie</th><th>Status</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
{% for m in modules %}
|
||||
<tr>
|
||||
<td>{{ m.icon }} {{ m.titel }}</td>
|
||||
<td>{{ m.kategorie or '–' }}</td>
|
||||
<td>
|
||||
{% if user.rolle == 'firmadmin' %}
|
||||
<span class="tag is-success">Immer aktiv</span>
|
||||
{% elif m.id in user_modules %}
|
||||
<span class="tag is-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="tag is-light">Inaktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.rolle == 'mitarbeiter' %}
|
||||
<a class="button is-small {{ 'is-warning' if m.id in user_modules else 'is-success' }}"
|
||||
href="{{ url_for('admin.mitarbeiter_module_toggle', user_id=user.id, module_id=m.id) }}">
|
||||
{{ 'Deaktivieren' if m.id in user_modules else 'Aktivieren' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Projekt-Zugriffe</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Projekt</th><th>Zugriff</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in projekte %}
|
||||
<tr>
|
||||
<td>{{ p.sm_nr }} – {{ p.bezeichnung or '' }}</td>
|
||||
<td>
|
||||
{% set pa = zugriffe.get(p.id) %}
|
||||
<span class="tag {{ 'is-success' if pa and pa.zugriff == 'schreiben' else ('is-link' if pa else 'is-light') }}">
|
||||
{{ pa.zugriff if pa else 'kein Zugriff' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('admin.mitarbeiter_projekt_zugriff', user_id=user.id) }}" class="field has-addons">
|
||||
<input type="hidden" name="project_id" value="{{ p.id }}">
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select name="zugriff">
|
||||
<option value="">–</option>
|
||||
<option value="lesen" {{ 'selected' if zugriffe.get(p.id) and zugriffe[p.id].zugriff == 'lesen' }}>Lesen</option>
|
||||
<option value="schreiben" {{ 'selected' if zugriffe.get(p.id) and zugriffe[p.id].zugriff == 'schreiben' }}>Schreiben</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control"><button class="button is-small is-info">Setzen</button></div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,249 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 5mm;
|
||||
}
|
||||
body {
|
||||
font-family: DejaVu Sans, Helvetica, sans-serif;
|
||||
font-size: 7pt;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
td, th {
|
||||
border: 1px solid #000;
|
||||
padding: 1px 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.header-row td {
|
||||
padding: 2px 4px;
|
||||
font-size: 7.5pt;
|
||||
text-align: left;
|
||||
}
|
||||
.label {
|
||||
font-weight: bold;
|
||||
background: #f2f2f2;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
.sm-header th {
|
||||
background: #2F5496;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
font-size: 6pt;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
#pos-tbl td {
|
||||
padding: 2px 3px;
|
||||
}
|
||||
tr.trenner td {
|
||||
background: #f5f5f5;
|
||||
height: 3px;
|
||||
}
|
||||
tr.sum-row td {
|
||||
font-weight: bold;
|
||||
font-size: 7pt;
|
||||
}
|
||||
.summary-title {
|
||||
background: #D6E4F0;
|
||||
color: #2F5496;
|
||||
font-weight: bold;
|
||||
font-size: 9pt;
|
||||
text-align: center;
|
||||
padding: 3px;
|
||||
}
|
||||
.summary-header th {
|
||||
background: #2F5496;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
font-size: 6pt;
|
||||
padding: 2px;
|
||||
}
|
||||
.summary-data td {
|
||||
font-size: 7pt;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
.summary-sum td {
|
||||
font-weight: bold;
|
||||
font-size: 7pt;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.num {
|
||||
text-align: right;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Row 1: Logo / Firmenname / Aufmaß -->
|
||||
<table style="margin-bottom:5mm; width:100%">
|
||||
<tr>
|
||||
{% if company and company.logo %}
|
||||
<td style="border:none; width:30mm">
|
||||
<img src="{{ company.logo }}" width="120">
|
||||
</td>
|
||||
<td style="border:none; text-align:center; vertical-align:middle; font-size:14pt; font-weight:bold; color:#2F5496">
|
||||
Aufmaß
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<td style="border:none; font-size:14pt; font-weight:bold; color:#2F5496" colspan="2">
|
||||
{% if company and company.name %}{{ company.name }}{% else %}Aufmaß{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
<!-- Header rows (Label-Spalten: 1,3,5,7 → jeweils 8%; Value-Spalten: 2,4,6,8 → Rest) -->
|
||||
<table class="header-row">
|
||||
<tr>
|
||||
<td class="label" style="width:8%">Vertrag:</td>
|
||||
<td style="width:17%">{{ _val(project.vertrag) or '' }}</td>
|
||||
<td class="label" style="width:8%">LV-Name:</td>
|
||||
<td colspan="3" style="width:42%">{{ _val(project.lv_name) or '' }}</td>
|
||||
<td class="label" style="width:8%">Aufmaß-Datum:</td>
|
||||
<td style="width:17%">{{ _fmt_date(project.datum) or '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Projekt:</td>
|
||||
<td>{{ _val(project.bezeichnung) or '' }}</td>
|
||||
<td class="label">Baustelle:</td>
|
||||
<td colspan="5">{{ _val(project.baustelle) or '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Typ:</td>
|
||||
<td>{{ _val(aufmass.typ if aufmass else none) or '' }}</td>
|
||||
<td class="label">Bauabschnitt:</td>
|
||||
<td colspan="5">{{ _val(project.bauabschnitt) or '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">SM-Nr.:</td>
|
||||
<td>{{ _val(project.sm_nr) or '' }}</td>
|
||||
<td class="label">Startdatum:</td>
|
||||
<td>{{ _fmt_date(project.datum_start) or '' }}</td>
|
||||
<td class="label">Name:</td>
|
||||
<td>{{ _val(ap_name) or '' }}</td>
|
||||
<td class="label">Tel:</td>
|
||||
<td>{{ _val(project.ansprechpartner_tel) or '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Abruf-Nr.:</td>
|
||||
<td>{{ _val(project.abruf_nr) or '' }}</td>
|
||||
<td class="label">Enddatum:</td>
|
||||
<td>{{ _fmt_date(project.datum_ende) or '' }}</td>
|
||||
<td class="label">Email:</td>
|
||||
<td colspan="3">{{ _val(project.ansprechpartner_email) or '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<!-- Position table (Spalten-% wie Excel max_widths) -->
|
||||
<table id="pos-tbl">
|
||||
<thead>
|
||||
<tr class="sm-header">
|
||||
<th style="width:7%">Abschn.</th>
|
||||
<th style="width:6%">Pos-Nr</th>
|
||||
<th style="width:4%">Fakt.</th>
|
||||
<th style="width:5%">Länge</th>
|
||||
<th style="width:5%">Breite</th>
|
||||
<th style="width:5%">Tiefe</th>
|
||||
<th style="width:6%">Menge</th>
|
||||
<th style="width:3%">EH</th>
|
||||
<th style="width:23%">Kurztext</th>
|
||||
<th style="width:18%">Bemerkung</th>
|
||||
<th style="width:6%">Menge</th>
|
||||
<th style="width:6%">EP (€)</th>
|
||||
<th style="width:6%">GP (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if positionen %}
|
||||
{% set ns = namespace(pos_counter=0, gesamt=0) %}
|
||||
{% for pos in positionen %}
|
||||
{% if _ist_trenner(pos) %}
|
||||
<tr class="trenner"><td colspan="13"></td></tr>
|
||||
{% else %}
|
||||
{% set ns.pos_counter = ns.pos_counter + 1 %}
|
||||
{% set menge = pos.menge if pos.menge else none %}
|
||||
{% set menge_hinten = pos.menge_hinten if pos.menge_hinten else none %}
|
||||
{% if pos.einheit in ('ST', 'LE', 'STD', 'h', 'Psch') %}
|
||||
{% set menge = pos.faktor * 1 if pos.faktor else none %}
|
||||
{% endif %}
|
||||
{% set ns.gesamt = ns.gesamt + (pos.gesamtpreis or 0) %}
|
||||
<tr>
|
||||
<td class="center">{{ _val(pos.abschnitt) or '' }}</td>
|
||||
<td>{{ pos.pos_nr or '' }}</td>
|
||||
<td class="num">{{ '%.2f'|format(pos.faktor) if pos.faktor else '' }}</td>
|
||||
<td class="num">{{ '%.2f'|format(pos.laenge) if pos.laenge else '' }}</td>
|
||||
<td class="num">{{ '%.2f'|format(pos.breite) if pos.breite else '' }}</td>
|
||||
<td class="num">{{ '%.2f'|format(pos.tiefe) if pos.tiefe else '' }}</td>
|
||||
<td class="num">{{ '%.2f'|format(menge) if menge else '' }}</td>
|
||||
<td class="center">{{ pos.einheit or '' }}</td>
|
||||
<td class="left">{{ _val(pos.kurztext) or '' }}</td>
|
||||
<td class="left">{{ _val(pos.bemerkung) or '' }}</td>
|
||||
<td class="num">{{ '%.2f'|format(menge_hinten) if menge_hinten else '' }}</td>
|
||||
<td class="num">{{ '%.2f'|format(pos.einzelpreis) if pos.einzelpreis else '' }}</td>
|
||||
<td class="num">{{ '%.2f'|format(pos.gesamtpreis) if pos.gesamtpreis else '' }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<tr class="sum-row">
|
||||
<td colspan="11"></td>
|
||||
<td class="num">Summe:</td>
|
||||
<td class="num">{{ '%.2f'|format(ns.gesamt) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Summary section -->
|
||||
{% if ns.pos_counter > 0 %}
|
||||
<br>
|
||||
<table style="width:100%">
|
||||
<tr><td class="summary-title" colspan="5">Mengen- und Positions-Zusammenfassung</td></tr>
|
||||
<tr class="summary-header">
|
||||
<th style="width:7%">Pos-Nr</th>
|
||||
<th style="width:48%">Kurztext</th>
|
||||
<th style="width:11%">Menge</th>
|
||||
<th style="width:20%">EP (€)</th>
|
||||
<th style="width:14%">GP (€)</th>
|
||||
</tr>
|
||||
{% set total_gp = namespace(val=0) %}
|
||||
{% for key in seen_pos %}
|
||||
{% set g = groups[key] %}
|
||||
{% set total_gp.val = total_gp.val + g['gp'] %}
|
||||
<tr class="summary-data">
|
||||
<td class="center">{{ key }}</td>
|
||||
<td class="left" style="max-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap" title="{{ g['kurztext'] }}">{{ g['kurztext'][:120] + '...' if g['kurztext']|length > 120 else g['kurztext'] }}</td>
|
||||
<td class="num">{{ '%.2f'|format(g['menge']) }}</td>
|
||||
<td class="num">{{ '%.2f'|format(g['ep']) }}</td>
|
||||
<td class="num">{{ '%.2f'|format(g['gp']) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="summary-sum">
|
||||
<td colspan="2"></td>
|
||||
<td class="num">Summe:</td>
|
||||
<td class="num">{{ '%.2f'|format(total_gp.val) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,851 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
:root {
|
||||
--p-primary: #2F5496;
|
||||
--p-primary-light: #4a7bc4;
|
||||
--p-primary-dark: #1a3055;
|
||||
--p-primary-glow: rgba(47,84,150,.25);
|
||||
--p-accent: #f0c040;
|
||||
--p-success: #27ae60;
|
||||
--p-success-light: #e8f8f0;
|
||||
--p-warning: #f39c12;
|
||||
--p-danger: #e74c3c;
|
||||
--p-bg: #f0f2f8;
|
||||
--p-card-bg: rgba(255,255,255,.85);
|
||||
--p-border: rgba(0,0,0,.06);
|
||||
--p-text: #1a1a2e;
|
||||
--p-text-light: #6b7280;
|
||||
--p-radius: 14px;
|
||||
--p-shadow: 0 1px 3px rgba(0,0,0,.04), 0 4px 16px rgba(0,0,0,.04);
|
||||
--p-shadow-hover: 0 4px 12px rgba(47,84,150,.12), 0 8px 32px rgba(0,0,0,.06);
|
||||
--p-transition: all .35s cubic-bezier(.25,.46,.45,.94);
|
||||
}
|
||||
|
||||
body { background: var(--p-bg); font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||
.projekt-page { max-width: 1100px; margin: 0 auto; padding: 0 16px; }
|
||||
|
||||
/* === Hero Header === */
|
||||
.projekt-hero {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 32px 0 24px; flex-wrap: wrap; gap: 16px;
|
||||
}
|
||||
.projekt-hero-left h1 {
|
||||
font-size: 1.75rem; font-weight: 700; letter-spacing: -.03em;
|
||||
background: linear-gradient(135deg, var(--p-primary-dark), var(--p-primary-light));
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
background-clip: text; margin: 0 0 4px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.projekt-hero-left h1 .icon { -webkit-text-fill-color: initial; font-size: 1.6rem; }
|
||||
.projekt-hero-left .hero-sub {
|
||||
font-size: .88rem; color: var(--p-text-light);
|
||||
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
||||
}
|
||||
.projekt-hero-left .hero-sub .stat {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
background: rgba(47,84,150,.08); padding: 3px 10px; border-radius: 20px;
|
||||
font-size: .78rem; font-weight: 500; color: var(--p-primary);
|
||||
}
|
||||
.btn-neu-projekt {
|
||||
background: linear-gradient(135deg, var(--p-primary), var(--p-primary-light));
|
||||
color: #fff; border: none; border-radius: 12px; padding: 12px 28px;
|
||||
font-size: .92rem; font-weight: 600; cursor: pointer;
|
||||
transition: var(--p-transition); text-decoration: none;
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
box-shadow: 0 4px 14px var(--p-primary-glow);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-neu-projekt:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 8px 28px var(--p-primary-glow);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-neu-projekt:active { transform: scale(.97); }
|
||||
|
||||
/* === Search Bar === */
|
||||
.search-wrap {
|
||||
display: flex; gap: 8px; margin-bottom: 28px; position: relative;
|
||||
}
|
||||
.search-wrap .control { flex: 1; position: relative; }
|
||||
.search-wrap .control input {
|
||||
width: 100%; padding: 14px 18px 14px 46px;
|
||||
background: var(--p-card-bg); backdrop-filter: blur(8px);
|
||||
border: 2px solid var(--p-border); border-radius: 14px;
|
||||
font-size: .92rem; font-family: inherit;
|
||||
transition: var(--p-transition); color: var(--p-text);
|
||||
box-shadow: var(--p-shadow);
|
||||
}
|
||||
.search-wrap .control input::placeholder { color: #b0b8c8; }
|
||||
.search-wrap .control input:focus {
|
||||
outline: none; border-color: var(--p-primary);
|
||||
box-shadow: 0 0 0 4px var(--p-primary-glow), var(--p-shadow);
|
||||
background: #fff;
|
||||
}
|
||||
.search-wrap .control .search-icon {
|
||||
position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
|
||||
font-size: 1.1rem; opacity: .35; pointer-events: none;
|
||||
transition: opacity .3s;
|
||||
}
|
||||
.search-wrap .control input:focus ~ .search-icon { opacity: .6; }
|
||||
.search-wrap .clear-btn {
|
||||
padding: 0 18px; border: 2px solid var(--p-border);
|
||||
border-radius: 14px; background: var(--p-card-bg); backdrop-filter: blur(8px);
|
||||
cursor: pointer; transition: var(--p-transition); font-size: .9rem;
|
||||
color: var(--p-text-light); box-shadow: var(--p-shadow);
|
||||
font-family: inherit; font-weight: 500;
|
||||
}
|
||||
.search-wrap .clear-btn:hover {
|
||||
background: #fff; border-color: var(--p-danger); color: var(--p-danger);
|
||||
transform: scale(1.04);
|
||||
}
|
||||
.search-wrap .result-count {
|
||||
position: absolute; right: 60px; top: 50%; transform: translateY(-50%);
|
||||
font-size: .75rem; color: var(--p-text-light); background: rgba(0,0,0,.04);
|
||||
padding: 2px 10px; border-radius: 10px; pointer-events: none;
|
||||
opacity: 0; transition: opacity .3s;
|
||||
}
|
||||
.search-wrap .result-count.visible { opacity: 1; }
|
||||
|
||||
/* === Card Container === */
|
||||
.projekt-container {
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
/* === Project Card === */
|
||||
.projekt-card {
|
||||
background: var(--p-card-bg); backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--p-border);
|
||||
border-radius: var(--p-radius);
|
||||
box-shadow: var(--p-shadow);
|
||||
transition: var(--p-transition);
|
||||
overflow: hidden;
|
||||
animation: cardIn .45s cubic-bezier(.25,.46,.45,.94) both;
|
||||
}
|
||||
.projekt-card:hover {
|
||||
box-shadow: var(--p-shadow-hover);
|
||||
border-color: rgba(47,84,150,.12);
|
||||
}
|
||||
.projekt-card.js-project-hide { display: none; }
|
||||
.projekt-card.dragging { opacity: .5; transform: scale(.98); }
|
||||
|
||||
@keyframes cardIn {
|
||||
from { opacity: 0; transform: translateY(20px) scale(.97); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
.projekt-card:nth-child(1) { animation-delay: 0s; }
|
||||
.projekt-card:nth-child(2) { animation-delay: .04s; }
|
||||
.projekt-card:nth-child(3) { animation-delay: .08s; }
|
||||
.projekt-card:nth-child(4) { animation-delay: .12s; }
|
||||
.projekt-card:nth-child(5) { animation-delay: .16s; }
|
||||
.projekt-card:nth-child(6) { animation-delay: .2s; }
|
||||
.projekt-card:nth-child(7) { animation-delay: .24s; }
|
||||
.projekt-card:nth-child(8) { animation-delay: .28s; }
|
||||
|
||||
/* === Card Header (Summary) === */
|
||||
.card-header {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 16px 20px; cursor: pointer; user-select: none;
|
||||
transition: background .2s;
|
||||
position: relative;
|
||||
}
|
||||
.card-header:hover { background: rgba(47,84,150,.03); }
|
||||
.card-header:active { background: rgba(47,84,150,.06); }
|
||||
.card-header .arrow {
|
||||
width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
|
||||
font-size: .5rem; color: #bbb; flex-shrink: 0;
|
||||
transition: transform .4s cubic-bezier(.34,1.56,.64,1);
|
||||
border-radius: 6px; background: rgba(0,0,0,.03);
|
||||
}
|
||||
.card-header.is-open .arrow { transform: rotate(90deg); background: rgba(47,84,150,.08); color: var(--p-primary); }
|
||||
.card-header .proj-icon {
|
||||
width: 40px; height: 40px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.2rem; flex-shrink: 0;
|
||||
background: linear-gradient(135deg, #eef2fa, #e0e7f5);
|
||||
transition: var(--p-transition);
|
||||
}
|
||||
.card-header:hover .proj-icon { transform: scale(1.05) rotate(-2deg); }
|
||||
.card-header .proj-info { flex: 1; min-width: 0; }
|
||||
.card-header .proj-info .proj-name {
|
||||
font-weight: 600; font-size: .95rem; color: var(--p-text);
|
||||
letter-spacing: -.01em; display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.card-header .proj-info .proj-name .edit-trigger {
|
||||
display: inline-flex; font-size: .7rem; opacity: 0;
|
||||
cursor: pointer; transition: opacity .2s; color: #bbb; padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.card-header:hover .proj-info .proj-name .edit-trigger { opacity: 1; }
|
||||
.card-header .proj-info .proj-name .edit-trigger:hover { color: var(--p-primary); background: rgba(47,84,150,.08); }
|
||||
.card-header .proj-info .proj-meta {
|
||||
display: flex; align-items: center; gap: 8px; margin-top: 3px;
|
||||
font-size: .75rem; color: var(--p-text-light); flex-wrap: wrap;
|
||||
}
|
||||
.card-header .proj-info .proj-meta .badge {
|
||||
display: inline-flex; align-items: center; gap: 3px; padding: 2px 10px;
|
||||
border-radius: 20px; font-weight: 500; font-size: .7rem;
|
||||
}
|
||||
.card-header .proj-info .proj-meta .badge-anz { background: #f0f2f8; color: #555; }
|
||||
.card-header .proj-info .proj-meta .badge-pos { background: rgba(47,84,150,.08); color: var(--p-primary); }
|
||||
.card-header .proj-info .proj-meta .badge-lv { background: rgba(243,156,18,.08); color: #b87310; }
|
||||
.card-header .proj-info .proj-meta .badge-summe { background: rgba(39,174,96,.08); color: #1a8a4a; }
|
||||
.card-header .proj-info .proj-meta .status-pill {
|
||||
padding: 2px 12px; border-radius: 20px; font-weight: 500; font-size: .68rem;
|
||||
transition: var(--p-transition);
|
||||
}
|
||||
.card-header .proj-info .proj-meta .status-pill.aktiv {
|
||||
background: linear-gradient(135deg, #e8f8f0, #d0f0e0);
|
||||
color: #1a8a4a; box-shadow: 0 0 0 1px rgba(39,174,96,.15);
|
||||
}
|
||||
.card-header .proj-info .proj-meta .status-pill.archiv {
|
||||
background: #f5f5f5; color: #aaa;
|
||||
}
|
||||
.card-header .proj-tools {
|
||||
display: flex; gap: 4px; flex-shrink: 0; align-items: center;
|
||||
}
|
||||
.card-header .proj-tools .icon-btn {
|
||||
width: 32px; height: 32px; border: none; background: transparent;
|
||||
border-radius: 8px; cursor: pointer; font-size: .85rem;
|
||||
transition: var(--p-transition); display: flex; align-items: center; justify-content: center;
|
||||
color: #bbb;
|
||||
}
|
||||
.card-header .proj-tools .icon-btn:hover { background: rgba(47,84,150,.08); color: var(--p-primary); transform: scale(1.1); }
|
||||
|
||||
/* === Inline Name Edit === */
|
||||
.name-edit-form { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.name-edit-form input {
|
||||
font-size: .85rem; padding: 4px 10px; border-radius: 8px;
|
||||
border: 2px solid var(--p-primary); background: #fff;
|
||||
font-family: inherit; font-weight: 600; width: 200px;
|
||||
box-shadow: 0 0 0 3px var(--p-primary-glow);
|
||||
}
|
||||
.name-edit-form input:focus { outline: none; }
|
||||
.name-edit-form .mini-btn {
|
||||
width: 26px; height: 26px; border-radius: 6px; border: none;
|
||||
cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: .65rem; transition: var(--p-transition);
|
||||
}
|
||||
.name-edit-form .mini-btn.save { background: var(--p-success); color: #fff; }
|
||||
.name-edit-form .mini-btn.save:hover { transform: scale(1.15); }
|
||||
.name-edit-form .mini-btn.cancel { background: #f0f2f8; color: #888; }
|
||||
.name-edit-form .mini-btn.cancel:hover { background: #fee8e8; color: var(--p-danger); }
|
||||
|
||||
/* === Card Body === */
|
||||
.card-body {
|
||||
padding: 0 20px 16px 76px;
|
||||
max-height: 0; overflow: hidden;
|
||||
transition: max-height .45s cubic-bezier(.25,.46,.45,.94), opacity .35s ease, padding .35s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
.card-body.is-open {
|
||||
max-height: 600px; opacity: 1; padding-bottom: 16px;
|
||||
}
|
||||
|
||||
/* === Aufmass Rows === */
|
||||
.aufmass-rows { display: flex; flex-direction: column; gap: 3px; }
|
||||
.aufmass-row {
|
||||
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
|
||||
border-radius: 10px; text-decoration: none; color: var(--p-text);
|
||||
transition: var(--p-transition); cursor: pointer; position: relative;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
.aufmass-row:hover {
|
||||
background: linear-gradient(135deg, rgba(47,84,150,.04), rgba(47,84,150,.02));
|
||||
transform: translateX(4px);
|
||||
}
|
||||
.aufmass-row:active { transform: translateX(2px) scale(.99); }
|
||||
.aufmass-row .row-icon {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--p-primary); opacity: .3; flex-shrink: 0;
|
||||
transition: var(--p-transition);
|
||||
}
|
||||
.aufmass-row:hover .row-icon { opacity: .7; transform: scale(1.3); }
|
||||
.aufmass-row .a-name {
|
||||
font-weight: 500; font-size: .85rem; flex: 1; min-width: 0;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.aufmass-row .a-meta {
|
||||
font-size: .72rem; color: var(--p-text-light);
|
||||
display: flex; align-items: center; gap: 6px; white-space: nowrap;
|
||||
}
|
||||
.aufmass-row .a-meta .typ-tag {
|
||||
background: rgba(240,192,64,.12); color: #b8941a;
|
||||
padding: 1px 8px; border-radius: 10px; font-size: .65rem; font-weight: 500;
|
||||
}
|
||||
.aufmass-row .a-actions {
|
||||
display: flex; gap: 2px; opacity: 0;
|
||||
transition: opacity .25s, transform .25s;
|
||||
transform: translateX(-6px);
|
||||
}
|
||||
.aufmass-row:hover .a-actions { opacity: 1; transform: translateX(0); }
|
||||
.aufmass-row .a-actions .act-btn {
|
||||
width: 28px; height: 28px; border: none; background: transparent;
|
||||
border-radius: 6px; cursor: pointer; font-size: .78rem;
|
||||
transition: var(--p-transition); display: flex; align-items: center; justify-content: center;
|
||||
color: #bbb;
|
||||
}
|
||||
.aufmass-row .a-actions .act-btn:hover { background: rgba(47,84,150,.08); color: var(--p-primary); transform: scale(1.15); }
|
||||
.aufmass-row .a-actions .act-btn.danger:hover { background: #fde8e8; color: var(--p-danger); }
|
||||
|
||||
/* === Toolbar === */
|
||||
.card-toolbar {
|
||||
display: flex; gap: 6px; margin-top: 10px; padding-top: 10px;
|
||||
border-top: 1px solid var(--p-border);
|
||||
}
|
||||
.card-toolbar .tb-btn {
|
||||
font-size: .75rem; padding: 6px 14px; border-radius: 8px;
|
||||
border: 1.5px solid var(--p-border); background: transparent;
|
||||
cursor: pointer; transition: var(--p-transition);
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
font-family: inherit; font-weight: 500; color: var(--p-text-light);
|
||||
}
|
||||
.card-toolbar .tb-btn:hover {
|
||||
background: rgba(47,84,150,.06); border-color: rgba(47,84,150,.2);
|
||||
color: var(--p-primary); transform: translateY(-1px);
|
||||
}
|
||||
.card-toolbar .tb-btn:active { transform: scale(.96); }
|
||||
.card-toolbar .tb-btn.primary {
|
||||
background: linear-gradient(135deg, var(--p-primary), var(--p-primary-light));
|
||||
color: #fff; border-color: transparent;
|
||||
box-shadow: 0 2px 8px var(--p-primary-glow);
|
||||
}
|
||||
.card-toolbar .tb-btn.primary:hover { box-shadow: 0 4px 16px var(--p-primary-glow); }
|
||||
|
||||
/* === Aufmass Neu Form === */
|
||||
.aufmass-neu-form {
|
||||
display: none; margin-top: 16px;
|
||||
animation: formSlide .35s cubic-bezier(.25,.46,.45,.94);
|
||||
}
|
||||
@keyframes formSlide {
|
||||
from { opacity: 0; transform: translateY(-12px) scale(.97); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
.aufmass-form-wrap {
|
||||
background: linear-gradient(135deg, #f8f9fd, #f0f2f8);
|
||||
border-radius: 12px; padding: 20px;
|
||||
border: 1px solid rgba(47,84,150,.08);
|
||||
}
|
||||
.aufmass-form-wrap .aufmass-card {
|
||||
background: #fff; border-radius: 10px; padding: 16px;
|
||||
margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.04);
|
||||
border: 1px solid var(--p-border);
|
||||
}
|
||||
.aufmass-form-wrap .aufmass-card h3 {
|
||||
font-size: .82rem; font-weight: 600; margin-bottom: 10px;
|
||||
color: var(--p-primary-dark); display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.aufmass-form-wrap .aufmass-grid-4 {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px;
|
||||
}
|
||||
.aufmass-form-wrap .aufmass-field label {
|
||||
display: block; font-size: .7rem; font-weight: 500;
|
||||
color: var(--p-text-light); margin-bottom: 3px;
|
||||
}
|
||||
.aufmass-form-wrap .aufmass-field .input,
|
||||
.aufmass-form-wrap .aufmass-field .select select {
|
||||
width: 100%; border-radius: 8px; border: 1.5px solid var(--p-border);
|
||||
padding: 6px 10px; font-size: .8rem; font-family: inherit;
|
||||
transition: var(--p-transition); background: #fff;
|
||||
}
|
||||
.aufmass-form-wrap .aufmass-field .input:focus,
|
||||
.aufmass-form-wrap .aufmass-field .select select:focus {
|
||||
outline: none; border-color: var(--p-primary);
|
||||
box-shadow: 0 0 0 3px var(--p-primary-glow);
|
||||
}
|
||||
.aufmass-form-wrap .aufmass-field-full { grid-column: 1 / -1; }
|
||||
.aufmass-form-wrap .form-footer {
|
||||
display: flex; justify-content: space-between; align-items: center; margin-top: 12px;
|
||||
}
|
||||
|
||||
/* === Toast / Confetti === */
|
||||
.toast-container { position: fixed; bottom: 30px; right: 30px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; }
|
||||
.toast {
|
||||
padding: 14px 22px; border-radius: 12px; color: #fff; font-size: .85rem; font-weight: 500;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.15); animation: toastIn .4s cubic-bezier(.34,1.56,.64,1);
|
||||
display: flex; align-items: center; gap: 8px; backdrop-filter: blur(12px);
|
||||
cursor: pointer; transition: opacity .3s, transform .3s;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
.toast:hover { transform: scale(1.03); }
|
||||
.toast.toast-success { background: linear-gradient(135deg, #27ae60, #2ecc71); }
|
||||
.toast.toast-error { background: linear-gradient(135deg, #e74c3c, #f06050); }
|
||||
.toast.toast-info { background: linear-gradient(135deg, var(--p-primary), var(--p-primary-light)); }
|
||||
@keyframes toastIn {
|
||||
from { opacity: 0; transform: translateX(40px) scale(.9); }
|
||||
to { opacity: 1; transform: translateX(0) scale(1); }
|
||||
}
|
||||
|
||||
/* Confetti canvas */
|
||||
#confetti-canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10000; }
|
||||
|
||||
/* === Empty State === */
|
||||
.empty-state {
|
||||
text-align: center; padding: 80px 20px;
|
||||
background: var(--p-card-bg); backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--p-border); border-radius: var(--p-radius);
|
||||
box-shadow: var(--p-shadow);
|
||||
}
|
||||
.empty-state .empty-icon {
|
||||
font-size: 4rem; margin-bottom: 16px; display: block;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
.empty-state h2 { font-size: 1.3rem; font-weight: 600; color: var(--p-text); margin-bottom: 8px; }
|
||||
.empty-state p { color: var(--p-text-light); font-size: .92rem; max-width: 360px; margin: 0 auto 24px; }
|
||||
|
||||
/* === Responsive === */
|
||||
@media(max-width:768px) {
|
||||
.projekt-hero { padding: 20px 0 16px; }
|
||||
.projekt-hero-left h1 { font-size: 1.3rem; }
|
||||
.card-header { padding: 12px 14px; flex-wrap: wrap; }
|
||||
.card-header .proj-icon { width: 32px; height: 32px; font-size: 1rem; }
|
||||
.card-body { padding: 0 14px 12px 60px; }
|
||||
.card-body.is-open { padding-bottom: 12px; }
|
||||
.card-header .proj-tools { display: flex; }
|
||||
.aufmass-row { padding: 6px 8px; flex-wrap: wrap; }
|
||||
.aufmass-row .a-actions { opacity: 1; }
|
||||
.aufmass-form-wrap { padding: 14px; }
|
||||
.aufmass-form-wrap .aufmass-grid-4 { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
/* === Scrollbar === */
|
||||
.projekt-container::-webkit-scrollbar { width: 6px; }
|
||||
.projekt-container::-webkit-scrollbar-track { background: transparent; }
|
||||
.projekt-container::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
|
||||
|
||||
/* === Ripple effect === */
|
||||
.ripple { position: relative; overflow: hidden; }
|
||||
.ripple::after {
|
||||
content: ''; position: absolute; border-radius: 50%;
|
||||
background: rgba(255,255,255,.4); width: 100px; height: 100px;
|
||||
margin-top: -50px; margin-left: -50px;
|
||||
top: 50%; left: 50%; transform: scale(0);
|
||||
opacity: 0; pointer-events: none;
|
||||
}
|
||||
.ripple:active::after {
|
||||
animation: rippleAnim .6s ease-out;
|
||||
}
|
||||
@keyframes rippleAnim {
|
||||
from { transform: scale(0); opacity: .5; }
|
||||
to { transform: scale(4); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="projekt-page">
|
||||
<!-- Hero -->
|
||||
<div class="projekt-hero">
|
||||
<div class="projekt-hero-left">
|
||||
<h1><span class="icon">📂</span> Projekte & Aufmaße</h1>
|
||||
<div class="hero-sub">
|
||||
<span>{{ projekte|length }} Projekte</span>
|
||||
{% set ns = namespace(aufmass_total=0) %}{% for item in projekte %}{% set ns.aufmass_total = ns.aufmass_total + item.aufmass_liste|length %}{% endfor %}
|
||||
<span>{{ ns.aufmass_total }} Aufmaße</span>
|
||||
{% if preise_sichtbar and gesamt_summe > 0 %}
|
||||
<span class="stat">{{ gesamt_summe|german_number }} €</span>
|
||||
<span class="stat">{{ gesamt_positionen }} Positionen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn-neu-projekt" href="{{ url_for('aufmass.neu') }}">
|
||||
<span>+</span> Neues Projekt
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="search-wrap">
|
||||
<div class="control">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input id="projekt-suche" placeholder="Projektname, Aufmaß, SM-Nr. …">
|
||||
<span class="result-count" id="result-count"></span>
|
||||
</div>
|
||||
<button class="clear-btn" id="search-clear">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- List -->
|
||||
<div class="projekt-container" id="projekt-tree">
|
||||
{% if projekte %}
|
||||
{% for item in projekte %}
|
||||
{% set p = item.project %}
|
||||
<div class="projekt-card" data-project-id="{{ p.id }}" data-project-name="{{ p.bezeichnung or p.sm_nr or '' }}">
|
||||
<div class="card-header js-card-toggle" role="button" tabindex="0">
|
||||
<span class="arrow">▶</span>
|
||||
<span class="proj-icon">📁</span>
|
||||
<div class="proj-info">
|
||||
<div class="proj-name js-name-ctnr">
|
||||
<span class="js-projekt-name">{{ p.bezeichnung or p.sm_nr }}</span>
|
||||
<span class="edit-trigger js-name-edit-trigger" title="Umbenennen">✎</span>
|
||||
<span class="name-edit-form js-name-edit-form" style="display:none">
|
||||
<input type="text" value="{{ p.bezeichnung or p.sm_nr or '' }}">
|
||||
<button class="mini-btn save js-name-save">✓</button>
|
||||
<button class="mini-btn cancel js-name-cancel">✕</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="proj-meta">
|
||||
<span class="badge badge-anz">{{ item.aufmass_liste|length }} Aufmaße</span>
|
||||
<span class="badge badge-pos">{{ item.positionen }} Pos.</span>
|
||||
{% if preise_sichtbar and item.summe > 0 %}
|
||||
<span class="badge badge-summe">{{ item.summe|german_number }} €</span>
|
||||
{% endif %}
|
||||
{% if p.lv_name %}
|
||||
<span class="badge badge-lv" title="LV">{{ p.lv_name }}</span>
|
||||
{% endif %}
|
||||
<span class="status-pill {{ 'aktiv' if p.status == 'aktiv' else 'archiv' }}">{{ p.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.is_firmadmin() or current_user.darf_aufmass_verwalten %}
|
||||
<div class="proj-tools">
|
||||
<button class="icon-btn js-card-settings" title="Einstellungen">⚙</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-body js-card-body">
|
||||
<div class="aufmass-rows">
|
||||
{% for a_item in item.aufmass_liste %}
|
||||
{% set a = a_item.aufmass %}
|
||||
<a class="aufmass-row" href="{{ url_for('aufmass.bearbeiten', project_id=p.id, aufmass_id=a.id) }}" data-aufmass-id="{{ a.id }}" data-project-id="{{ p.id }}" data-aufmass-name="{{ a.name }}">
|
||||
<span class="row-icon"></span>
|
||||
<span class="a-name">{{ a.name }}</span>
|
||||
<span class="a-meta">
|
||||
{% if a.typ %}<span class="typ-tag">{{ a.typ }}</span>{% endif %}
|
||||
{{ a_item.positionen }} Pos.
|
||||
{% if preise_sichtbar and a_item.summe > 0 %}
|
||||
· {{ a_item.summe|german_number }} €
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if current_user.is_firmadmin() or current_user.darf_aufmass_verwalten %}
|
||||
<span class="a-actions">
|
||||
<button class="act-btn js-aufmass-rename" title="Umbenennen">✎</button>
|
||||
<form method="POST" action="{{ url_for('aufmass.aufmass_duplizieren', project_id=p.id, aufmass_id=a.id) }}" style="display:inline" onclick="event.stopPropagation()">
|
||||
<button class="act-btn" title="Duplizieren">📋</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('aufmass.aufmass_loeschen', project_id=p.id, aufmass_id=a.id) }}" style="display:inline" onsubmit="return confirm('Aufmaß wirklich löschen?')" onclick="event.stopPropagation()">
|
||||
<button class="act-btn danger" title="Löschen">✕</button>
|
||||
</form>
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if current_user.is_firmadmin() or current_user.darf_aufmass_verwalten %}
|
||||
<div class="card-toolbar">
|
||||
<button class="tb-btn primary js-aufmass-neu-btn" data-project-id="{{ p.id }}">
|
||||
<span>+</span> Neues Aufmaß
|
||||
</button>
|
||||
<button class="tb-btn" onclick="document.getElementById('import-file-{{ p.id }}').click()">
|
||||
📥 Import
|
||||
</button>
|
||||
<form method="POST" action="{{ url_for('aufmass.aufmass_import', project_id=p.id) }}" enctype="multipart/form-data" style="display:none">
|
||||
<input type="file" name="file" accept=".txt" id="import-file-{{ p.id }}" onchange="this.form.submit()">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="aufmass-neu-form js-aufmass-neu-form">
|
||||
<div class="aufmass-form-wrap">
|
||||
<form method="POST" action="{{ url_for('aufmass.aufmass_neu_voll', project_id=p.id) }}" class="aufmass-form">
|
||||
<input type="hidden" name="ev_details_id" value="{{ p.ev_details_id or '' }}">
|
||||
<input type="hidden" name="name" id="aufmass-name-{{ p.id }}">
|
||||
<div class="aufmass-card"><h3>Basisdaten</h3>
|
||||
<div class="aufmass-grid-4">
|
||||
<div class="aufmass-field"><label>Vertrag</label><div class="select is-small" style="width:100%"><select name="contract_id"><option value="">– Kein Vertrag –</option>{% for c in contracts %}<option value="{{ c.id }}" {{ 'selected' if p.contract_id == c.id }}>{{ c.name }}</option>{% endfor %}</select></div></div>
|
||||
<div class="aufmass-field"><label>LV-Name</label><input class="input" name="lv_name" value="{{ p.lv_name or '' }}"></div>
|
||||
<div class="aufmass-field"><label>Typ</label><div class="select is-small" style="width:100%"><select name="typ"><option value="">– Typ wählen –</option>{% for t in typen %}<option value="{{ t.name }}">{{ t.name }}</option>{% endfor %}</select></div></div>
|
||||
<div class="aufmass-field"><label>Aufmaß-Datum</label><input class="input" name="datum" type="date" value="{{ p.datum or '' }}"></div>
|
||||
</div>
|
||||
<div class="aufmass-field aufmass-field-full" style="margin-top:8px"><label>Bezeichnung / Baustelle</label><input class="input js-aufmass-auto-name js-validate-name" name="bezeichnung" value=""><span class="js-name-warn is-size-7 has-text-danger" style="display:none">Ungültige Zeichen</span></div>
|
||||
<div class="aufmass-field aufmass-field-full"><label>Bauabschnitt</label><input class="input js-aufmass-auto-name js-validate-name" name="bauabschnitt" value="{{ p.bauabschnitt or '' }}"></div>
|
||||
</div>
|
||||
<div class="aufmass-card"><h3>🕐 Zeitraum & Referenz</h3>
|
||||
<div class="aufmass-grid-4">
|
||||
<div class="aufmass-field"><label>SM-Nr.</label><input class="input js-aufmass-auto-name" name="sm_nr" value="{{ p.sm_nr or '' }}"></div>
|
||||
<div class="aufmass-field"><label>Abruf-Nr.</label><input class="input js-aufmass-auto-name" name="abruf_nr" value="{{ p.abruf_nr or '' }}"></div>
|
||||
<div class="aufmass-field"><label>Startdatum</label><input class="input" name="datum_start" type="date" value="{{ p.datum_start or '' }}"></div>
|
||||
<div class="aufmass-field"><label>Enddatum</label><input class="input" name="datum_ende" type="date" value="{{ p.datum_ende or '' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aufmass-card"><h3>👤 Ansprechpartner</h3>
|
||||
<div class="aufmass-grid-4">
|
||||
<div class="aufmass-field"><label>Vorname</label><input class="input" name="ansprechpartner_vorname" value="{{ p.ansprechpartner_vorname or '' }}"></div>
|
||||
<div class="aufmass-field"><label>Nachname</label><input class="input" name="ansprechpartner_nachname" value="{{ p.ansprechpartner_nachname or '' }}"></div>
|
||||
<div class="aufmass-field"><label>Telefon</label><input class="input" name="ansprechpartner_tel" value="{{ p.ansprechpartner_tel or '' }}"></div>
|
||||
<div class="aufmass-field"><label>Email</label><input class="input" name="ansprechpartner_email" value="{{ p.ansprechpartner_email or '' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button class="tb-btn" type="button" onclick="this.closest('.aufmass-neu-form').style.display='none'">Abbrechen</button>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="tb-btn js-aufmass-neu-reset" type="button">🗑️ Zurücksetzen</button>
|
||||
<button class="tb-btn primary" type="submit">Aufmaß anlegen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<span class="empty-icon">📂</span>
|
||||
<h2>Noch keine Projekte</h2>
|
||||
<p>Erstelle dein erstes Projekt und beginne mit der Aufmaß-Erfassung.</p>
|
||||
<a class="btn-neu-projekt" href="{{ url_for('aufmass.neu') }}">+ Projekt anlegen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<!-- Confetti Canvas -->
|
||||
<canvas id="confetti-canvas"></canvas>
|
||||
|
||||
<script>
|
||||
/* === Confetti System === */
|
||||
(function(){
|
||||
var c=document.getElementById('confetti-canvas'),ctx=c.getContext('2d');
|
||||
var W,H,particles=[],frame;
|
||||
function resize(){W=c.width=window.innerWidth;H=c.height=window.innerHeight;}
|
||||
window.addEventListener('resize',resize);resize();
|
||||
var colors=['#2F5496','#f0c040','#27ae60','#e74c3c','#8e44ad','#3498db','#e67e22','#1abc9c'];
|
||||
function launch(count){
|
||||
for(var i=0;i<count;i++){
|
||||
particles.push({
|
||||
x:W/2+(Math.random()-.5)*W*.6,y:-30,
|
||||
r:Math.random()*6+3,color:colors[Math.floor(Math.random()*colors.length)],
|
||||
vx:(Math.random()-.5)*3,vy:Math.random()*6+4,
|
||||
rot:Math.random()*360,rotV:(Math.random()-.5)*8,
|
||||
gravity:.12,friction:.98,alpha:1,shape:Math.floor(Math.random()*3),
|
||||
life:0,maxLife:120+Math.random()*80
|
||||
});
|
||||
}
|
||||
if(!frame)requestAnimationFrame(animate);
|
||||
}
|
||||
function animate(){
|
||||
ctx.clearRect(0,0,W,H);var keep=false;
|
||||
for(var i=particles.length-1;i>=0;i--){
|
||||
var p=particles[i];p.vy+=p.gravity;p.x+=p.vx;p.y+=p.vy;
|
||||
p.vx*=p.friction;p.vy*=p.friction;p.rot+=p.rotV;p.life++;
|
||||
if(p.life>p.maxLife)p.alpha-=.02;
|
||||
if(p.y>H+30||p.x<-30||p.x>W+30||p.alpha<=0){particles.splice(i,1);continue;}
|
||||
keep=true;
|
||||
ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.rot*Math.PI/180);ctx.globalAlpha=Math.max(0,p.alpha);
|
||||
ctx.fillStyle=p.color;
|
||||
if(p.shape===0){ctx.fillRect(-p.r,-p.r/2,p.r*2,p.r);}
|
||||
else if(p.shape===1){ctx.beginPath();ctx.arc(0,0,p.r,0,Math.PI*2);ctx.fill();}
|
||||
else{ctx.beginPath();ctx.moveTo(0,-p.r);ctx.lineTo(p.r,p.r);ctx.lineTo(-p.r,p.r);ctx.closePath();ctx.fill();}
|
||||
ctx.restore();
|
||||
}
|
||||
if(keep){frame=requestAnimationFrame(animate);}
|
||||
else{frame=null;}
|
||||
}
|
||||
window.launchConfetti=function(count){launch(count||80);};
|
||||
})();
|
||||
|
||||
/* === Toast System === */
|
||||
function showToast(msg,type){
|
||||
type=type||'success';
|
||||
var c=document.getElementById('toast-container');
|
||||
var t=document.createElement('div');t.className='toast toast-'+type;
|
||||
var icons={success:'✓',error:'✕',info:'ℹ'};
|
||||
t.innerHTML=(icons[type]||'')+' '+msg;
|
||||
c.appendChild(t);
|
||||
setTimeout(function(){t.style.opacity='0';t.style.transform='translateX(40px) scale(.9)';setTimeout(function(){if(t.parentNode)t.remove();},400);},3000);
|
||||
t.addEventListener('click',function(){t.style.opacity='0';t.style.transform='translateX(40px) scale(.9)';setTimeout(function(){if(t.parentNode)t.remove();},400);});
|
||||
}
|
||||
|
||||
/* === Settings Gear → Project Detail === */
|
||||
document.querySelectorAll('.js-card-settings').forEach(function(btn){
|
||||
btn.addEventListener('click',function(e){
|
||||
e.stopPropagation();e.preventDefault();
|
||||
var card=this.closest('.projekt-card');
|
||||
if(card)window.location.href='/projekt/'+card.dataset.projectId;
|
||||
});
|
||||
});
|
||||
|
||||
/* === Card Toggle === */
|
||||
document.querySelectorAll('.js-card-toggle').forEach(function(header){
|
||||
header.addEventListener('click',function(e){
|
||||
if(e.target.closest('.name-edit-form')||e.target.closest('.js-name-edit-trigger'))return;
|
||||
var card=header.closest('.projekt-card');
|
||||
var body=card.querySelector('.js-card-body');
|
||||
var isOpen=body.classList.contains('is-open');
|
||||
body.classList.toggle('is-open');header.classList.toggle('is-open');
|
||||
var pid=card.dataset.projectId;
|
||||
localStorage.setItem('tree_open_'+pid,isOpen?'0':'1');
|
||||
});
|
||||
});
|
||||
|
||||
/* Restore open state */
|
||||
document.querySelectorAll('.projekt-card').forEach(function(card){
|
||||
var pid=card.dataset.projectId;
|
||||
if(localStorage.getItem('tree_open_'+pid)==='1'){
|
||||
card.querySelector('.js-card-body').classList.add('is-open');
|
||||
card.querySelector('.js-card-toggle').classList.add('is-open');
|
||||
}
|
||||
});
|
||||
|
||||
/* === Filter === */
|
||||
function filterProjects(){
|
||||
var q=document.getElementById('projekt-suche').value.toLowerCase();
|
||||
var count=0,visible=0;
|
||||
document.querySelectorAll('.projekt-card').forEach(function(c){
|
||||
var n=(c.dataset.projectName||'').toLowerCase();
|
||||
var aufmassNames=Array.from(c.querySelectorAll('.aufmass-row')).map(function(r){return(r.dataset.aufmassName||'').toLowerCase();}).join(' ');
|
||||
var match=!q||n.indexOf(q)!==-1||aufmassNames.indexOf(q)!==-1;
|
||||
c.classList.toggle('js-project-hide',!!q&&!match);
|
||||
visible+=match?1:0;count++;
|
||||
});
|
||||
var rc=document.getElementById('result-count');
|
||||
if(q){rc.textContent=visible+'/'+count;rc.classList.add('visible');}
|
||||
else{rc.classList.remove('visible');}
|
||||
}
|
||||
document.getElementById('projekt-suche').addEventListener('input',filterProjects);
|
||||
document.getElementById('search-clear').addEventListener('click',function(){
|
||||
document.getElementById('projekt-suche').value='';filterProjects();
|
||||
document.getElementById('projekt-suche').focus();
|
||||
});
|
||||
|
||||
/* === Auto Aufmass Name === */
|
||||
function updateAufmassName(pid){
|
||||
var nameInput=document.getElementById('aufmass-name-'+pid);
|
||||
if(!nameInput)return;
|
||||
var f=nameInput.closest('form');
|
||||
var parts=[f.querySelector('[name="bezeichnung"]').value.trim(),f.querySelector('[name="bauabschnitt"]').value.trim(),f.querySelector('[name="sm_nr"]').value.trim(),f.querySelector('[name="abruf_nr"]').value.trim()].filter(Boolean);
|
||||
nameInput.value=parts.join(' - ').replace(/[<>:"\/\\|?*&#%{}~\[\]]/g,'').replace(/\s+/g,' ').trim();
|
||||
}
|
||||
document.querySelectorAll('.js-aufmass-auto-name').forEach(function(inp){
|
||||
inp.addEventListener('input',function(){var f=inp.closest('form');var ni=f.querySelector('[name="name"]');if(ni)updateAufmassName(ni.id.replace('aufmass-name-',''));});
|
||||
});
|
||||
|
||||
/* === Aufmass-Neu Toggle + Reset === */
|
||||
document.querySelectorAll('.js-aufmass-neu-btn').forEach(function(btn){
|
||||
btn.addEventListener('click',function(e){
|
||||
e.preventDefault();
|
||||
var f=this.closest('.card-body').querySelector('.aufmass-neu-form');
|
||||
if(f.style.display==='none'||!f.style.display){f.style.display='block';}
|
||||
else{f.style.display='none';}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.aufmass-neu-form [type="button"]').forEach(function(b){
|
||||
var txt=b.textContent.trim();
|
||||
if(txt.includes('Abbrechen')){
|
||||
b.addEventListener('click',function(e){
|
||||
e.preventDefault();this.closest('.aufmass-neu-form').style.display='none';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.js-aufmass-neu-reset').forEach(function(b){
|
||||
b.addEventListener('click',function(e){
|
||||
e.preventDefault();
|
||||
var rf=this.closest('form');
|
||||
if(rf){rf.querySelectorAll('input[name]:not([type=hidden])').forEach(function(i){i.value=''});var pid=rf.closest('.projekt-card').dataset.projectId;updateAufmassName(pid);}
|
||||
});
|
||||
});
|
||||
|
||||
/* === Aufmass Inline Rename === */
|
||||
document.querySelectorAll('.js-aufmass-rename').forEach(function(btn){
|
||||
btn.addEventListener('click',function(e){
|
||||
e.stopPropagation();e.preventDefault();
|
||||
var row=this.closest('.aufmass-row');
|
||||
var ns=row.querySelector('.a-name'),link=row.querySelector('a');
|
||||
if(!ns||!link)return;
|
||||
var aid=row.dataset.aufmassId,pid=row.dataset.projectId,old=ns.textContent.trim();
|
||||
link.style.display='none';
|
||||
var startInput=document.createElement('input');
|
||||
startInput.className='input is-small';startInput.value=old;
|
||||
startInput.style.cssText='flex:1;min-width:60px;font-size:.82rem;padding:4px 10px;border-radius:8px;border:2px solid #2F5496;background:#fff;font-family:inherit;';
|
||||
row.insertBefore(startInput,this.closest('.a-actions'));
|
||||
function done(){if(startInput.parentNode)startInput.remove();link.style.display=''}
|
||||
function save(){
|
||||
var v=startInput.value.trim();if(!v){done();return}
|
||||
ns.textContent=v;done();
|
||||
fetch('/projekt/'+pid+'/aufmass/'+aid+'/umbenennen',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:v})})
|
||||
.then(function(r){if(!r.ok)throw new Error('Fehler');showToast('Aufmaß umbenannt','success');launchConfetti(30);})
|
||||
.catch(function(){ns.textContent=old;showToast('Fehler beim Umbenennen','error');});
|
||||
}
|
||||
startInput.addEventListener('blur',save);
|
||||
startInput.addEventListener('keydown',function(ev){
|
||||
if(ev.key==='Enter'){ev.preventDefault();save()}
|
||||
if(ev.key==='Escape'){ev.preventDefault();done()}
|
||||
});
|
||||
startInput.focus();
|
||||
});
|
||||
});
|
||||
|
||||
/* === Project Name Inline Edit === */
|
||||
document.querySelectorAll('.js-name-edit-trigger').forEach(function(trigger){
|
||||
trigger.addEventListener('click',function(e){
|
||||
e.stopPropagation();
|
||||
var c=this.closest('.js-name-ctnr');
|
||||
c.querySelector('.js-projekt-name').style.display='none';
|
||||
this.style.display='none';
|
||||
var ef=c.querySelector('.js-name-edit-form');
|
||||
ef.style.display='inline-flex';
|
||||
ef.querySelector('input').focus();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.js-name-save').forEach(function(btn){
|
||||
btn.addEventListener('click',function(e){
|
||||
e.stopPropagation();
|
||||
var c=this.closest('.js-name-ctnr'),i=c.querySelector('input'),n=i.value,d=c.closest('.projekt-card');
|
||||
if(!d)return;
|
||||
fetch('/projekt/'+d.dataset.projectId+'/update-name',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'name='+encodeURIComponent(n)})
|
||||
.then(function(r){if(!r.ok)return r.json().then(function(e){throw new Error(e.error)});return r.json()})
|
||||
.then(function(dt){
|
||||
c.querySelector('.js-projekt-name').textContent=dt.name;
|
||||
c.querySelector('.js-projekt-name').style.display='';
|
||||
btn.style.display='';
|
||||
c.querySelector('.js-name-edit-form').style.display='none';
|
||||
c.querySelector('.js-name-edit-trigger').style.display='';
|
||||
d.dataset.projectName=dt.name;
|
||||
showToast('Projekt umbenannt','success');launchConfetti(40);
|
||||
})
|
||||
.catch(function(e){alert('Fehler: '+e.message)});
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.js-name-cancel').forEach(function(btn){
|
||||
btn.addEventListener('click',function(e){
|
||||
e.stopPropagation();
|
||||
var c=this.closest('.js-name-ctnr');
|
||||
c.querySelector('.js-projekt-name').style.display='';
|
||||
c.querySelector('.js-name-edit-trigger').style.display='';
|
||||
this.closest('.js-name-edit-form').style.display='none';
|
||||
});
|
||||
});
|
||||
|
||||
/* === Flash messages as toasts === */
|
||||
(function(){
|
||||
var notices=document.querySelectorAll('.notification');
|
||||
notices.forEach(function(n){
|
||||
var msg=n.textContent.trim();var cat='info';
|
||||
if(n.classList.contains('is-success'))cat='success';
|
||||
else if(n.classList.contains('is-danger'))cat='error';
|
||||
showToast(msg,cat);
|
||||
n.style.display='none';
|
||||
});
|
||||
})();
|
||||
|
||||
/* === Keyboard shortcut: focus search === */
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='/'&&!e.ctrlKey&&!e.metaKey&&!e.target.closest('input,textarea,select')){
|
||||
e.preventDefault();
|
||||
document.getElementById('projekt-suche').focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,508 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
:root {
|
||||
--lp-primary: #2F5496;
|
||||
--lp-primary-light: #4a7bc4;
|
||||
--lp-primary-dark: #1a3055;
|
||||
--lp-primary-glow: rgba(47,84,150,.25);
|
||||
--lp-success: #27ae60;
|
||||
--lp-bg: #f0f2f8;
|
||||
--lp-card-bg: rgba(255,255,255,.85);
|
||||
--lp-border: rgba(0,0,0,.06);
|
||||
--lp-text: #1a1a2e;
|
||||
--lp-text-light: #6b7280;
|
||||
--lp-radius: 14px;
|
||||
--lp-shadow: 0 1px 3px rgba(0,0,0,.04), 0 4px 16px rgba(0,0,0,.04);
|
||||
--lp-shadow-hover: 0 4px 12px rgba(47,84,150,.12), 0 8px 32px rgba(0,0,0,.06);
|
||||
--lp-transition: all .35s cubic-bezier(.25,.46,.45,.94);
|
||||
}
|
||||
body { background: var(--lp-bg); font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||
.lp-page { max-width: 1000px; margin: 0 auto; padding: 0 16px; }
|
||||
|
||||
/* Hero */
|
||||
.lp-hero {
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:32px 0 24px;flex-wrap:wrap;gap:16px;
|
||||
}
|
||||
.lp-hero-left{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||||
.lp-hero-left .proj-icon {
|
||||
width:44px;height:44px;border-radius:12px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:1.3rem;flex-shrink:0;
|
||||
background:linear-gradient(135deg,#eef2fa,#e0e7f5);
|
||||
}
|
||||
.lp-hero-left h1 {
|
||||
font-size:1.45rem;font-weight:700;letter-spacing:-.02em;margin:0;
|
||||
background:linear-gradient(135deg,var(--lp-primary-dark),var(--lp-primary-light));
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
|
||||
display:flex;align-items:center;gap:8px;
|
||||
}
|
||||
.lp-hero-left .hero-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.lp-hero-left .status-pill{
|
||||
padding:3px 14px;border-radius:20px;font-size:.72rem;font-weight:600;
|
||||
}
|
||||
.lp-hero-left .status-pill.aktiv{
|
||||
background:linear-gradient(135deg,#e8f8f0,#d0f0e0);color:#1a8a4a;
|
||||
box-shadow:0 0 0 1px rgba(39,174,96,.15);
|
||||
}
|
||||
.lp-hero-left .status-pill.archiv{background:#f0f2f8;color:#999}
|
||||
.lp-hero-left .status-pill.storniert{background:#fde8e8;color:#c0392b}
|
||||
.lp-hero-right{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.lp-btn{
|
||||
padding:8px 16px;border-radius:10px;border:1.5px solid var(--lp-border);
|
||||
background:var(--lp-card-bg);backdrop-filter:blur(8px);
|
||||
font-size:.78rem;font-weight:500;cursor:pointer;text-decoration:none;
|
||||
transition:var(--lp-transition);color:var(--lp-text);
|
||||
display:inline-flex;align-items:center;gap:5px;font-family:inherit;
|
||||
box-shadow:var(--lp-shadow);white-space:nowrap;
|
||||
}
|
||||
.lp-btn:hover{transform:translateY(-2px);box-shadow:var(--lp-shadow-hover);border-color:rgba(47,84,150,.2)}
|
||||
.lp-btn:active{transform:scale(.97)}
|
||||
.lp-btn.primary{
|
||||
background:linear-gradient(135deg,var(--lp-primary),var(--lp-primary-light));
|
||||
color:#fff;border-color:transparent;box-shadow:0 4px 14px var(--lp-primary-glow);
|
||||
}
|
||||
.lp-btn.primary:hover{box-shadow:0 8px 24px var(--lp-primary-glow);}
|
||||
.lp-btn.danger:hover{border-color:#e74c3c;color:#e74c3c;background:#fef2f2}
|
||||
|
||||
/* Main Card */
|
||||
.lp-main-card{
|
||||
background:var(--lp-card-bg);backdrop-filter:blur(12px);
|
||||
border:1px solid var(--lp-border);border-radius:var(--lp-radius);
|
||||
box-shadow:var(--lp-shadow);overflow:hidden;
|
||||
animation:cardIn .45s cubic-bezier(.25,.46,.45,.94) both;
|
||||
}
|
||||
@keyframes cardIn{from{opacity:0;transform:translateY(20px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}
|
||||
|
||||
.lp-card-header{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:16px 20px;border-bottom:1px solid var(--lp-border);flex-wrap:wrap;gap:8px;
|
||||
}
|
||||
.lp-card-header h2{font-size:1rem;font-weight:600;margin:0;color:var(--lp-text);display:flex;align-items:center;gap:6px}
|
||||
.lp-card-body{padding:14px 20px 20px}
|
||||
|
||||
/* Aufmass Cards */
|
||||
.lp-aufmass-grid{display:flex;flex-direction:column;gap:6px}
|
||||
.lp-aufmass-card{
|
||||
display:flex;align-items:center;gap:10px;
|
||||
padding:10px 14px;border-radius:10px;text-decoration:none;color:var(--lp-text);
|
||||
transition:var(--lp-transition);cursor:pointer;position:relative;
|
||||
background:rgba(255,255,255,.5);border:1px solid var(--lp-border);
|
||||
}
|
||||
.lp-aufmass-card:hover{
|
||||
background:#fff;border-color:rgba(47,84,150,.12);
|
||||
box-shadow:0 2px 8px rgba(0,0,0,.04);transform:translateX(4px);
|
||||
}
|
||||
.lp-aufmass-card:active{transform:translateX(2px) scale(.99)}
|
||||
.lp-aufmass-card .row-dot{
|
||||
width:8px;height:8px;border-radius:50%;flex-shrink:0;
|
||||
background:var(--lp-primary);opacity:.25;transition:var(--lp-transition);
|
||||
}
|
||||
.lp-aufmass-card:hover .row-dot{opacity:.6;transform:scale(1.3)}
|
||||
.lp-aufmass-card .a-name{font-weight:500;font-size:.85rem;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.lp-aufmass-card .a-meta{font-size:.72rem;color:var(--lp-text-light);display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0}
|
||||
.lp-aufmass-card .a-meta .tag{
|
||||
font-size:.65rem;padding:2px 10px;border-radius:10px;font-weight:500;
|
||||
}
|
||||
.lp-aufmass-card .a-meta .tag-aktiv{background:rgba(39,174,96,.1);color:#1a8a4a}
|
||||
.lp-aufmass-card .a-meta .tag-abgeschlossen{background:rgba(47,84,150,.08);color:var(--lp-primary)}
|
||||
.lp-aufmass-card .a-meta .tag-storniert{background:#fde8e8;color:#c0392b}
|
||||
.lp-aufmass-card .a-actions{
|
||||
display:flex;gap:2px;opacity:0;transition:opacity .25s,transform .25s;transform:translateX(-6px);
|
||||
}
|
||||
.lp-aufmass-card:hover .a-actions{opacity:1;transform:translateX(0)}
|
||||
.lp-aufmass-card .a-actions .act-btn{
|
||||
width:28px;height:28px;border:none;background:transparent;
|
||||
border-radius:6px;cursor:pointer;font-size:.78rem;
|
||||
transition:var(--lp-transition);display:flex;align-items:center;justify-content:center;color:#bbb;
|
||||
}
|
||||
.lp-aufmass-card .a-actions .act-btn:hover{background:rgba(47,84,150,.08);color:var(--lp-primary);transform:scale(1.15)}
|
||||
.lp-aufmass-card .a-actions .act-btn.danger:hover{background:#fde8e8;color:#e74c3c}
|
||||
@media(max-width:768px){
|
||||
.lp-aufmass-card .a-actions{opacity:1}
|
||||
.lp-aufmass-card{flex-wrap:wrap}
|
||||
}
|
||||
|
||||
/* Empty */
|
||||
.lp-empty{
|
||||
text-align:center;padding:50px 20px;
|
||||
}
|
||||
.lp-empty .empty-icon{font-size:3rem;margin-bottom:10px;display:block;animation:float 3s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
|
||||
.lp-empty p{color:var(--lp-text-light);font-size:.9rem;margin-bottom:16px}
|
||||
|
||||
/* Settings Section */
|
||||
.lp-settings{
|
||||
margin-top:12px;background:var(--lp-card-bg);backdrop-filter:blur(12px);
|
||||
border:1px solid var(--lp-border);border-radius:var(--lp-radius);
|
||||
box-shadow:var(--lp-shadow);overflow:hidden;animation:cardIn .45s both;animation-delay:.1s;
|
||||
}
|
||||
.lp-settings .lp-card-body{padding:20px}
|
||||
.lp-settings-grid{display:flex;flex-direction:column;gap:14px}
|
||||
.lp-setting-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.lp-setting-row label{font-size:.8rem;font-weight:500;color:var(--lp-text-light);min-width:100px}
|
||||
.lp-setting-row select{
|
||||
padding:6px 10px;border-radius:8px;border:1.5px solid var(--lp-border);
|
||||
font-size:.78rem;background:#fff;font-family:inherit;min-width:180px;
|
||||
transition:var(--lp-transition);
|
||||
}
|
||||
.lp-setting-row select:focus{outline:none;border-color:var(--lp-primary);box-shadow:0 0 0 3px var(--lp-primary-glow)}
|
||||
.lp-setting-row .lp-btn{padding:6px 14px;font-size:.75rem}
|
||||
|
||||
/* Form Styles */
|
||||
.lp-neu-form{display:none;margin-top:12px;animation:formSlide .35s cubic-bezier(.25,.46,.45,.94)}
|
||||
@keyframes formSlide{from{opacity:0;transform:translateY(-12px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}
|
||||
.lp-form-wrap{
|
||||
background:linear-gradient(135deg,#f8f9fd,#f0f2f8);
|
||||
border-radius:12px;padding:20px;border:1px solid rgba(47,84,150,.08);
|
||||
}
|
||||
.lp-form-wrap .form-card{
|
||||
background:#fff;border-radius:10px;padding:16px;margin-bottom:12px;
|
||||
box-shadow:0 1px 4px rgba(0,0,0,.04);border:1px solid var(--lp-border);
|
||||
}
|
||||
.lp-form-wrap .form-card h3{font-size:.82rem;font-weight:600;margin-bottom:10px;color:var(--lp-primary-dark);display:flex;align-items:center;gap:6px}
|
||||
.lp-form-wrap .form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px}
|
||||
.lp-form-wrap .form-field label{display:block;font-size:.7rem;font-weight:500;color:var(--lp-text-light);margin-bottom:3px}
|
||||
.lp-form-wrap .form-field .input,.lp-form-wrap .form-field select{
|
||||
width:100%;border-radius:8px;border:1.5px solid var(--lp-border);
|
||||
padding:6px 10px;font-size:.8rem;font-family:inherit;
|
||||
transition:var(--lp-transition);background:#fff;
|
||||
}
|
||||
.lp-form-wrap .form-field .input:focus,.lp-form-wrap .form-field select:focus{
|
||||
outline:none;border-color:var(--lp-primary);box-shadow:0 0 0 3px var(--lp-primary-glow);
|
||||
}
|
||||
.lp-form-wrap .form-field-full{grid-column:1/-1}
|
||||
.lp-form-wrap .form-footer{display:flex;justify-content:space-between;align-items:center;margin-top:12px}
|
||||
|
||||
/* Toast reuse */
|
||||
.toast-container{position:fixed;bottom:30px;right:30px;z-index:9999;display:flex;flex-direction:column;gap:8px}
|
||||
.toast{padding:14px 22px;border-radius:12px;color:#fff;font-size:.85rem;font-weight:500;box-shadow:0 8px 32px rgba(0,0,0,.15);animation:toastIn .4s cubic-bezier(.34,1.56,.64,1);display:flex;align-items:center;gap:8px;backdrop-filter:blur(12px);cursor:pointer;transition:opacity .3s,transform .3s;font-family:'Inter',sans-serif}
|
||||
.toast:hover{transform:scale(1.03)}
|
||||
.toast.toast-success{background:linear-gradient(135deg,#27ae60,#2ecc71)}
|
||||
.toast.toast-error{background:linear-gradient(135deg,#e74c3c,#f06050)}
|
||||
.toast.toast-info{background:linear-gradient(135deg,var(--lp-primary),var(--lp-primary-light))}
|
||||
@keyframes toastIn{from{opacity:0;transform:translateX(40px) scale(.9)}to{opacity:1;transform:translateX(0) scale(1)}}
|
||||
|
||||
/* Inline edit */
|
||||
.inline-edit-input{
|
||||
border:2px solid var(--lp-primary);border-radius:8px;padding:4px 10px;
|
||||
font-size:.82rem;font-family:inherit;background:#fff;
|
||||
box-shadow:0 0 0 3px var(--lp-primary-glow);width:100%;
|
||||
}
|
||||
.inline-edit-input:focus{outline:none}
|
||||
</style>
|
||||
|
||||
<div class="lp-page">
|
||||
<!-- Hero -->
|
||||
<div class="lp-hero">
|
||||
<div class="lp-hero-left">
|
||||
<span class="proj-icon">📁</span>
|
||||
<div>
|
||||
<h1 class="js-name-ctnr">
|
||||
<span class="js-projekt-name">{{ project.bezeichnung or project.sm_nr }}</span>
|
||||
<span class="js-name-edit-trigger" style="font-size:.65rem;cursor:pointer;opacity:0;transition:opacity .2s;padding:2px 6px;border-radius:4px;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0'" onclick="event.stopPropagation();projectNameEdit(this)">✎</span>
|
||||
<span class="js-name-edit-form" style="display:none;font-size:.85rem">
|
||||
<input type="text" value="{{ project.bezeichnung or project.sm_nr or '' }}" style="border:2px solid var(--lp-primary);border-radius:8px;padding:4px 10px;font-size:.82rem;font-family:inherit;width:220px;box-shadow:0 0 0 3px var(--lp-primary-glow)">
|
||||
<button class="mini-btn" style="width:26px;height:26px;border-radius:6px;border:none;background:#27ae60;color:#fff;cursor:pointer;font-size:.65rem;transition:all .2s" onclick="projectNameSave(this)">✓</button>
|
||||
<button class="mini-btn" style="width:26px;height:26px;border-radius:6px;border:none;background:#f0f2f8;color:#888;cursor:pointer;font-size:.65rem;transition:all .2s" onclick="projectNameCancel(this)">✕</button>
|
||||
</span>
|
||||
</h1>
|
||||
<div class="hero-meta">
|
||||
<span class="status-pill {{ project.status }}">{{ project.status }}</span>
|
||||
<span style="font-size:.75rem;color:var(--lp-text-light)">{{ project.sm_nr or '–' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lp-hero-right">
|
||||
<a class="lp-btn" href="{{ url_for('export.excel', project_id=project.id) }}">📊 Excel</a>
|
||||
<a class="lp-btn" href="{{ url_for('export.pdf', project_id=project.id) }}">📄 PDF</a>
|
||||
{% if current_user.is_firmadmin() or current_user.is_superadmin() %}
|
||||
<a class="lp-btn" href="{{ url_for('aufmass.typen_liste') }}">🏷 Typen</a>
|
||||
{% endif %}
|
||||
<a class="lp-btn" href="{{ url_for('aufmass.index') }}">← Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Card -->
|
||||
<div class="lp-main-card">
|
||||
<div class="lp-card-header">
|
||||
<h2>📋 Aufmaße</h2>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
<button class="lp-btn primary js-aufmass-neu-btn">+ Neues Aufmaß</button>
|
||||
<button class="lp-btn" onclick="document.getElementById('import-file').click()">📥 Import</button>
|
||||
<form method="POST" action="{{ url_for('aufmass.aufmass_import', project_id=project.id) }}" enctype="multipart/form-data" style="display:none">
|
||||
<input type="file" name="file" accept=".txt" id="import-file" onchange="this.form.submit()">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lp-card-body">
|
||||
<!-- Neu Form -->
|
||||
<div class="lp-neu-form js-aufmass-neu-form">
|
||||
<div class="lp-form-wrap">
|
||||
<form method="POST" action="{{ url_for('aufmass.aufmass_neu_voll', project_id=project.id) }}">
|
||||
<input type="hidden" name="ev_details_id" value="{{ project.ev_details_id or '' }}">
|
||||
<input type="hidden" name="name" id="aufmass-name-{{ project.id }}">
|
||||
<div class="form-card"><h3>Basisdaten</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-field"><label>Vertrag</label><select name="contract_id"><option value="">– Kein Vertrag –</option>{% for c in contracts %}<option value="{{ c.id }}" {{ 'selected' if project.contract_id == c.id }}>{{ c.name }}</option>{% endfor %}</select></div>
|
||||
<div class="form-field"><label>LV-Name</label><input class="input" name="lv_name" value="{{ project.lv_name or '' }}"></div>
|
||||
<div class="form-field"><label>Typ</label><select name="typ"><option value="">– Typ wählen –</option>{% for t in typen %}<option value="{{ t.name }}">{{ t.name }}</option>{% endfor %}</select></div>
|
||||
<div class="form-field"><label>Aufmaß-Datum</label><input class="input" name="datum" type="date" value="{{ project.datum or '' }}"></div>
|
||||
</div>
|
||||
<div class="form-field form-field-full" style="margin-top:8px"><label>Projekt</label><input class="input js-aufmass-auto-name js-validate-name" name="bezeichnung" value=""><span class="js-name-warn" style="display:none;font-size:.7rem;color:#e74c3c">Ungültige Zeichen</span></div>
|
||||
<div class="form-field form-field-full"><label>Baustelle</label><input class="input" name="baustelle" value="{{ project.baustelle or '' }}"></div>
|
||||
<div class="form-field form-field-full"><label>Bauabschnitt</label><input class="input js-aufmass-auto-name js-validate-name" name="bauabschnitt" value="{{ project.bauabschnitt or '' }}"></div>
|
||||
</div>
|
||||
<div class="form-card"><h3>🕐 Zeitraum & Referenz</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-field"><label>SM-Nr.</label><input class="input js-aufmass-auto-name" name="sm_nr" value="{{ project.sm_nr or '' }}"></div>
|
||||
<div class="form-field"><label>Abruf-Nr.</label><input class="input js-aufmass-auto-name" name="abruf_nr" value="{{ project.abruf_nr or '' }}"></div>
|
||||
<div class="form-field"><label>Startdatum</label><input class="input" name="datum_start" type="date" value="{{ project.datum_start or '' }}"></div>
|
||||
<div class="form-field"><label>Enddatum</label><input class="input" name="datum_ende" type="date" value="{{ project.datum_ende or '' }}"></div>
|
||||
</div>
|
||||
{% if current_user.darf_evergabe_nutzen and current_user.darf_kopfdaten_holen and company.evergabe_aktiviert and company.evergabe_benutzer and company.evergabe_passwort %}
|
||||
<button class="lp-btn" type="button" style="margin-top:8px" onclick="kopfdatenHolen({{ project.id }})">⬇️ Kopfdaten EV holen</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-card"><h3>👤 Ansprechpartner</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-field"><label>Vorname</label><input class="input" name="ansprechpartner_vorname" value="{{ project.ansprechpartner_vorname or '' }}"></div>
|
||||
<div class="form-field"><label>Nachname</label><input class="input" name="ansprechpartner_nachname" value="{{ project.ansprechpartner_nachname or '' }}"></div>
|
||||
<div class="form-field"><label>Telefon</label><input class="input" name="ansprechpartner_tel" value="{{ project.ansprechpartner_tel or '' }}"></div>
|
||||
<div class="form-field"><label>Email</label><input class="input" name="ansprechpartner_email" value="{{ project.ansprechpartner_email or '' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button class="lp-btn" type="button" onclick="this.closest('.lp-neu-form').style.display='none'">Abbrechen</button>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="lp-btn js-aufmass-neu-reset" type="button">🗑️ Zurücksetzen</button>
|
||||
<button class="lp-btn primary" type="submit">Aufmaß anlegen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if aufmass_liste %}
|
||||
<div class="lp-aufmass-grid">
|
||||
{% for a in aufmass_liste %}
|
||||
<div class="lp-aufmass-card" data-id="{{ a.id }}" data-name="{{ a.name }}" data-typ="{{ a.typ }}" data-status="{{ a.status }}">
|
||||
<span class="row-dot"></span>
|
||||
<span class="a-name" data-field="name">{{ a.name }}</span>
|
||||
<span class="a-meta">
|
||||
{% if a.typ %}<span class="tag" style="background:rgba(240,192,64,.12);color:#b8941a">{{ a.typ }}</span>{% endif %}
|
||||
<span class="tag tag-{{ a.status }}">{{ a.status }}</span>
|
||||
<span>{{ a.positionen.count() }} Pos.</span>
|
||||
{% if preise_sichtbar %}<span>{{ aufmass_preise.get(a.id, 0)|german_number }} €</span>{% endif %}
|
||||
</span>
|
||||
<span class="a-actions">
|
||||
<button class="act-btn js-edit-btn" title="Bearbeiten">✎</button>
|
||||
<a class="act-btn" href="{{ url_for('aufmass.bearbeiten', project_id=project.id, aufmass_id=a.id) }}" title="Öffnen">↗</a>
|
||||
<form method="POST" action="{{ url_for('aufmass.aufmass_duplizieren', project_id=project.id, aufmass_id=a.id) }}" style="display:inline" onclick="event.stopPropagation()">
|
||||
<button class="act-btn" title="Duplizieren">📋</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('aufmass.aufmass_loeschen', project_id=project.id, aufmass_id=a.id) }}" style="display:inline" onsubmit="return confirm('Aufmaß wirklich löschen?')" onclick="event.stopPropagation()">
|
||||
<button class="act-btn danger" title="Löschen">✕</button>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="lp-empty">
|
||||
<span class="empty-icon">📋</span>
|
||||
<p>Noch keine Aufmaße vorhanden.</p>
|
||||
<button class="lp-btn primary js-aufmass-neu-btn">+ Erstes Aufmaß anlegen</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="lp-settings">
|
||||
<div class="lp-card-header"><h2>⚙ Projekt-Einstellungen</h2></div>
|
||||
<div class="lp-card-body">
|
||||
<div class="lp-settings-grid">
|
||||
<div class="lp-setting-row">
|
||||
<label>Vertrag & LV</label>
|
||||
<form method="POST" action="{{ url_for('aufmass.project_lv_set', project_id=project.id) }}" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||
<select name="contract_id" id="settings-contract-select" onchange="loadSettingsLV(this.value)">
|
||||
<option value="">– Kein Vertrag –</option>
|
||||
{% for c in contracts %}
|
||||
<option value="{{ c.id }}" {{ 'selected' if project.contract_id == c.id }}>{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="lv_name">
|
||||
<option value="">– LV wählen –</option>
|
||||
{% for n in lv_names %}
|
||||
<option value="{{ n }}" {{ 'selected' if project.lv_name == n }}>{{ n }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="lp-btn primary" style="padding:6px 14px;font-size:.75rem">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="lp-setting-row">
|
||||
<label>Status</label>
|
||||
<form method="POST" action="{{ url_for('aufmass.status_aendern', project_id=project.id) }}" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||
<select name="status">
|
||||
<option value="aktiv" {{ 'selected' if project.status == 'aktiv' }}>Aktiv</option>
|
||||
<option value="abgeschlossen" {{ 'selected' if project.status == 'abgeschlossen' }}>Abgeschlossen</option>
|
||||
<option value="storniert" {{ 'selected' if project.status == 'storniert' }}>Storniert</option>
|
||||
</select>
|
||||
<button class="lp-btn" style="padding:6px 14px;font-size:.75rem">Status ändern</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="lp-setting-row" style="justify-content:flex-end">
|
||||
<form method="POST" action="{{ url_for('aufmass.project_loeschen', project_id=project.id) }}" onsubmit="return confirm('WIRKLICH das ganze Projekt löschen?')">
|
||||
<button class="lp-btn danger" style="padding:6px 14px;font-size:.75rem">Projekt löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<script>
|
||||
/* Toast */
|
||||
function showToast(msg,type){
|
||||
type=type||'success';
|
||||
var c=document.getElementById('toast-container');
|
||||
var t=document.createElement('div');t.className='toast toast-'+type;
|
||||
var icons={success:'✓',error:'✕',info:'ℹ'};
|
||||
t.innerHTML=(icons[type]||'')+' '+msg;
|
||||
c.appendChild(t);
|
||||
setTimeout(function(){t.style.opacity='0';t.style.transform='translateX(40px) scale(.9)';setTimeout(function(){if(t.parentNode)t.remove()},400)},3000);
|
||||
t.addEventListener('click',function(){t.style.opacity='0';t.style.transform='translateX(40px) scale(.9)';setTimeout(function(){if(t.parentNode)t.remove()},400)});
|
||||
}
|
||||
|
||||
/* Flash toasts */
|
||||
(function(){
|
||||
document.querySelectorAll('.notification').forEach(function(n){
|
||||
var msg=n.textContent.trim();var cat='info';
|
||||
if(n.classList.contains('is-success'))cat='success';
|
||||
else if(n.classList.contains('is-danger'))cat='error';
|
||||
showToast(msg,cat);n.style.display='none';
|
||||
});
|
||||
})();
|
||||
|
||||
/* Toggle neu form */
|
||||
document.querySelectorAll('.js-aufmass-neu-btn').forEach(function(btn){
|
||||
btn.addEventListener('click',function(e){
|
||||
e.preventDefault();
|
||||
var f=document.querySelector('.js-aufmass-neu-form');
|
||||
if(f)f.style.display=f.style.display==='none'?'block':'none';
|
||||
});
|
||||
});
|
||||
|
||||
/* Auto name */
|
||||
function updateAufmassName(pid){
|
||||
var nameInput=document.getElementById('aufmass-name-'+pid);
|
||||
if(!nameInput)return;
|
||||
var f=nameInput.closest('form');
|
||||
var parts=[f.querySelector('[name="bezeichnung"]').value.trim(),f.querySelector('[name="bauabschnitt"]').value.trim(),f.querySelector('[name="sm_nr"]').value.trim(),f.querySelector('[name="abruf_nr"]').value.trim()].filter(Boolean);
|
||||
nameInput.value=parts.join(' - ').replace(/[<>:"\/\\|?*&#%{}~\[\]]/g,'').replace(/\s+/g,' ').trim();
|
||||
}
|
||||
document.querySelectorAll('.js-aufmass-auto-name').forEach(function(inp){
|
||||
inp.addEventListener('input',function(){var f=inp.closest('form');var ni=f.querySelector('[name="name"]');if(ni)updateAufmassName(ni.id.replace('aufmass-name-',''));});
|
||||
});
|
||||
document.querySelectorAll('.js-validate-name').forEach(function(inp){
|
||||
inp.addEventListener('input',function(){
|
||||
var warn=inp.closest('.lp-form-wrap').querySelector('.js-name-warn');
|
||||
if(warn)warn.style.display=/[<>:"\/\\|?*&#%{}~\[\]]/.test(inp.value)?'block':'none';
|
||||
});
|
||||
});
|
||||
|
||||
/* Reset */
|
||||
document.querySelector('.js-aufmass-neu-reset')?.addEventListener('click',function(e){
|
||||
e.preventDefault();
|
||||
var rf=this.closest('form');
|
||||
if(rf){rf.querySelectorAll('input[name]:not([type=hidden])').forEach(function(i){i.value=''});updateAufmassName({{ project.id }});}
|
||||
});
|
||||
|
||||
/* Inline edit aufmass */
|
||||
document.querySelector('.lp-aufmass-grid')?.addEventListener('click',function(e){
|
||||
var btn=e.target.closest('.js-edit-btn');
|
||||
if(!btn)return;
|
||||
e.stopPropagation();
|
||||
var card=btn.closest('.lp-aufmass-card');
|
||||
var nameEl=card.querySelector('.a-name');
|
||||
var old=nameEl.textContent.trim();
|
||||
var inp=document.createElement('input');
|
||||
inp.className='inline-edit-input';inp.value=old;
|
||||
nameEl.style.display='none';
|
||||
nameEl.parentNode.insertBefore(inp,nameEl);
|
||||
inp.focus();
|
||||
inp.addEventListener('blur',save);
|
||||
inp.addEventListener('keydown',function(ev){
|
||||
if(ev.key==='Enter'){ev.preventDefault();save()}
|
||||
if(ev.key==='Escape'){ev.preventDefault();inp.remove();nameEl.style.display=''}
|
||||
});
|
||||
function save(){
|
||||
var v=inp.value.trim();if(!v){inp.remove();nameEl.style.display='';return}
|
||||
nameEl.textContent=v;inp.remove();nameEl.style.display='';
|
||||
fetch('/projekt/'+card.closest('[data-project-id]')?.dataset.projectId||{{ project.id }}+'/aufmass/'+card.dataset.id+'/umbenennen',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:v})})
|
||||
.then(function(r){if(!r.ok)throw new Error('Fehler');showToast('Aufmaß umbenannt','success')})
|
||||
.catch(function(){nameEl.textContent=old;showToast('Fehler beim Umbenennen','error')});
|
||||
}
|
||||
});
|
||||
|
||||
/* Projekt name inline edit */
|
||||
function projectNameEdit(btn){
|
||||
var c=btn.closest('.js-name-ctnr');
|
||||
c.querySelector('.js-projekt-name').style.display='none';
|
||||
btn.style.display='none';
|
||||
c.querySelector('.js-name-edit-form').style.display='inline';
|
||||
c.querySelector('.js-name-edit-form input').focus();
|
||||
}
|
||||
function projectNameSave(btn){
|
||||
var c=btn.closest('.js-name-ctnr'),i=c.querySelector('input'),n=i.value;
|
||||
fetch('/projekt/{{ project.id }}/update-name',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'name='+encodeURIComponent(n)})
|
||||
.then(function(r){if(!r.ok)return r.json().then(function(e){throw new Error(e.error)});return r.json()})
|
||||
.then(function(dt){c.querySelector('.js-projekt-name').textContent=dt.name;c.querySelector('.js-projekt-name').style.display='';btn.style.display='';c.querySelector('.js-name-edit-form').style.display='none';showToast('Projekt umbenannt','success')})
|
||||
.catch(function(e){alert('Fehler: '+e.message)});
|
||||
}
|
||||
function projectNameCancel(btn){
|
||||
var c=btn.closest('.js-name-ctnr');
|
||||
c.querySelector('.js-projekt-name').style.display='';
|
||||
btn.style.display='';
|
||||
c.querySelector('.js-name-edit-form').style.display='none';
|
||||
}
|
||||
|
||||
/* Load LV names */
|
||||
function loadSettingsLV(contractId){
|
||||
var url=contractId?'/contracts/api/lv-names?contract_id='+contractId:'/contracts/api/lv-names';
|
||||
fetch(url).then(function(r){return r.json()}).then(function(names){
|
||||
var sel=document.querySelector('[name="lv_name"]');
|
||||
var current=sel.value;
|
||||
sel.innerHTML='<option value="">– LV wählen –</option>';
|
||||
names.forEach(function(n){sel.innerHTML+='<option value="'+n.replace(/"/g,'"')+'">'+n+'</option>'});
|
||||
sel.value=current;
|
||||
});
|
||||
}
|
||||
|
||||
/* Kopfdaten holen */
|
||||
function kopfdatenHolen(pid){
|
||||
var form=document.querySelector('.lp-neu-form form');
|
||||
if(!form)return;
|
||||
var smNr=form.querySelector('[name="sm_nr"]').value.trim();
|
||||
if(!smNr){alert('Bitte SM-Nr eingeben.');return}
|
||||
var btn=form.querySelector('[onclick*="kopfdatenHolen"]');if(btn){btn.disabled=true;btn.textContent='Laden...'}
|
||||
fetch('/projekt/'+pid+'/kopfdaten-ev-holen',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sm_nr:smNr})})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(data){
|
||||
if(btn){btn.disabled=false;btn.textContent='⬇️ Kopfdaten EV holen'}
|
||||
if(data.error){alert(data.error);return}
|
||||
['bauabschnitt','sm_nr','abruf_nr','datum_start','datum_ende','datum','ev_details_id','ansprechpartner_vorname','ansprechpartner_nachname','ansprechpartner_tel','ansprechpartner_email'].forEach(function(f){
|
||||
var inp=form.querySelector('[name="'+f+'"]');
|
||||
if(inp&&data[f])inp.value=data[f];
|
||||
});
|
||||
updateAufmassName(pid);
|
||||
}).catch(function(err){if(btn){btn.disabled=false;btn.textContent='⬇️ Kopfdaten EV holen'}alert('Fehler: '+err)});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">Neues Aufmaß-Projekt</h1></div>
|
||||
<div class="level-right"><a class="button is-light" href="{{ url_for('aufmass.index') }}">← Zurück</a></div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
<form method="POST" class="box">
|
||||
<div class="field">
|
||||
<label class="label">Projektname / Bezeichnung *</label>
|
||||
<div class="control"><input class="input" name="bezeichnung" required></div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Vertrag</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select name="contract_id" id="contract-select" onchange="loadLVNames()">
|
||||
<option value="">– Kein Vertrag –</option>
|
||||
{% for c in contracts %}
|
||||
<option value="{{ c.id }}">{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">LV-Name</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select name="lv_name" id="lv-select">
|
||||
<option value="">– LV wählen –</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-4">
|
||||
<button class="button is-primary" type="submit">Projekt anlegen</button>
|
||||
<a class="button is-light" href="{{ url_for('aufmass.index') }}">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function loadLVNames() {
|
||||
const cid = document.getElementById('contract-select').value;
|
||||
const url = cid ? '/contracts/api/lv-names?contract_id=' + cid : '/contracts/api/lv-names';
|
||||
fetch(url)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(names) {
|
||||
const sel = document.getElementById('lv-select');
|
||||
sel.innerHTML = '<option value="">– LV wählen –</option>';
|
||||
names.forEach(function(n) {
|
||||
sel.innerHTML += '<option value="' + n.replace(/&/g,'&').replace(/"/g,'"') + '">' + n + '</option>';
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">Aufmaß-Typen</h1></div>
|
||||
<div class="level-right">
|
||||
<a class="button is-small is-light" href="javascript:history.back()">← Zurück</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Name</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
{% for t in typen %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="has-text-weight-bold">{{ t.name }}</span>
|
||||
{% if t.company_id %}<span class="tag is-light is-small">eigen</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="button is-small is-info is-outlined" onclick="editTyp({{ t.id }}, '{{ t.name }}')">Bearbeiten</button>
|
||||
<form method="POST" action="{{ url_for('aufmass.typ_loeschen', typ_id=t.id) }}"
|
||||
style="display:inline" onsubmit="return confirm('Typ wirklich löschen?')">
|
||||
<button class="button is-small is-danger is-outlined">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<details class="mt-3">
|
||||
<summary class="has-text-link">+ Neuen Typ anlegen</summary>
|
||||
<form method="POST" action="{{ url_for('aufmass.typ_neu') }}" class="mt-2 field has-addons">
|
||||
<div class="control">
|
||||
<input class="input" name="name" placeholder="Typ-Name" required>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary">Anlegen</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<dialog id="edit-typ-dialog" class="box" style="border:none;border-radius:8px;padding:2rem;max-width:400px">
|
||||
<form method="POST" id="edit-typ-form">
|
||||
<h3 class="title is-5">Typ bearbeiten</h3>
|
||||
<div class="field">
|
||||
<label class="label">Name</label>
|
||||
<input class="input" name="name" id="edit-typ-name" required>
|
||||
</div>
|
||||
<div class="field is-grouped mt-3">
|
||||
<button class="button is-primary" type="submit">Speichern</button>
|
||||
<button class="button is-light" type="button" onclick="document.getElementById('edit-typ-dialog').close()">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
function editTyp(id, name){
|
||||
document.getElementById('edit-typ-name').value = name;
|
||||
document.getElementById('edit-typ-form').action = '/projekt/typen/' + id + '/edit';
|
||||
document.getElementById('edit-typ-dialog').showModal();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="columns is-centered mt-6">
|
||||
<div class="column is-one-third">
|
||||
<div class="box">
|
||||
<h1 class="title is-4 has-text-centered">AufmaßWeb – Anmeldung</h1>
|
||||
<form method="POST">
|
||||
<div class="field">
|
||||
<label class="label">E-Mail</label>
|
||||
<div class="control">
|
||||
<input class="input" type="email" name="email" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Passwort</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-4">
|
||||
<button class="button is-primary is-fullwidth" type="submit">Anmelden</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if registration_enabled %}
|
||||
<p class="has-text-centered mt-3">
|
||||
<a href="{{ url_for('auth.register') }}">Neue Firma registrieren</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="columns is-centered mt-6">
|
||||
<div class="column is-half">
|
||||
<div class="box">
|
||||
<h1 class="title is-4 has-text-centered">Neue Firma registrieren</h1>
|
||||
<form method="POST">
|
||||
<div class="field">
|
||||
<label class="label">Firmenname *</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="firmenname" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Vorname</label>
|
||||
<div class="control"><input class="input" type="text" name="vorname"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Nachname</label>
|
||||
<div class="control"><input class="input" type="text" name="nachname"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">E-Mail *</label>
|
||||
<div class="control">
|
||||
<input class="input" type="email" name="email" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Passwort *</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="password" required minlength="6">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-4">
|
||||
<button class="button is-primary is-fullwidth" type="submit">Registrieren</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="has-text-centered mt-3">
|
||||
<a href="{{ url_for('auth.login') }}">Bereits registriert? Anmelden</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% if titel %}{{ titel }} – {% endif %}AufmaßWeb</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modul.css') }}">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
|
||||
<style>
|
||||
:root { --bulma-font-size: {{ session.get('font_size', '1') }}rem; }
|
||||
</style>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% if current_user.is_authenticated %}
|
||||
<nav class="navbar is-dark" role="navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item has-text-weight-bold" href="{{ url_for('admin.dashboard') }}">AufmaßWeb</a>
|
||||
<a role="button" class="navbar-burger" data-target="navMenu">
|
||||
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="navMenu" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
{% if current_user.is_superadmin() %}
|
||||
<a class="navbar-item" href="{{ url_for('superadmin.dashboard') }}">🌐 Superadmin</a>
|
||||
<a class="navbar-item" href="{{ url_for('superadmin.dashboard') }}">🏢 Firmenverwaltung</a>
|
||||
<a class="navbar-item" href="{{ url_for('superadmin.dashboard') }}">🔑 Lizenzverwaltung</a>
|
||||
{% endif %}
|
||||
<a class="navbar-item" href="{{ url_for('admin.dashboard') }}">Dashboard</a>
|
||||
<a class="navbar-item" href="{{ url_for('aufmass.index') }}">Projekte / Aufmaße</a>
|
||||
</div>
|
||||
{% set initials = (current_user.vorname[0] if current_user.vorname else '') + (current_user.nachname[0] if current_user.nachname else '') %}
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" style="cursor:pointer;align-items:center;gap:6px">
|
||||
{% if current_user.profile_image %}
|
||||
<img src="{{ url_for('static', filename='avatars/'+current_user.profile_image) }}" alt="" style="width:28px;height:28px;border-radius:50%;object-fit:cover">
|
||||
{% else %}
|
||||
<span style="display:inline-block;width:28px;height:28px;border-radius:50%;background:#2F5496;color:#fff;font-size:12px;font-weight:bold;line-height:28px;text-align:center">{{ initials or '?' }}</span>
|
||||
{% endif %}
|
||||
<span>{{ current_user.full_name }}</span>
|
||||
<span class="tag is-info is-light">{{ current_user.rolle }}</span>
|
||||
</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="{{ url_for('admin.profil') }}">👤 Profil</a>
|
||||
<a class="navbar-item" href="{{ url_for('admin.profil') }}">⚙ Einstellungen</a>
|
||||
<hr class="navbar-divider">
|
||||
{% if current_user.is_superadmin() %}
|
||||
<a class="navbar-item" href="{{ url_for('superadmin.dashboard') }}">🏢 Firmenverwaltung</a>
|
||||
<a class="navbar-item" href="{{ url_for('superadmin.dashboard') }}">🔑 Lizenzverwaltung</a>
|
||||
{% endif %}
|
||||
{% if current_user.is_firmadmin() %}
|
||||
<a class="navbar-item" href="{{ url_for('admin.firma') }}">🏢 Firmenverwaltung</a>
|
||||
{% endif %}
|
||||
<a class="navbar-item" href="{{ url_for('contracts.index') }}">📋 Verträge</a>
|
||||
<a class="navbar-item" href="{{ url_for('custom_modules.index') }}">🔧 Modul-Builder</a>
|
||||
<a class="navbar-item" href="{{ url_for('lv.index') }}">📂 LV-Verwaltung</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item" href="{{ url_for('auth.logout') }}" style="color:#e74c3c">🚪 Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<section class="section">
|
||||
<div class="container is-fluid">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="notification is-{{ category }} is-light py-3">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.navbar-burger').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const target = document.getElementById(el.dataset.target);
|
||||
el.classList.toggle('is-active');
|
||||
target.classList.toggle('is-active');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='absperrung') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-8-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>Absperrung</h4></div>
|
||||
<label class="gl-label">Abschnitt</label><input class="gl-input" name="abschnitt">
|
||||
<div class="gl-grid-2" style="margin-top:8px">
|
||||
<div><label class="gl-label">Absperrung (Meter)</label><input class="gl-input" name="meter" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Meter pro Stück</label><input class="gl-input" name="m_pro_st" type="number" step="0.1" value="10"></div>
|
||||
<div><label class="gl-label">Anzahl Tage</label><input class="gl-input" name="anz_tage" type="number" step="1"></div>
|
||||
<div><label class="gl-label">Abzug Tage lt. Pos.</label><input class="gl-input" name="abzug_tage" type="number" step="1" value="5"></div>
|
||||
<div><label class="gl-label">E-Preis</label><input class="gl-input gl-input-sm" name="epreis" type="number" step="0.01"></div>
|
||||
<div><label class="gl-label">M pro Tag</label><input class="gl-input gl-input-sm" name="m_pro_tag" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Gesamtpreis</label><input class="gl-input gl-input-sm" name="gesamtpreis" type="number" step="0.01"></div>
|
||||
</div>
|
||||
<label class="gl-label" style="margin-top:8px">Bemerkung</label><input class="gl-input" name="bemerkung">
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="gl-card-actions" style="display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap;padding:16px 20px;background:rgba(255,255,255,.7);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(0,0,0,.06);border-radius:14px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 4px 16px rgba(0,0,0,.04);transition:all .35s cubic-bezier(.25,.46,.45,.94)">
|
||||
<button class="gl-btn gl-btn-primary gl-btn-pulse" type="submit">? hinzufügen</button>
|
||||
<button class="gl-btn gl-btn-secondary" type="submit" formaction="{{ url_for('modules.berechnen', module_name='absperrung') }}/aktualisieren">?? aktualisieren</button>
|
||||
<button class="gl-btn gl-btn-secondary" type="button" onclick="this.closest('form').querySelectorAll('input:not([type=hidden]),textarea,select').forEach(function(el){if(el.type==='checkbox'||el.type==='radio'){el.checked=false}else{el.value=''}})">?? zurücksetzen</button>
|
||||
<button class="gl-btn gl-btn-secondary" type="button" onclick="var m=document.getElementById('modul-modal');if(m)m.classList.remove('is-active')">✕ schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="column is-12">
|
||||
<div class="gl-card-actions" style="display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap;padding:16px 20px;background:rgba(255,255,255,.7);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(0,0,0,.06);border-radius:14px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 4px 16px rgba(0,0,0,.04);transition:all .35s cubic-bezier(.25,.46,.45,.94)">
|
||||
<button class="gl-btn gl-btn-primary gl-btn-pulse" type="submit">➕ hinzufügen</button>
|
||||
<button class="gl-btn gl-btn-secondary" type="button" onclick="this.closest('form').querySelectorAll('input:not([type=hidden]),textarea,select').forEach(function(el){if(el.type==='checkbox'||el.type==='radio'){el.checked=false}else{el.value=''}});try{localStorage.removeItem('modul_form_state')}catch(e){}">🔄 zurücksetzen</button>
|
||||
<button class="gl-btn gl-btn-secondary" type="button" onclick="var m=document.getElementById('modul-modal');if(m){saveModulFormState();m.classList.remove('is-active')}">✕ schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='cu') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>CU-Montage</h4></div>
|
||||
<label class="gl-label">Abschnitt</label><input class="gl-input" name="abschnitt">
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Montage</div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="muffe_bis10"><span class="toggle-track"></span><span class="toggle-label"> Muffe bis 10 DA montieren</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="apl_bis10"><span class="toggle-track"></span><span class="toggle-label"> APL bis 10 DA montieren</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="anschl_trenn"><span class="toggle-track"></span><span class="toggle-label"> Anschluss-/Trennleisten einbauen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="muffe_schrumpf"><span class="toggle-track"></span><span class="toggle-label"> Schrumpfmuffe herstellen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="muffe_klemm"><span class="toggle-track"></span><span class="toggle-label"> Klemmmuffe herstellen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="muffe_gel"><span class="toggle-track"></span><span class="toggle-label"> Gel-Muffen herstellen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="muffe_klemm_dlr"><span class="toggle-track"></span><span class="toggle-label"> Klemmmuffen für DLR</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#edf7ed,#e6f3e6);border-color:rgba(39,174,96,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(39,174,96,.1)">??</span><h4>Verbindungen & EV</h4></div>
|
||||
<label class="gl-label">Anzahl CU-Verbinden</label><input class="gl-input gl-input-sm" name="anz_cu_verb" type="number" step="1">
|
||||
<div class="gl-grid-auto" style="margin-top:8px">
|
||||
<label class="gl-toggle"><input type="checkbox" name="cu_da_gr"><span class="toggle-track"></span><span class="toggle-label"> CU-DA > 0,8 mm verbinden</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="cu_da_kl"><span class="toggle-track"></span><span class="toggle-label"> CU-DA = 0,8 mm verbinden</span></label>
|
||||
</div>
|
||||
<div class="gl-subcard" style="margin-top:10px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">EVs</div>
|
||||
<label class="gl-label">Anzahl Stück</label><input class="gl-input gl-input-sm" name="anz_stk" type="number" step="1">
|
||||
<label class="gl-toggle"><input type="checkbox" name="evs_einbauen"><span class="toggle-track"></span><span class="toggle-label"> EVs einbauen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kabel_anlegen_ev"><span class="toggle-track"></span><span class="toggle-label"> Kabel anlegen EVs/TrLe</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>Stopfstellen</h4></div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="stopfstelle"><span class="toggle-track"></span><span class="toggle-label"> Druckluftstutzen Stopfstelle einbauen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="zulage_stopf"><span class="toggle-track"></span><span class="toggle-label"> Zulage Stopfstelle DLR</span></label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,35 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='doku') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-6-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>Dokumentation</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">MP-Einarbeitung von Gelände/Gebäude</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">Länge (m)</label><input class="gl-input gl-input-sm" name="doku_laenge" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Breite (m)</label><input class="gl-input gl-input-sm" name="doku_breite" type="number" step="0.1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Dokumentation von Cu/GF-Trassen</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">HK-Trasse (m)</label><input class="gl-input gl-input-sm" name="doku_hktr_m" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">VzK-Trasse (m)</label><input class="gl-input gl-input-sm" name="doku_vzktr_m" type="number" step="0.1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">GF-Hausanschluss</div>
|
||||
<label class="gl-label">Anzahl</label><input class="gl-input gl-input-sm" name="doku_gf_haus_anz" type="number" step="1">
|
||||
</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="doku_geh"><span class="toggle-track"></span><span class="toggle-label"> Dokumentation von Gehäusen</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-6-desktop">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,59 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='ftth') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>Tiefbau</h4></div>
|
||||
<label class="gl-label">Abschnitt</label><input class="gl-input" name="abschnitt">
|
||||
<div class="gl-grid-2" style="margin-top:8px">
|
||||
<div><label class="gl-label">Länge Privat (m)</label><input class="gl-input" name="laenge_privat" type="number" step="0.01"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="oO"><span class="toggle-track"></span><span class="toggle-label"> o.O</span></label></div>
|
||||
</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ring_gg"><span class="toggle-track"></span><span class="toggle-label"> Ring GG</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#edf7ed,#e6f3e6);border-color:rgba(39,174,96,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(39,174,96,.1)">??</span><h4>GF-Montage</h4></div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ad_gf_montage"><span class="toggle-track"></span><span class="toggle-label"> GF-Montage hinzufügen</span></label>
|
||||
<div style="margin-top:8px">
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">NVT</label><input class="gl-input" name="nvt_name"></div>
|
||||
<div><label class="gl-label">AP-Nr.</label><input class="gl-input" name="ap_nr"></div>
|
||||
<div><label class="gl-label">Straße</label><input class="gl-input" name="strasse"></div>
|
||||
<div><label class="gl-label">Hs-Nr.</label><input class="gl-input gl-input-sm" name="hs_nr"></div>
|
||||
</div>
|
||||
<div class="gl-grid-2" style="margin-top:8px">
|
||||
<div><label class="gl-label">Anzahl Mess.</label><input class="gl-input gl-input-sm" name="anz_we" type="number" step="1"></div>
|
||||
<div><label class="gl-label">GF-Kabel</label><select class="gl-select" name="gf_kabel"><option value="4E">4E</option><option value="12E">12E</option></select></div>
|
||||
<div><label class="gl-label">Einblas Länge (m)</label><input class="gl-input gl-input-sm" name="einblas_laenge" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Install. Rohr (m)</label><input class="gl-input gl-input-sm" name="inst_rohr" type="number" step="0.1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-grid-auto" style="margin-top:8px">
|
||||
<label class="gl-toggle"><input type="checkbox" name="gfap_mon"><span class="toggle-track"></span><span class="toggle-label"> GF-AP montieren</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="b_k_abl"><span class="toggle-track"></span><span class="toggle-label"> Bayern kein Fasern Ablegen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="einbl_geraet"><span class="toggle-track"></span><span class="toggle-label"> Einblas Gerät vorhalten</span></label>
|
||||
</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="koordination"><span class="toggle-track"></span><span class="toggle-label"> Koordinationspauschale</span></label>
|
||||
<label class="gl-label" style="margin-top:8px">Ausgabe</label><input class="gl-input" name="ausgabe">
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>Messung & Aktionen</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">GPON Messung</div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="gpon_mess_konv"><span class="toggle-track"></span><span class="toggle-label"> GPON Mess konventionell</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="gpon_mess_ein"><span class="toggle-track"></span><span class="toggle-label"> GPON Mess vereinfacht</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,79 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='gf_montage') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>GF-Montage</h4></div>
|
||||
<label class="gl-label">NVT Bezeichnung</label><input class="gl-input" name="nvt_bez">
|
||||
<label class="gl-label" style="margin-top:6px">Muffen Bezeichnung</label><input class="gl-input" name="muffen_bez">
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Anzahl Fasern</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">A1</label><input class="gl-input" name="faser_a1" type="number" step="1"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="a1_neu"><span class="toggle-track"></span><span class="toggle-label"> neu</span></label></div>
|
||||
<div><label class="gl-label">G1</label><input class="gl-input" name="faser_g1" type="number" step="1"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="g1_neu"><span class="toggle-track"></span><span class="toggle-label"> neu</span></label></div>
|
||||
<div><label class="gl-label">G2</label><input class="gl-input" name="faser_g2" type="number" step="1"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="g2_neu"><span class="toggle-track"></span><span class="toggle-label"> neu</span></label></div>
|
||||
<div><label class="gl-label">G3</label><input class="gl-input" name="faser_g3" type="number" step="1"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="g3_neu"><span class="toggle-track"></span><span class="toggle-label"> neu</span></label></div>
|
||||
<div><label class="gl-label">G4</label><input class="gl-input" name="faser_g4" type="number" step="1"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="g4_neu"><span class="toggle-track"></span><span class="toggle-label"> neu</span></label></div>
|
||||
<div><label class="gl-label">G5</label><input class="gl-input" name="faser_g5" type="number" step="1"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="g5_neu"><span class="toggle-track"></span><span class="toggle-label"> neu</span></label></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#edf7ed,#e6f3e6);border-color:rgba(39,174,96,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(39,174,96,.1)">??</span><h4>Messung & Verbindungen</h4></div>
|
||||
<label class="gl-label">Anzahl Fasern verbinden</label><input class="gl-input gl-input-sm" name="anz_fasern_verb" type="number" step="1">
|
||||
<label class="gl-label" style="margin-top:8px">Anzahl Pigtails Ein. IN</label><input class="gl-input gl-input-sm" name="anz_pig_ein" type="number" step="1">
|
||||
<label class="gl-label" style="margin-top:8px">Anzahl Pigtails Ein. OUT</label><input class="gl-input gl-input-sm" name="anz_pig_eout" type="number" step="1">
|
||||
<label class="gl-label" style="margin-top:8px">Anzahl Gruppen-Pigtails</label><input class="gl-input gl-input-sm" name="anz_g_pig" type="number" step="1">
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Messung</div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="mess_masse"><span class="toggle-track"></span><span class="toggle-label"> Masse</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="mess_individual"><span class="toggle-track"></span><span class="toggle-label"> Individual</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="mess_gpon"><span class="toggle-track"></span><span class="toggle-label"> GPON FTTH</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="mess_kontrol"><span class="toggle-track"></span><span class="toggle-label"> Kontrol (FTTH Bayern)</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="mess_pegel"><span class="toggle-track"></span><span class="toggle-label"> Pegel (Bayern)</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<label class="gl-label" style="margin-top:8px">Anzahl Fasern messen</label><input class="gl-input gl-input-sm" name="anz_fasern_mess" type="number" step="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>Baugruppen & Arbeit</h4></div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="indoor"><span class="toggle-track"></span><span class="toggle-label"> Indoor</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="outdoor"><span class="toggle-track"></span><span class="toggle-label"> Outdoor</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="muffe_im_ksch"><span class="toggle-track"></span><span class="toggle-label"> Muffe im Ksch</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="muffe_neu"><span class="toggle-track"></span><span class="toggle-label"> Muffe neu</span></label>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<label class="gl-toggle"><input type="checkbox" name="spleiss_bau_ein"><span class="toggle-track"></span><span class="toggle-label"> Spleiß-Baugruppe einbauen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="patch_bau_ein"><span class="toggle-track"></span><span class="toggle-label"> Patch-Baugruppe einbauen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="spatch_bau_ein"><span class="toggle-track"></span><span class="toggle-label"> Spleiß/Patch-Baugr. einbauen</span></label>
|
||||
</div>
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Kabelarbeiten</div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="gf_innen"><span class="toggle-track"></span><span class="toggle-label"> GF-Innenkabel befestigen/einziehen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kanal_snr"><span class="toggle-track"></span><span class="toggle-label"> Install. Kanäle/SNR befestigen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="aussen_kabel"><span class="toggle-track"></span><span class="toggle-label"> Außenkabel befestigen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="gfap"><span class="toggle-track"></span><span class="toggle-label"> GF-AP befestigen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="gfap_mfg"><span class="toggle-track"></span><span class="toggle-label"> GF-AP in MFG</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,109 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='graben') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg" style="font-size:.85rem">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
|
||||
<!-- Graben -->
|
||||
<div class="column is-12-tablet is-3-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>Graben</h4></div>
|
||||
<label class="gl-label">Abschnitt</label>
|
||||
<input class="gl-input" name="abschnitt" placeholder="z.B. Abschnitt A">
|
||||
<div class="gl-grid-3" style="margin-top:8px">
|
||||
<div><label class="gl-label">Länge (m)</label><input class="gl-input gl-input-sm" name="laenge" type="number" step="0.01"></div>
|
||||
<div><label class="gl-label">Breite (m)</label><input class="gl-input gl-input-sm" name="breite" type="number" step="0.01"></div>
|
||||
<div><label class="gl-label">Tiefe (m)</label><input class="gl-input gl-input-sm" name="tiefe" type="number" step="0.01"></div>
|
||||
</div>
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:4px">Mindertiefe</div>
|
||||
<div style="font-size:.68rem;color:#888;margin-bottom:6px">Breite 0,15–0,30 · Tiefe 0,3–0,45</div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="mind_langstrasse"><span class="toggle-track"></span><span class="toggle-label"> Längstrasse</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="mind_ftth_ha"><span class="toggle-track"></span><span class="toggle-label"> FTTH HA</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<label class="gl-label" style="margin-top:6px">Bemerkung</label>
|
||||
<input class="gl-input" name="bemerkung">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Oberfläche -->
|
||||
<div class="column is-12-tablet is-3-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#edf7ed,#e6f3e6);border-color:rgba(39,174,96,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(39,174,96,.1)">??</span><h4>Oberfläche</h4></div>
|
||||
<div class="gl-grid-2">
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_wiese"><span class="toggle-track"></span><span class="toggle-label"> Wiese</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_kies"><span class="toggle-track"></span><span class="toggle-label"> Kies</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_asphalt"><span class="toggle-track"></span><span class="toggle-label"> Asphalt</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_pflaster"><span class="toggle-track"></span><span class="toggle-label"> Pflaster</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_mosaik"><span class="toggle-track"></span><span class="toggle-label"> Mosaik</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_bodentausch"><span class="toggle-track"></span><span class="toggle-label"> Bodentausch</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_fels"><span class="toggle-track"></span><span class="toggle-label"> Fels</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_winterbau"><span class="toggle-track"></span><span class="toggle-label"> Winterbau</span></label>
|
||||
</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_gr_natur_pfl"><span class="toggle-track"></span><span class="toggle-label"> Groß/Natursteinpflaster</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_in_beton"><span class="toggle-track"></span><span class="toggle-label"> in Beton Pflaster/Mosaik</span></label>
|
||||
<div style="margin-top:8px">
|
||||
<label class="gl-label">Asphaltstärke (cm)</label>
|
||||
<input class="gl-input gl-input-sm" name="asphaltstaerke" type="number" step="0.5">
|
||||
</div>
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Reststreifen (l × b)</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">Länge</label><input class="gl-input" name="rest_laenge" type="number" step="0.01"></div>
|
||||
<div><label class="gl-label">Breite</label><input class="gl-input" name="rest_breite" type="number" step="0.01"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-grid-2" style="margin-top:8px">
|
||||
<div><label class="gl-label">Anzahl Einzeiler</label><input class="gl-input" name="anz_einzeiler" type="number" step="1"></div>
|
||||
<div><label class="gl-label">m Bord/Rinne</label><input class="gl-input" name="lm_bre" type="number" step="0.01"></div>
|
||||
</div>
|
||||
<div class="gl-grid-auto" style="margin-top:6px">
|
||||
<label class="gl-toggle"><input type="checkbox" name="bord"><span class="toggle-track"></span><span class="toggle-label"> Bordstein</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kante"><span class="toggle-track"></span><span class="toggle-label"> Kantenstein</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="rinne"><span class="toggle-track"></span><span class="toggle-label"> Einzeiler/Rinne</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="liefern"><span class="toggle-track"></span><span class="toggle-label"> liefern</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Medien + Aktionen -->
|
||||
<div class="column is-12-tablet is-3-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>Medien</h4></div>
|
||||
<div class="gl-grid-2">
|
||||
<label class="gl-toggle"><input type="checkbox" name="cu_kabel"><span class="toggle-track"></span><span class="toggle-label"> 6–100 DA CU</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="cu_kabel_gr"><span class="toggle-track"></span><span class="toggle-label"> >100 DA CU</span></label>
|
||||
</div>
|
||||
<div style="margin:4px 0 8px"><label class="gl-label">Anzahl Kupfer</label><input class="gl-input gl-input-sm" name="anz_cu" type="number" step="1"></div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="dn110"><span class="toggle-track"></span><span class="toggle-label"> DN110</span></label><input class="gl-input" name="anz_dn110" type="number" step="1" placeholder="Anz." style="font-size:.75rem;padding:4px 8px;max-width:80px"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="dn50_1"><span class="toggle-track"></span><span class="toggle-label"> 1×DN50</span></label><input class="gl-input" name="anz_dn50_1" type="number" step="1" placeholder="Anz." style="font-size:.75rem;padding:4px 8px;max-width:80px"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="dn50_2"><span class="toggle-track"></span><span class="toggle-label"> 2×DN50</span></label><input class="gl-input" name="anz_dn50_2" type="number" step="1" placeholder="Anz." style="font-size:.75rem;padding:4px 8px;max-width:80px"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="dn50_3"><span class="toggle-track"></span><span class="toggle-label"> 3×DN50</span></label><input class="gl-input" name="anz_dn50_3" type="number" step="1" placeholder="Anz." style="font-size:.75rem;padding:4px 8px;max-width:80px"></div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="snrve_7x12"><span class="toggle-track"></span><span class="toggle-label"> SNRVe 7×12</span></label><input class="gl-input" name="anz_snrve_7x12" type="number" step="1" placeholder="Anz." style="font-size:.75rem;padding:4px 8px;max-width:80px"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="snrve_22x7"><span class="toggle-track"></span><span class="toggle-label"> SNRVe 22×7</span></label><input class="gl-input" name="anz_snrve_22x7" type="number" step="1" placeholder="Anz." style="font-size:.75rem;padding:4px 8px;max-width:80px"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="snrve_8x7"><span class="toggle-track"></span><span class="toggle-label"> SNRVe 8×7</span></label><input class="gl-input" name="anz_snrve_8x7" type="number" step="1" placeholder="Anz." style="font-size:.75rem;padding:4px 8px;max-width:80px"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="snrve_1x7"><span class="toggle-track"></span><span class="toggle-label"> SNRVe 1×7</span></label><input class="gl-input" name="anz_snrve_1x7" type="number" step="1" placeholder="Anz." style="font-size:.75rem;padding:4px 8px;max-width:80px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-3-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#f3e8fd,#ede0f7);border-color:rgba(142,68,173,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(142,68,173,.1)">?</span><h4>Aktionen</h4></div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="trasseeinmessen"><span class="toggle-track"></span><span class="toggle-label"> Trasse einmessen</span></label>
|
||||
<div style="margin-top:6px">
|
||||
<label class="gl-toggle"><input type="checkbox" name="stahlplatte"><span class="toggle-track"></span><span class="toggle-label"> Stahlplatte</span></label>
|
||||
<input class="gl-input" name="stahlplatte_bemerk" placeholder="Bemerkung" style="margin-top:4px">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,71 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='gruben') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">???</span><h4>Grube</h4></div>
|
||||
<label class="gl-label">Abschnitt</label>
|
||||
<input class="gl-input" name="abschnitt">
|
||||
<div class="gl-grid-3" style="margin-top:8px">
|
||||
<div><label class="gl-label">Länge (m)</label><input class="gl-input" name="laenge" type="number" step="0.01"></div>
|
||||
<div><label class="gl-label">Breite (m)</label><input class="gl-input" name="breite" type="number" step="0.01"></div>
|
||||
<div><label class="gl-label">Tiefe (m)</label><input class="gl-input" name="tiefe" type="number" step="0.01"></div>
|
||||
</div>
|
||||
<label class="gl-label" style="margin-top:8px">Bemerkung</label>
|
||||
<input class="gl-input" name="bemerkung">
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#edf7ed,#e6f3e6);border-color:rgba(39,174,96,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(39,174,96,.1)">??</span><h4>Oberfläche</h4></div>
|
||||
<div class="gl-grid-2">
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_wiese"><span class="toggle-track"></span><span class="toggle-label"> Wiese</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_kies"><span class="toggle-track"></span><span class="toggle-label"> Kies</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_asphalt"><span class="toggle-track"></span><span class="toggle-label"> Asphalt</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_pflaster"><span class="toggle-track"></span><span class="toggle-label"> Pflaster</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_mosaik"><span class="toggle-track"></span><span class="toggle-label"> Mosaik</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_bodentausch"><span class="toggle-track"></span><span class="toggle-label"> Bodentausch</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_fels"><span class="toggle-track"></span><span class="toggle-label"> Fels</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_winterbau"><span class="toggle-track"></span><span class="toggle-label"> Winterbau</span></label>
|
||||
</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_gr_natur_pfl"><span class="toggle-track"></span><span class="toggle-label"> Groß/Natursteinpflaster</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ob_in_beton"><span class="toggle-track"></span><span class="toggle-label"> in Beton Pflaster/Mosaik</span></label>
|
||||
<div style="margin-top:8px"><label class="gl-label">Asphaltstärke (cm)</label><input class="gl-input gl-input-sm" name="asphaltstaerke" type="number" step="0.5"></div>
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Reststreifen (l × b)</div>
|
||||
<div class="gl-grid-2"><div><label class="gl-label">Länge</label><input class="gl-input" name="rest_laenge" type="number" step="0.01"></div><div><label class="gl-label">Breite</label><input class="gl-input" name="rest_breite" type="number" step="0.01"></div></div>
|
||||
</div>
|
||||
<div style="margin-top:8px"><label class="gl-label">Anzahl Einzeiler</label><input class="gl-input gl-input-sm" name="anz_einzeiler" type="number" step="1"></div>
|
||||
<div class="gl-grid-auto" style="margin-top:6px">
|
||||
<label class="gl-toggle"><input type="checkbox" name="bord"><span class="toggle-track"></span><span class="toggle-label"> Bordstein</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kante"><span class="toggle-track"></span><span class="toggle-label"> Kantenstein</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="rinne"><span class="toggle-track"></span><span class="toggle-label"> Einzeiler/Rinne</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="liefern"><span class="toggle-track"></span><span class="toggle-label"> liefern</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#f3e8fd,#ede0f7);border-color:rgba(142,68,173,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(142,68,173,.1)">?</span><h4>Aktionen & Vorlagen</h4></div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="einmessen"><span class="toggle-track"></span><span class="toggle-label"> Grube/Muffe einmessen</span></label>
|
||||
<input class="gl-input" name="einmessen_bemerk" placeholder="Bemerkung" style="margin-top:4px">
|
||||
<label class="gl-toggle"><input type="checkbox" name="kugelmarker"><span class="toggle-track"></span><span class="toggle-label"> Kugelmarker</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="rohrinstand"><span class="toggle-track"></span><span class="toggle-label"> Punktuelle Rohrinstansetzung</span></label>
|
||||
<div class="gl-subcard" style="margin-top:10px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Vorlagen Gruben</div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="vorlage_gfm"><span class="toggle-track"></span><span class="toggle-label"> GF-Muffe</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="vorlage_mfg12"><span class="toggle-track"></span><span class="toggle-label"> MFG 12</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="vorlage_mfg15"><span class="toggle-track"></span><span class="toggle-label"> MFG 15</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="vorlage_mfg18"><span class="toggle-track"></span><span class="toggle-label"> MFG 18</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="vorlage_enas"><span class="toggle-track"></span><span class="toggle-label"> ENAS</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,72 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='kabelzug') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>Abschnitt</h4></div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">Abschnitt von</label><input class="gl-input" name="abschnitt_von"></div>
|
||||
<div><label class="gl-label">Abschnitt bis</label><input class="gl-input" name="abschnitt_bis"></div>
|
||||
</div>
|
||||
<label class="gl-label" style="margin-top:8px">Abschnitt Länge (m)</label><input class="gl-input gl-input-sm" name="abschnitt_laenge" type="number" step="0.1">
|
||||
<label class="gl-toggle"><input type="checkbox" name="belegtes_rohr"><span class="toggle-track"></span><span class="toggle-label"> in belegtes Rohr</span></label>
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Kabelschacht</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">Anzahl Ksch öffnen</label><input class="gl-input" name="anz_ksch" type="number" step="1"></div>
|
||||
<div><label class="gl-label">Bezeichnung Ksch</label><input class="gl-input" name="bezeichnung_ksch"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>Medien Rohre</h4></div>
|
||||
<div class="gl-grid-2">
|
||||
<label class="gl-toggle"><input type="checkbox" name="rohr_3pipes"><span class="toggle-track"></span><span class="toggle-label"> 3 Pipes in MFR kl.</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="rohr_5pipes"><span class="toggle-track"></span><span class="toggle-label"> 5 Pipes in MFR gr.</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="rohr_7pipes"><span class="toggle-track"></span><span class="toggle-label"> 7 Pipes in DN 50</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="rohr_7x12"><span class="toggle-track"></span><span class="toggle-label"> SNRVe 7×12 einziehen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="rohr_8x7"><span class="toggle-track"></span><span class="toggle-label"> SNRVe 8×7</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="rohr_22x7"><span class="toggle-track"></span><span class="toggle-label"> SNRVe 22×7</span></label>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<label class="gl-label">Verbandfarbe</label>
|
||||
<select class="gl-select" style="max-width:130px" name="verbandfarbe">
|
||||
<option value="or">or</option><option value="or/ws">or/ws</option><option value="or/sw">or/sw</option><option value="or/rt">or/rt</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<label class="gl-label">Pipe Farbe</label>
|
||||
<select class="gl-select" style="max-width:130px" name="pipefarbe">
|
||||
<option value="">–</option><option value="rt">rt</option><option value="gn">gn</option><option value="bl">bl</option><option value="ge">ge</option><option value="ws">ws</option><option value="gr">gr</option><option value="br">br</option><option value="vi">vi</option><option value="tk">tk</option><option value="sw">sw</option><option value="or">or</option><option value="rs">rs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#edf7ed,#e6f3e6);border-color:rgba(39,174,96,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(39,174,96,.1)">??</span><h4>Medien Kabel</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">GF-Kabel</div>
|
||||
<div class="gl-grid-2">
|
||||
<label class="gl-toggle"><input type="checkbox" name="kabel_12gf"><span class="toggle-track"></span><span class="toggle-label"> 12 GF</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kabel_24gf"><span class="toggle-track"></span><span class="toggle-label"> 24 GF</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kabel_36gf"><span class="toggle-track"></span><span class="toggle-label"> 36 GF</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kabel_48gf"><span class="toggle-track"></span><span class="toggle-label"> 48 GF</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kabel_72gf"><span class="toggle-track"></span><span class="toggle-label"> 72 GF</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kabel_96gf"><span class="toggle-track"></span><span class="toggle-label"> 96 GF</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-grid-2">
|
||||
<label class="gl-toggle"><input type="checkbox" name="kabel_cu_kl"><span class="toggle-track"></span><span class="toggle-label"> 6–100 DA CU</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kabel_cu_gr"><span class="toggle-track"></span><span class="toggle-label"> >100 DA CU</span></label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,62 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='neff_achberg') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">🏠</span><h4>Hausanschluss</h4></div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ha_herstellen"><span class="toggle-track"></span><span class="toggle-label"> HA herstellen</span></label>
|
||||
<label class="gl-label" style="margin-top:6px">Scan Name</label><input class="gl-input" name="scan_name">
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Kabelgraben</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="kg_gruen"><span class="toggle-track"></span><span class="toggle-label"> Grünflächen</span></label><input class="gl-input gl-input-sm" name="kg_gruen_m" type="number" step="0.1" placeholder="m"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="kg_asphalt"><span class="toggle-track"></span><span class="toggle-label"> Asphaltflächen</span></label><input class="gl-input gl-input-sm" name="kg_asphalt_m" type="number" step="0.1" placeholder="m"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="kg_pflaster"><span class="toggle-track"></span><span class="toggle-label"> Pflasterflächen</span></label><input class="gl-input gl-input-sm" name="kg_pflaster_m" type="number" step="0.1" placeholder="m"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Kopfloch</div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="kopfl_geb"><span class="toggle-track"></span><span class="toggle-label"> Gebäude</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kopfl_tr"><span class="toggle-track"></span><span class="toggle-label"> Trasse</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">🔌</span><h4>Leitungen & Sicherung</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">vorh. Leitungen Unterfahren</div>
|
||||
<div class="gl-grid-2">
|
||||
<label class="gl-toggle"><input type="checkbox" name="unt_kabel"><span class="toggle-track"></span><span class="toggle-label"> Kabel u. Rohre</span></label><input class="gl-input gl-input-sm" name="unt_kabel_anz" type="number" step="1" placeholder="Anz.">
|
||||
<label class="gl-toggle"><input type="checkbox" name="unt_vers"><span class="toggle-track"></span><span class="toggle-label"> Versorgungsleitungen</span></label><input class="gl-input gl-input-sm" name="unt_vers_anz" type="number" step="1" placeholder="Anz.">
|
||||
<label class="gl-toggle"><input type="checkbox" name="unt_abw"><span class="toggle-track"></span><span class="toggle-label"> Abwasserleitungen</span></label><input class="gl-input gl-input-sm" name="unt_abw_anz" type="number" step="1" placeholder="Anz.">
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">vorh. LTG sichern Längs</div>
|
||||
<div class="gl-grid-2">
|
||||
<label class="gl-toggle"><input type="checkbox" name="sich_kabel"><span class="toggle-track"></span><span class="toggle-label"> Kabel u. Rohr</span></label><input class="gl-input gl-input-sm" name="sich_kabel_m" type="number" step="0.1" placeholder="m">
|
||||
<label class="gl-toggle"><input type="checkbox" name="sich_vers"><span class="toggle-track"></span><span class="toggle-label"> Versorgungsleitungen</span></label><input class="gl-input gl-input-sm" name="sich_vers_m" type="number" step="0.1" placeholder="m">
|
||||
<label class="gl-toggle"><input type="checkbox" name="sich_abw"><span class="toggle-track"></span><span class="toggle-label"> Abwasserleitungen</span></label><input class="gl-input gl-input-sm" name="sich_abw_m" type="number" step="0.1" placeholder="m">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#f3e8fd,#ede0f7);border-color:rgba(142,68,173,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(142,68,173,.1)">⚙️</span><h4>Sonstiges & Aktionen</h4></div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="ha_koordinieren"><span class="toggle-track"></span><span class="toggle-label"> HA Koordinieren</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kernbohrung"><span class="toggle-track"></span><span class="toggle-label"> Kernbohrung herstellen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="md1"><span class="toggle-track"></span><span class="toggle-label"> MD1 FttH herstellen</span></label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,52 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='planung') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-6-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>Planung FTTH/NBG</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<label class="gl-toggle"><input type="checkbox" name="proj_struk_mtb"><span class="toggle-track"></span><span class="toggle-label"> Projektierung n. Strukturplanung mit TB bis 100m</span></label>
|
||||
<div><label class="gl-label">m Zulage ab 101m</label><input class="gl-input gl-input-sm" name="zulage_struk_mtb" type="number" step="0.1"></div>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<label class="gl-toggle"><input type="checkbox" name="proj_struk_otb"><span class="toggle-track"></span><span class="toggle-label"> Projektierung n. Strukturplanung ohne TB bis 100m</span></label>
|
||||
<div><label class="gl-label">m Zulage ab 101m</label><input class="gl-input gl-input-sm" name="zulage_struk_otb" type="number" step="0.1"></div>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<label class="gl-toggle"><input type="checkbox" name="mpp_gfk_nvt_ap"><span class="toggle-track"></span><span class="toggle-label"> MP-Proj. FTTH-Gf-Kabel v. NVT ? GF-AP</span></label>
|
||||
<label class="gl-label">m MP-Proj. Gf-Kabel im ZN-Netz</label><input class="gl-input gl-input-sm" name="mpp_gfk_zn" type="number" step="0.1">
|
||||
</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="mpp_gfap"><span class="toggle-track"></span><span class="toggle-label"> MP-Proj. GF-AP</span></label></div>
|
||||
<div><label class="gl-label">m MP-Proj. KR-Anlagen</label><input class="gl-input gl-input-sm" name="mpp_kr" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">m MP-Proj.</label><input class="gl-input gl-input-sm" name="mpp_snrv" type="number" step="0.1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-6-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>Planung M/L & S</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Planung M/L</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="ap_sgs"><span class="toggle-track"></span><span class="toggle-label"> GF-AP + Patchfeld</span></label>
|
||||
<div class="gl-grid-2" style="margin-top:6px">
|
||||
<div><label class="gl-label">m GF-Kabel einbl./verlegen</label><input class="gl-input gl-input-sm" name="gf_minik" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">m IVK</label><input class="gl-input gl-input-sm" name="ivk_m" type="number" step="0.1"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="proj_kl_baum"><span class="toggle-track"></span><span class="toggle-label"> Proj. kl. Baumaßnahme</span></label></div>
|
||||
<div><label class="gl-label">m Zulage kl. Baumaßnahme</label><input class="gl-input gl-input-sm" name="zulage_kl_baum" type="number" step="0.1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Planung S</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="s_planung_05"><span class="toggle-track"></span><span class="toggle-label"> S-Planung 0,5</span></label>
|
||||
<label class="gl-label" style="margin-top:4px">S-Liste (eine Position pro Zeile)</label>
|
||||
<textarea class="gl-input" name="s_liste" rows="3" placeholder="Position 1 Position 2 …"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,59 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='sas_mecka') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-2">
|
||||
<div class="column is-12-tablet is-6-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:6px">
|
||||
<span style="display:flex;align-items:center;gap:6px"><span class="icon" style="background:rgba(47,84,150,.1)">🏠</span><h4 style="margin:0">Hausanschluss</h4></span>
|
||||
<label class="gl-toggle" style="font-size:0.7rem"><input type="checkbox" name="ha_herstellen" value="an"><span class="toggle-track"></span><span class="toggle-label"> HA Herstellen</span></label>
|
||||
</div>
|
||||
<label class="gl-label" style="margin-top:6px">Scan Name</label><input class="gl-input" name="scan_name_ha">
|
||||
<div class="gl-grid-2" style="margin-top:8px">
|
||||
<div><label class="gl-label">Trassenmeter</label><input class="gl-input gl-input-sm" name="trassenmeter" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Kabelsichern (m)</label><input class="gl-input gl-input-sm" name="kabelmeter_ha" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Anzahl Q-Kabel/Rohre</label><input class="gl-input gl-input-sm" name="anz_qkr" type="number" step="1"></div>
|
||||
<div><label class="gl-label">Anzahl Querungsstellen</label><input class="gl-input gl-input-sm" name="anz_qst" type="number" step="1"></div>
|
||||
<div><label class="gl-label">Straßenquerung (m)</label><input class="gl-input gl-input-sm" name="strqm" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Anzahl Suchgrube</label><input class="gl-input gl-input-sm" name="anz_suchgrube_ha" type="number" step="1"></div>
|
||||
</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="einzug_10er" value="an"><span class="toggle-track"></span><span class="toggle-label"> Einzug 10er Pipes</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-6-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#edf7ed,#e6f3e6);border-color:rgba(39,174,96,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(39,174,96,.1)">🚧</span><h4>Tiefbau</h4></div>
|
||||
<div><label class="gl-label">Scan Name</label><input class="gl-input" name="scan_name_tb" style="width:100%"></div>
|
||||
<div style="display:flex;align-items:flex-end;gap:10px;margin-top:8px;flex-wrap:wrap">
|
||||
<label class="gl-toggle" style="font-size:0.8rem;white-space:nowrap"><input type="checkbox" name="tb_unbefestigt" value="an"><span class="toggle-track"></span><span class="toggle-label"> unbefestigt (Wiese, Kies)</span></label>
|
||||
<label class="gl-toggle" style="font-size:0.8rem;white-space:nowrap"><input type="checkbox" name="tb_befestigt" value="an"><span class="toggle-track"></span><span class="toggle-label"> befestigt (Pflaster, Asphalt)</span></label>
|
||||
<div style="flex:1;min-width:80px"><label class="gl-label">Länge (m)</label><input class="gl-input gl-input-sm" name="tb_laenge" type="number" step="0.1" style="width:100%"></div>
|
||||
<div style="flex:1;min-width:80px"><label class="gl-label">Tiefe (m)</label><input class="gl-input gl-input-sm" name="tb_tiefe" type="number" step="0.1" style="width:100%"></div>
|
||||
</div>
|
||||
<div class="gl-subcard" style="margin-top:8px;padding:8px;background:rgba(255,255,255,.5);border-radius:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Rohre & Suchgrube</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<div style="flex:1"><label class="gl-label" style="font-size:0.7rem;white-space:nowrap">Anzahl Rohre 12×10</label><input class="gl-input gl-input-sm" name="tb_anz_12x10" type="number" step="1" style="width:100%"></div>
|
||||
<div style="flex:1"><label class="gl-label" style="font-size:0.7rem;white-space:nowrap">Anzahl Rohre 4×12</label><input class="gl-input gl-input-sm" name="tb_anz_4x12" type="number" step="1" style="width:100%"></div>
|
||||
<div style="flex:1"><label class="gl-label" style="font-size:0.7rem;white-space:nowrap">Anzahl Suchgrube</label><input class="gl-input gl-input-sm" name="tb_anz_suchgrube" type="number" step="1" style="width:100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-subcard" style="margin-top:8px;padding:8px;background:rgba(255,255,255,.5);border-radius:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Kabel & Querungen</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<div style="flex:1"><label class="gl-label" style="font-size:0.7rem;white-space:nowrap">Anzahl Q-Kabel/Rohre</label><input class="gl-input gl-input-sm" name="tb_anz_qk" type="number" step="1" style="width:100%"></div>
|
||||
<div style="flex:1"><label class="gl-label" style="font-size:0.7rem;white-space:nowrap">Anzahl Querungsst.</label><input class="gl-input gl-input-sm" name="tb_anz_qs" type="number" step="1" style="width:100%"></div>
|
||||
<div style="flex:1"><label class="gl-label" style="font-size:0.7rem;white-space:nowrap">Kabelsichern (m)</label><input class="gl-input gl-input-sm" name="tb_kabelmeter" type="number" step="0.1" style="width:100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;margin-top:8px;flex-wrap:wrap">
|
||||
<label class="gl-toggle"><input type="checkbox" name="tb_kg2" value="an"><span class="toggle-track"></span><span class="toggle-label"> Kabelgraben 2</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="tb_kg4" value="an"><span class="toggle-track"></span><span class="toggle-label"> Kabelgraben 4</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="tb_kg6" value="an"><span class="toggle-track"></span><span class="toggle-label"> Kabelgraben 6</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,16 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='sto_sammler') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-8-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>Störungs-Sammelliste</h4></div>
|
||||
<label class="gl-label">Störungsliste (eine Position pro Zeile)</label>
|
||||
<textarea class="gl-input" name="sto_liste" rows="8" placeholder="Störung 1: Beschreibung Störung 2: Beschreibung Störung 3: Beschreibung …"></textarea>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,82 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='stoerung') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>CU-Messung & Montage</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">CU-Messung</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="cu_fehlerortung"><span class="toggle-track"></span><span class="toggle-label"> Fehlerortung von Kabelfehlern an Cu-Kabel</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="cu_zul_instan"><span class="toggle-track"></span><span class="toggle-label"> Zulage Instandsetzung v. Kabelfehlern</span></label>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">CU-Kabelabriss</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="cu_schaden_beweiss"><span class="toggle-track"></span><span class="toggle-label"> Schadens-/Beweissicherung an TK-Anlagen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="cu_instan_beweis"><span class="toggle-track"></span><span class="toggle-label"> Instands. nach Schadensbeweis</span></label>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">CU-Montage</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">Cu-DA > 0,8 mm verbinden</label><input class="gl-input" name="cu_da_gr_anz" type="number" step="1"></div>
|
||||
<div><label class="gl-label">Cu-DA = 0,8 mm verbinden</label><input class="gl-input" name="cu_da_kl_anz" type="number" step="1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#edf7ed,#e6f3e6);border-color:rgba(39,174,96,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(39,174,96,.1)">??</span><h4>GF-Messung & Montage</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">GF-Messung</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="gf_fehlerortung"><span class="toggle-track"></span><span class="toggle-label"> Fehlerortung v. Kabelfehlern an GF-Kabel</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="gf_inst_n_fehl"><span class="toggle-track"></span><span class="toggle-label"> Instands. v. GF-Kabel nach Fehlerortung</span></label>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">GF-Kabelabriss</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="gf_beweis"><span class="toggle-track"></span><span class="toggle-label"> Schadens-/Beweissicherung an TK-Anlagen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="gf_instand_ohne"><span class="toggle-track"></span><span class="toggle-label"> Instands. v. GF-Kabel ohne Fehlerortung</span></label>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">GF-Montage</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">GF ungeschweißt in Kassetten (m)</label><input class="gl-input gl-input-sm" name="gf_unge_kas" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">GF verbinden Indoor</label><input class="gl-input gl-input-sm" name="gf_verb_indoor" type="number" step="1"></div>
|
||||
<div><label class="gl-label">GF verbinden Outdoor</label><input class="gl-input gl-input-sm" name="gf_verb_outdoor" type="number" step="1"></div>
|
||||
</div>
|
||||
<div class="gl-grid-auto" style="margin-top:6px">
|
||||
<label class="gl-toggle"><input type="checkbox" name="gf_muffe_neu"><span class="toggle-track"></span><span class="toggle-label"> Neue GF-Muffe öffnen</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="gf_muffe_bestand"><span class="toggle-track"></span><span class="toggle-label"> Bestandsmuffe öffnen</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>CU-Kabel & Sonstiges</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">CU-Kabel auslegen</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">Kabel bis 30mm (m)</label><input class="gl-input" name="kabel_kl30" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Kabel >30mm (m)</label><input class="gl-input" name="kabel_gr30" type="number" step="0.1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">GF-Kabel</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">Ausblasen (m)</label><input class="gl-input gl-input-sm" name="gf_ausbl_m" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Einblasen (m)</label><input class="gl-input gl-input-sm" name="gf_einbl_m" type="number" step="0.1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Sonstiges</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="anfahrt_montage"><span class="toggle-track"></span><span class="toggle-label"> Anfahrt Montagestelle</span></label>
|
||||
<label class="gl-label" style="margin-top:4px">VAO mit Preis von</label><input class="gl-input gl-input-sm" name="vao_preis" type="number" step="0.01">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,45 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='tvum') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>TV/UM GF-AP</h4></div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="tvum_ap_mont"><span class="toggle-track"></span><span class="toggle-label"> TVUM-AP montieren Wand</span></label>
|
||||
<div style="margin-top:8px">
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">Inst. Kanäle/SNR bef.</label><input class="gl-input gl-input-sm" name="inst_snr" type="number" step="1"></div>
|
||||
<div><label class="gl-label">GF-Innenkabel bef./einziehen</label><input class="gl-input gl-input-sm" name="gf_innen_bef" type="number" step="1"></div>
|
||||
<div><label class="gl-label">Anzahl Spleiße</label><input class="gl-input gl-input-sm" name="tvum_anz_spl" type="number" step="1"></div>
|
||||
<div><label class="gl-label">Anzahl Messungen</label><input class="gl-input gl-input-sm" name="tvum_anz_mess" type="number" step="1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#edf7ed,#e6f3e6);border-color:rgba(39,174,96,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(39,174,96,.1)">??</span><h4>FTTH & NVT</h4></div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">FTTH GF-Spleißbox</div>
|
||||
<label class="gl-label">Anzahl Spleiße</label><input class="gl-input gl-input-sm" name="ftth_anz_spl" type="number" step="1">
|
||||
</div>
|
||||
<div class="gl-subcard">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">NVT</div>
|
||||
<label class="gl-label">Anzahl Spleiße</label><input class="gl-input gl-input-sm" name="nvt_anz_spl" type="number" step="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#fef9e7,#fdf3d0);border-color:rgba(243,156,18,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(243,156,18,.1)">??</span><h4>Kabelzug & Aktionen</h4></div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-label">Meter ausblasen</label><input class="gl-input gl-input-sm" name="ausbl_m" type="number" step="0.1"></div>
|
||||
<div><label class="gl-label">Meter einblasen</label><input class="gl-input gl-input-sm" name="einbl_m" type="number" step="0.1"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,34 @@
|
||||
<form hx-post="{{ url_for('modules.berechnen', module_name='zw_rv') }}" hx-target="#modul-modal-body" hx-swap="innerHTML">
|
||||
|
||||
<div class="modal-modul mg">
|
||||
<div class="columns is-multiline is-variable is-3">
|
||||
<div class="column is-12-tablet is-6-desktop">
|
||||
<div class="gl-card" style="background:linear-gradient(135deg,#eef2fa,#e8edf5);border-color:rgba(47,84,150,.1)">
|
||||
<div class="gl-card-header"><span class="icon" style="background:rgba(47,84,150,.1)">??</span><h4>Hausanschluss</h4></div>
|
||||
<label class="gl-label">Abschnitt</label><input class="gl-input" name="abschnitt">
|
||||
<div class="gl-subcard" style="margin-top:8px">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:6px">Oberfläche</div>
|
||||
<div class="gl-grid-2">
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="ob_vegetation"><span class="toggle-track"></span><span class="toggle-label"> Vegetation</span></label><input class="gl-input gl-input-sm" name="vegetation_m" type="number" step="0.1" placeholder="m"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="ob_pflaster"><span class="toggle-track"></span><span class="toggle-label"> Pflaster</span></label><input class="gl-input gl-input-sm" name="pflaster_m" type="number" step="0.1" placeholder="m"></div>
|
||||
<div><label class="gl-toggle"><input type="checkbox" name="ob_asphalt"><span class="toggle-track"></span><span class="toggle-label"> Asphalt</span></label><input class="gl-input gl-input-sm" name="asphalt_m" type="number" step="0.1" placeholder="m"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-grid-auto">
|
||||
<label class="gl-toggle"><input type="checkbox" name="kopfloch_geb"><span class="toggle-track"></span><span class="toggle-label"> Kopfloch Gebäude</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kopfloch_tr"><span class="toggle-track"></span><span class="toggle-label"> Kopfloch Haupttrasse</span></label>
|
||||
</div>
|
||||
<label class="gl-toggle"><input type="checkbox" name="koordinieren_ha"><span class="toggle-track"></span><span class="toggle-label"> Koordinieren Hausanschluss</span></label>
|
||||
<div class="gl-grid-auto" style="margin-top:6px">
|
||||
<label class="gl-toggle"><input type="checkbox" name="md1_ftth"><span class="toggle-track"></span><span class="toggle-label"> MD1-FttH</span></label>
|
||||
<label class="gl-toggle"><input type="checkbox" name="kernbohrung"><span class="toggle-track"></span><span class="toggle-label"> Kernbohrung</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-6-desktop">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% include 'components/modul_actions.html' %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,78 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<h1 class="title is-3">{{ contract.name }}</h1>
|
||||
<span class="tag is-medium {{ 'is-success' if contract.status == 'Angenommen' else 'is-warning' if contract.status == 'Zur Prüfung' else 'is-light' }} ml-3">
|
||||
{{ contract.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<a class="button is-light" href="{{ url_for('contracts.index') }}">← Alle Verträge</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<div class="box">
|
||||
<h2 class="title is-6">Details</h2>
|
||||
<form method="POST" action="{{ url_for('contracts.detail_update', contract_id=contract.id) }}">
|
||||
<div class="field">
|
||||
<label class="label is-small">Belegnummer</label>
|
||||
<div class="control"><input class="input is-small" name="belegnummer" value="{{ contract.belegnummer or '' }}"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-small">Beleg-Datum</label>
|
||||
<div class="control"><input class="input is-small" type="date" name="beleg_datum" value="{{ contract.beleg_datum.strftime('%Y-%m-%d') if contract.beleg_datum else '' }}"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-small">Laufzeit Start</label>
|
||||
<div class="control"><input class="input is-small" type="date" name="laufzeit_start" value="{{ contract.laufzeit_start.strftime('%Y-%m-%d') if contract.laufzeit_start else '' }}"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-small">Laufzeit Ende</label>
|
||||
<div class="control"><input class="input is-small" type="date" name="laufzeit_ende" value="{{ contract.laufzeit_ende.strftime('%Y-%m-%d') if contract.laufzeit_ende else '' }}"></div>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control"><button class="button is-small is-info">Speichern</button></div>
|
||||
</div>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('contracts.status_set', contract_id=contract.id) }}" class="mt-3">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select name="status">
|
||||
<option value="NEU" {{ 'selected' if contract.status == 'NEU' }}>NEU</option>
|
||||
<option value="Zur Prüfung" {{ 'selected' if contract.status == 'Zur Prüfung' }}>Zur Prüfung</option>
|
||||
<option value="Angenommen" {{ 'selected' if contract.status == 'Angenommen' }}>Angenommen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-small is-info">Status setzen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<div class="box">
|
||||
<h2 class="title is-6">Zugehörige Leistungsverzeichnisse</h2>
|
||||
{% if lv_names %}
|
||||
<ul>
|
||||
{% for name in lv_names %}
|
||||
<li><a href="{{ url_for('lv.index', lv=name) }}">{{ name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Diesem Vertrag sind noch keine LVs zugeordnet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('contracts.delete', contract_id=contract.id) }}"
|
||||
onsubmit="return confirm('Vertrag wirklich löschen?')" class="mt-3">
|
||||
<button class="button is-danger is-outlined is-small">Vertrag löschen</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,142 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">Verträge</h1></div>
|
||||
<div class="level-right">
|
||||
<a class="button is-primary" href="{{ url_for('contracts.neu') }}">+ Neuer Vertrag</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
{% if contracts %}
|
||||
<table class="table is-fullwidth is-hoverable is-striped" id="contracts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vertrag</th>
|
||||
<th>Belegnummer</th>
|
||||
<th>Beleg-Datum</th>
|
||||
<th>Laufzeit</th>
|
||||
<th>Status</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in contracts %}
|
||||
<tr id="contract-row-{{ c.id }}">
|
||||
<td><strong>{{ c.name }}</strong></td>
|
||||
<td><code>{{ c.belegnummer or '–' }}</code></td>
|
||||
<td>{{ c.beleg_datum.strftime('%d.%m.%Y') if c.beleg_datum else '–' }}</td>
|
||||
<td>
|
||||
{{ c.laufzeit_start.strftime('%d.%m.%Y') if c.laufzeit_start else '?' }}
|
||||
– {{ c.laufzeit_ende.strftime('%d.%m.%Y') if c.laufzeit_ende else '?' }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="tag {{ 'is-success' if c.status == 'Angenommen' else 'is-warning' if c.status == 'Zur Prüfung' else 'is-light' }}">
|
||||
{{ c.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="button is-small is-link" href="{{ url_for('contracts.detail', contract_id=c.id) }}">Öffnen</a>
|
||||
<button class="button is-small is-light" onclick="openEditModal({{ c.id }})" title="Bearbeiten">✏️</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-centered has-text-grey is-size-5">Noch keine Verträge angelegt.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal" id="edit-modal">
|
||||
<div class="modal-background" onclick="closeEditModal()"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title" id="edit-modal-title">Vertrag bearbeiten</p>
|
||||
<button class="delete" onclick="closeEditModal()" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<form id="edit-form">
|
||||
<div class="field">
|
||||
<label class="label is-small">Belegnummer</label>
|
||||
<div class="control"><input class="input is-small" name="belegnummer" id="edit-belegnummer"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-small">Beleg-Datum</label>
|
||||
<div class="control"><input class="input is-small" type="date" name="beleg_datum" id="edit-beleg-datum"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-small">Laufzeit Start</label>
|
||||
<div class="control"><input class="input is-small" type="date" name="laufzeit_start" id="edit-laufzeit-start"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-small">Laufzeit Ende</label>
|
||||
<div class="control"><input class="input is-small" type="date" name="laufzeit_ende" id="edit-laufzeit-ende"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-small">Status</label>
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select name="status" id="edit-status">
|
||||
<option value="NEU">NEU</option>
|
||||
<option value="Zur Prüfung">Zur Prüfung</option>
|
||||
<option value="Angenommen">Angenommen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-info" onclick="saveEdit()">Speichern</button>
|
||||
<button class="button" onclick="closeEditModal()">Abbrechen</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var editContractId = null;
|
||||
|
||||
function openEditModal(id) {
|
||||
editContractId = id;
|
||||
var row = document.getElementById('contract-row-' + id);
|
||||
var cells = row.querySelectorAll('td');
|
||||
document.getElementById('edit-modal-title').textContent = cells[0].textContent.trim();
|
||||
document.getElementById('edit-belegnummer').value = cells[1].textContent.trim() === '–' ? '' : cells[1].textContent.trim();
|
||||
var belegDatum = cells[2].textContent.trim();
|
||||
document.getElementById('edit-beleg-datum').value = belegDatum !== '–' ? toDateInput(belegDatum) : '';
|
||||
var laufzeit = cells[3].textContent.trim().split('–');
|
||||
document.getElementById('edit-laufzeit-start').value = laufzeit[0] && laufzeit[0].trim() !== '?' ? toDateInput(laufzeit[0].trim()) : '';
|
||||
document.getElementById('edit-laufzeit-ende').value = laufzeit[1] && laufzeit[1].trim() !== '?' ? toDateInput(laufzeit[1].trim()) : '';
|
||||
var statusTag = row.querySelector('.tag');
|
||||
document.getElementById('edit-status').value = statusTag.textContent.trim();
|
||||
document.getElementById('edit-modal').classList.add('is-active');
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
document.getElementById('edit-modal').classList.remove('is-active');
|
||||
editContractId = null;
|
||||
}
|
||||
|
||||
function toDateInput(ddmm) {
|
||||
var parts = ddmm.split('.');
|
||||
if (parts.length === 3) return parts[2] + '-' + parts[1] + '-' + parts[0];
|
||||
return '';
|
||||
}
|
||||
|
||||
function saveEdit() {
|
||||
var form = document.getElementById('edit-form');
|
||||
var data = new FormData(form);
|
||||
data.append('contract_id', editContractId);
|
||||
fetch('/contracts/' + editContractId + '/update', {
|
||||
method: 'POST',
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
body: data
|
||||
}).then(function(r) {
|
||||
if (r.ok) location.reload();
|
||||
else alert('Fehler beim Speichern');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,61 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">Neuer Vertrag</h1></div>
|
||||
<div class="level-right"><a class="button is-light" href="{{ url_for('contracts.index') }}">← Zurück</a></div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form method="POST" class="box">
|
||||
<div class="field">
|
||||
<label class="label">Vertragsname *</label>
|
||||
<div class="control"><input class="input" name="name" required placeholder="z.B. SW Proj+Doku 2024ff"></div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Belegnummer</label>
|
||||
<div class="control"><input class="input" name="belegnummer" placeholder="z.B. 4650014601"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Beleg-Datum</label>
|
||||
<div class="control"><input class="input" name="beleg_datum" type="date"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Laufzeit Beginn</label>
|
||||
<div class="control"><input class="input" name="laufzeit_start" type="date"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Laufzeit Ende</label>
|
||||
<div class="control"><input class="input" name="laufzeit_ende" type="date"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Status</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select name="status">
|
||||
<option value="NEU">NEU</option>
|
||||
<option value="Zur Prüfung">Zur Prüfung</option>
|
||||
<option value="Angenommen" selected>Angenommen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-4">
|
||||
<button class="button is-primary" type="submit">Vertrag anlegen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{{ url_for('custom_modules.index') }}">Modul-Builder</a></li>
|
||||
<li class="is-active"><a href="#">{{ module.name if module else 'Neues Modul' }}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<h2 class="title is-4">{{ '✏️' if module else '➕' }} {{ module.name if module else 'Neues Modul' }}</h2>
|
||||
|
||||
<form method="POST" class="box">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6">
|
||||
<label class="label">Name *</label>
|
||||
<input class="input" name="name" value="{{ module.name if module else '' }}" required>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<label class="label">Icon (Emoji)</label>
|
||||
<input class="input" name="icon" value="{{ module.icon if module else '🔧' }}">
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<label class="label">Kategorie</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select name="kategorie">
|
||||
<option value="allgemein" {% if module and module.kategorie=='allgemein' %}selected{% endif %}>Allgemein</option>
|
||||
<option value="tiefbau" {% if module and module.kategorie=='tiefbau' %}selected{% endif %}>Tiefbau</option>
|
||||
<option value="graben" {% if module and module.kategorie=='graben' %}selected{% endif %}>Graben</option>
|
||||
<option value="montage" {% if module and module.kategorie=='montage' %}selected{% endif %}>Montage</option>
|
||||
<option value="sonder" {% if module and module.kategorie=='sonder' %}selected{% endif %}>Sonder</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<label class="label">Beschreibung</label>
|
||||
<textarea class="textarea" name="description" rows="2">{{ module.description if module else '' }}</textarea>
|
||||
</div>
|
||||
{% if is_superadmin %}
|
||||
<div class="column is-3">
|
||||
<label class="checkbox mt-5">
|
||||
<input type="checkbox" name="is_template" value="1" {% if module and module.is_template %}checked{% endif %}>
|
||||
Globale Vorlage
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="column is-3">
|
||||
<label class="checkbox mt-5">
|
||||
<input type="checkbox" name="is_active" value="1" {% if not module or module.is_active %}checked{% endif %}>
|
||||
Aktiv
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons mt-3">
|
||||
<button class="button is-primary" type="submit">Speichern</button>
|
||||
<a href="{{ url_for('custom_modules.index') }}" class="button is-light">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if module %}
|
||||
<div class="buttons mt-4">
|
||||
<a href="{{ url_for('custom_modules.builder', module_id=module.id) }}" class="button is-info is-large">
|
||||
🎨 Zum Builder
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if module.company_id %}
|
||||
<hr>
|
||||
<h3 class="title is-5">👥 Mitarbeiter-Zugriff</h3>
|
||||
<p class="subtitle is-6">Lege fest, welche Mitarbeiter dieses Modul nutzen dürfen.</p>
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr><th>Mitarbeiter</th><th>Zugriff</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.full_name }}</td>
|
||||
<td>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
class="js-user-toggle"
|
||||
data-module-id="{{ module.id }}"
|
||||
data-user-id="{{ u.id }}"
|
||||
{% if u.id in assignments %}checked{% endif %}>
|
||||
Freigegeben
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.querySelectorAll('.js-user-toggle').forEach(function(cb) {
|
||||
cb.addEventListener('change', function() {
|
||||
var moduleId = this.dataset.moduleId;
|
||||
var userId = this.dataset.userId;
|
||||
fetch('/custom-modules/' + moduleId + '/user/' + userId + '/toggle', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({active: this.checked, can_edit: false})
|
||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (data.message) console.log(data.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,165 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<h2 class="title is-4">🔧 Modul-Builder</h2>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<a href="{{ url_for('custom_modules.neu') }}" class="button is-primary">+ Neues Modul</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_superadmin() and templates %}
|
||||
<h3 class="title is-5 mt-4">🌐 Globale Vorlagen</h3>
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px"></th>
|
||||
<th>Name</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="js-sort-templates">
|
||||
{% for m in templates %}
|
||||
<tr data-id="{{ m.id }}">
|
||||
<td class="drag-handle" style="cursor:grab;text-align:center;color:#aaa">⠿</td>
|
||||
<td>{{ m.icon }} {{ m.name }}</td>
|
||||
<td>{{ m.kategorie }}</td>
|
||||
<td>{{ m.created_at.strftime('%d.%m.%Y') if m.created_at else '' }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('custom_modules.edit', module_id=m.id) }}" class="button is-small">Bearbeiten</a>
|
||||
<a href="{{ url_for('custom_modules.builder', module_id=m.id) }}" class="button is-small is-info">Builder</a>
|
||||
<form method="POST" action="{{ url_for('custom_modules.loeschen', module_id=m.id) }}" style="display:inline">
|
||||
<button class="button is-small is-danger" onclick="return confirm('Wirklich löschen?')">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_superadmin() %}
|
||||
<h3 class="title is-5 mt-4">🏢 Alle Firmen-Module</h3>
|
||||
{% else %}
|
||||
<h3 class="title is-5 mt-4">🏢 Meine Module</h3>
|
||||
{% endif %}
|
||||
|
||||
{% if not current_user.is_superadmin() and templates %}
|
||||
<div class="message is-info">
|
||||
<div class="message-body">
|
||||
<strong>Vorlagen verfügbar:</strong>
|
||||
{% for t in templates %}
|
||||
<span class="tag is-medium ml-2">
|
||||
{{ t.icon }} {{ t.name }}
|
||||
<form method="POST" action="{{ url_for('custom_modules.importieren', module_id=t.id) }}" style="display:inline">
|
||||
<button class="button is-small is-ghost ml-1" title="Importieren">📥</button>
|
||||
</form>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px"></th>
|
||||
<th>Name</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Firma</th>
|
||||
{% if current_user.is_superadmin() %}<th>Vorlage</th>{% endif %}
|
||||
<th>Aktiv</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="js-sort-company">
|
||||
{% for m in company_modules %}
|
||||
<tr data-id="{{ m.id }}">
|
||||
<td class="drag-handle" style="cursor:grab;text-align:center;color:#aaa">⠿</td>
|
||||
<td>{{ m.icon }} {{ m.name }}</td>
|
||||
<td>{{ m.kategorie }}</td>
|
||||
<td>{{ m.company.name if m.company else '—' }}</td>
|
||||
{% if current_user.is_superadmin() %}
|
||||
<td>
|
||||
{% if m.original_template_id %}
|
||||
<span class="tag is-light">kopiert</span>
|
||||
{% else %}
|
||||
<span class="tag is-white">eigenes</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<span class="tag {% if m.is_active %}is-success{% else %}is-light{% endif %}">
|
||||
{{ 'Aktiv' if m.is_active else 'Inaktiv' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if current_user.is_superadmin() or (current_user.is_firmadmin() and m.company_id == current_user.company_id) %}
|
||||
<a href="{{ url_for('custom_modules.edit', module_id=m.id) }}" class="button is-small">Bearbeiten</a>
|
||||
<a href="{{ url_for('custom_modules.builder', module_id=m.id) }}" class="button is-small is-info">Builder</a>
|
||||
<form method="POST" action="{{ url_for('custom_modules.loeschen', module_id=m.id) }}" style="display:inline">
|
||||
<button class="button is-small is-danger" onclick="return confirm('Wirklich löschen?')">Löschen</button>
|
||||
</form>
|
||||
{% if current_user.is_superadmin() %}
|
||||
<form method="POST" action="{{ url_for('custom_modules.als_vorlage', module_id=m.id) }}" style="display:inline">
|
||||
<button class="button is-small is-warning">Als Vorlage</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="has-text-centered"><em>Keine Module vorhanden.</em></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sortable-ghost{opacity:0.4}
|
||||
.sortable-chosen{background:#d4e3ff}
|
||||
.drag-handle:hover{color:#2F5496!important}
|
||||
#js-sort-templates tr.dragging, #js-sort-company tr.dragging{background:#f0f4ff}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
function commitSort(listId, endpoint) {
|
||||
var el = document.getElementById(listId);
|
||||
if (!el) return;
|
||||
if (el._sortable) el._sortable.destroy();
|
||||
el._sortable = new Sortable(el, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'dragging',
|
||||
onEnd: function() {
|
||||
var rows = el.querySelectorAll('tr[data-id]');
|
||||
var order = [];
|
||||
rows.forEach(function(r, idx) {
|
||||
order.push({id: parseInt(r.dataset.id), sort_index: idx});
|
||||
});
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(order)
|
||||
}).then(function(r){return r.json()}).then(function(d){
|
||||
if (!d.ok) console.warn('Sort failed', d);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
commitSort('js-sort-templates', '{{ url_for("custom_modules.sort_batch")|safe }}');
|
||||
commitSort('js-sort-company', '{{ url_for("custom_modules.sort_batch")|safe }}');
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="columns is-centered mt-6">
|
||||
<div class="column is-half has-text-centered">
|
||||
<h1 class="title is-1" style="font-size:6rem;color:#2F5496;">404</h1>
|
||||
<p class="subtitle is-4">Seite nicht gefunden</p>
|
||||
<p class="has-text-grey mb-4">Die angeforderte Seite existiert nicht.</p>
|
||||
<a class="button is-primary" href="{{ url_for('admin.dashboard') }}">Zum Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="columns is-centered mt-6">
|
||||
<div class="column is-half has-text-centered">
|
||||
<h1 class="title is-1" style="font-size:6rem;color:#e74c3c;">500</h1>
|
||||
<p class="subtitle is-4">Interner Serverfehler</p>
|
||||
<p class="has-text-grey mb-4">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</p>
|
||||
<a class="button is-primary" href="{{ url_for('admin.dashboard') }}">Zum Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,316 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 1.2cm 0.8cm 1.5cm 0.8cm;
|
||||
@bottom-right {
|
||||
content: counter(page) " / " counter(pages);
|
||||
font-family: 'DejaVu Sans', 'Calibri', sans-serif;
|
||||
font-size: 9pt;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
body {
|
||||
font-family: 'DejaVu Sans', 'Calibri', sans-serif;
|
||||
font-size: 10pt;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
.logo-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.logo-title img {
|
||||
max-width: 120px;
|
||||
max-height: 60px;
|
||||
}
|
||||
.logo-title .firmen-name {
|
||||
font-size: 16pt;
|
||||
font-weight: bold;
|
||||
color: #2F5496;
|
||||
}
|
||||
.logo-title .aufmass-titel {
|
||||
font-size: 16pt;
|
||||
font-weight: bold;
|
||||
color: #2F5496;
|
||||
text-align: right;
|
||||
}
|
||||
table.header-grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 9pt;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
table.header-grid td {
|
||||
padding: 3px 5px;
|
||||
border: 0.5pt solid #999;
|
||||
vertical-align: middle;
|
||||
}
|
||||
table.header-grid td.label {
|
||||
background: #f2f2f2;
|
||||
font-weight: bold;
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.header-grid td.value {
|
||||
font-weight: normal;
|
||||
padding-left: 6px;
|
||||
}
|
||||
table.header-grid td.spacer {
|
||||
border: none;
|
||||
width: 1%;
|
||||
}
|
||||
table.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
table.data-table th {
|
||||
background: #2F5496;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
padding: 4px 3px;
|
||||
border: 0.5pt solid #2F5496;
|
||||
text-align: center;
|
||||
font-size: 8pt;
|
||||
}
|
||||
table.data-table td {
|
||||
padding: 2px 3px;
|
||||
border: 0.5pt solid #ccc;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.data-table tr.trenner td {
|
||||
background: #f5f5f5;
|
||||
height: 10px;
|
||||
border-top: 1pt dashed #ccc;
|
||||
border-bottom: 1pt dashed #ccc;
|
||||
}
|
||||
table.data-table tr.summe td {
|
||||
font-weight: bold;
|
||||
border-top: 1pt solid #999;
|
||||
}
|
||||
.summary-section {
|
||||
margin-top: 14px;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.summary-title {
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
color: #2F5496;
|
||||
background: #D6E4F0;
|
||||
padding: 4px 8px;
|
||||
border: 0.5pt solid #999;
|
||||
text-align: center;
|
||||
}
|
||||
table.summary-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 9pt;
|
||||
}
|
||||
table.summary-table th {
|
||||
background: #2F5496;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
padding: 3px 5px;
|
||||
border: 0.5pt solid #2F5496;
|
||||
text-align: center;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
table.summary-table td {
|
||||
padding: 2px 5px;
|
||||
border: 0.5pt solid #ccc;
|
||||
}
|
||||
table.summary-table tr.summe td {
|
||||
font-weight: bold;
|
||||
border-top: 1pt solid #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="logo-title">
|
||||
<div>
|
||||
{% if company and company.logo %}
|
||||
<img src="{{ company.logo }}" alt="Logo">
|
||||
{% else %}
|
||||
<span class="firmen-name">{{ company.name if company else 'Aufmaß' }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="aufmass-titel">Aufmaß</div>
|
||||
</div>
|
||||
|
||||
<table class="header-grid">
|
||||
<tr>
|
||||
<td class="label">Vertrag:</td>
|
||||
<td class="value">{{ project.vertrag or '' }}</td>
|
||||
<td class="spacer"></td>
|
||||
<td class="label">LV-Name:</td>
|
||||
<td class="value">{{ project.lv_name or '' }}</td>
|
||||
<td class="spacer"></td>
|
||||
<td class="label">Typ:</td>
|
||||
<td class="value">{{ aufmass.typ if aufmass else '' }}</td>
|
||||
<td class="spacer"></td>
|
||||
<td class="label">Aufmaß-Datum:</td>
|
||||
<td class="value" colspan="3">{{ project.datum.strftime('%d.%m.%Y') if project.datum else '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="header-grid">
|
||||
<tr>
|
||||
<td class="label" style="width:5%">Projekt:</td>
|
||||
<td class="value" colspan="3" style="font-weight:bold">{{ project.bezeichnung or '' }}</td>
|
||||
<td class="spacer" style="width:0.5%"></td>
|
||||
<td class="label" style="width:6%">Baustelle:</td>
|
||||
<td class="value" colspan="3">{{ project.baustelle or '' }}</td>
|
||||
<td class="spacer" style="width:0.5%"></td>
|
||||
<td class="label" style="width:8%">Bauabschnitt:</td>
|
||||
<td class="value" colspan="2">{{ project.bauabschnitt or '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="header-grid">
|
||||
<tr>
|
||||
<td class="label">SM-Nr.:</td>
|
||||
<td class="value">{{ project.sm_nr or '' }}</td>
|
||||
<td class="spacer"></td>
|
||||
<td class="label">Abruf-Nr.:</td>
|
||||
<td class="value">{{ project.abruf_nr or '' }}</td>
|
||||
<td class="spacer"></td>
|
||||
<td class="label">Startdatum:</td>
|
||||
<td class="value">{{ project.datum_start.strftime('%d.%m.%Y') if project.datum_start else '' }}</td>
|
||||
<td class="spacer"></td>
|
||||
<td class="label">Enddatum:</td>
|
||||
<td class="value" colspan="3">{{ project.datum_ende.strftime('%d.%m.%Y') if project.datum_ende else '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="header-grid">
|
||||
<tr>
|
||||
<td class="label" style="width:10%">Ansprechpartner:</td>
|
||||
<td class="value" colspan="3">{{ (project.ansprechpartner_vorname or '') + ' ' + (project.ansprechpartner_nachname or '') }}</td>
|
||||
<td class="spacer" style="width:0.5%"></td>
|
||||
<td class="value" colspan="2" style="text-align:left">{{ project.ansprechpartner_tel or '' }}</td>
|
||||
<td class="spacer" style="width:0.5%"></td>
|
||||
<td class="label" style="width:4%">Email:</td>
|
||||
<td class="value" colspan="4">{{ project.ansprechpartner_email or '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:7%">Abschnitt</th>
|
||||
<th style="width:8%">Pos-Nr</th>
|
||||
<th style="width:5%">Faktor</th>
|
||||
<th style="width:6%">Länge</th>
|
||||
<th style="width:6%">Breite</th>
|
||||
<th style="width:6%">Tiefe</th>
|
||||
<th style="width:7%">Menge</th>
|
||||
<th style="width:4%">EH</th>
|
||||
<th style="width:18%">Kurztext</th>
|
||||
<th style="width:12%">Bemerkung</th>
|
||||
<th style="width:7%">Menge</th>
|
||||
<th style="width:7%">EP (€)</th>
|
||||
<th style="width:7%">GP (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set ns = namespace(pos_counter=0) %}
|
||||
{% for pos in positionen %}
|
||||
{% set ist_trenner = not pos.pos_nr and (pos.faktor == 0 or pos.faktor is none) and (pos.laenge == 0 or pos.laenge is none) and (pos.breite == 0 or pos.breite is none) and (pos.tiefe == 0 or pos.tiefe is none) and (pos.menge == 0 or pos.menge is none) and (pos.einzelpreis == 0 or pos.einzelpreis is none) and (pos.gesamtpreis == 0 or pos.gesamtpreis is none) %}
|
||||
{% if ist_trenner %}
|
||||
<tr class="trenner">
|
||||
<td colspan="13"> </td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% set ns.pos_counter = ns.pos_counter + 1 %}
|
||||
{% set menge = pos.menge if pos.menge else none %}
|
||||
{% set menge_hinten = pos.menge_hinten if pos.menge_hinten else none %}
|
||||
{% if pos.einheit in ('ST', 'LE', 'STD', 'h', 'Psch') %}
|
||||
{% set menge = pos.faktor * 1 if pos.faktor else none %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{{ pos.abschnitt or '' }}</td>
|
||||
<td>{{ pos.pos_nr or '' }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(pos.faktor) if pos.faktor else '' }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(pos.laenge) if pos.laenge else '' }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(pos.breite) if pos.breite else '' }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(pos.tiefe) if pos.tiefe else '' }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(menge) if menge else '' }}</td>
|
||||
<td>{{ pos.einheit or '' }}</td>
|
||||
<td>{{ pos.kurztext or '' }}</td>
|
||||
<td>{{ pos.bemerkung or '' }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(menge_hinten) if menge_hinten else '' }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(pos.einzelpreis) if pos.einzelpreis else '' }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(pos.gesamtpreis) if pos.gesamtpreis else '' }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if positionen %}
|
||||
{% set gesamt = positionen|selectattr('gesamtpreis')|sum(attribute='gesamtpreis') %}
|
||||
<tr class="summe">
|
||||
<td colspan="11" style="text-align:right">Summe:</td>
|
||||
<td></td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(gesamt) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if ns.pos_counter > 0 %}
|
||||
{% set groups = {} %}
|
||||
{% for pos in positionen %}
|
||||
{% set ist_trenner = not pos.pos_nr and (pos.faktor == 0 or pos.faktor is none) and (pos.laenge == 0 or pos.laenge is none) and (pos.breite == 0 or pos.breite is none) and (pos.tiefe == 0 or pos.tiefe is none) and (pos.menge == 0 or pos.menge is none) and (pos.einzelpreis == 0 or pos.einzelpreis is none) and (pos.gesamtpreis == 0 or pos.gesamtpreis is none) %}
|
||||
{% if not ist_trenner and pos.pos_nr %}
|
||||
{% if pos.pos_nr not in groups %}
|
||||
{% set _ = groups.update({pos.pos_nr: {'kurztext': pos.kurztext or '', 'menge': 0.0, 'ep': 0.0, 'gp': 0.0}}) %}
|
||||
{% endif %}
|
||||
{% set _ = groups[pos.pos_nr].update({'menge': groups[pos.pos_nr]['menge'] + (pos.menge_hinten if pos.menge_hinten else (pos.menge or 0))}) %}
|
||||
{% set _ = groups[pos.pos_nr].update({'ep': pos.einzelpreis or 0}) %}
|
||||
{% set _ = groups[pos.pos_nr].update({'gp': groups[pos.pos_nr]['gp'] + (pos.gesamtpreis or 0)}) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="summary-section">
|
||||
<div class="summary-title">Mengen- und Positions-Zusammenfassung</div>
|
||||
<table class="summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:12%">Pos-Nr</th>
|
||||
<th>Kurztext</th>
|
||||
<th style="width:12%">Menge</th>
|
||||
<th style="width:12%">EP (€)</th>
|
||||
<th style="width:12%">GP (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key, g in groups.items() %}
|
||||
<tr>
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ g.kurztext }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(g.menge) }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(g.ep) }}</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(g.gp) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% set total_gp = groups.values()|sum(attribute='gp') %}
|
||||
<tr class="summe">
|
||||
<td colspan="3"></td>
|
||||
<td style="text-align:right">Summe:</td>
|
||||
<td style="text-align:right">{{ '%.2f'|format(total_gp) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,422 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,107 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">Superadmin – Dashboard</h1></div>
|
||||
<div class="level-right">
|
||||
<a class="button is-primary" href="{{ url_for('superadmin.firma_create') }}">+ Neue Firma</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline mt-3">
|
||||
<div class="column is-one-third">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Firmen</p>
|
||||
<p class="title">{{ firmsen|length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Benutzer (gesamt)</p>
|
||||
<p class="title">{{ gesamt_user }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Projekte (gesamt)</p>
|
||||
<p class="title">{{ gesamt_projekte }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-one-quarter">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Lizenzen (gesamt)</p>
|
||||
<p class="title">{{ gesamt_lizenzen }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-quarter">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Mitarbeiter-Plätze</p>
|
||||
<p class="title">{{ gesamt_user }} / {{ gesamt_max_mitarbeiter }}</p>
|
||||
<p class="is-size-7 has-text-grey">belegt / verfügbar</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-quarter">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Modul-Plätze (belegt)</p>
|
||||
<p class="title">{{ belegte_module_slots }} / {{ gesamt_max_module_slots }}</p>
|
||||
<p class="is-size-7 has-text-grey">belegt / verfügbar</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-quarter">
|
||||
<div class="box has-text-centered">
|
||||
<p class="heading">Module (verfügbar)</p>
|
||||
<p class="title">{{ gesamt_module_anzahl }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box mt-4" style="background:#fff8e6;border-color:#ffc107">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px">
|
||||
<div>
|
||||
<h3 class="title is-5 mb-1">Neue Registrierungen</h3>
|
||||
<p class="has-text-grey is-size-7">Aktiviert/deaktiviert die Registrierung neuer Firmen über die Startseite.</p>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<input type="checkbox" id="reg-checkbox" {{ 'checked' if registration_enabled else '' }}
|
||||
hx-post="{{ url_for('superadmin.registration_toggle') }}"
|
||||
hx-swap="none"
|
||||
onchange="var l=document.getElementById('reg-label');l.textContent=this.checked?'Aktiviert':'Deaktiviert';l.className='has-text-weight-semibold '+(this.checked?'has-text-success':'has-text-danger')">
|
||||
<label for="reg-checkbox" id="reg-label" class="has-text-weight-semibold {{ 'has-text-success' if registration_enabled else 'has-text-danger' }}">
|
||||
{{ 'Aktiviert' if registration_enabled else 'Deaktiviert' }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box mt-4">
|
||||
<h2 class="title is-5">Alle Firmen</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Benutzer</th><th>Lizenz-Plätze</th><th>Modul-Plätze</th><th>E-Vergabe</th><th>Aktiv</th><th>Aktion</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in firmsen %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('superadmin.firma_detail', company_id=f.id) }}">{{ f.name }}</a></td>
|
||||
<td>{{ f._user_count }}</td>
|
||||
<td>{{ f._license_slots }}</td>
|
||||
<td>{{ f._module_slots }}</td>
|
||||
<td><span class="tag {{ 'is-info' if f.evergabe_aktiviert else 'is-light' }}">{{ 'aktiv' if f.evergabe_aktiviert else '–' }}</span></td>
|
||||
<td><span class="tag {{ 'is-success' if f.aktiv else 'is-danger' }}">{{ 'aktiv' if f.aktiv else 'deaktiviert' }}</span></td>
|
||||
<td>
|
||||
<a class="button is-small is-link" href="{{ url_for('superadmin.firma_detail', company_id=f.id) }}">Details</a>
|
||||
<a class="button is-small {{ 'is-warning' if f.aktiv else 'is-success' }}" href="{{ url_for('superadmin.firma_toggle', company_id=f.id) }}">
|
||||
{{ 'Deaktivieren' if f.aktiv else 'Aktivieren' }}
|
||||
</a>
|
||||
<a class="button is-small {{ 'is-info' if f.evergabe_aktiviert else 'is-light' }}" href="{{ url_for('superadmin.firma_evergabe_toggle', company_id=f.id) }}">
|
||||
{{ 'EV ✕' if f.evergabe_aktiviert else 'EV +' }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,225 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">{{ company.name }}</h1></div>
|
||||
<div class="level-right" style="display:flex;gap:4px">
|
||||
<a class="button is-small is-link" href="{{ url_for('superadmin.firma_edit', company_id=company.id) }}">⚙ Bearbeiten</a>
|
||||
<a class="button is-small" href="{{ url_for('superadmin.dashboard') }}">← Alle Firmen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Firmendaten</h2>
|
||||
<div class="columns is-multiline is-size-7">
|
||||
<div class="column is-3"><strong>Name:</strong> {{ company.name }}</div>
|
||||
<div class="column is-5"><strong>Str./HsNr:</strong> {{ company.strasse or '–' }} {{ company.house_number or '–' }}</div>
|
||||
<div class="column is-4"><strong>PLZ/Ort:</strong> {{ company.plz or '–' }} {{ company.ort or '–' }}</div>
|
||||
<div class="column is-3"><strong>Telefon:</strong> {{ company.telefon or '–' }}</div>
|
||||
<div class="column is-3"><strong>E-Mail:</strong> {{ company.email or '–' }}</div>
|
||||
<div class="column is-3"><strong>Status:</strong> <span class="tag {{ 'is-success' if company.aktiv else 'is-danger' }}">{{ 'aktiv' if company.aktiv else 'deaktiviert' }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Benutzer</h2>
|
||||
<details class="mb-3">
|
||||
<summary class="has-text-link" style="cursor:pointer">+ Benutzer hinzufügen</summary>
|
||||
<form method="POST" action="{{ url_for('superadmin.firma_user_create', company_id=company.id) }}" class="mt-2 box" style="padding:1rem">
|
||||
<div class="columns is-multiline is-variable is-2">
|
||||
<div class="column is-4"><input class="input is-small" type="email" name="email" placeholder="E-Mail *" required></div>
|
||||
<div class="column is-3"><input class="input is-small" type="text" name="vorname" placeholder="Vorname"></div>
|
||||
<div class="column is-3"><input class="input is-small" type="text" name="nachname" placeholder="Nachname"></div>
|
||||
<div class="column is-2">
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="rolle">
|
||||
<option value="mitarbeiter">Mitarbeiter</option>
|
||||
<option value="firmadmin">Firmadmin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4"><input class="input is-small" type="password" name="password" placeholder="Passwort *" minlength="6" required></div>
|
||||
<div class="column is-8">
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_projekte_anlegen" value="1" checked> Projekte anlegen</label>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_lv_verwalten" value="1" checked> LV verwalten</label>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_preise_sehen" value="1"> Preise sehen</label>
|
||||
<label class="checkbox is-size-7"><input type="checkbox" name="darf_aufmass_verwalten" value="1" checked> Aufmaße verwalten</label>
|
||||
{% if company.evergabe_aktiviert %}
|
||||
<br>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_evergabe_nutzen" value="1"> E-Vergabe</label>
|
||||
<label class="checkbox is-size-7 mr-3"><input type="checkbox" name="darf_kopfdaten_holen" value="1"> Kopfdaten holen</label>
|
||||
<label class="checkbox is-size-7"><input type="checkbox" name="darf_aufmass_uebertragen" value="1"> Aufmaße übertragen</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="column is-12"><button class="button is-small is-primary" type="submit">Anlegen</button></div>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>E-Mail</th><th>Name</th><th>Rolle</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.email }}</td>
|
||||
<td>{{ u.full_name }}</td>
|
||||
<td><span class="tag {{ 'is-info' if u.is_superadmin() else ('is-warning' if u.is_firmadmin() else 'is-light') }}">{{ u.rolle }}</span></td>
|
||||
<td><span class="tag {{ 'is-success' if u.aktiv else 'is-danger' }}">{{ 'aktiv' if u.aktiv else 'deaktiviert' }}</span></td>
|
||||
<td>
|
||||
<a class="button is-small {{ 'is-warning' if u.aktiv else 'is-success' }}" href="{{ url_for('superadmin.user_toggle', user_id=u.id) }}">{{ 'Deaktivieren' if u.aktiv else 'Aktivieren' }}</a>
|
||||
<a class="button is-small is-link" href="{{ url_for('admin.mitarbeiter_bearbeiten', user_id=u.id) }}">Bearbeiten</a>
|
||||
<a class="button is-small is-link" href="{{ url_for('admin.mitarbeiter_rechte', user_id=u.id) }}">Rechte</a>
|
||||
{% if not u.is_superadmin() %}
|
||||
<a class="button is-small is-link" href="{{ url_for('superadmin.user_make_superadmin', user_id=u.id) }}">Zum Superadmin</a>
|
||||
{% else %}
|
||||
<a class="button is-small is-light" href="{{ url_for('superadmin.user_make_superadmin', user_id=u.id) }}">Superadmin entziehen</a>
|
||||
{% endif %}
|
||||
{% if u.id != current_user.id %}
|
||||
<form method="POST" action="{{ url_for('superadmin.user_loeschen', user_id=u.id) }}" style="display:inline" onsubmit="return confirm('Benutzer {{ u.email }} wirklich löschen?')">
|
||||
<button class="button is-small is-danger">Löschen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Lizenzen</h2>
|
||||
{% if licenses %}
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>UID</th><th>Max. Benutzer</th><th>Max. Module</th><th>Unlimited Benutzer</th><th>Unlimited Module</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
{% for lic in licenses %}
|
||||
<tr>
|
||||
<td><code style="font-size:0.7rem">{{ lic.uid[:16] }}…</code></td>
|
||||
<td>{{ lic.user_slots_display() }}</td>
|
||||
<td>{{ lic.module_slots_display() }}</td>
|
||||
<td><span class="tag {{ 'is-success' if lic.unlimited_users else 'is-light' }}">{{ 'Ja' if lic.unlimited_users else 'Nein' }}</span></td>
|
||||
<td><span class="tag {{ 'is-success' if lic.unlimited_modules else 'is-light' }}">{{ 'Ja' if lic.unlimited_modules else 'Nein' }}</span></td>
|
||||
<td><span class="tag {{ 'is-success' if lic.aktiv else 'is-danger' }}">{{ 'aktiv' if lic.aktiv else 'inaktiv' }}</span></td>
|
||||
<td>
|
||||
<a class="button is-small is-link" onclick="toggleEditLic(this, {{ lic.id }})">Bearbeiten</a>
|
||||
<form method="POST" action="{{ url_for('superadmin.license_delete', license_id=lic.id) }}" style="display:inline" onsubmit="return confirm('Lizenz wirklich löschen?')">
|
||||
<button class="button is-small is-danger">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="edit-lic-{{ lic.id }}" style="display:none">
|
||||
<td colspan="7" style="padding:0">
|
||||
<form method="POST" action="{{ url_for('superadmin.license_edit', license_id=lic.id) }}" class="box" style="margin:0.5rem;padding:0.75rem">
|
||||
<div class="columns is-multiline is-variable is-2">
|
||||
<div class="column is-3"><label class="is-size-7">Max. Benutzer</label><input class="input is-small" type="number" name="max_mitarbeiter" value="{{ lic.max_mitarbeiter }}" min="1"></div>
|
||||
<div class="column is-3"><label class="is-size-7">Max. Module</label><input class="input is-small" type="number" name="max_module_slots" value="{{ lic.max_module_slots }}" min="1"></div>
|
||||
<div class="column is-3"><label class="checkbox is-size-7"><input type="checkbox" name="unlimited_users" value="1" {{ 'checked' if lic.unlimited_users }}> Unbegrenzte Benutzer</label></div>
|
||||
<div class="column is-3"><label class="checkbox is-size-7"><input type="checkbox" name="unlimited_modules" value="1" {{ 'checked' if lic.unlimited_modules }}> Unbegrenzte Module</label></div>
|
||||
<div class="column is-12"><label class="is-size-7">Zugewiesene Module</label>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:4px">
|
||||
{% for m in all_modules %}
|
||||
{% set lm_active = lic.modules.filter_by(module_id=m.id, aktiv=True).first() %}
|
||||
<label class="checkbox is-size-7"><input type="checkbox" name="modules" value="{{ m.id }}" {{ 'checked' if lm_active }}> {{ m.icon }} {{ m.titel }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12"><button class="button is-small is-primary">Speichern</button> <a class="button is-small" onclick="toggleEditLic(this, {{ lic.id }})">Abbrechen</a></div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="is-size-7 has-text-grey mb-2">Keine Lizenzen vorhanden.</p>
|
||||
{% endif %}
|
||||
<details>
|
||||
<summary class="has-text-link" style="cursor:pointer">+ Neue Lizenz erstellen</summary>
|
||||
<form method="POST" action="{{ url_for('superadmin.license_create', company_id=company.id) }}" class="mt-2 box" style="padding:0.75rem">
|
||||
<div class="columns is-multiline is-variable is-2">
|
||||
<div class="column is-3"><label class="is-size-7">Max. Benutzer</label><input class="input is-small" type="number" name="max_mitarbeiter" value="5" min="1"></div>
|
||||
<div class="column is-3"><label class="is-size-7">Max. Module</label><input class="input is-small" type="number" name="max_module_slots" value="5" min="1"></div>
|
||||
<div class="column is-3"><label class="checkbox is-size-7"><input type="checkbox" name="unlimited_users" value="1"> Unbegrenzte Benutzer</label></div>
|
||||
<div class="column is-3"><label class="checkbox is-size-7"><input type="checkbox" name="unlimited_modules" value="1"> Unbegrenzte Module</label></div>
|
||||
<div class="column is-12"><label class="is-size-7">Module auswählen</label>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:4px">
|
||||
{% for m in all_modules %}
|
||||
<label class="checkbox is-size-7"><input type="checkbox" name="modules" value="{{ m.id }}"> {{ m.icon }} {{ m.titel }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12"><button class="button is-small is-primary">Lizenz erstellen</button></div>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Firmen-Modul-Zuweisung</h2>
|
||||
<p class="is-size-7 has-text-grey mb-2">Module direkt der Firma zuweisen (ohne Lizenz). Aktivierte Module stehen dem Firmadmin + allen Mitarbeitern zur Verfügung.</p>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Modul</th><th>Kategorie</th><th>Standard</th><th>Status</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
{% for m in all_modules %}
|
||||
<tr>
|
||||
<td>{{ m.icon }} {{ m.titel }}</td>
|
||||
<td>{{ m.kategorie or '–' }}</td>
|
||||
<td><span class="tag {{ 'is-success' if m.standard else 'is-light' }}">{{ 'Standard' if m.standard else '–' }}</span></td>
|
||||
<td>
|
||||
{% if m.id in company_modules %}
|
||||
<span class="tag is-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="tag is-light">Inaktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a class="button is-small {{ 'is-warning' if m.id in company_modules else 'is-success' }}"
|
||||
href="{{ url_for('superadmin.company_module_toggle', company_id=company.id, module_id=m.id) }}">
|
||||
{{ 'Deaktivieren' if m.id in company_modules else 'Aktivieren' }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">E-Vergabe Addon</h2>
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<p class="is-size-6">Status: <span class="tag {{ 'is-info' if company.evergabe_aktiviert else 'is-light' }}">{{ 'Freigeschaltet' if company.evergabe_aktiviert else 'Nicht aktiv' }}</span></p>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<a class="button is-small {{ 'is-warning' if company.evergabe_aktiviert else 'is-info' }}" href="{{ url_for('superadmin.firma_evergabe_toggle', company_id=company.id) }}">
|
||||
{{ 'Deaktivieren' if company.evergabe_aktiviert else 'Freischalten' }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if company.evergabe_aktiviert %}
|
||||
<p class="is-size-7 has-text-grey mt-2">Benutzer: {{ company.evergabe_benutzer or '–' }} | Name: {{ company.evergabe_name or '–' }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Projekte</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Projekte</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in projekte %}
|
||||
<tr>
|
||||
<td>{{ p.bezeichnung or '–' }}</td>
|
||||
<td><span class="tag {{ 'is-success' if p.status == 'aktiv' else 'is-light' }}">{{ p.status }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleEditLic(el, licId){
|
||||
var row=document.getElementById('edit-lic-'+licId);
|
||||
if(!row)return;
|
||||
row.style.display=row.style.display==='none'?'':'none';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,125 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left"><h1 class="title is-3">{{ titel }}</h1></div>
|
||||
<div class="level-right">
|
||||
<a class="button is-small" href="{{ url_for('superadmin.dashboard') }}">← Zurück</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" style="max-width:800px;margin:0 auto">
|
||||
<form method="POST">
|
||||
<h2 class="title is-5 mb-3">Firmendaten</h2>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Firmenname *</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="name" value="{{ company.name if company else '' }}" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">E-Mail (Firma)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="email" name="email" value="{{ company.email if company else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<label class="label">Straße</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="strasse" value="{{ company.strasse if company else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Hausnummer</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="house_number" value="{{ company.house_number if company else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">PLZ</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="plz" value="{{ company.plz if company else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Ort</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="ort" value="{{ company.ort if company else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Telefon</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="telefon" value="{{ company.telefon if company else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not company %}
|
||||
<hr class="my-5">
|
||||
<h2 class="title is-5 mb-3">Firmadmin-User direkt anlegen (optional)</h2>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">E-Mail *</label>
|
||||
<div class="control">
|
||||
<input class="input" type="email" name="admin_email" placeholder="admin@firma.de">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Passwort *</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="admin_password" minlength="6" placeholder="mind. 6 Zeichen">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label"> </label>
|
||||
<div class="control">
|
||||
<label class="checkbox is-size-7"><input type="checkbox" id="chk-admin" onchange="document.getElementById('admin-fields').style.display=this.checked?'':'none'"> Admin-User anlegen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12" id="admin-fields" style="display:none">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Vorname</label>
|
||||
<div class="control"><input class="input is-small" type="text" name="admin_vorname"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Nachname</label>
|
||||
<div class="control"><input class="input is-small" type="text" name="admin_nachname"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="field mt-5">
|
||||
<button class="button is-primary" type="submit">{{ 'Speichern' if company else 'Firma anlegen' }}</button>
|
||||
<a class="button is-light ml-2" href="{{ url_for('superadmin.dashboard') }}">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user