Initial commit
Dieser Commit ist enthalten in:
359
backend/routes/projects.js
Normale Datei
359
backend/routes/projects.js
Normale Datei
@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 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;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren