360 Zeilen
11 KiB
JavaScript
360 Zeilen
11 KiB
JavaScript
/**
|
|
* TASKMATE - Project Routes
|
|
* =========================
|
|
* CRUD für Projekte
|
|
*/
|
|
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const { getDb } = require('../database');
|
|
const logger = require('../utils/logger');
|
|
const { validators } = require('../middleware/validation');
|
|
|
|
/**
|
|
* GET /api/projects
|
|
* Alle Projekte abrufen
|
|
*/
|
|
router.get('/', (req, res) => {
|
|
try {
|
|
const db = getDb();
|
|
const includeArchived = req.query.archived === 'true';
|
|
|
|
let query = `
|
|
SELECT p.*, u.display_name as creator_name,
|
|
(SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.archived = 0) as task_count,
|
|
(SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.archived = 0 AND t.column_id IN
|
|
(SELECT c.id FROM columns c WHERE c.project_id = p.id ORDER BY c.position DESC LIMIT 1)) as completed_count
|
|
FROM projects p
|
|
LEFT JOIN users u ON p.created_by = u.id
|
|
`;
|
|
|
|
if (!includeArchived) {
|
|
query += ' WHERE p.archived = 0';
|
|
}
|
|
|
|
query += ' ORDER BY p.created_at DESC';
|
|
|
|
const projects = db.prepare(query).all();
|
|
|
|
res.json(projects.map(p => ({
|
|
id: p.id,
|
|
name: p.name,
|
|
description: p.description,
|
|
archived: !!p.archived,
|
|
createdAt: p.created_at,
|
|
createdBy: p.created_by,
|
|
creatorName: p.creator_name,
|
|
taskCount: p.task_count,
|
|
completedCount: p.completed_count
|
|
})));
|
|
} catch (error) {
|
|
logger.error('Fehler beim Abrufen der Projekte:', { error: error.message });
|
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/projects/:id
|
|
* Einzelnes Projekt mit Spalten und Aufgaben
|
|
*/
|
|
router.get('/:id', (req, res) => {
|
|
try {
|
|
const db = getDb();
|
|
const projectId = req.params.id;
|
|
|
|
// Projekt abrufen
|
|
const project = db.prepare(`
|
|
SELECT p.*, u.display_name as creator_name
|
|
FROM projects p
|
|
LEFT JOIN users u ON p.created_by = u.id
|
|
WHERE p.id = ?
|
|
`).get(projectId);
|
|
|
|
if (!project) {
|
|
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
|
}
|
|
|
|
// Spalten abrufen
|
|
const columns = db.prepare(`
|
|
SELECT * FROM columns WHERE project_id = ? ORDER BY position
|
|
`).all(projectId);
|
|
|
|
// Labels abrufen
|
|
const labels = db.prepare(`
|
|
SELECT * FROM labels WHERE project_id = ?
|
|
`).all(projectId);
|
|
|
|
res.json({
|
|
id: project.id,
|
|
name: project.name,
|
|
description: project.description,
|
|
archived: !!project.archived,
|
|
createdAt: project.created_at,
|
|
createdBy: project.created_by,
|
|
creatorName: project.creator_name,
|
|
columns: columns.map(c => ({
|
|
id: c.id,
|
|
name: c.name,
|
|
position: c.position,
|
|
color: c.color
|
|
})),
|
|
labels: labels.map(l => ({
|
|
id: l.id,
|
|
name: l.name,
|
|
color: l.color
|
|
}))
|
|
});
|
|
} catch (error) {
|
|
logger.error('Fehler beim Abrufen des Projekts:', { error: error.message });
|
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/projects
|
|
* Neues Projekt erstellen
|
|
*/
|
|
router.post('/', (req, res) => {
|
|
try {
|
|
const { name, description } = req.body;
|
|
|
|
// Validierung
|
|
const nameError = validators.required(name, 'Name') ||
|
|
validators.maxLength(name, 100, 'Name');
|
|
if (nameError) {
|
|
return res.status(400).json({ error: nameError });
|
|
}
|
|
|
|
const db = getDb();
|
|
|
|
// Projekt erstellen
|
|
const result = db.prepare(`
|
|
INSERT INTO projects (name, description, created_by)
|
|
VALUES (?, ?, ?)
|
|
`).run(name, description || null, req.user.id);
|
|
|
|
const projectId = result.lastInsertRowid;
|
|
|
|
// Standard-Spalten erstellen
|
|
const insertColumn = db.prepare(`
|
|
INSERT INTO columns (project_id, name, position) VALUES (?, ?, ?)
|
|
`);
|
|
|
|
insertColumn.run(projectId, 'Offen', 0);
|
|
insertColumn.run(projectId, 'In Arbeit', 1);
|
|
insertColumn.run(projectId, 'Erledigt', 2);
|
|
|
|
// Standard-Labels erstellen
|
|
const insertLabel = db.prepare(`
|
|
INSERT INTO labels (project_id, name, color) VALUES (?, ?, ?)
|
|
`);
|
|
|
|
insertLabel.run(projectId, 'Bug', '#DC2626');
|
|
insertLabel.run(projectId, 'Feature', '#059669');
|
|
insertLabel.run(projectId, 'Dokumentation', '#3182CE');
|
|
|
|
// Projekt mit Spalten und Labels zurückgeben
|
|
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
|
const columns = db.prepare('SELECT * FROM columns WHERE project_id = ? ORDER BY position').all(projectId);
|
|
const labels = db.prepare('SELECT * FROM labels WHERE project_id = ?').all(projectId);
|
|
|
|
logger.info(`Projekt erstellt: ${name} (ID: ${projectId}) von ${req.user.username}`);
|
|
|
|
// WebSocket: Andere Clients benachrichtigen
|
|
const io = req.app.get('io');
|
|
io.emit('project:created', {
|
|
id: projectId,
|
|
name: project.name,
|
|
description: project.description,
|
|
createdBy: req.user.id
|
|
});
|
|
|
|
res.status(201).json({
|
|
id: project.id,
|
|
name: project.name,
|
|
description: project.description,
|
|
archived: false,
|
|
createdAt: project.created_at,
|
|
createdBy: project.created_by,
|
|
columns: columns.map(c => ({ id: c.id, name: c.name, position: c.position, color: c.color })),
|
|
labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color }))
|
|
});
|
|
} catch (error) {
|
|
logger.error('Fehler beim Erstellen des Projekts:', { error: error.message });
|
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/projects/:id
|
|
* Projekt aktualisieren
|
|
*/
|
|
router.put('/:id', (req, res) => {
|
|
try {
|
|
const projectId = req.params.id;
|
|
const { name, description } = req.body;
|
|
|
|
// Validierung
|
|
if (name) {
|
|
const nameError = validators.maxLength(name, 100, 'Name');
|
|
if (nameError) {
|
|
return res.status(400).json({ error: nameError });
|
|
}
|
|
}
|
|
|
|
const db = getDb();
|
|
|
|
// Prüfen ob Projekt existiert
|
|
const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
|
if (!existing) {
|
|
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
|
}
|
|
|
|
// Aktualisieren
|
|
db.prepare(`
|
|
UPDATE projects
|
|
SET name = COALESCE(?, name), description = COALESCE(?, description)
|
|
WHERE id = ?
|
|
`).run(name || null, description !== undefined ? description : null, projectId);
|
|
|
|
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
|
|
|
logger.info(`Projekt aktualisiert: ${project.name} (ID: ${projectId})`);
|
|
|
|
// WebSocket
|
|
const io = req.app.get('io');
|
|
io.emit('project:updated', {
|
|
id: project.id,
|
|
name: project.name,
|
|
description: project.description
|
|
});
|
|
|
|
res.json({
|
|
id: project.id,
|
|
name: project.name,
|
|
description: project.description,
|
|
archived: !!project.archived,
|
|
createdAt: project.created_at
|
|
});
|
|
} catch (error) {
|
|
logger.error('Fehler beim Aktualisieren des Projekts:', { error: error.message });
|
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/projects/:id/archive
|
|
* Projekt archivieren/wiederherstellen
|
|
*/
|
|
router.put('/:id/archive', (req, res) => {
|
|
try {
|
|
const projectId = req.params.id;
|
|
const { archived } = req.body;
|
|
|
|
const db = getDb();
|
|
|
|
const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
|
if (!existing) {
|
|
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
|
}
|
|
|
|
db.prepare('UPDATE projects SET archived = ? WHERE id = ?')
|
|
.run(archived ? 1 : 0, projectId);
|
|
|
|
logger.info(`Projekt ${archived ? 'archiviert' : 'wiederhergestellt'}: ${existing.name}`);
|
|
|
|
// WebSocket
|
|
const io = req.app.get('io');
|
|
io.emit('project:archived', { id: projectId, archived: !!archived });
|
|
|
|
res.json({ message: archived ? 'Projekt archiviert' : 'Projekt wiederhergestellt' });
|
|
} catch (error) {
|
|
logger.error('Fehler beim Archivieren:', { error: error.message });
|
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/projects/:id
|
|
* Projekt löschen
|
|
* Query param: force=true um alle zugehörigen Aufgaben mitzulöschen
|
|
*/
|
|
router.delete('/:id', (req, res) => {
|
|
try {
|
|
const projectId = req.params.id;
|
|
const forceDelete = req.query.force === 'true';
|
|
const db = getDb();
|
|
|
|
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
|
if (!project) {
|
|
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
|
}
|
|
|
|
// Anzahl der Aufgaben ermitteln
|
|
const taskCount = db.prepare(
|
|
'SELECT COUNT(*) as count FROM tasks WHERE project_id = ?'
|
|
).get(projectId).count;
|
|
|
|
// Ohne force: Prüfen ob noch aktive Aufgaben existieren
|
|
if (!forceDelete && taskCount > 0) {
|
|
return res.status(400).json({
|
|
error: 'Projekt enthält noch Aufgaben. Verwende force=true um alles zu löschen.',
|
|
taskCount: taskCount
|
|
});
|
|
}
|
|
|
|
// Bei force=true: Explizit alle zugehörigen Daten löschen
|
|
if (forceDelete && taskCount > 0) {
|
|
// Alle Task-IDs für das Projekt holen
|
|
const taskIds = db.prepare('SELECT id FROM tasks WHERE project_id = ?')
|
|
.all(projectId)
|
|
.map(t => t.id);
|
|
|
|
if (taskIds.length > 0) {
|
|
const placeholders = taskIds.map(() => '?').join(',');
|
|
|
|
// Anhänge löschen
|
|
db.prepare(`DELETE FROM attachments WHERE task_id IN (${placeholders})`).run(...taskIds);
|
|
// Kommentare löschen
|
|
db.prepare(`DELETE FROM comments WHERE task_id IN (${placeholders})`).run(...taskIds);
|
|
// Task-Labels löschen
|
|
db.prepare(`DELETE FROM task_labels WHERE task_id IN (${placeholders})`).run(...taskIds);
|
|
// Task-Assignees löschen (Mehrfachzuweisung)
|
|
db.prepare(`DELETE FROM task_assignees WHERE task_id IN (${placeholders})`).run(...taskIds);
|
|
// Unteraufgaben löschen
|
|
db.prepare(`DELETE FROM subtasks WHERE task_id IN (${placeholders})`).run(...taskIds);
|
|
// Links löschen
|
|
db.prepare(`DELETE FROM links WHERE task_id IN (${placeholders})`).run(...taskIds);
|
|
// Historie löschen
|
|
db.prepare(`DELETE FROM history WHERE task_id IN (${placeholders})`).run(...taskIds);
|
|
// Tasks löschen
|
|
db.prepare(`DELETE FROM tasks WHERE project_id = ?`).run(projectId);
|
|
}
|
|
|
|
logger.info(`${taskCount} Aufgaben gelöscht für Projekt: ${project.name}`);
|
|
}
|
|
|
|
// Labels des Projekts löschen
|
|
db.prepare('DELETE FROM labels WHERE project_id = ?').run(projectId);
|
|
|
|
// Spalten löschen
|
|
db.prepare('DELETE FROM columns WHERE project_id = ?').run(projectId);
|
|
|
|
// Projekt löschen
|
|
db.prepare('DELETE FROM projects WHERE id = ?').run(projectId);
|
|
|
|
logger.info(`Projekt gelöscht: ${project.name} (ID: ${projectId}), ${taskCount} Aufgaben entfernt`);
|
|
|
|
// WebSocket
|
|
const io = req.app.get('io');
|
|
io.emit('project:deleted', { id: projectId });
|
|
|
|
res.json({ message: 'Projekt gelöscht', deletedTasks: taskCount });
|
|
} catch (error) {
|
|
logger.error('Fehler beim Löschen des Projekts:', { error: error.message });
|
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|