Statuskarten via Drag&Drop verschiebbar

Unteraufgaben lassen sich verschieben und bearbeiten
Dieser Commit ist enthalten in:
HG
2025-12-30 19:55:39 +00:00
committet von Server Deploy
Ursprung 15627cce99
Commit 9bf298c26b
20 geänderte Dateien mit 1609 neuen und 39 gelöschten Zeilen

Datei anzeigen

@ -909,26 +909,54 @@ class TaskModalManager {
clearElement(container);
this.subtasks.forEach(subtask => {
this.subtasks.forEach((subtask, index) => {
const item = createElement('div', {
className: `subtask-item ${subtask.completed ? 'completed' : ''}`,
dataset: { subtaskId: subtask.id }
dataset: { subtaskId: subtask.id, position: index },
draggable: 'true'
}, [
// Drag Handle
createElement('span', {
className: 'subtask-drag-handle',
title: 'Ziehen zum Verschieben'
}, ['⋮⋮']),
// Checkbox
createElement('input', {
type: 'checkbox',
checked: subtask.completed,
onchange: () => this.toggleSubtask(subtask.id)
}),
// Titel (Doppelklick zum Bearbeiten)
createElement('span', {
className: 'subtask-title'
className: 'subtask-title',
ondblclick: (e) => this.startEditSubtask(subtask.id, e.target)
}, [subtask.title]),
createElement('button', {
type: 'button',
className: 'subtask-delete',
onclick: () => this.deleteSubtask(subtask.id)
}, ['×'])
// Aktionen
createElement('div', {
className: 'subtask-actions'
}, [
createElement('button', {
type: 'button',
className: 'subtask-edit',
title: 'Bearbeiten',
onclick: (e) => this.startEditSubtask(subtask.id, e.target.closest('.subtask-item').querySelector('.subtask-title'))
}, ['✎']),
createElement('button', {
type: 'button',
className: 'subtask-delete',
title: 'Löschen',
onclick: () => this.deleteSubtask(subtask.id)
}, ['×'])
])
]);
// Drag & Drop Events
item.addEventListener('dragstart', (e) => this.handleSubtaskDragStart(e, subtask.id, index));
item.addEventListener('dragover', (e) => this.handleSubtaskDragOver(e));
item.addEventListener('dragleave', (e) => this.handleSubtaskDragLeave(e));
item.addEventListener('drop', (e) => this.handleSubtaskDrop(e, subtask.id, index));
item.addEventListener('dragend', (e) => this.handleSubtaskDragEnd(e));
container.appendChild(item);
});
@ -936,6 +964,136 @@ class TaskModalManager {
this.updateSubtaskProgress();
}
// Subtask Drag & Drop
handleSubtaskDragStart(e, subtaskId, position) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', JSON.stringify({ subtaskId, position }));
e.target.classList.add('dragging');
this.draggedSubtaskId = subtaskId;
}
handleSubtaskDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const item = e.target.closest('.subtask-item');
if (item && !item.classList.contains('dragging')) {
const rect = item.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
item.classList.remove('drag-over-top', 'drag-over-bottom');
if (e.clientY < midpoint) {
item.classList.add('drag-over-top');
} else {
item.classList.add('drag-over-bottom');
}
}
}
handleSubtaskDragLeave(e) {
const item = e.target.closest('.subtask-item');
if (item) {
item.classList.remove('drag-over-top', 'drag-over-bottom');
}
}
handleSubtaskDragEnd(e) {
e.target.classList.remove('dragging');
$$('.subtask-item').forEach(item => {
item.classList.remove('drag-over-top', 'drag-over-bottom');
});
this.draggedSubtaskId = null;
}
async handleSubtaskDrop(e, targetSubtaskId, targetPosition) {
e.preventDefault();
const item = e.target.closest('.subtask-item');
if (item) {
item.classList.remove('drag-over-top', 'drag-over-bottom');
}
if (!this.draggedSubtaskId || this.draggedSubtaskId === targetSubtaskId) return;
const draggedIndex = this.subtasks.findIndex(s => s.id === this.draggedSubtaskId);
if (draggedIndex === -1) return;
// Berechne neue Position basierend auf Drop-Position
const rect = item.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
let newPosition = targetPosition;
if (e.clientY > midpoint && draggedIndex < targetPosition) {
// Nach unten, hinter das Ziel
newPosition = targetPosition;
} else if (e.clientY > midpoint && draggedIndex > targetPosition) {
newPosition = targetPosition + 1;
} else if (e.clientY <= midpoint && draggedIndex > targetPosition) {
newPosition = targetPosition;
} else if (e.clientY <= midpoint && draggedIndex < targetPosition) {
newPosition = targetPosition - 1;
}
if (newPosition === draggedIndex) return;
// Lokale Reihenfolge aktualisieren
const [moved] = this.subtasks.splice(draggedIndex, 1);
this.subtasks.splice(newPosition, 0, moved);
this.renderSubtasks();
// API-Call
if (this.mode === 'edit' && this.taskId) {
const projectId = store.get('currentProjectId');
try {
await api.reorderSubtasks(projectId, this.taskId, this.draggedSubtaskId, newPosition);
} catch (error) {
console.error('Fehler beim Neuordnen der Subtask:', error);
}
}
}
// Subtask bearbeiten
startEditSubtask(subtaskId, titleElement) {
const subtask = this.subtasks.find(s => s.id === subtaskId);
if (!subtask) return;
const currentTitle = subtask.title;
const input = createElement('input', {
type: 'text',
className: 'subtask-edit-input',
value: currentTitle
});
// Titel durch Input ersetzen
titleElement.replaceWith(input);
input.focus();
input.select();
const saveEdit = async () => {
const newTitle = input.value.trim();
if (newTitle && newTitle !== currentTitle) {
subtask.title = newTitle;
if (this.mode === 'edit' && this.taskId) {
const projectId = store.get('currentProjectId');
try {
await api.updateSubtask(projectId, this.taskId, subtaskId, { title: newTitle });
} catch (error) {
console.error('Fehler beim Aktualisieren der Subtask:', error);
subtask.title = currentTitle; // Rollback
}
}
}
this.renderSubtasks();
};
input.addEventListener('blur', saveEdit);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
input.blur();
} else if (e.key === 'Escape') {
subtask.title = currentTitle; // Keine Änderung
this.renderSubtasks();
}
});
}
updateSubtaskProgress() {
const progressContainer = $('#subtask-progress');
if (!progressContainer) return;