Files

852 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block content %}
<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 %}