mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
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
This commit is contained in:
4 files changed
+71
-25
No files matched your search
@@ -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);
|
||||
@@ -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;
|
||||
+25
-10
@@ -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);
|
||||
|
||||
+25
-10
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user