feat(projekt): Anheftungen per Drag & Drop zwischen Unterreitern verschieben

Pin-Zeilen in der Ordneransicht sind draggable, die Reiter sind Ablageziele.
Ein Drop schickt PATCH /api/projects/:id/pins/:pinId {tab_id}; movePin schreibt
project_pins.tab_id um. Verschoben wird nur die Referenz.

- Zielebene hat die Aufnahme schon: Quell-Pin loeschen statt an UNIQUE uniq_pin
  scheitern (merged:true)
- Zielreiter serverseitig gegen den Ordner pruefen, sonst 404
- .ptab-n braucht pointer-events:none, sonst flackert die Ablagemarkierung

Verifiziert gegen die laufende App inkl. 404- und Merge-Faellen; Testdaten
zurueckgesetzt. Doku: Kapitel 10 + Changelog.
This commit is contained in:
2026-08-14 14:52:14 +02:00
parent 2be938c91b
commit 3d628b4b96
6 changed files with 1842 additions and 1678 deletions
+27
View File
@@ -208,6 +208,33 @@ export async function pinRecording(projectId, tabId, recordingId) {
export async function unpinRecording(pinId) {
await pool.execute('DELETE FROM project_pins WHERE id=?', [pinId]);
}
// Einen Pin auf eine andere Ebene DESSELBEN Ordners schieben (Drag&Drop in der
// Projektansicht). Hängt die Aufnahme auf der Zielebene schon, entsteht kein
// zweiter Pin — der Quell-Pin verschwindet stattdessen (Zusammenführen statt
// Duplikat; ein UPDATE würde sonst an UNIQUE uniq_pin scheitern).
export async function movePin(pinId, projectId, tabId) {
const [rows] = await pool.execute(
'SELECT tab_id, recording_id FROM project_pins WHERE id=? AND project_id=?', [pinId, projectId]);
if (!rows.length) return null;
const src = rows[0];
const dst = tabId || 0;
// Zielreiter muss zu diesem Ordner gehören (0 = Ordner-Ebene, gibt es immer).
if (dst) {
const [t] = await pool.execute(
'SELECT id FROM project_tabs WHERE id=? AND project_id=?', [dst, projectId]);
if (!t.length) return null;
}
if (src.tab_id === dst) return { ok: true, moved: false, tab_id: dst };
const [dup] = await pool.execute(
'SELECT id FROM project_pins WHERE project_id=? AND tab_id=? AND recording_id=?',
[projectId, dst, src.recording_id]);
if (dup.length) {
await pool.execute('DELETE FROM project_pins WHERE id=?', [pinId]);
return { ok: true, moved: true, merged: true, tab_id: dst, pin_id: dup[0].id };
}
await pool.execute('UPDATE project_pins SET tab_id=? WHERE id=?', [dst, pinId]);
return { ok: true, moved: true, tab_id: dst, pin_id: pinId };
}
// Wo hängt diese Aufnahme überall? Treibt die Markierung des 📌-Buttons in der
// Aufnahmenliste.
export async function listPinsForRecording(recId) {