Files
TaskMate/backend/routes/tasks.js
Claude Project Manager ab1e5be9a9 Initial commit
2025-12-28 21:36:45 +00:00

900 Zeilen
29 KiB
JavaScript

/**
* TASKMATE - Task Routes
* ======================
* CRUD für Aufgaben
*/
const express = require('express');
const router = express.Router();
const { getDb } = require('../database');
const logger = require('../utils/logger');
const { validators } = require('../middleware/validation');
const notificationService = require('../services/notificationService');
/**
* Hilfsfunktion: Historie-Eintrag erstellen
*/
function addHistory(db, taskId, userId, action, fieldChanged = null, oldValue = null, newValue = null) {
db.prepare(`
INSERT INTO history (task_id, user_id, action, field_changed, old_value, new_value)
VALUES (?, ?, ?, ?, ?, ?)
`).run(taskId, userId, action, fieldChanged, oldValue, newValue);
}
/**
* Hilfsfunktion: Vollständige Task-Daten laden
*/
function getFullTask(db, taskId) {
const task = db.prepare(`
SELECT t.*,
c.username as creator_name
FROM tasks t
LEFT JOIN users c ON t.created_by = c.id
WHERE t.id = ?
`).get(taskId);
if (!task) return null;
// Zugewiesene Mitarbeiter laden (Mehrfachzuweisung)
const assignees = db.prepare(`
SELECT u.id, u.username, u.display_name, u.color
FROM task_assignees ta
JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
ORDER BY u.username
`).all(taskId);
// Labels laden
const labels = db.prepare(`
SELECT l.* FROM labels l
JOIN task_labels tl ON l.id = tl.label_id
WHERE tl.task_id = ?
`).all(taskId);
// Subtasks laden
const subtasks = db.prepare(`
SELECT * FROM subtasks WHERE task_id = ? ORDER BY position
`).all(taskId);
// Anhänge zählen
const attachmentCount = db.prepare(
'SELECT COUNT(*) as count FROM attachments WHERE task_id = ?'
).get(taskId).count;
// Links zählen
const linkCount = db.prepare(
'SELECT COUNT(*) as count FROM links WHERE task_id = ?'
).get(taskId).count;
// Kommentare zählen
const commentCount = db.prepare(
'SELECT COUNT(*) as count FROM comments WHERE task_id = ?'
).get(taskId).count;
// Verknüpfte Genehmigungen laden
const proposals = db.prepare(`
SELECT p.id, p.title, p.approved, p.approved_by,
u.display_name as approved_by_name
FROM proposals p
LEFT JOIN users u ON p.approved_by = u.id
WHERE p.task_id = ? AND p.archived = 0
ORDER BY p.created_at DESC
`).all(taskId);
return {
id: task.id,
projectId: task.project_id,
columnId: task.column_id,
title: task.title,
description: task.description,
priority: task.priority,
startDate: task.start_date,
dueDate: task.due_date,
// Neues Format: Array von Mitarbeitern
assignees: assignees.map(a => ({
id: a.id,
username: a.username,
display_name: a.display_name,
color: a.color
})),
// Rückwärtskompatibilität: assignedTo als erster Mitarbeiter (falls vorhanden)
assignedTo: assignees.length > 0 ? assignees[0].id : null,
assignedName: assignees.length > 0 ? assignees[0].username : null,
assignedColor: assignees.length > 0 ? assignees[0].color : null,
timeEstimateMin: task.time_estimate_min,
dependsOn: task.depends_on,
position: task.position,
archived: !!task.archived,
createdAt: task.created_at,
createdBy: task.created_by,
creatorName: task.creator_name,
updatedAt: task.updated_at,
labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color })),
subtasks: subtasks.map(s => ({
id: s.id,
title: s.title,
completed: !!s.completed,
position: s.position
})),
subtaskProgress: {
total: subtasks.length,
completed: subtasks.filter(s => s.completed).length
},
attachmentCount,
linkCount,
commentCount,
proposals: proposals.map(p => ({
id: p.id,
title: p.title,
approved: !!p.approved,
approvedByName: p.approved_by_name
}))
};
}
/**
* GET /api/tasks/all
* Alle aktiven Aufgaben (nicht archiviert) fuer Auswahl in Vorschlaegen
*/
router.get('/all', (req, res) => {
try {
const db = getDb();
const tasks = db.prepare(`
SELECT t.id, t.title, t.project_id, p.name as project_name
FROM tasks t
LEFT JOIN projects p ON t.project_id = p.id
WHERE t.archived = 0
ORDER BY p.name, t.title
`).all();
res.json(tasks);
} catch (error) {
logger.error('Fehler beim Abrufen aller Aufgaben:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* GET /api/tasks/project/:projectId
* Alle Aufgaben eines Projekts
*/
router.get('/project/:projectId', (req, res) => {
try {
const db = getDb();
const projectId = req.params.projectId;
const includeArchived = req.query.archived === 'true';
let query = `
SELECT t.id FROM tasks t
WHERE t.project_id = ?
`;
if (!includeArchived) {
query += ' AND t.archived = 0';
}
query += ' ORDER BY t.column_id, t.position';
const taskIds = db.prepare(query).all(projectId);
const tasks = taskIds.map(t => getFullTask(db, t.id));
res.json(tasks);
} catch (error) {
logger.error('Fehler beim Abrufen der Aufgaben:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* GET /api/tasks/search
* Aufgaben suchen - durchsucht auch Subtasks, Links, Anhänge und Kommentare
* WICHTIG: Diese Route MUSS vor /:id definiert werden!
*/
router.get('/search', (req, res) => {
try {
const { q, projectId, assignedTo, priority, dueBefore, dueAfter, labels, archived } = req.query;
const db = getDb();
const params = [];
// Basis-Query mit LEFT JOINs für tiefe Suche
let query = `
SELECT DISTINCT t.id FROM tasks t
LEFT JOIN subtasks s ON t.id = s.task_id
LEFT JOIN links l ON t.id = l.task_id
LEFT JOIN attachments a ON t.id = a.task_id
LEFT JOIN comments c ON t.id = c.task_id
WHERE 1=1
`;
if (projectId) {
query += ' AND t.project_id = ?';
params.push(projectId);
}
// Erweiterte Textsuche: Titel, Beschreibung, Subtasks, Links, Anhänge, Kommentare
if (q) {
const searchTerm = `%${q}%`;
query += ` AND (
t.title LIKE ?
OR t.description LIKE ?
OR s.title LIKE ?
OR l.url LIKE ?
OR l.title LIKE ?
OR a.original_name LIKE ?
OR c.content LIKE ?
)`;
params.push(searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm);
}
if (assignedTo) {
query += ' AND t.assigned_to = ?';
params.push(assignedTo);
}
if (priority) {
query += ' AND t.priority = ?';
params.push(priority);
}
if (dueBefore) {
query += ' AND t.due_date <= ?';
params.push(dueBefore);
}
if (dueAfter) {
query += ' AND t.due_date >= ?';
params.push(dueAfter);
}
if (archived !== 'true') {
query += ' AND t.archived = 0';
}
if (labels) {
const labelIds = labels.split(',').map(id => parseInt(id)).filter(id => !isNaN(id));
if (labelIds.length > 0) {
query += ` AND t.id IN (
SELECT task_id FROM task_labels WHERE label_id IN (${labelIds.map(() => '?').join(',')})
)`;
params.push(...labelIds);
}
}
query += ' ORDER BY t.due_date ASC, t.priority DESC, t.updated_at DESC LIMIT 100';
const taskIds = db.prepare(query).all(...params);
const tasks = taskIds.map(t => getFullTask(db, t.id));
logger.info(`Suche nach "${q}" in Projekt ${projectId}: ${tasks.length} Treffer`);
res.json(tasks);
} catch (error) {
logger.error('Fehler bei der Suche:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* GET /api/tasks/:id
* Einzelne Aufgabe mit allen Details
*/
router.get('/:id', (req, res) => {
try {
const db = getDb();
const task = getFullTask(db, req.params.id);
if (!task) {
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
}
// Historie laden
const history = db.prepare(`
SELECT h.*, u.display_name, u.color
FROM history h
JOIN users u ON h.user_id = u.id
WHERE h.task_id = ?
ORDER BY h.timestamp DESC
LIMIT 50
`).all(req.params.id);
task.history = history.map(h => ({
id: h.id,
action: h.action,
fieldChanged: h.field_changed,
oldValue: h.old_value,
newValue: h.new_value,
timestamp: h.timestamp,
userName: h.display_name,
userColor: h.color
}));
res.json(task);
} catch (error) {
logger.error('Fehler beim Abrufen der Aufgabe:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* POST /api/tasks
* Neue Aufgabe erstellen
*/
router.post('/', (req, res) => {
try {
const {
projectId, columnId, title, description, priority,
startDate, dueDate, assignees, assignedTo, timeEstimateMin, dependsOn, labels
} = req.body;
// Validierung
const errors = [];
errors.push(validators.required(projectId, 'Projekt-ID'));
errors.push(validators.required(columnId, 'Spalten-ID'));
errors.push(validators.required(title, 'Titel'));
errors.push(validators.maxLength(title, 200, 'Titel'));
if (priority) errors.push(validators.enum(priority, ['low', 'medium', 'high'], 'Priorität'));
if (startDate) errors.push(validators.date(startDate, 'Startdatum'));
if (dueDate) errors.push(validators.date(dueDate, 'Fälligkeitsdatum'));
if (timeEstimateMin) errors.push(validators.positiveInteger(timeEstimateMin, 'Zeitschätzung'));
const firstError = errors.find(e => e !== null);
if (firstError) {
return res.status(400).json({ error: firstError });
}
const db = getDb();
// Höchste Position in der Spalte ermitteln
const maxPos = db.prepare(
'SELECT COALESCE(MAX(position), -1) as max FROM tasks WHERE column_id = ?'
).get(columnId).max;
// Aufgabe erstellen (ohne assigned_to, wird über task_assignees gemacht)
const result = db.prepare(`
INSERT INTO tasks (
project_id, column_id, title, description, priority,
start_date, due_date, time_estimate_min, depends_on, position, created_by
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
projectId, columnId, title, description || null, priority || 'medium',
startDate || null, dueDate || null, timeEstimateMin || null,
dependsOn || null, maxPos + 1, req.user.id
);
const taskId = result.lastInsertRowid;
// Mitarbeiter zuweisen (Mehrfachzuweisung)
const assigneeIds = assignees && Array.isArray(assignees) ? assignees :
(assignedTo ? [assignedTo] : []);
if (assigneeIds.length > 0) {
const insertAssignee = db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)');
assigneeIds.forEach(userId => {
try {
insertAssignee.run(taskId, userId);
} catch (e) {
// User existiert nicht oder bereits zugewiesen
}
});
}
// Labels zuweisen
if (labels && Array.isArray(labels)) {
const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)');
labels.forEach(labelId => {
try {
insertLabel.run(taskId, labelId);
} catch (e) {
// Label existiert nicht, ignorieren
}
});
}
// Historie
addHistory(db, taskId, req.user.id, 'created');
const task = getFullTask(db, taskId);
logger.info(`Aufgabe erstellt: ${title} (ID: ${taskId}) von ${req.user.username}`);
// WebSocket
const io = req.app.get('io');
io.to(`project:${projectId}`).emit('task:created', task);
// Benachrichtigungen an zugewiesene Mitarbeiter (außer Ersteller)
if (assigneeIds.length > 0) {
assigneeIds.forEach(assigneeId => {
if (assigneeId !== req.user.id) {
notificationService.create(assigneeId, 'task:assigned', {
taskId: taskId,
taskTitle: title,
projectId: projectId,
actorId: req.user.id,
actorName: req.user.display_name || req.user.username
}, io);
}
});
}
res.status(201).json(task);
} catch (error) {
logger.error('Fehler beim Erstellen der Aufgabe:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* PUT /api/tasks/:id
* Aufgabe aktualisieren
*/
router.put('/:id', (req, res) => {
try {
const taskId = req.params.id;
const {
title, description, priority, columnId, startDate, dueDate, assignees, assignedTo,
timeEstimateMin, dependsOn, labels
} = req.body;
const db = getDb();
const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
if (!existing) {
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
}
// Validierung
if (title) {
const titleError = validators.maxLength(title, 200, 'Titel');
if (titleError) return res.status(400).json({ error: titleError });
}
if (priority) {
const prioError = validators.enum(priority, ['low', 'medium', 'high'], 'Priorität');
if (prioError) return res.status(400).json({ error: prioError });
}
if (startDate) {
const startDateError = validators.date(startDate, 'Startdatum');
if (startDateError) return res.status(400).json({ error: startDateError });
}
if (dueDate) {
const dateError = validators.date(dueDate, 'Fälligkeitsdatum');
if (dateError) return res.status(400).json({ error: dateError });
}
// Änderungen tracken für Historie
const changes = [];
if (title !== undefined && title !== existing.title) {
changes.push({ field: 'title', old: existing.title, new: title });
}
if (description !== undefined && description !== existing.description) {
changes.push({ field: 'description', old: existing.description, new: description });
}
if (priority !== undefined && priority !== existing.priority) {
changes.push({ field: 'priority', old: existing.priority, new: priority });
}
if (columnId !== undefined && columnId !== existing.column_id) {
const oldColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(existing.column_id);
const newColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(columnId);
changes.push({ field: 'column', old: oldColumn?.name, new: newColumn?.name });
}
if (startDate !== undefined && startDate !== existing.start_date) {
changes.push({ field: 'start_date', old: existing.start_date, new: startDate });
}
if (dueDate !== undefined && dueDate !== existing.due_date) {
changes.push({ field: 'due_date', old: existing.due_date, new: dueDate });
}
if (timeEstimateMin !== undefined && timeEstimateMin !== existing.time_estimate_min) {
changes.push({ field: 'time_estimate', old: String(existing.time_estimate_min), new: String(timeEstimateMin) });
}
if (dependsOn !== undefined && dependsOn !== existing.depends_on) {
changes.push({ field: 'depends_on', old: String(existing.depends_on), new: String(dependsOn) });
}
// Aufgabe aktualisieren (ohne assigned_to)
db.prepare(`
UPDATE tasks SET
title = COALESCE(?, title),
description = ?,
priority = COALESCE(?, priority),
column_id = ?,
start_date = ?,
due_date = ?,
time_estimate_min = ?,
depends_on = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(
title || null,
description !== undefined ? description : existing.description,
priority || null,
columnId !== undefined ? columnId : existing.column_id,
startDate !== undefined ? startDate : existing.start_date,
dueDate !== undefined ? dueDate : existing.due_date,
timeEstimateMin !== undefined ? timeEstimateMin : existing.time_estimate_min,
dependsOn !== undefined ? dependsOn : existing.depends_on,
taskId
);
// Mitarbeiter aktualisieren (Mehrfachzuweisung)
if (assignees !== undefined && Array.isArray(assignees)) {
// Alte Zuweisungen entfernen
db.prepare('DELETE FROM task_assignees WHERE task_id = ?').run(taskId);
// Neue Zuweisungen hinzufügen
const insertAssignee = db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)');
assignees.forEach(userId => {
try {
insertAssignee.run(taskId, userId);
} catch (e) {
// User existiert nicht oder bereits zugewiesen
}
});
changes.push({ field: 'assignees', old: 'changed', new: 'changed' });
} else if (assignedTo !== undefined) {
// Rückwärtskompatibilität: einzelne Zuweisung
db.prepare('DELETE FROM task_assignees WHERE task_id = ?').run(taskId);
if (assignedTo) {
try {
db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)').run(taskId, assignedTo);
} catch (e) {
// Ignorieren
}
}
changes.push({ field: 'assignees', old: 'changed', new: 'changed' });
}
// Labels aktualisieren
if (labels !== undefined && Array.isArray(labels)) {
// Alte Labels entfernen
db.prepare('DELETE FROM task_labels WHERE task_id = ?').run(taskId);
// Neue Labels zuweisen
const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)');
labels.forEach(labelId => {
try {
insertLabel.run(taskId, labelId);
} catch (e) {
// Label existiert nicht
}
});
changes.push({ field: 'labels', old: 'changed', new: 'changed' });
}
// Historie-Einträge
changes.forEach(change => {
addHistory(db, taskId, req.user.id, 'updated', change.field, change.old, change.new);
});
const task = getFullTask(db, taskId);
logger.info(`Aufgabe aktualisiert: ${task.title} (ID: ${taskId})`);
// WebSocket
const io = req.app.get('io');
io.to(`project:${existing.project_id}`).emit('task:updated', task);
// Benachrichtigungen für Änderungen senden
const taskTitle = title || existing.title;
// Zuweisung geändert - neue Mitarbeiter benachrichtigen
if (assignees !== undefined && Array.isArray(assignees)) {
// Alte Mitarbeiter ermitteln
const oldAssignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
const oldAssigneeIds = oldAssignees.map(a => a.user_id);
// Neue Mitarbeiter (die vorher nicht zugewiesen waren)
const newAssigneeIds = assignees.filter(id => !oldAssigneeIds.includes(id));
newAssigneeIds.forEach(assigneeId => {
if (assigneeId !== req.user.id) {
notificationService.create(assigneeId, 'task:assigned', {
taskId: parseInt(taskId),
taskTitle: taskTitle,
projectId: existing.project_id,
actorId: req.user.id,
actorName: req.user.display_name || req.user.username
}, io);
}
});
// Entfernte Mitarbeiter
const removedAssigneeIds = oldAssigneeIds.filter(id => !assignees.includes(id));
removedAssigneeIds.forEach(assigneeId => {
if (assigneeId !== req.user.id) {
notificationService.create(assigneeId, 'task:unassigned', {
taskId: parseInt(taskId),
taskTitle: taskTitle,
projectId: existing.project_id,
actorId: req.user.id,
actorName: req.user.display_name || req.user.username
}, io);
}
});
}
// Priorität auf hoch gesetzt
if (priority === 'high' && existing.priority !== 'high') {
// Alle Assignees benachrichtigen
const currentAssignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
currentAssignees.forEach(a => {
if (a.user_id !== req.user.id) {
notificationService.create(a.user_id, 'task:priority_up', {
taskId: parseInt(taskId),
taskTitle: taskTitle,
projectId: existing.project_id,
actorId: req.user.id,
actorName: req.user.display_name || req.user.username
}, io);
}
});
}
// Fälligkeitsdatum geändert
if (dueDate !== undefined && dueDate !== existing.due_date) {
const currentAssignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
currentAssignees.forEach(a => {
if (a.user_id !== req.user.id) {
notificationService.create(a.user_id, 'task:due_changed', {
taskId: parseInt(taskId),
taskTitle: taskTitle,
projectId: existing.project_id,
actorId: req.user.id,
actorName: req.user.display_name || req.user.username
}, io);
}
});
}
res.json(task);
} catch (error) {
logger.error('Fehler beim Aktualisieren der Aufgabe:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* PUT /api/tasks/:id/move
* Aufgabe verschieben (Spalte/Position)
*/
router.put('/:id/move', (req, res) => {
try {
const taskId = req.params.id;
const { columnId, position } = req.body;
const db = getDb();
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
if (!task) {
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
}
const oldColumnId = task.column_id;
const oldPosition = task.position;
const newColumnId = columnId || oldColumnId;
const newPosition = position !== undefined ? position : oldPosition;
// Spaltenname für Historie
const oldColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(oldColumnId);
const newColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(newColumnId);
if (oldColumnId !== newColumnId) {
// In andere Spalte verschoben
// Positionen in alter Spalte anpassen
db.prepare(`
UPDATE tasks SET position = position - 1
WHERE column_id = ? AND position > ?
`).run(oldColumnId, oldPosition);
// Positionen in neuer Spalte anpassen
db.prepare(`
UPDATE tasks SET position = position + 1
WHERE column_id = ? AND position >= ?
`).run(newColumnId, newPosition);
// Task verschieben
db.prepare(`
UPDATE tasks SET column_id = ?, position = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(newColumnId, newPosition, taskId);
addHistory(db, taskId, req.user.id, 'moved', 'column', oldColumn?.name, newColumn?.name);
} else if (oldPosition !== newPosition) {
// Innerhalb der Spalte verschoben
if (newPosition > oldPosition) {
db.prepare(`
UPDATE tasks SET position = position - 1
WHERE column_id = ? AND position > ? AND position <= ?
`).run(newColumnId, oldPosition, newPosition);
} else {
db.prepare(`
UPDATE tasks SET position = position + 1
WHERE column_id = ? AND position >= ? AND position < ?
`).run(newColumnId, newPosition, oldPosition);
}
db.prepare(`
UPDATE tasks SET position = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(newPosition, taskId);
}
const updatedTask = getFullTask(db, taskId);
logger.info(`Aufgabe verschoben: ${task.title} -> ${newColumn?.name || 'Position ' + newPosition}`);
// WebSocket
const io = req.app.get('io');
io.to(`project:${task.project_id}`).emit('task:moved', {
task: updatedTask,
oldColumnId,
newColumnId,
oldPosition,
newPosition
});
// Benachrichtigung wenn in Erledigt-Spalte verschoben
if (oldColumnId !== newColumnId) {
const newColumnFull = db.prepare('SELECT filter_category FROM columns WHERE id = ?').get(newColumnId);
const oldColumnFull = db.prepare('SELECT filter_category FROM columns WHERE id = ?').get(oldColumnId);
// Prüfen ob in Erledigt-Spalte verschoben (und vorher nicht dort war)
if (newColumnFull?.filter_category === 'completed' && oldColumnFull?.filter_category !== 'completed') {
// Alle Assignees benachrichtigen
const assignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
assignees.forEach(a => {
if (a.user_id !== req.user.id) {
notificationService.create(a.user_id, 'task:completed', {
taskId: parseInt(taskId),
taskTitle: task.title,
projectId: task.project_id,
actorId: req.user.id,
actorName: req.user.display_name || req.user.username
}, io);
}
});
}
}
res.json(updatedTask);
} catch (error) {
logger.error('Fehler beim Verschieben der Aufgabe:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* POST /api/tasks/:id/duplicate
* Aufgabe duplizieren
*/
router.post('/:id/duplicate', (req, res) => {
try {
const taskId = req.params.id;
const db = getDb();
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
if (!task) {
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
}
// Höchste Position ermitteln
const maxPos = db.prepare(
'SELECT COALESCE(MAX(position), -1) as max FROM tasks WHERE column_id = ?'
).get(task.column_id).max;
// Aufgabe duplizieren
const result = db.prepare(`
INSERT INTO tasks (
project_id, column_id, title, description, priority,
due_date, assigned_to, time_estimate_min, position, created_by
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
task.project_id, task.column_id, task.title + ' (Kopie)', task.description,
task.priority, task.due_date, task.assigned_to, task.time_estimate_min,
maxPos + 1, req.user.id
);
const newTaskId = result.lastInsertRowid;
// Labels kopieren
const taskLabels = db.prepare('SELECT label_id FROM task_labels WHERE task_id = ?').all(taskId);
const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)');
taskLabels.forEach(tl => insertLabel.run(newTaskId, tl.label_id));
// Subtasks kopieren
const subtasks = db.prepare('SELECT * FROM subtasks WHERE task_id = ? ORDER BY position').all(taskId);
const insertSubtask = db.prepare('INSERT INTO subtasks (task_id, title, position) VALUES (?, ?, ?)');
subtasks.forEach((st, idx) => insertSubtask.run(newTaskId, st.title, idx));
addHistory(db, newTaskId, req.user.id, 'created', null, null, `Kopie von #${taskId}`);
const newTask = getFullTask(db, newTaskId);
logger.info(`Aufgabe dupliziert: ${task.title} -> ${newTask.title}`);
// WebSocket
const io = req.app.get('io');
io.to(`project:${task.project_id}`).emit('task:created', newTask);
res.status(201).json(newTask);
} catch (error) {
logger.error('Fehler beim Duplizieren:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* PUT /api/tasks/:id/archive
* Aufgabe archivieren
*/
router.put('/:id/archive', (req, res) => {
try {
const taskId = req.params.id;
const { archived } = req.body;
const db = getDb();
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
if (!task) {
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
}
db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(archived ? 1 : 0, taskId);
addHistory(db, taskId, req.user.id, archived ? 'archived' : 'restored');
logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`);
// WebSocket
const io = req.app.get('io');
io.to(`project:${task.project_id}`).emit('task:archived', { id: taskId, archived: !!archived });
res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt' });
} catch (error) {
logger.error('Fehler beim Archivieren:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* DELETE /api/tasks/:id
* Aufgabe löschen
*/
router.delete('/:id', (req, res) => {
try {
const taskId = req.params.id;
const db = getDb();
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
if (!task) {
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
}
db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
// Positionen neu nummerieren
const remainingTasks = db.prepare(
'SELECT id FROM tasks WHERE column_id = ? ORDER BY position'
).all(task.column_id);
remainingTasks.forEach((t, idx) => {
db.prepare('UPDATE tasks SET position = ? WHERE id = ?').run(idx, t.id);
});
logger.info(`Aufgabe gelöscht: ${task.title} (ID: ${taskId})`);
// WebSocket
const io = req.app.get('io');
io.to(`project:${task.project_id}`).emit('task:deleted', {
id: taskId,
columnId: task.column_id
});
res.json({ message: 'Aufgabe gelöscht' });
} catch (error) {
logger.error('Fehler beim Löschen:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
module.exports = router;