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

@ -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 columnBody = e.target.closest('.column-body');
if (columnBody && !columnBody.contains(e.relatedTarget)) {
columnBody.classList.remove('drag-over');
$$('.drop-indicator', columnBody).forEach(el => el.remove());
}
const dragState = store.get('dragState');
const column = e.target.closest('.column');
if (column) {
column.style.borderLeft = '';
column.style.borderRight = '';
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');
// 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]),
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;

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;