280 lines
13 KiB
HTML
280 lines
13 KiB
HTML
|
|
{% extends "base.html" %}
|
|||
|
|
{% block title %}Projekte{% endblock %}
|
|||
|
|
{% block content %}
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
.kat-tabelle { width: 100%; border-collapse: collapse; }
|
|||
|
|
.kat-tabelle th {
|
|||
|
|
background: #F2F3F4; padding: 0.6rem 0.5rem;
|
|||
|
|
text-align: left; font-size: 0.85rem; cursor: pointer;
|
|||
|
|
user-select: none; white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.kat-tabelle th:hover { background: #E5E7E8; }
|
|||
|
|
.kat-tabelle th .sort-pfeil {
|
|||
|
|
display: inline-block; margin-left: 0.3rem; opacity: 0.3; font-size: 0.7rem;
|
|||
|
|
}
|
|||
|
|
.kat-tabelle th.sort-aktiv .sort-pfeil { opacity: 1; color: var(--primary); }
|
|||
|
|
.kat-tabelle td {
|
|||
|
|
padding: 0.5rem; border-bottom: 1px solid var(--border);
|
|||
|
|
vertical-align: middle; font-size: 0.9rem;
|
|||
|
|
}
|
|||
|
|
.kat-zeile { cursor: pointer; }
|
|||
|
|
.kat-zeile:hover { background: #F8F9FA; }
|
|||
|
|
.kat-zeile.offen { background: #EBF5FB; }
|
|||
|
|
.kat-thumb {
|
|||
|
|
width: 40px; height: 40px; border-radius: 6px; object-fit: cover;
|
|||
|
|
background: linear-gradient(135deg, #ECF0F1, #D5DBDB);
|
|||
|
|
display: flex; align-items: center; justify-content: center;
|
|||
|
|
font-size: 1.2rem; color: var(--text-light);
|
|||
|
|
}
|
|||
|
|
.kat-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 6px; }
|
|||
|
|
.kat-detail { display: none; background: #F8F9FA; }
|
|||
|
|
.kat-detail.offen { display: table-row; }
|
|||
|
|
.kat-detail-inner {
|
|||
|
|
display: flex; gap: 1.5rem; padding: 1rem 0.5rem; flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
.kat-detail-bild img {
|
|||
|
|
max-width: 260px; max-height: 260px; border-radius: 8px;
|
|||
|
|
border: 1px solid var(--border); background: #fff;
|
|||
|
|
}
|
|||
|
|
.kat-detail-bild-platzhalter {
|
|||
|
|
width: 260px; height: 200px; border-radius: 8px;
|
|||
|
|
background: linear-gradient(135deg, #ECF0F1, #D5DBDB);
|
|||
|
|
display: flex; align-items: center; justify-content: center;
|
|||
|
|
font-size: 3rem; color: var(--text-light);
|
|||
|
|
}
|
|||
|
|
.kat-detail-info { flex: 1; min-width: 260px; }
|
|||
|
|
.kat-detail-info h3 { margin-top: 0; }
|
|||
|
|
.kat-detail-info .meta {
|
|||
|
|
font-size: 0.85rem; color: var(--text-light); margin-bottom: 0.6rem;
|
|||
|
|
}
|
|||
|
|
.kat-dateien { margin-top: 0.5rem; }
|
|||
|
|
.kat-dateien a {
|
|||
|
|
display: inline-block; margin: 0.2rem 0.4rem 0.2rem 0;
|
|||
|
|
padding: 0.2rem 0.6rem; background: #fff; border: 1px solid var(--border);
|
|||
|
|
border-radius: 4px; text-decoration: none; font-size: 0.85rem; color: var(--text);
|
|||
|
|
}
|
|||
|
|
.kat-dateien a:hover { background: #ECF0F1; }
|
|||
|
|
</style>
|
|||
|
|
|
|||
|
|
<h1>Projekte</h1>
|
|||
|
|
<p style="color: var(--text-light);">
|
|||
|
|
Was die AG gerade macht — laufende und fertige Projekte. Klick auf eine Zeile fuer Details.
|
|||
|
|
Spalten-Header klicken zum Sortieren.
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
{% if projekte %}
|
|||
|
|
<div class="card" style="padding: 0;">
|
|||
|
|
<table class="kat-tabelle" id="kat-tabelle">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th></th>
|
|||
|
|
<th data-sort="titel">Titel<span class="sort-pfeil">▲▼</span></th>
|
|||
|
|
<th data-sort="person">Wer<span class="sort-pfeil">▲▼</span></th>
|
|||
|
|
<th data-sort="status">Status<span class="sort-pfeil">▲▼</span></th>
|
|||
|
|
<th data-sort="erfolg">Erfolg<span class="sort-pfeil">▲▼</span></th>
|
|||
|
|
<th data-sort="datum">Datum<span class="sort-pfeil">▲▼</span></th>
|
|||
|
|
<th title="MakerWorld">MW</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
{% for p in projekte %}
|
|||
|
|
{# ====== Kompakte Zeile ====== #}
|
|||
|
|
<tr class="kat-zeile" data-projekt="{{ p.id }}"
|
|||
|
|
data-titel="{{ p.titel|lower }}"
|
|||
|
|
data-person="{{ (p.vorname or '')|lower }}"
|
|||
|
|
data-status="{{ p.status_lebenszyklus }}"
|
|||
|
|
data-erfolg="{% if p.selbsteinschaetzung == 'zufrieden' %}1{% elif p.selbsteinschaetzung == 'mittel' %}2{% elif p.selbsteinschaetzung == 'gelernt' %}3{% else %}9{% endif %}"
|
|||
|
|
data-datum="{{ p.abgeschlossen_am or p.geaendert_am or '' }}">
|
|||
|
|
<td>
|
|||
|
|
<div class="kat-thumb">
|
|||
|
|
{% if p.hat_cover %}
|
|||
|
|
<img src="{{ url_for('api.projekt_cover', projekt_id=p.id) }}" alt="">
|
|||
|
|
{% else %}🎯{% endif %}
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td><strong>{{ p.titel }}</strong></td>
|
|||
|
|
<td>{{ tier_emoji(p.tier or '') }} {{ p.vorname or '—' }}</td>
|
|||
|
|
<td>
|
|||
|
|
{% if p.status_lebenszyklus == 'in_arbeit' %}<span class="badge badge-mittel">🔧 laeuft</span>
|
|||
|
|
{% else %}<span class="badge badge-anfaenger">✅ fertig</span>{% endif %}
|
|||
|
|
</td>
|
|||
|
|
<td>
|
|||
|
|
{% if p.selbsteinschaetzung == 'zufrieden' %}🟢
|
|||
|
|
{% elif p.selbsteinschaetzung == 'mittel' %}🟡
|
|||
|
|
{% elif p.selbsteinschaetzung == 'gelernt' %}🔴
|
|||
|
|
{% else %}—{% endif %}
|
|||
|
|
</td>
|
|||
|
|
<td style="font-size: 0.8rem; color: var(--text-light); white-space: nowrap;">
|
|||
|
|
{{ (p.abgeschlossen_am or p.geaendert_am or '')|iso_format('%d.%m.%y') }}
|
|||
|
|
</td>
|
|||
|
|
<td style="text-align: center;">
|
|||
|
|
{% if p.makerworld_url %}
|
|||
|
|
<a href="{{ p.makerworld_url }}" target="_blank" rel="noopener noreferrer"
|
|||
|
|
onclick="event.stopPropagation();" title="Auf MakerWorld" style="text-decoration: none;">🌐</a>
|
|||
|
|
{% endif %}
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
|
|||
|
|
{# ====== Detail-Zeile (versteckt bis Klick) ====== #}
|
|||
|
|
<tr class="kat-detail" data-fuer="{{ p.id }}">
|
|||
|
|
<td colspan="7">
|
|||
|
|
<div class="kat-detail-inner">
|
|||
|
|
<div class="kat-detail-bild">
|
|||
|
|
{% if p.hat_cover %}
|
|||
|
|
<img src="{{ url_for('api.projekt_cover', projekt_id=p.id) }}" alt="">
|
|||
|
|
{% else %}
|
|||
|
|
<div class="kat-detail-bild-platzhalter">🎯</div>
|
|||
|
|
{% endif %}
|
|||
|
|
</div>
|
|||
|
|
<div class="kat-detail-info">
|
|||
|
|
<h3>{{ p.titel }}</h3>
|
|||
|
|
<div class="meta">
|
|||
|
|
{{ tier_emoji(p.tier or '') }} <strong>{{ p.vorname or '—' }}</strong>
|
|||
|
|
{% if p.software_titel %} · 🖥️ {{ p.software_titel }}{% endif %}
|
|||
|
|
{% if p.drucker_name %} · 🖨️ {{ p.drucker_name }}{% endif %}
|
|||
|
|
{% if p.abgeschlossen_am %} · abgeschlossen {{ p.abgeschlossen_am|iso_format('%d.%m.%Y') }}{% endif %}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{% if p.beschreibung_md %}
|
|||
|
|
<div style="margin-bottom: 0.8rem;">{{ p.beschreibung_md|markdown }}</div>
|
|||
|
|
{% else %}
|
|||
|
|
<p style="color: var(--text-light);"><em>Keine Beschreibung.</em></p>
|
|||
|
|
{% endif %}
|
|||
|
|
|
|||
|
|
{% if p.dateien %}
|
|||
|
|
<div class="kat-dateien">
|
|||
|
|
<strong style="font-size: 0.85rem;">Dateien:</strong>
|
|||
|
|
{% for d in p.dateien %}
|
|||
|
|
<a href="{{ url_for('api.projekt_datei_download', projekt_id=p.id, datei_id=d.id) }}">
|
|||
|
|
📎 {{ d.dateiname }}
|
|||
|
|
<span style="color: var(--text-light); font-size: 0.75rem;">
|
|||
|
|
({{ d.art }}, {{ (d.groesse_bytes // 1024) if d.groesse_bytes else 0 }} KB)
|
|||
|
|
</span>
|
|||
|
|
</a>
|
|||
|
|
{% endfor %}
|
|||
|
|
</div>
|
|||
|
|
{% endif %}
|
|||
|
|
|
|||
|
|
{% if p.makerworld_url %}
|
|||
|
|
<div style="margin-top: 0.8rem;">
|
|||
|
|
<a href="{{ p.makerworld_url }}" target="_blank" rel="noopener noreferrer"
|
|||
|
|
class="btn btn-secondary" style="font-size: 0.85rem;">
|
|||
|
|
🌐 Auf MakerWorld ansehen ↗
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
{% endif %}
|
|||
|
|
|
|||
|
|
{# ====== "Sowas mache ich auch"-Button ====== #}
|
|||
|
|
<div style="margin-top: 1rem; padding-top: 0.8rem; border-top: 1px solid var(--border);">
|
|||
|
|
{% if p.profil_id == eigene_profil_id %}
|
|||
|
|
<span style="font-size: 0.85rem; color: var(--text-light); font-style: italic;">
|
|||
|
|
Dein eigenes Projekt.
|
|||
|
|
</span>
|
|||
|
|
{% elif darf_wiederholen %}
|
|||
|
|
<form method="post"
|
|||
|
|
action="{{ url_for('profil.projekt_aus_vorlage', vorlage_id=p.id) }}"
|
|||
|
|
onclick="event.stopPropagation();"
|
|||
|
|
onsubmit="return confirm('Neues Projekt mit Vorlage \'{{ p.titel }}\' anlegen?')">
|
|||
|
|
<button class="btn btn-primary" style="font-size: 0.9rem;">
|
|||
|
|
+ Sowas mache ich auch
|
|||
|
|
</button>
|
|||
|
|
</form>
|
|||
|
|
{% else %}
|
|||
|
|
<span style="font-size: 0.85rem; color: var(--text-light);">
|
|||
|
|
ℹ️ {{ wiederhol_sperr_grund }}
|
|||
|
|
</span>
|
|||
|
|
{% endif %}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
{% endfor %}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
{% else %}
|
|||
|
|
<div class="card">
|
|||
|
|
<p style="color: var(--text-light);">Noch keine Projekte im Katalog.</p>
|
|||
|
|
</div>
|
|||
|
|
{% endif %}
|
|||
|
|
|
|||
|
|
{% endblock %}
|
|||
|
|
|
|||
|
|
{% block scripts %}
|
|||
|
|
<script>
|
|||
|
|
(function() {
|
|||
|
|
var tabelle = document.getElementById('kat-tabelle');
|
|||
|
|
if (!tabelle) return;
|
|||
|
|
var tbody = tabelle.querySelector('tbody');
|
|||
|
|
|
|||
|
|
// ===== Aufklappen =====
|
|||
|
|
tabelle.querySelectorAll('.kat-zeile').forEach(function(zeile) {
|
|||
|
|
zeile.addEventListener('click', function(e) {
|
|||
|
|
if (e.target.tagName === 'A') return; // MakerWorld-Link nicht abfangen
|
|||
|
|
var pid = zeile.dataset.projekt;
|
|||
|
|
var detail = tabelle.querySelector('.kat-detail[data-fuer="' + pid + '"]');
|
|||
|
|
if (!detail) return;
|
|||
|
|
var offen = detail.classList.toggle('offen');
|
|||
|
|
zeile.classList.toggle('offen', offen);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ===== Sortieren =====
|
|||
|
|
var sortRichtung = {}; // key -> 'asc' | 'desc'
|
|||
|
|
|
|||
|
|
function sortiereNach(key) {
|
|||
|
|
var richtung = sortRichtung[key] === 'asc' ? 'desc' : 'asc';
|
|||
|
|
sortRichtung = {}; // nur eine Sortierung aktiv
|
|||
|
|
sortRichtung[key] = richtung;
|
|||
|
|
|
|||
|
|
// Zeilen-Paare einsammeln: [kat-zeile, kat-detail]
|
|||
|
|
var paare = [];
|
|||
|
|
tabelle.querySelectorAll('.kat-zeile').forEach(function(zeile) {
|
|||
|
|
var pid = zeile.dataset.projekt;
|
|||
|
|
var detail = tabelle.querySelector('.kat-detail[data-fuer="' + pid + '"]');
|
|||
|
|
paare.push({ zeile: zeile, detail: detail, key: zeile.dataset[key] || '' });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Sortier-Helper: erfolg + status sind numerisch/string-codiert, titel/person/datum string-lexikografisch
|
|||
|
|
paare.sort(function(a, b) {
|
|||
|
|
var va = a.key, vb = b.key;
|
|||
|
|
if (key === 'erfolg') { va = parseInt(va) || 9; vb = parseInt(vb) || 9; }
|
|||
|
|
if (va < vb) return richtung === 'asc' ? -1 : 1;
|
|||
|
|
if (va > vb) return richtung === 'asc' ? 1 : -1;
|
|||
|
|
return 0;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Neu in tbody einhaengen (paar fuer paar — Detail folgt direkt)
|
|||
|
|
paare.forEach(function(p) {
|
|||
|
|
tbody.appendChild(p.zeile);
|
|||
|
|
if (p.detail) tbody.appendChild(p.detail);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Sortier-Indikator
|
|||
|
|
tabelle.querySelectorAll('th[data-sort]').forEach(function(th) {
|
|||
|
|
th.classList.remove('sort-aktiv');
|
|||
|
|
var pfeil = th.querySelector('.sort-pfeil');
|
|||
|
|
if (pfeil) pfeil.textContent = '▲▼';
|
|||
|
|
});
|
|||
|
|
var aktivTh = tabelle.querySelector('th[data-sort="' + key + '"]');
|
|||
|
|
if (aktivTh) {
|
|||
|
|
aktivTh.classList.add('sort-aktiv');
|
|||
|
|
aktivTh.querySelector('.sort-pfeil').textContent = richtung === 'asc' ? '▲' : '▼';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tabelle.querySelectorAll('th[data-sort]').forEach(function(th) {
|
|||
|
|
th.addEventListener('click', function() {
|
|||
|
|
sortiereNach(th.dataset.sort);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Initial: nach Datum absteigend (neueste oben)
|
|||
|
|
sortiereNach('datum');
|
|||
|
|
sortiereNach('datum'); // zweimal -> desc
|
|||
|
|
})();
|
|||
|
|
</script>
|
|||
|
|
{% endblock %}
|