Files
3d.lehrstun.de/templates/admin_dashboard.html
Hans Puettmann 8cbd803328 Software-Sektion + Drucker-Verwaltung + Slot-System + Selbstlern-Loop
Vier zusammenhaengende Iterationen, die das Funktions-Geruest der AG aufbauen.
Alle 6 Migrationen sind idempotent und legen Schema-Erweiterungen + initiale
Seeds an (Mock-Projekte nur, falls Profil 100 existiert — auf prod nicht).

Software-Sektion (Etappe 3):
  - software_seite-Tabelle: schlanker Karten-Katalog (slug, titel, urls,
    plattform, kategorie, sortierung, Cover-BLOB)
  - 7 Initial-Karten (Bambu Studio/Handy, Printables, MakerWorld,
    TinkerCAD, Onshape, Gridfinity-Generator)
  - Admin-Verwaltung: Liste, Anlegen-Form, Bearbeiten, Aktiv-Toggle,
    Hart-Loeschen, Bild-Upload via Crop-Tool (Strg+V/Drag&Drop, 400x400)

Drucker-Verwaltung (Etappe 5-vorgezogen):
  - drucker.freigegeben + gesperrt_grund (Lehrer-Sperre fuer Reparatur)
  - drucker.manual_url + troubleshooting_url (Override fuer Modell-Defaults)
  - SuS-View /drucker (eingeloggt): Kacheln + Sperre-Banner + Belegung
  - Admin /admin/drucker: Status-Override (idle/printing/failed/...),
    Sperren/Freigeben mit Grund, Inline-Edit fuer URLs

Slot-System / Reservierung (Etappe 7-vorgezogen):
  - profil.reservieren_freigeschaltet (Lehrer schaltet manuell frei,
    ersetzt L1-Gate-Automatismus)
  - services/slots.py: Konflikt-Check, Belegungs-Helper
  - SuS-Routen /cockpit/slot/neu (mit Konflikt-Check), /cockpit/slots,
    Stornieren
  - Admin /admin/slots Uebersicht + /admin/slot/neu (Lehrer reserviert
    fuer SuS) + Hart-Loeschen + Profil-Freigabe-Toggle

Selbstlern-Loop (Etappe 6 — Kern):
  - projekt-Spalten: 3-Achsen-Eval (Gesamt + Planung + Ausfuehrung),
    was_klappte_nicht, cover_blob, modellier_software_slug,
    drucker_id_used, makerworld_url
  - Regel: max. 1 aktives Projekt pro SuS, neues nur nach Freigabe
    (auto bei zufrieden/mittel, Lehrer-Review bei gelernt)
  - SuS-Werkstatt-Sektion: Titel + Beschreibung + Software-Dropdown +
    Drucker-Dropdown + MakerWorld-URL + Files-Upload (drag&drop,
    base64, max 30 MB)
  - Cover-Bild via Crop-Tool (Re-use admin_software_bild-Pattern)
  - Abschluss-Block: Pflicht Cover + mindestens 1 Datei
  - Eval-Form: visuelle 3x3 Radio-Karten + 2 Freitexte
  - Lehrer-Review: /admin/projekte mit Filter (wartet/in_arbeit/
    abgeschlossen/rueckfrage), Aktionen freigeben/rueckfrage/loeschen
  - Oeffentlicher Katalog /projekte: Sortier-Tabelle (Titel/Person/
    Status/Erfolg/Datum), aufklappbare Detail mit Cover/Beschreibung/
    Datei-Downloads/MakerWorld-Link
  - Wiederhol-Logik: "Sowas mache ich auch" via Verknuepfung
    (vorlage_projekt_id), bidirektionale Lineage-Banner

Cockpit-Refactoring:
  - Projekt-Karte ganz oben (vor Onboarding) mit "Hier weitermachen"
  - Check-In-Quick-Actions: nach Auswahl direkt zu /cockpit/projekt/neu,
    /drucker, /lektionen, /cockpit/projekt/<aktiv>
  - "Modus aendern"-Button im Eingecheckt-Banner
  - Admin-Dashboard-Hub mit Karten: SuS-Profile, Software, Lektionen,
    Drucker, Reservierungen, Projekte (Badge bei wartenden), Alph

Bug-Fixes:
  - Admin-Login redirected jetzt auf /admin/dashboard statt /admin/profile
  - app.py: neue Jinja-Filter iso_format + dauer_min
  - .gitignore: .claude/ ausschliessen (lokale launch.json)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:04:18 +02:00

242 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 title %}Dashboard — Admin{% endblock %}
{% block content %}
<div class="section-header">
<h1>Dashboard</h1>
<div>
<a href="{{ url_for('admin.admin_logout') }}" class="btn btn-secondary">Admin-Logout</a>
</div>
</div>
{# ====== Schnellnavigation (Verwaltungs-Karten) ====== #}
<div class="grid"
style="grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.8rem; margin-bottom: 1.5rem;">
<a href="{{ url_for('admin.profile_liste') }}" class="card"
style="text-decoration: none; color: inherit; text-align: center; padding: 1.2rem 0.8rem;">
<div style="font-size: 2rem;">👥</div>
<div style="font-weight: 600; margin-top: 0.3rem; color: var(--primary-dark);">SuS-Profile</div>
<div style="font-size: 0.8rem; color: var(--text-light); margin-top: 0.3rem;">Anlegen, PIN, Karten drucken</div>
</a>
<a href="{{ url_for('admin.software_liste') }}" class="card"
style="text-decoration: none; color: inherit; text-align: center; padding: 1.2rem 0.8rem;">
<div style="font-size: 2rem;">🖥️</div>
<div style="font-weight: 600; margin-top: 0.3rem; color: var(--primary-dark);">Software &amp; Generatoren</div>
<div style="font-size: 0.8rem; color: var(--text-light); margin-top: 0.3rem;">Karten, Bilder, Tutorials</div>
</a>
<a href="{{ url_for('admin.admin_lektionen') }}" class="card"
style="text-decoration: none; color: inherit; text-align: center; padding: 1.2rem 0.8rem; opacity: 0.6;"
title="Verwaltung kommt mit Etappe 3-Inline-Edit-UI">
<div style="font-size: 2rem;">📚</div>
<div style="font-weight: 600; margin-top: 0.3rem; color: var(--primary-dark);">Lektionen</div>
<div style="font-size: 0.8rem; color: var(--text-light); margin-top: 0.3rem;">Bearbeiten — kommt</div>
</a>
<a href="{{ url_for('admin.admin_drucker') }}" class="card"
style="text-decoration: none; color: inherit; text-align: center; padding: 1.2rem 0.8rem;">
<div style="font-size: 2rem;">🖨️</div>
<div style="font-weight: 600; margin-top: 0.3rem; color: var(--primary-dark);">Drucker</div>
<div style="font-size: 0.8rem; color: var(--text-light); margin-top: 0.3rem;">Status, Sperre, Links</div>
</a>
<a href="{{ url_for('admin.slot_uebersicht') }}" class="card"
style="text-decoration: none; color: inherit; text-align: center; padding: 1.2rem 0.8rem;">
<div style="font-size: 2rem;">🔖</div>
<div style="font-weight: 600; margin-top: 0.3rem; color: var(--primary-dark);">Reservierungen</div>
<div style="font-size: 0.8rem; color: var(--text-light); margin-top: 0.3rem;">Slot-Plan, fuer SuS anlegen</div>
</a>
<a href="{{ url_for('admin.projekte_uebersicht') }}" class="card"
style="text-decoration: none; color: inherit; text-align: center; padding: 1.2rem 0.8rem;
{% if projekte_warten > 0 %}border-top: 4px solid var(--warning);{% endif %}">
<div style="font-size: 2rem; position: relative; display: inline-block;">
🎯
{% if projekte_warten > 0 %}
<span style="position: absolute; top: -4px; right: -16px; background: var(--warning); color: white;
border-radius: 10px; padding: 0.1rem 0.5rem; font-size: 0.7rem; font-weight: 700;">
{{ projekte_warten }}
</span>
{% endif %}
</div>
<div style="font-weight: 600; margin-top: 0.3rem; color: var(--primary-dark);">Projekte</div>
<div style="font-size: 0.8rem; color: var(--text-light); margin-top: 0.3rem;">
{% if projekte_warten > 0 %}<strong style="color: var(--warning);">{{ projekte_warten }} wartet</strong>
{% else %}alle Projekte ansehen{% endif %}
</div>
</a>
<a href="/admin/feedback" class="card"
style="text-decoration: none; color: inherit; text-align: center; padding: 1.2rem 0.8rem;">
<div style="font-size: 2rem;">🐝</div>
<div style="font-weight: 600; margin-top: 0.3rem; color: var(--primary-dark);">Alph-Feedback</div>
<div style="font-size: 0.8rem; color: var(--text-light); margin-top: 0.3rem;">SuS-Rueckmeldungen</div>
</a>
</div>
{# ====== Schnell-Zahlen ====== #}
<div class="grid" style="grid-template-columns: repeat(3, 1fr); gap: 1rem;">
<div class="stat-card">
<div class="number">{{ zahlen.heute_n }}</div>
<div class="label">heute eingecheckt</div>
</div>
<div class="stat-card" style="{% if zahlen.review_n > 0 %}border: 2px solid var(--warning);{% endif %}">
<div class="number" style="{% if zahlen.review_n > 0 %}color: var(--warning);{% endif %}">{{ zahlen.review_n }}</div>
<div class="label">wartet auf Review</div>
</div>
<div class="stat-card" style="{% if zahlen.drucker_problem_n > 0 %}border: 2px solid var(--danger);{% endif %}">
<div class="number" style="{% if zahlen.drucker_problem_n > 0 %}color: var(--danger);{% endif %}">{{ zahlen.drucker_problem_n }}</div>
<div class="label">Drucker-Problem</div>
</div>
</div>
{# ====== Sektion 1: Wer ist heute hier ====== #}
<div class="card" style="margin-top: 1.5rem;">
<h2>👋 Wer ist heute hier ({{ heute|length }})</h2>
{% if heute %}
<table>
<thead>
<tr><th></th><th>Vorname</th><th>Modus</th><th>Notiz</th><th>seit</th><th></th></tr>
</thead>
<tbody>
{% for h in heute %}
<tr>
<td style="font-size: 1.6rem;">{{ tier_emoji(h.tier) }}</td>
<td><strong>{{ h.vorname }}</strong></td>
<td>
{% if h.modus == 'weiter_wie_bisher' %}Arbeitet weiter
{% elif h.modus == 'neues_projekt' %}🆕 Neues Projekt
{% elif h.modus == 'onboarding' %}📚 Onboarding
{% elif h.modus == 'drucken' %}🖨️ Drucken
{% elif h.modus == 'helfen' %}🤝 Hilft anderen
{% else %}{{ h.modus }}{% endif %}
</td>
<td style="font-style: italic; color: var(--text-light); font-size: 0.9rem;">{{ h.freitext or "" }}</td>
<td style="font-size: 0.85rem; color: var(--text-light);">{{ h.erstellt_am[11:16] if h.erstellt_am else '' }}</td>
<td>
<details>
<summary class="btn btn-secondary" style="padding: 0.2rem 0.5rem; font-size: 0.8rem; list-style: none;">Notiz schreiben</summary>
<form method="post" action="{{ url_for('admin.profil_notiz', profil_id=h.profil_id) }}" style="margin-top: 0.4rem; display: flex; gap: 0.3rem;">
<input type="text" name="text" placeholder="Kurze Nachricht …" required
style="padding: 0.3rem; border: 1px solid var(--border); border-radius: 4px; width: 220px;">
<button class="btn btn-primary" style="padding: 0.3rem 0.6rem; font-size: 0.85rem;">senden</button>
</form>
</details>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p style="color: var(--text-light);">Noch keiner heute eingecheckt.</p>
{% endif %}
</div>
{# ====== Sektion 2: Wartet auf Review ====== #}
<div class="card">
<h2>⏳ Wartet auf Review ({{ review|length }})</h2>
{% if review %}
<table>
<thead><tr><th></th><th>Projekt</th><th>SuS</th><th>Selbsteinschaetzung</th><th>abgeschlossen</th><th></th></tr></thead>
<tbody>
{% for r in review %}
<tr>
<td style="font-size: 1.4rem;">{{ tier_emoji(r.tier) }}</td>
<td><strong>{{ r.titel }}</strong></td>
<td>{{ r.vorname }}</td>
<td>
{% if r.selbsteinschaetzung == 'zufrieden' %}🟢 zufrieden
{% elif r.selbsteinschaetzung == 'mittel' %}🟡 mittel
{% elif r.selbsteinschaetzung == 'gelernt' %}🔴 gelernt
{% endif %}
</td>
<td style="font-size: 0.85rem; color: var(--text-light);">{{ r.abgeschlossen_am[:10] if r.abgeschlossen_am else '' }}</td>
<td>
<a href="#" class="btn btn-primary" style="padding: 0.2rem 0.6rem; font-size: 0.85rem; opacity: 0.5;" title="Etappe 6">Reviewen</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p style="color: var(--text-light);">Keine offenen Reviews. <em>(Selbstlern-Loop kommt mit Etappe 6.)</em></p>
{% endif %}
</div>
{# ====== Sektion 3: Drucker-Status ====== #}
<div class="card">
<h2>🖨️ Drucker-Status</h2>
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.8rem;">
{% for d in drucker %}
<div class="drucker-kachel drucker-{{ d.status or 'idle' }}">
<h3 style="margin-bottom: 0.2rem;">{{ d.name }}</h3>
<p style="font-size: 0.85rem; color: var(--text-light); margin: 0;">
{{ d.modell }}{% if not d.geschlossenes_gehaeuse %} · <span style="color: var(--danger);">offen</span>{% endif %}
</p>
<p style="margin-top: 0.5rem; font-weight: 600;">
{% if d.status == 'printing' %}🟢 drucken
{% elif d.status == 'idle' %}⚪ idle
{% elif d.status == 'paused' %}🟡 pausiert
{% elif d.status == 'finished' %}✅ fertig
{% elif d.status == 'failed' %}🔴 Fehler
{% elif d.status == 'offline' %}⚫ offline
{% else %}⚪ kein Status (API noch nicht in Etappe 5){% endif %}
</p>
{% if d.job_name %}<p style="font-size: 0.85rem;">{{ d.job_name }}</p>{% endif %}
{% if d.progress_pct %}<p style="font-size: 0.85rem;">{{ d.progress_pct|round|int }} %{% if d.restzeit_min %} · noch {{ d.restzeit_min }} min{% endif %}</p>{% endif %}
{% if d.fehlermeldung %}<p style="font-size: 0.85rem; color: var(--danger);">{{ d.fehlermeldung }}</p>{% endif %}
{% if d.notiz %}<p style="font-size: 0.8rem; color: var(--text-light); margin-top: 0.5rem;"> {{ d.notiz }}</p>{% endif %}
</div>
{% endfor %}
</div>
</div>
{# ====== Sektion 4: Wer haengt fest ====== #}
<div class="card">
<h2>💤 Wer hängt fest (>2 Wochen kein Check-In)</h2>
{% if haengen_fest %}
<table>
<thead><tr><th></th><th>Vorname</th><th>letzter Check-In</th><th></th></tr></thead>
<tbody>
{% for h in haengen_fest %}
<tr>
<td style="font-size: 1.4rem;">{{ tier_emoji(h.tier) }}</td>
<td><strong>{{ h.vorname }}</strong></td>
<td style="font-size: 0.85rem; color: var(--text-light);">
{% if h.letzter_check_in %}{{ h.letzter_check_in }}{% else %}<em>noch nie</em>{% endif %}
</td>
<td>
<details>
<summary class="btn btn-secondary" style="padding: 0.2rem 0.5rem; font-size: 0.8rem; list-style: none;">Erinnerung</summary>
<form method="post" action="{{ url_for('admin.profil_notiz', profil_id=h.id) }}" style="margin-top: 0.4rem; display: flex; gap: 0.3rem;">
<input type="hidden" name="typ" value="erinnerung">
<input type="text" name="text" placeholder="Hey, lange nicht gesehen — alles okay?" required
style="padding: 0.3rem; border: 1px solid var(--border); border-radius: 4px; width: 240px;">
<button class="btn btn-primary" style="padding: 0.3rem 0.6rem; font-size: 0.85rem;">senden</button>
</form>
</details>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p style="color: var(--text-light);">Alle dabei. 👍</p>
{% endif %}
</div>
{# ====== Sektion: Offene Rueckfragen ====== #}
{% if rueckfragen %}
<div class="card" style="border-left: 4px solid var(--warning);">
<h2>❓ Offene Rückfragen ({{ rueckfragen|length }})</h2>
<p style="font-size: 0.85rem; color: var(--text-light);">SuS hat noch nicht nachgebessert.</p>
<ul style="margin-top: 0.5rem; padding-left: 1.2rem;">
{% for r in rueckfragen %}
<li style="margin: 0.3rem 0;">
{{ tier_emoji(r.tier) }} <strong>{{ r.vorname }}</strong> ·
„{{ r.projekt_titel }}" ·
seit {{ r.erstellt_am[:10] }} ·
<em>„{{ r.text[:80] }}{% if r.text|length > 80 %}…{% endif %}"</em>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}