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

@ -1,6 +1,59 @@
TASKMATE - CHANGELOG
====================
================================================================================
30.12.2025 - Checklisten-Unteraufgaben: Drag & Drop und Bearbeiten
================================================================================
FEATURE: UNTERAUFGABEN REIHENFOLGE ÄNDERN (DRAG & DROP)
--------------------------------------------------------------------------------
- Drag-Handle (⋮⋮) links neben jeder Unteraufgabe
- Visueller Indikator beim Ziehen (farbiger Rand oben/unten)
- Reihenfolge wird in der Datenbank gespeichert (position-Feld)
FEATURE: UNTERAUFGABEN BEARBEITEN
--------------------------------------------------------------------------------
- Doppelklick auf den Titel zum Bearbeiten
- Bearbeiten-Icon (✎) erscheint beim Hover
- Enter speichert, Escape bricht ab
- Änderungen werden sofort in der Datenbank gespeichert
BETROFFENE DATEIEN
--------------------------------------------------------------------------------
- frontend/js/task-modal.js: renderSubtasks() erweitert, Drag & Drop Events,
startEditSubtask() Methode hinzugefügt
- frontend/css/modal.css: Styles für Drag-Handle, Drag-Indikatoren, Edit-Button,
Edit-Input
- frontend/sw.js: Cache-Version auf 138 erhöht
================================================================================
30.12.2025 - Bugfix: Statuskarten Drag & Drop Reihenfolge
================================================================================
BUGFIX & VERBESSERUNG: STATUSKARTEN DRAG & DROP
--------------------------------------------------------------------------------
- Drag & Drop von Statuskarten funktionierte nicht korrekt
- Ursache 1: API-Aufruf mit falschen Parametern (Array statt columnId + Position)
- Ursache 2: Kein visuelles Feedback beim Ziehen (CSS fehlte)
- Ursache 3: DragLeave entfernte Indikatoren zu früh
ÄNDERUNGEN
--------------------------------------------------------------------------------
- board.js: reorderColumns() übergibt jetzt korrekt moved.id und toIndex
- board.js: handleDragOver() nutzt jetzt CSS-Klassen statt inline-styles
- board.js: handleDragLeave() prüft jetzt ob Spalte wirklich verlassen wird
- board.js: handleDrop() berechnet Position basierend auf Maus-Position (links/rechts)
- board.css: Neue Styles für .column.dragging (Opacity, Scale, Shadow)
- board.css: Neue Styles für .column.drag-over-left/right (farbiger Rand)
BETROFFENE DATEIEN
--------------------------------------------------------------------------------
- frontend/js/board.js: Drag & Drop Logik komplett überarbeitet
- frontend/css/board.css: Column Dragging Styles hinzugefügt
- frontend/sw.js: Cache-Version auf 135 erhöht
================================================================================
30.12.2025 - Browser-Upload: Lokale Verzeichnisse ins Gitea pushen
================================================================================

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Datei anzeigen

@ -609,12 +609,19 @@
border-bottom: none;
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
cursor: grab;
user-select: none;
-webkit-user-select: none;
}
.column-header:active {
cursor: grabbing;
}
.column-header.dragging,
.column-header[draggable="true"]:active {
cursor: grabbing;
}
.column-title {
display: flex;
align-items: center;
@ -750,6 +757,24 @@
transform: rotate(3deg);
}
/* Column Dragging */
.column.dragging {
opacity: 0.6;
transform: scale(0.98);
box-shadow: var(--shadow-xl);
z-index: 1000;
}
.column.drag-over-left {
border-left: 4px solid var(--primary) !important;
margin-left: -2px;
}
.column.drag-over-right {
border-right: 4px solid var(--primary) !important;
margin-right: -2px;
}
.task-card.overdue {
border-left: 4px solid var(--error);
}

Datei anzeigen

@ -270,10 +270,44 @@
.subtask-item {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-3);
gap: var(--spacing-2);
padding: var(--spacing-2) var(--spacing-3);
background: var(--bg-tertiary);
border-radius: var(--radius-lg);
transition: all var(--transition-fast);
border: 2px solid transparent;
}
.subtask-item.dragging {
opacity: 0.5;
background: var(--bg-secondary);
}
.subtask-item.drag-over-top {
border-top-color: var(--primary);
}
.subtask-item.drag-over-bottom {
border-bottom-color: var(--primary);
}
/* Drag Handle */
.subtask-drag-handle {
cursor: grab;
color: var(--text-muted);
font-size: var(--text-xs);
padding: 2px 4px;
user-select: none;
opacity: 0.5;
transition: opacity var(--transition-fast);
}
.subtask-item:hover .subtask-drag-handle {
opacity: 1;
}
.subtask-drag-handle:active {
cursor: grabbing;
}
.subtask-item input[type="checkbox"] {
@ -299,19 +333,33 @@
color: var(--text-muted);
}
/* Subtask Actions */
.subtask-actions {
display: flex;
gap: var(--spacing-1);
opacity: 0;
transition: opacity var(--transition-fast);
}
.subtask-item:hover .subtask-actions {
opacity: 1;
}
.subtask-edit,
.subtask-delete {
padding: 4px;
padding: 4px 6px;
color: var(--text-muted);
background: none;
border: none;
cursor: pointer;
opacity: 0;
transition: all var(--transition-fast);
border-radius: var(--radius-md);
font-size: var(--text-sm);
}
.subtask-item:hover .subtask-delete {
opacity: 1;
.subtask-edit:hover {
color: var(--primary);
background: var(--primary-light);
}
.subtask-delete:hover {
@ -319,6 +367,17 @@
background: var(--error-bg);
}
/* Edit Input */
.subtask-edit-input {
flex: 1;
padding: var(--spacing-1) var(--spacing-2);
font-size: var(--text-sm);
background: var(--bg-card);
border: 1px solid var(--primary);
border-radius: var(--radius-md);
outline: none;
}
/* Add Subtask Form */
.add-subtask-form {
display: flex;
@ -333,6 +392,7 @@
flex: 1;
font-size: var(--text-sm);
color: var(--text-primary);
cursor: text;
}
/* Links */

Datei anzeigen

@ -179,12 +179,14 @@ class BoardManager {
const columnEl = createElement('div', {
className: 'column',
dataset: { columnId: column.id },
draggable: 'true'
dataset: { columnId: column.id }
});
// Header with column color
const header = createElement('div', { className: 'column-header' });
// Header with column color - draggable
const header = createElement('div', {
className: 'column-header',
draggable: 'true'
});
// Apply column color to header background
const columnColor = column.color || '#6B7280';
@ -536,6 +538,7 @@ class BoardManager {
handleDragStart(e) {
const taskCard = e.target.closest('.task-card');
const header = e.target.closest('.column-header');
const column = e.target.closest('.column');
if (taskCard) {
@ -548,7 +551,8 @@ class BoardManager {
type: 'task',
taskId: parseInt(taskCard.dataset.taskId)
});
} else if (column && e.target.closest('.column-header')) {
} else if (header && column) {
// Header wird gezogen -> ganze Spalte verschieben
this.draggedColumn = column;
column.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
@ -575,6 +579,10 @@ class BoardManager {
// Remove all drop indicators
$$('.column-body.drag-over').forEach(el => el.classList.remove('drag-over'));
$$('.drop-indicator').forEach(el => el.remove());
// Remove column drag indicators
$$('.column.drag-over-left, .column.drag-over-right').forEach(el => {
el.classList.remove('drag-over-left', 'drag-over-right');
});
store.setDragState(null);
}
@ -597,32 +605,44 @@ class BoardManager {
}
} else if (dragState.type === 'column') {
const column = e.target.closest('.column');
// Entferne alle vorherigen Drag-Over-Klassen
$$('.column.drag-over-left, .column.drag-over-right').forEach(col => {
if (col !== column) {
col.classList.remove('drag-over-left', 'drag-over-right');
}
});
if (column && column !== this.draggedColumn) {
const rect = column.getBoundingClientRect();
const midpoint = rect.left + rect.width / 2;
if (e.clientX < midpoint) {
column.style.borderLeft = '3px solid var(--accent)';
column.style.borderRight = '';
column.classList.add('drag-over-left');
column.classList.remove('drag-over-right');
} else {
column.style.borderRight = '3px solid var(--accent)';
column.style.borderLeft = '';
column.classList.add('drag-over-right');
column.classList.remove('drag-over-left');
}
}
}
}
handleDragLeave(e) {
const dragState = store.get('dragState');
if (dragState?.type === 'task') {
const columnBody = e.target.closest('.column-body');
if (columnBody && !columnBody.contains(e.relatedTarget)) {
columnBody.classList.remove('drag-over');
$$('.drop-indicator', columnBody).forEach(el => el.remove());
}
} else if (dragState?.type === 'column') {
const column = e.target.closest('.column');
if (column) {
column.style.borderLeft = '';
column.style.borderRight = '';
// Nur entfernen wenn wirklich die Spalte verlassen wird
if (column && !column.contains(e.relatedTarget)) {
column.classList.remove('drag-over-left', 'drag-over-right');
}
}
}
@ -664,18 +684,45 @@ class BoardManager {
this.moveTask(taskId, columnId, position);
} else if (dragState.type === 'column') {
const targetColumn = e.target.closest('.column');
if (!targetColumn || targetColumn === this.draggedColumn) return;
if (!targetColumn || targetColumn === this.draggedColumn) {
// Cleanup
$$('.column.drag-over-left, .column.drag-over-right').forEach(el => {
el.classList.remove('drag-over-left', 'drag-over-right');
});
return;
}
const columns = store.get('columns');
const fromIndex = columns.findIndex(c => c.id === dragState.columnId);
const toIndex = columns.findIndex(c => c.id === parseInt(targetColumn.dataset.columnId));
let toIndex = columns.findIndex(c => c.id === parseInt(targetColumn.dataset.columnId));
if (fromIndex !== -1 && toIndex !== -1) {
// Berechne Position basierend auf Maus-Position (links oder rechts der Ziel-Spalte)
const rect = targetColumn.getBoundingClientRect();
const midpoint = rect.left + rect.width / 2;
const dropOnRight = e.clientX > midpoint;
// Wenn rechts gedroppt und von links kommend, nach rechts verschieben
if (dropOnRight && fromIndex < toIndex) {
// toIndex bleibt gleich (Position nach der Ziel-Spalte)
} else if (dropOnRight && fromIndex > toIndex) {
// Von rechts kommend, rechts droppend -> nach der Ziel-Spalte
toIndex = toIndex + 1;
} else if (!dropOnRight && fromIndex > toIndex) {
// toIndex bleibt gleich (Position vor der Ziel-Spalte)
} else if (!dropOnRight && fromIndex < toIndex) {
// Von links kommend, links droppend -> vor der Ziel-Spalte
toIndex = toIndex - 1;
}
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
this.reorderColumns(fromIndex, toIndex);
}
targetColumn.style.borderLeft = '';
targetColumn.style.borderRight = '';
// Cleanup
$$('.column.drag-over-left, .column.drag-over-right').forEach(el => {
el.classList.remove('drag-over-left', 'drag-over-right');
});
}
}
@ -948,7 +995,8 @@ class BoardManager {
store.reorderColumns(columnIds);
try {
await api.reorderColumns(projectId, columnIds);
// API erwartet: columnId und newPosition
await api.reorderColumns(projectId, moved.id, toIndex);
syncManager.notifyColumnsReordered(columnIds);
} catch (error) {
console.error('Failed to reorder columns:', error);

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]),
// 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;

Datei anzeigen

@ -4,7 +4,7 @@
* Offline support and caching
*/
const CACHE_VERSION = '133';
const CACHE_VERSION = '138';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;

Datei-Diff unterdrückt, da er zu groß ist Diff laden