Statuskarten via Drag&Drop verschiebbar
Unteraufgaben lassen sich verschieben und bearbeiten
Dieser Commit ist enthalten in:
@ -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);
|
||||
}
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren