Files
3d.lehrstun.de/templates/cockpit.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

324 lines
16 KiB
HTML
Raw 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 %}Cockpit &mdash; {{ vorname }}{% endblock %}
{% block content %}
{# ====== Header ====== #}
<div class="section-header">
<h1 style="display:flex; align-items:center; gap:0.5rem;">
<span style="font-size: 2.2rem;">{{ tier_emoji(tier) }}</span>
Hallo, {{ vorname }}!
</h1>
<a href="{{ url_for('profil.logout') }}" class="btn btn-secondary">Abmelden</a>
</div>
{# ====== BANNER: Lehrer-Notizen (V5-Quick-Action) ====== #}
{% for n in status.notizen %}
<div class="card" style="background: #FEF9E7; border-left: 5px solid var(--warning); margin-bottom: 0.5rem;">
<p style="margin: 0; color: #B7950B;">
<strong>📌 Lehrer-Notiz:</strong> {{ n.text }}
</p>
<form method="post" action="{{ url_for('profil.notiz_gelesen', notiz_id=n.id) }}" style="margin-top: 0.5rem;">
<button class="btn btn-secondary" style="padding: 0.2rem 0.6rem; font-size: 0.85rem;">verstanden</button>
</form>
</div>
{% endfor %}
{# ====== BANNER: Offene Rueckfragen zu eigenen Projekten (V6) ====== #}
{% for r in status.rueckfragen %}
<div class="card" style="background: #FEF9E7; border-left: 5px solid var(--warning); margin-bottom: 0.5rem;">
<p style="margin: 0; color: #B7950B;">
<strong>❓ Rueckfrage zu „{{ r.projekt_titel }}":</strong> {{ r.text }}
</p>
<p style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);">
Bessere das Projekt nach, dann melde es erneut zur Freigabe.
</p>
</div>
{% endfor %}
{# ====== BANNER: Eingegangene Paarungs-Anfragen ====== #}
{% for a in status.offene_anfragen %}
<div class="card" style="background: #EBF5FB; border-left: 5px solid var(--primary); margin-bottom: 0.5rem;">
<p style="margin: 0;">
<strong>{{ tier_emoji(a.von_tier) }} {{ a.von_vorname }}</strong> moechte mit dir
zusammenarbeiten. Magst du?
</p>
<div style="margin-top: 0.5rem; display: flex; gap: 0.5rem;">
<form method="post" action="{{ url_for('profil.paarung_anfrage_beantworten', anfrage_id=a.id, aktion='annehmen') }}">
<button class="btn btn-primary" style="padding: 0.3rem 0.8rem;">Ja, gerne</button>
</form>
<form method="post" action="{{ url_for('profil.paarung_anfrage_beantworten', anfrage_id=a.id, aktion='ablehnen') }}">
<button class="btn btn-secondary" style="padding: 0.3rem 0.8rem;">Nein, lieber nicht</button>
</form>
</div>
</div>
{% endfor %}
{# ====== Check-In-Modal — wenn heute noch nichts gewaehlt wurde ====== #}
{% if not status.check_in_heute %}
<div class="card" style="border-top: 4px solid var(--primary);">
<h2>Was machst du heute? 🎯</h2>
<p style="color: var(--text-light); font-size: 0.9rem;">
Damit Herb und Herr Rechmann auf einen Blick sehen, woran du arbeitest.
Ein Klick reicht — Freitext nur, wenn du willst.
</p>
<form method="post" action="{{ url_for('profil.check_in') }}" style="margin-top: 1rem;">
<div class="check-in-optionen">
{% set hat_projekte = status.aktive_projekte | length > 0 %}
{% if hat_projekte %}
<label class="ci-option">
<input type="radio" name="modus" value="weiter_wie_bisher" checked>
<span>Ich arbeite an „{{ status.aktive_projekte[0].titel }}" weiter</span>
</label>
{% endif %}
<label class="ci-option">
<input type="radio" name="modus" value="neues_projekt" {% if not hat_projekte %}checked{% endif %}>
<span>Neues Projekt anfangen</span>
</label>
{% if status.ist_neu %}
<label class="ci-option">
<input type="radio" name="modus" value="onboarding">
<span>Onboarding-Lektion machen</span>
</label>
{% endif %}
<label class="ci-option">
<input type="radio" name="modus" value="drucken">
<span>Drucken / auf Drucker warten</span>
</label>
<label class="ci-option">
<input type="radio" name="modus" value="helfen">
<span>Jemandem helfen</span>
</label>
<label class="ci-option">
<input type="radio" name="modus" value="sonstiges">
<span>Sonstiges</span>
</label>
</div>
<input type="text" name="freitext" placeholder="(optional) was genau …"
style="width: 100%; padding: 0.5rem; margin-top: 0.6rem; border: 1px solid var(--border); border-radius: 4px;">
<button class="btn btn-primary" style="margin-top: 0.6rem;">Los geht's</button>
</form>
</div>
{% else %}
<div class="card" style="background: #EAFBF1; border-left: 4px solid var(--success); display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap;">
<p style="margin: 0; font-size: 0.9rem;">
<strong>Heute eingecheckt:</strong>
{% if status.check_in_heute.modus == 'weiter_wie_bisher' %}Arbeite weiter
{% elif status.check_in_heute.modus == 'neues_projekt' %}Neues Projekt
{% elif status.check_in_heute.modus == 'onboarding' %}Onboarding
{% elif status.check_in_heute.modus == 'drucken' %}Drucken
{% elif status.check_in_heute.modus == 'helfen' %}Hilft anderen
{% else %}{{ status.check_in_heute.modus }}{% endif %}
{% if status.check_in_heute.freitext %} &mdash; „{{ status.check_in_heute.freitext }}"{% endif %}
</p>
<form method="post" action="{{ url_for('profil.check_in_zuruecksetzen') }}" style="margin: 0;">
<button class="btn btn-secondary" style="padding: 0.3rem 0.7rem; font-size: 0.8rem;">
Modus aendern
</button>
</form>
</div>
{% endif %}
{# ====== PROJEKT-KARTE — ganz oben, damit "Hier weitermachen" sofort sichtbar ist ====== #}
{% if status.hat_projekte or not status.ist_neu or status.projekt.letztes %}
<div class="card" style="margin-top: 1rem;
{% if status.projekt.aktiv %}border-left: 4px solid var(--success);{% endif %}">
<h2>{% if status.paarung %}Euer Projekt{% else %}Dein Projekt{% endif %}</h2>
{% if status.projekt.aktiv %}
{# === Aktives Projekt: Weitermachen-Karte === #}
<div style="display: flex; gap: 1rem; align-items: center; margin-top: 0.5rem; flex-wrap: wrap;">
<div style="flex: 0 0 120px;">
{% if status.projekt.aktiv.hat_cover %}
<img src="{{ url_for('api.projekt_cover', projekt_id=status.projekt.aktiv.id) }}" alt=""
style="width: 120px; height: 120px; object-fit: cover; border-radius: 8px;">
{% else %}
<div style="width: 120px; height: 120px; background: linear-gradient(135deg, #ECF0F1, #D5DBDB);
border-radius: 8px; display: flex; align-items: center; justify-content: center;
font-size: 2.5rem; color: var(--text-light);">🎯</div>
{% endif %}
</div>
<div style="flex: 1; min-width: 250px;">
<h3 style="margin: 0 0 0.4rem 0;">{{ status.projekt.aktiv.titel }}</h3>
{% if status.projekt.aktiv.beschreibung_md %}
<div style="font-size: 0.9rem; color: var(--text); max-height: 4.5em; overflow: hidden;">
{{ status.projekt.aktiv.beschreibung_md|markdown }}
</div>
{% endif %}
<div style="margin-top: 0.8rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
<a href="{{ url_for('profil.projekt_detail', projekt_id=status.projekt.aktiv.id) }}"
class="btn btn-primary">Hier weitermachen →</a>
<a href="{{ url_for('profil.projekt_abschluss', projekt_id=status.projekt.aktiv.id) }}"
class="btn btn-secondary" style="font-size: 0.85rem;">Abschliessen</a>
</div>
</div>
</div>
{% elif status.projekt.letztes and status.projekt.letztes.freigabe_status == 'wartet_auf_freigabe' %}
<div style="background: #FEF9E7; padding: 0.8rem; border-radius: 6px; border-left: 4px solid var(--warning);">
<strong>⏳ Wartet auf Freigabe:</strong>
<a href="{{ url_for('profil.projekt_detail', projekt_id=status.projekt.letztes.id) }}">{{ status.projekt.letztes.titel }}</a>
<div style="font-size: 0.85rem; margin-top: 0.3rem; color: var(--text-light);">
Der Lehrer schaut drueber. Sobald freigegeben, kannst du ein neues Projekt anfangen.
</div>
</div>
{% elif status.projekt.letztes and status.projekt.letztes.freigabe_status == 'rueckfrage' %}
<div style="background: #FADBD8; padding: 0.8rem; border-radius: 6px; border-left: 4px solid var(--danger);">
<strong>❓ Rueckfrage offen zu:</strong>
<a href="{{ url_for('profil.projekt_detail', projekt_id=status.projekt.letztes.id) }}">{{ status.projekt.letztes.titel }}</a>
<div style="font-size: 0.85rem; margin-top: 0.3rem;">
Siehe Rueckfrage-Banner oben, beantworte sie — dann kann der Lehrer freigeben.
</div>
</div>
{% else %}
<p style="color: var(--text-light); margin-bottom: 0.8rem;">
Du hast gerade kein laufendes Projekt. Bereit fuer was Neues?
</p>
{% if status.projekt.darf_neues %}
<a href="{{ url_for('profil.projekt_neu') }}" class="btn btn-primary">+ Neues Projekt anfangen</a>
{% else %}
<div style="background: #FEF9E7; padding: 0.6rem 0.8rem; border-radius: 6px; font-size: 0.9rem;">
{{ status.projekt.sperr_grund }}
</div>
{% endif %}
{% endif %}
{% if status.projekt.aktiv or status.projekt.letztes %}
<div style="margin-top: 0.8rem; font-size: 0.85rem;">
<a href="{{ url_for('profil.projekte_liste') }}" style="color: var(--text-light);">Alle meine Projekte ansehen →</a>
</div>
{% endif %}
</div>
{% endif %}
<div class="grid" style="grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem;">
{# ====== ONBOARDING-KARTE (V1: nur wenn nicht fertig) ====== #}
{% if status.ist_neu %}
<div class="card" style="grid-column: span 2; border-top: 4px solid var(--success);">
<h2>Dein Onboarding-Pfad 🚀</h2>
<p style="color: var(--text-light); margin-bottom: 1rem;">
Drei Lektionen, damit du allein klarkommst. Du klickst selbst „fertig", wenn du soweit bist.
</p>
<div class="onboarding-liste">
{% for l in status.onboarding.lektionen %}
<div class="onb-zeile onb-{{ l.status }}{% if l.aktuell %} onb-aktuell{% endif %}">
<span class="onb-icon">
{% if l.status == 'fertig' %}✅
{% elif l.status == 'laeuft' %}🟡
{% else %}⚪{% endif %}
</span>
<span class="onb-titel">
<strong>L{{ l.nummer }}</strong> &mdash; {{ l.titel }}
</span>
<span class="onb-aktion">
{% if l.status != 'fertig' %}
<a href="{{ url_for('oeffentlich.lektion_detail', nummer=l.nummer) }}"
class="btn btn-secondary" style="padding: 0.2rem 0.6rem; font-size: 0.8rem;">
ansehen
</a>
<form method="post" action="{{ url_for('profil.lektion_status_setzen', nummer=l.nummer) }}" style="display:inline;">
{% if l.status == 'offen' %}
<button name="status" value="laeuft" class="btn btn-secondary" style="padding: 0.2rem 0.6rem; font-size: 0.8rem;">starte</button>
{% else %}
<button name="status" value="fertig" class="btn btn-primary" style="padding: 0.2rem 0.6rem; font-size: 0.8rem;">fertig ✓</button>
{% endif %}
</form>
{% else %}
<form method="post" action="{{ url_for('profil.lektion_status_setzen', nummer=l.nummer) }}" style="display:inline;">
<button name="status" value="laeuft" class="btn btn-secondary" style="padding: 0.2rem 0.6rem; font-size: 0.75rem; opacity: 0.6;">nochmal</button>
</form>
{% endif %}
</span>
</div>
{% endfor %}
</div>
{% if not status.darf_slots_buchen %}
<p style="margin-top: 1rem; font-size: 0.85rem; color: var(--text-light);">
Drucker reservieren kannst du, sobald <strong>L1 (Sicherheit)</strong> erledigt ist.
</p>
{% endif %}
</div>
{% endif %}
{# ====== PAARUNG / TEAM-KARTE — immer wenn Paarung aktiv, sonst nur fuer Erfahrene ====== #}
{% if status.paarung or not status.ist_neu %}
<div class="card">
<h2>{% if status.paarung %}Dein Team{% else %}Allein arbeiten?{% endif %}</h2>
{% if status.paarung %}
<p style="font-size: 1.1rem;">
Du arbeitest mit
<strong>{{ tier_emoji(status.paarung.partner.tier) }} {{ status.paarung.partner.vorname }}</strong>
zusammen seit {{ status.paarung.gebildet_am[:10] }}.
</p>
<form method="post" action="{{ url_for('profil.paarung_aufloesen') }}"
onsubmit="return confirm('Paarung wirklich aufloesen?')"
style="margin-top: 0.8rem;">
<button class="btn btn-secondary" style="font-size: 0.85rem;">Paarung aufloesen</button>
</form>
{% else %}
<p style="color: var(--text-light); margin-bottom: 0.5rem;">
Du kannst dich mit jemandem zusammenschliessen — frag im Cockpit-Untermenue.
</p>
{% if andere_profile %}
<details>
<summary style="cursor:pointer; color: var(--primary);">Anfrage stellen</summary>
<div id="paarung-suche" style="margin-top: 0.5rem;">
{% for andere in andere_profile %}
<form method="post" action="{{ url_for('profil.paarung_anfragen', andere_profil_id=andere.id) }}"
style="display: inline-block; margin: 0.2rem;">
<button class="btn btn-secondary" style="padding: 0.3rem 0.6rem; font-size: 0.85rem;">
{{ tier_emoji(andere.tier) }} {{ andere.vorname }}
</button>
</form>
{% endfor %}
</div>
</details>
{% else %}
<p style="font-size: 0.85rem; color: var(--text-light); margin-top: 0.5rem;">
<em>Noch niemand sonst in der AG.</em>
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{# ====== DRUCKER-/SLOT-KARTE (V1: nur wenn L1 fertig) ====== #}
{% if status.darf_slots_buchen %}
<div class="card" style="grid-column: span 2; opacity: 0.7;">
<h2>🖨️ Drucker reservieren</h2>
<p style="color: var(--text-light);">
Slot-System kommt mit <em>Etappe 7</em>. Bis dahin: sprich dich vor Ort ab.
</p>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<style>
.check-in-optionen { display: flex; flex-direction: column; gap: 0.4rem; }
.ci-option {
display: flex; gap: 0.6rem; align-items: center;
padding: 0.5rem 0.7rem; border-radius: 6px; cursor: pointer;
transition: background 0.15s;
}
.ci-option:hover { background: #F2F3F4; }
.ci-option input[type=radio] { margin: 0; }
.onboarding-liste { display: flex; flex-direction: column; gap: 0.5rem; }
.onb-zeile {
display: flex; gap: 0.7rem; align-items: center;
padding: 0.5rem 0.7rem; border-radius: 6px;
background: #F2F3F4;
}
.onb-zeile.onb-fertig { background: #EAFBF1; opacity: 0.8; }
.onb-zeile.onb-aktuell { background: #EBF5FB; border-left: 4px solid var(--primary); }
.onb-icon { font-size: 1.2rem; }
.onb-titel { flex: 1; }
.onb-aktion { display: flex; gap: 0.3rem; }
</style>
{% endblock %}