From bde406bc3e7254c16dff83bc49adb67bb8d8832c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Nov 2025 13:51:42 +0000 Subject: [PATCH] feat: add backend support for hierarchical subtasks - Create migration 005 to add parent_id and level columns to todos tables - Fix migration 003 to use correct table name (note_todos instead of todos) - Update POST /api/notes/:id/todos to accept parent_id parameter - Update POST /api/todos to accept parent_id for global todos - Add automatic level calculation based on parent depth - Update GET endpoints to return parent_id and level fields - Add validation to ensure parent todo exists before creating subtask - Support CASCADE DELETE for subtasks when parent is deleted --- migrations/003_add_due_date_to_todos.sql | 10 +++---- migrations/005_add_subtasks_support.sql | 16 +++++++++++ routes/notes.routes.js | 35 +++++++++++++++++------- routes/todos.routes.js | 35 +++++++++++++++++------- 4 files changed, 71 insertions(+), 25 deletions(-) create mode 100644 migrations/005_add_subtasks_support.sql diff --git a/migrations/003_add_due_date_to_todos.sql b/migrations/003_add_due_date_to_todos.sql index 83dda9d..3c67484 100644 --- a/migrations/003_add_due_date_to_todos.sql +++ b/migrations/003_add_due_date_to_todos.sql @@ -1,11 +1,11 @@ --- Migration: Add due_date to todos table +-- Migration: Add due_date to note_todos table -- Description: Adds a due_date column to track task deadlines --- Add due_date column to todos table -ALTER TABLE todos ADD COLUMN IF NOT EXISTS due_date TIMESTAMPTZ; +-- Add due_date column to note_todos table +ALTER TABLE note_todos ADD COLUMN IF NOT EXISTS due_date TIMESTAMPTZ; -- Create index on due_date for faster queries -CREATE INDEX IF NOT EXISTS idx_todos_due_date ON todos(due_date); +CREATE INDEX IF NOT EXISTS idx_note_todos_due_date ON note_todos(due_date); -- Create index on combination of completed and due_date for efficient queries of pending tasks -CREATE INDEX IF NOT EXISTS idx_todos_completed_due_date ON todos(completed, due_date); +CREATE INDEX IF NOT EXISTS idx_note_todos_completed_due_date ON note_todos(completed, due_date); diff --git a/migrations/005_add_subtasks_support.sql b/migrations/005_add_subtasks_support.sql new file mode 100644 index 0000000..8727a30 --- /dev/null +++ b/migrations/005_add_subtasks_support.sql @@ -0,0 +1,16 @@ +-- Migration: Add subtasks support to todos tables +-- Description: Adds parent_id column to enable hierarchical task structure + +-- Add parent_id column to note_todos table (note todos) +ALTER TABLE note_todos ADD COLUMN IF NOT EXISTS parent_id INTEGER REFERENCES note_todos(id) ON DELETE CASCADE; + +-- Add parent_id column to global_todos table +ALTER TABLE global_todos ADD COLUMN IF NOT EXISTS parent_id INTEGER REFERENCES global_todos(id) ON DELETE CASCADE; + +-- Create indexes for faster subtask queries +CREATE INDEX IF NOT EXISTS idx_note_todos_parent_id ON note_todos(parent_id); +CREATE INDEX IF NOT EXISTS idx_global_todos_parent_id ON global_todos(parent_id); + +-- Add level column to track depth (optional, for UI optimization) +ALTER TABLE note_todos ADD COLUMN IF NOT EXISTS level INTEGER DEFAULT 0; +ALTER TABLE global_todos ADD COLUMN IF NOT EXISTS level INTEGER DEFAULT 0; diff --git a/routes/notes.routes.js b/routes/notes.routes.js index 14650e3..f78dbc7 100644 --- a/routes/notes.routes.js +++ b/routes/notes.routes.js @@ -61,7 +61,7 @@ router.get('/', async (req, res) => { // Charger les todos, images, fichiers et tags pour chaque note for (const note of notes) { const todos = await getAll(` - SELECT id, text, completed, position + SELECT id, text, completed, position, parent_id, level FROM note_todos WHERE note_id = $1 ORDER BY position ASC, id ASC @@ -391,10 +391,13 @@ router.delete('/:id/image', async (req, res) => { /** * POST /api/notes/:id/todos - * Ajouter un todo à une note + * Ajouter un todo à une note (supports subtasks via parent_id) */ router.post('/:id/todos', - [body('text').trim().notEmpty().withMessage('Le texte est requis')], + [ + body('text').trim().notEmpty().withMessage('Le texte est requis'), + body('parent_id').optional().isInt() + ], async (req, res) => { try { const errors = validationResult(req); @@ -408,17 +411,27 @@ router.post('/:id/todos', return res.status(404).json({ error: 'Note non trouvée' }); } - const { text } = req.body; + const { text, parent_id } = req.body; + let level = 0; - logger.info(`[CREATE TODO] Ajout todo à note ${req.params.id}: "${text}"`); + // Si c'est un subtask, vérifier que le parent existe et calculer le niveau + if (parent_id) { + const parent = await getOne('SELECT id, level FROM note_todos WHERE id = $1 AND note_id = $2', [parent_id, req.params.id]); + if (!parent) { + return res.status(404).json({ error: 'Todo parent non trouvé' }); + } + level = (parent.level || 0) + 1; + } + + logger.info(`[CREATE TODO] Ajout todo à note ${req.params.id}: "${text}"${parent_id ? ` (subtask of ${parent_id})` : ''}`); const result = await runQuery(` - INSERT INTO note_todos (note_id, text, position) - VALUES ($1, $2, (SELECT COALESCE(MAX(position), 0) + 1 FROM note_todos WHERE note_id = $3)) + INSERT INTO note_todos (note_id, text, parent_id, level, position) + VALUES ($1, $2, $3, $4, (SELECT COALESCE(MAX(position), 0) + 1 FROM note_todos WHERE note_id = $5 AND COALESCE(parent_id, 0) = COALESCE($6, 0))) RETURNING * - `, [req.params.id, text, req.params.id]); + `, [req.params.id, text, parent_id || null, level, req.params.id, parent_id || null]); - logger.info(`[CREATE TODO] Todo créé - ID: ${result.id}, note_id: ${req.params.id}`); + logger.info(`[CREATE TODO] Todo créé - ID: ${result.id}, note_id: ${req.params.id}, level: ${level}`); await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [req.params.id]); @@ -426,7 +439,9 @@ router.post('/:id/todos', id: result.id, text, completed: false, - position: 0 + position: result.position || 0, + parent_id: parent_id || null, + level: level }); } catch (error) { logger.error('Erreur lors de l\'ajout du todo:', error); diff --git a/routes/todos.routes.js b/routes/todos.routes.js index 7b86d8e..297be55 100644 --- a/routes/todos.routes.js +++ b/routes/todos.routes.js @@ -19,7 +19,7 @@ router.get('/', async (req, res) => { // Essayer d'abord avec le champ priority (nouvelle version) try { const todos = await getAll(` - SELECT id, text, completed, priority, in_progress, created_at + SELECT id, text, completed, priority, in_progress, parent_id, level, created_at FROM global_todos WHERE user_id = $1 ORDER BY priority DESC, created_at DESC @@ -38,7 +38,7 @@ router.get('/', async (req, res) => { `, [req.user.id]); // Ajouter priority=false et in_progress=false par défaut pour compatibilité frontend - const todosWithPriority = todos.map(t => ({ ...t, priority: false, in_progress: false })); + const todosWithPriority = todos.map(t => ({ ...t, priority: false, in_progress: false, parent_id: null, level: 0 })); res.json(todosWithPriority); } } catch (error) { @@ -49,10 +49,13 @@ router.get('/', async (req, res) => { /** * POST /api/todos - * Créer un nouveau todo global + * Créer un nouveau todo global (supports subtasks via parent_id) */ router.post('/', - [body('text').trim().notEmpty().withMessage('Le texte est requis')], + [ + body('text').trim().notEmpty().withMessage('Le texte est requis'), + body('parent_id').optional().isInt() + ], async (req, res) => { try { const errors = validationResult(req); @@ -60,17 +63,27 @@ router.post('/', return res.status(400).json({ error: 'Données invalides', details: errors.array() }); } - const { text } = req.body; + const { text, parent_id } = req.body; + let level = 0; - logger.info(`[CREATE GLOBAL TODO] Début création - user_id: ${req.user.id}, text: "${text}"`); + // Si c'est un subtask, vérifier que le parent existe et calculer le niveau + if (parent_id) { + const parent = await getOne('SELECT id, level FROM global_todos WHERE id = $1 AND user_id = $2', [parent_id, req.user.id]); + if (!parent) { + return res.status(404).json({ error: 'Todo parent non trouvé' }); + } + level = (parent.level || 0) + 1; + } + + logger.info(`[CREATE GLOBAL TODO] Début création - user_id: ${req.user.id}, text: "${text}"${parent_id ? ` (subtask of ${parent_id})` : ''}`); const result = await runQuery(` - INSERT INTO global_todos (user_id, text) - VALUES ($1, $2) + INSERT INTO global_todos (user_id, text, parent_id, level) + VALUES ($1, $2, $3, $4) RETURNING * - `, [req.user.id, text]); + `, [req.user.id, text, parent_id || null, level]); - logger.info(`[CREATE GLOBAL TODO] Todo global créé avec succès - ID: ${result.id}, user: ${req.user.username}`); + logger.info(`[CREATE GLOBAL TODO] Todo global créé avec succès - ID: ${result.id}, user: ${req.user.username}, level: ${level}`); res.status(201).json({ id: result.id, @@ -78,6 +91,8 @@ router.post('/', completed: false, priority: false, in_progress: false, + parent_id: parent_id || null, + level: level, created_at: new Date().toISOString() }); } catch (error) {