feat: CRM-Dashboard, Content-Verwaltung und Wort-Autocomplete
- Home-Seite nach Login mit Begrüßung und 3 Kacheln (Content erstellen, Content verwalten, User verwalten) - AuthContext speichert User-Profil + Rolle; AdminRoute blockt Nicht-Admins - Content verwalten (admin-only): Status-Dashboard pro Collection, Liste/Kachel-View, generisches Edit-Formular - Nur aktive db_-Collections im Dashboard (alte pictures/objects/words/questions entfernt) - Wort-Autocomplete in DrawIt: ab dem ersten Buchstaben Vorschläge aus db_words, Tastatur-Navigation, Duplikat-Filter - Backend: /users/me Proxy, db-words/search Endpoint, generische Collection-Endpoints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
184
app.py
184
app.py
@@ -71,6 +71,15 @@ def directus_auth_login():
|
||||
return jsonify(data), status
|
||||
|
||||
|
||||
@app.route("/api/directus/users/me", methods=["GET"])
|
||||
def directus_users_me():
|
||||
"""Proxy: aktueller User inkl. Rolle (für Begrüßung + Admin-Check)."""
|
||||
token = request.headers.get("Authorization", "")
|
||||
fields = "id,first_name,last_name,email,role.id,role.name,role.admin_access"
|
||||
data, status = _directus("GET", f"/users/me?fields={fields}", token)
|
||||
return jsonify(data), status
|
||||
|
||||
|
||||
@app.route("/api/directus/pictures", methods=["GET"])
|
||||
def directus_pictures():
|
||||
"""Proxy: Directus-Bilder nach Status filtern."""
|
||||
@@ -2111,6 +2120,181 @@ def directus_db_pair(pair_id):
|
||||
return jsonify({"ok": s in (200, 204)})
|
||||
|
||||
|
||||
@app.route("/api/directus/db-words/search", methods=["GET"])
|
||||
def directus_db_words_search():
|
||||
"""Schlanke Suche in db_words für Autocomplete.
|
||||
Liefert id, titel_de, level — case-insensitive contains, sortiert alphabetisch."""
|
||||
token = request.headers.get("Authorization", "")
|
||||
q = (request.args.get("q") or "").strip()
|
||||
try:
|
||||
limit = max(1, min(int(request.args.get("limit", "10")), 50))
|
||||
except ValueError:
|
||||
limit = 10
|
||||
|
||||
if not q:
|
||||
return jsonify({"data": []}), 200
|
||||
|
||||
qs = (
|
||||
"fields=id,titel_de,level"
|
||||
f"&filter[titel_de][_icontains]={urllib.parse.quote(q)}"
|
||||
"&sort=titel_de"
|
||||
f"&limit={limit}"
|
||||
)
|
||||
data, status = _directus("GET", f"/items/db_words?{qs}", token)
|
||||
return jsonify({"data": data.get("data") or []}), status
|
||||
|
||||
|
||||
# =====================================================
|
||||
# CONTENT-MANAGEMENT DASHBOARD (admin)
|
||||
# =====================================================
|
||||
|
||||
# Allowlist: nur diese Collections sind über die generischen
|
||||
# Management-Endpoints erreichbar. Erweiterbar.
|
||||
DASHBOARD_COLLECTIONS = [
|
||||
{"name": "db_pictures", "label": "Bilder", "kind": "image", "group": "neu",
|
||||
"fields": "id,media,status,date_created,design", "preview": "media", "title_field": None},
|
||||
{"name": "db_objects", "label": "Objekte", "kind": "image", "group": "neu",
|
||||
"fields": "id,picture,status,date_created,user_notes", "preview": None, "title_field": "user_notes"},
|
||||
{"name": "db_words", "label": "Wörter", "kind": "text", "group": "neu",
|
||||
"fields": "id,titel_de,level,status,date_created", "preview": None, "title_field": "titel_de"},
|
||||
{"name": "db_question", "label": "Fragen", "kind": "text", "group": "neu",
|
||||
"fields": "id,question_de,level,status,date_created", "preview": None, "title_field": "question_de"},
|
||||
{"name": "db_statement", "label": "Statements", "kind": "text", "group": "neu",
|
||||
"fields": "id,statement_de,level,status,date_created", "preview": None, "title_field": "statement_de"},
|
||||
{"name": "db_pairs", "label": "Q&A-Paare", "kind": "text", "group": "neu",
|
||||
"fields": "id,level,status,date_created", "preview": None, "title_field": None},
|
||||
]
|
||||
|
||||
DASHBOARD_BY_NAME = {c["name"]: c for c in DASHBOARD_COLLECTIONS}
|
||||
|
||||
|
||||
def _collection_config_or_404(name):
|
||||
cfg = DASHBOARD_BY_NAME.get(name)
|
||||
if not cfg:
|
||||
return None, (jsonify({"error": "Unbekannte Collection"}), 404)
|
||||
return cfg, None
|
||||
|
||||
|
||||
@app.route("/api/directus/dashboard/summary", methods=["GET"])
|
||||
def directus_dashboard_summary():
|
||||
"""Liefert pro Collection Total + Status-Counts. Admin-Token vom User."""
|
||||
token = request.headers.get("Authorization", "")
|
||||
out = []
|
||||
for cfg in DASHBOARD_COLLECTIONS:
|
||||
entry = {
|
||||
"name": cfg["name"],
|
||||
"label": cfg["label"],
|
||||
"kind": cfg["kind"],
|
||||
"group": cfg["group"],
|
||||
"total": 0,
|
||||
"by_status": [],
|
||||
"has_status": False,
|
||||
"error": None,
|
||||
}
|
||||
|
||||
# 1) Total-Count via aggregate
|
||||
data, status = _directus("GET",
|
||||
f"/items/{cfg['name']}?aggregate[count]=*", token)
|
||||
if status == 200:
|
||||
try:
|
||||
entry["total"] = int(((data.get("data") or [{}])[0] or {}).get("count", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
entry["total"] = 0
|
||||
else:
|
||||
entry["error"] = (data.get("errors") or [{}])[0].get("message") or f"HTTP {status}"
|
||||
|
||||
# 2) Status-Groups (kann fehlschlagen, wenn Feld fehlt → ignorieren)
|
||||
data2, status2 = _directus("GET",
|
||||
f"/items/{cfg['name']}?aggregate[count]=*&groupBy[]=status&limit=-1", token)
|
||||
if status2 == 200:
|
||||
rows = data2.get("data") or []
|
||||
grouped = []
|
||||
for r in rows:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
grouped.append({
|
||||
"status": r.get("status"),
|
||||
"count": int((r.get("count") or {}).get("id", 0) if isinstance(r.get("count"), dict) else r.get("count") or 0),
|
||||
})
|
||||
grouped.sort(key=lambda x: -x["count"])
|
||||
entry["by_status"] = grouped
|
||||
entry["has_status"] = any(g.get("status") is not None for g in grouped)
|
||||
|
||||
out.append(entry)
|
||||
return jsonify({"data": out}), 200
|
||||
|
||||
|
||||
@app.route("/api/directus/collection/<name>", methods=["GET"])
|
||||
def directus_collection_list(name):
|
||||
"""Generische Liste mit optionalem Status-Filter + Pagination."""
|
||||
cfg, err = _collection_config_or_404(name)
|
||||
if err:
|
||||
return err
|
||||
token = request.headers.get("Authorization", "")
|
||||
|
||||
status_filter = request.args.get("status", "").strip()
|
||||
limit = request.args.get("limit", "50")
|
||||
offset = request.args.get("offset", "0")
|
||||
sort = request.args.get("sort", "-date_created")
|
||||
|
||||
parts = [f"fields={cfg['fields']}", f"limit={limit}", f"offset={offset}", f"sort={sort}"]
|
||||
if status_filter:
|
||||
parts.append(f"filter[status][_eq]={urllib.parse.quote(status_filter)}")
|
||||
query = "&".join(parts)
|
||||
|
||||
data, status = _directus("GET", f"/items/{name}?{query}", token)
|
||||
|
||||
# Total für aktuellen Filter
|
||||
count_parts = ["aggregate[count]=*"]
|
||||
if status_filter:
|
||||
count_parts.append(f"filter[status][_eq]={urllib.parse.quote(status_filter)}")
|
||||
count_data, _ = _directus("GET", f"/items/{name}?{'&'.join(count_parts)}", token)
|
||||
total = 0
|
||||
try:
|
||||
total = int(((count_data.get("data") or [{}])[0] or {}).get("count", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
"data": data.get("data") or [],
|
||||
"meta": {"total": total, "limit": int(limit), "offset": int(offset)},
|
||||
"collection": {
|
||||
"name": cfg["name"],
|
||||
"label": cfg["label"],
|
||||
"kind": cfg["kind"],
|
||||
"preview": cfg.get("preview"),
|
||||
"title_field": cfg.get("title_field"),
|
||||
},
|
||||
}), status
|
||||
|
||||
|
||||
@app.route("/api/directus/collection/<name>/<item_id>", methods=["GET", "PATCH"])
|
||||
def directus_collection_item(name, item_id):
|
||||
"""Eintrag laden oder aktualisieren."""
|
||||
cfg, err = _collection_config_or_404(name)
|
||||
if err:
|
||||
return err
|
||||
token = request.headers.get("Authorization", "")
|
||||
|
||||
if request.method == "GET":
|
||||
data, status = _directus("GET", f"/items/{name}/{item_id}", token)
|
||||
return jsonify({
|
||||
"data": data.get("data"),
|
||||
"collection": {
|
||||
"name": cfg["name"],
|
||||
"label": cfg["label"],
|
||||
"kind": cfg["kind"],
|
||||
"preview": cfg.get("preview"),
|
||||
"title_field": cfg.get("title_field"),
|
||||
},
|
||||
}), status
|
||||
|
||||
# PATCH
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
data, status = _directus("PATCH", f"/items/{name}/{item_id}", token, body)
|
||||
return jsonify(data), status
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user