Initial commit – AufmaßCreater v2.35

This commit is contained in:
2026-06-10 11:03:43 +02:00
commit 84c933ea9c
2823 changed files with 490495 additions and 0 deletions
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,'&quot;')+'">'+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,'&amp;').replace(/"/g,'&quot;') + '">' + 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 %}