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
|
-- Description: Adds a due_date column to track task deadlines
|
||||||
|
|
||||||
-- Add due_date column to todos table
|
-- Add due_date column to note_todos table
|
||||||
ALTER TABLE todos ADD COLUMN IF NOT EXISTS due_date TIMESTAMPTZ;
|
ALTER TABLE note_todos ADD COLUMN IF NOT EXISTS due_date TIMESTAMPTZ;
|
||||||
|
|
||||||
-- Create index on due_date for faster queries
|
-- 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 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
|
// Charger les todos, images, fichiers et tags pour chaque note
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
const todos = await getAll(`
|
const todos = await getAll(`
|
||||||
SELECT id, text, completed, position
|
SELECT id, text, completed, position, parent_id, level
|
||||||
FROM note_todos
|
FROM note_todos
|
||||||
WHERE note_id = $1
|
WHERE note_id = $1
|
||||||
ORDER BY position ASC, id ASC
|
ORDER BY position ASC, id ASC
|
||||||
@@ -391,10 +391,13 @@ router.delete('/:id/image', async (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/notes/:id/todos
|
* POST /api/notes/:id/todos
|
||||||
* Ajouter un todo à une note
|
* Ajouter un todo à une note (supports subtasks via parent_id)
|
||||||
*/
|
*/
|
||||||
router.post('/:id/todos',
|
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) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
@@ -408,17 +411,27 @@ router.post('/:id/todos',
|
|||||||
return res.status(404).json({ error: 'Note non trouvée' });
|
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(`
|
const result = await runQuery(`
|
||||||
INSERT INTO note_todos (note_id, text, position)
|
INSERT INTO note_todos (note_id, text, parent_id, level, position)
|
||||||
VALUES ($1, $2, (SELECT COALESCE(MAX(position), 0) + 1 FROM note_todos WHERE note_id = $3))
|
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 *
|
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]);
|
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,
|
id: result.id,
|
||||||
text,
|
text,
|
||||||
completed: false,
|
completed: false,
|
||||||
position: 0
|
position: result.position || 0,
|
||||||
|
parent_id: parent_id || null,
|
||||||
|
level: level
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de l\'ajout du todo:', 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)
|
// Essayer d'abord avec le champ priority (nouvelle version)
|
||||||
try {
|
try {
|
||||||
const todos = await getAll(`
|
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
|
FROM global_todos
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY priority DESC, created_at DESC
|
ORDER BY priority DESC, created_at DESC
|
||||||
@@ -38,7 +38,7 @@ router.get('/', async (req, res) => {
|
|||||||
`, [req.user.id]);
|
`, [req.user.id]);
|
||||||
|
|
||||||
// Ajouter priority=false et in_progress=false par défaut pour compatibilité frontend
|
// 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);
|
res.json(todosWithPriority);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -49,10 +49,13 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/todos
|
* POST /api/todos
|
||||||
* Créer un nouveau todo global
|
* Créer un nouveau todo global (supports subtasks via parent_id)
|
||||||
*/
|
*/
|
||||||
router.post('/',
|
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) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
@@ -60,17 +63,27 @@ router.post('/',
|
|||||||
return res.status(400).json({ error: 'Données invalides', details: errors.array() });
|
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(`
|
const result = await runQuery(`
|
||||||
INSERT INTO global_todos (user_id, text)
|
INSERT INTO global_todos (user_id, text, parent_id, level)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING *
|
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({
|
res.status(201).json({
|
||||||
id: result.id,
|
id: result.id,
|
||||||
@@ -78,6 +91,8 @@ router.post('/',
|
|||||||
completed: false,
|
completed: false,
|
||||||
priority: false,
|
priority: false,
|
||||||
in_progress: false,
|
in_progress: false,
|
||||||
|
parent_id: parent_id || null,
|
||||||
|
level: level,
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in new issue
Block a user