Drag-Griff statt ganzem Element + Uebungs-Reihenfolge beim Start uebernehmen
Image bauen und in die Registry schieben / build (push) Successful in 6s

- Sortieren nur ueber einen kleinen Griff (Punkt-Icon) links im
  Uebungskopf; das ganze Element ist nicht mehr draggable (kein
  versehentliches Ziehen beim Tippen/Auswaehlen)
- Beim Session-Start wird die Uebungs-Reihenfolge der letzten
  abgeschlossenen Session desselben Tags uebernommen (server-seitig in
  create_session, in session.fields.exerciseOrder)
- Tests: 170 Checks gruen (40+18+52+32+28)
This commit is contained in:
AI-Agent committed 2026-10-05 11:31:07 +00:00
1 parent 7c5045cec8
commit aebd7f40e7
5 files changed
+60 -12

No files matched your search

+13 -6
View File
@@ -782,17 +782,16 @@ function reorderExercise(a, model, fromKey, toKey) {
scheduleAutosave(model);
}
function attachDragReorder(a, model, ex, block) {
block.draggable = true;
block.title = t("reorder_hint");
block.addEventListener("dragstart", ev => {
function attachDragReorder(a, model, ex, block, grip) {
grip.draggable = true;
grip.addEventListener("dragstart", ev => {
app.dragKey = ex.key;
try {
ev.dataTransfer = ev.dataTransfer || {};
ev.dataTransfer.setData("text/plain", ex.key);
} catch (_) {}
});
block.addEventListener("dragend", () => {
grip.addEventListener("dragend", () => {
app.dragKey = null;
for (const b of $$(".ex-block.drag-over")) b.classList.remove("drag-over");
});
@@ -850,6 +849,14 @@ function buildExBlock(a, model, ex, removable) {
model.exByKey[ex.key] = ex;
const head = el("div", "ex-head");
const grip = el("span", "ex-grip", "");
grip.innerHTML = '<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">'
+ '<circle cx="6" cy="4" r="1.4"/><circle cx="10" cy="4" r="1.4"/>'
+ '<circle cx="6" cy="8" r="1.4"/><circle cx="10" cy="8" r="1.4"/>'
+ '<circle cx="6" cy="12" r="1.4"/><circle cx="10" cy="12" r="1.4"/></svg>';
grip.title = t("reorder_hint");
grip.setAttribute("aria-label", t("reorder_hint"));
head.appendChild(grip);
const infoBtn = el("button", "icon-btn", "?");
infoBtn.type = "button";
infoBtn.setAttribute("aria-label", t("info_aria") + ex.name);
@@ -878,7 +885,7 @@ function buildExBlock(a, model, ex, removable) {
block.appendChild(el("div", "set-rows"));
renderExRows(a, model, ex);
attachDragReorder(a, model, ex, block);
attachDragReorder(a, model, ex, block, grip);
return block;
}
+26 -3
View File
@@ -434,6 +434,26 @@ def fetch_session(user_id, sid):
return d
def last_exercise_order(user_id, activity, day):
"""Uebungs-Reihenfolge der letzten abgeschlossenen Session (Tag/Kategorie)."""
con = db()
clause, extra = _user_clause(user_id)
rows = con.execute(
f"SELECT fields FROM sessions WHERE activity=? AND day=? AND status='done' "
f"AND {clause} ORDER BY date DESC, id DESC LIMIT 10",
[activity, day] + extra).fetchall()
con.close()
for r in rows:
try:
f = json.loads(r["fields"] or "{}")
except Exception:
continue
order = f.get("exerciseOrder")
if isinstance(order, list) and order:
return order
return None
def create_session(user_id, data):
activity = _clean_str(data.get("activity"))
if not activity:
@@ -441,12 +461,15 @@ def create_session(user_id, data):
sdate = _clean_str(data.get("date")) or date.today().isoformat()
day = _clean_str(data.get("day"))
start = _clean_str(data.get("start_time")) or datetime.now().strftime("%H:%M")
# Reihenfolge der Uebungen aus der letzten abgeschlossenen Session uebernehmen
order = last_exercise_order(user_id, activity, day)
fields = json.dumps({"exerciseOrder": order}) if order else "{}"
con = db()
cur = con.execute(
"INSERT INTO sessions(activity,day,date,start_time,status,created_at,user_id) "
"VALUES(?,?,?,?,?,?,?)",
"INSERT INTO sessions(activity,day,date,start_time,status,created_at,user_id,fields) "
"VALUES(?,?,?,?,?,?,?,?)",
(activity, day, sdate, start, "active",
datetime.now().isoformat(timespec="seconds"), user_id))
datetime.now().isoformat(timespec="seconds"), user_id, fields))
con.commit()
sid = cur.lastrowid
con.close()
+10 -1
View File
@@ -400,9 +400,18 @@ h3 { font-size: 15px; font-weight: 700; }
.ex-block {
border: 2px solid var(--border);
margin: 0;
cursor: grab;
}
.ex-grip {
display: inline-flex;
align-items: center;
color: var(--muted);
cursor: grab;
user-select: none;
}
.ex-grip:active { cursor: grabbing; }
.ex-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(460px, 100%), 1fr));
+9
View File
@@ -212,6 +212,15 @@ def main():
check("Statistik: bestes Gewicht je Termin",
s0["weight"] == 22 and s1["weight"] == 20, stats)
# Uebungs-Reihenfolge der letzten abgeschlossenen Session uebernehmen
order = ["Push/Schrägbank KH (je Hand)", "Push/Bankdrücken KH (je Hand)"]
req("PATCH", f"/api/sessions/{pid}",
{"meta": {"fields": {"exerciseOrder": order}}})
st, ns = req("POST", "/api/sessions", {"activity": "gym", "day": "Push"})
check("Uebungs-Reihenfolge beim Start uebernommen",
ns["fields"].get("exerciseOrder") == order, ns["fields"])
req("DELETE", f"/api/sessions/{ns['id']}")
st, exp = req("GET", "/api/export?format=json")
check("Export JSON Format", st == 200
and exp["format"] == "trainingsplan-export-v2"
+2 -2
View File
@@ -238,8 +238,8 @@ async function main() {
check("Hinweiszeile ausgeblendet",
!gymCard().querySelector(".ex-block .ex-hint"));
// --- Uebungen per Drag & Drop sortieren --------------------------------
gymCard().querySelectorAll(".ex-block")[0]
// --- Uebungen per Drag & Drop (Griff) sortieren ------------------------
gymCard().querySelectorAll(".ex-block")[0].querySelector(".ex-grip")
.dispatchEvent(new window.Event("dragstart", { bubbles: true }));
const target = gymCard().querySelectorAll(".ex-block")[1];
target.dispatchEvent(new window.Event("drop", { bubbles: true }));