From 2665ba5cc1bf914ea75d64848bdb9e58c5b61928 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 23 Nov 2025 15:40:03 +0100 Subject: [PATCH] feat: add comprehensive dashboard page with todo management, RSS feeds, calendar, and AI integration --- package-lock.json | 44 ++++++++++++++---------------- routes/todos.routes.js | 53 ++++++++++++++++++++++++++++++++---- scripts/migrate_003.js | 32 ++++++++++++++++++++++ src/pages/Index.tsx | 19 ++++++++++++- src/services/TodosService.ts | 11 ++++++++ 5 files changed, 129 insertions(+), 30 deletions(-) create mode 100644 scripts/migrate_003.js diff --git a/package-lock.json b/package-lock.json index a77256f..15c3881 100644 --- a/package-lock.json +++ b/package-lock.json @@ -138,6 +138,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3681,6 +3682,7 @@ "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3691,6 +3693,7 @@ "integrity": "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3701,6 +3704,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3757,6 +3761,7 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -4042,6 +4047,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4476,6 +4482,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5328,7 +5335,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -5373,29 +5381,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -5552,6 +5537,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7006,6 +6992,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -8239,6 +8226,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -8378,6 +8366,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8768,6 +8757,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8808,6 +8798,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9860,6 +9851,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -10064,6 +10056,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10192,6 +10185,7 @@ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10439,6 +10433,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -10530,6 +10525,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/routes/todos.routes.js b/routes/todos.routes.js index 501ecb5..7b86d8e 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, created_at + SELECT id, text, completed, priority, in_progress, created_at FROM global_todos WHERE user_id = $1 ORDER BY priority DESC, created_at DESC @@ -37,8 +37,8 @@ router.get('/', async (req, res) => { ORDER BY created_at DESC `, [req.user.id]); - // Ajouter priority=false par défaut pour compatibilité frontend - const todosWithPriority = todos.map(t => ({ ...t, priority: false })); + // Ajouter priority=false et in_progress=false par défaut pour compatibilité frontend + const todosWithPriority = todos.map(t => ({ ...t, priority: false, in_progress: false })); res.json(todosWithPriority); } } catch (error) { @@ -77,6 +77,7 @@ router.post('/', text, completed: false, priority: false, + in_progress: false, created_at: new Date().toISOString() }); } catch (error) { @@ -94,7 +95,8 @@ router.put('/:id', [ body('text').optional().trim().notEmpty(), body('completed').optional().isBoolean(), - body('priority').optional().isBoolean() + body('priority').optional().isBoolean(), + body('in_progress').optional().isBoolean() ], async (req, res) => { try { @@ -103,7 +105,7 @@ router.put('/:id', return res.status(400).json({ error: 'Données invalides', details: errors.array() }); } - const { text, completed, priority } = req.body; + const { text, completed, priority, in_progress } = req.body; // Vérifier que le todo appartient à l'utilisateur const todo = await getOne('SELECT id FROM global_todos WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]); @@ -130,6 +132,11 @@ router.put('/:id', updates.push(`priority = $${paramCount}`); params.push(priority ? 1 : 0); } + if (in_progress !== undefined) { + paramCount++; + updates.push(`in_progress = $${paramCount}`); + params.push(in_progress ? 1 : 0); + } if (updates.length > 0) { paramCount++; @@ -205,6 +212,42 @@ router.patch('/:id/priority', async (req, res) => { } }); +/** + * PATCH /api/todos/:id/in-progress + * Basculer l'état in_progress d'un todo + */ +router.patch('/:id/in-progress', async (req, res) => { + try { + // Vérifier que le todo appartient à l'utilisateur + try { + const todo = await getOne('SELECT id, in_progress FROM global_todos WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]); + if (!todo) { + return res.status(404).json({ error: 'Todo non trouvé' }); + } + + const newInProgress = todo.in_progress ? 0 : 1; + await runQuery('UPDATE global_todos SET in_progress = $1 WHERE id = $2', [newInProgress, req.params.id]); + + logger.info(`Todo global ${newInProgress ? 'marqué en cours' : 'démarqué en cours'} (ID: ${req.params.id}) par ${req.user.username}`); + + res.json({ message: 'Statut en cours modifié avec succès', in_progress: newInProgress === 1 }); + } catch (inProgressError) { + // Si le champ in_progress n'existe pas encore + if (inProgressError.message && inProgressError.message.includes('in_progress')) { + logger.warn('Tentative d\'utilisation de la fonctionnalité in_progress sans migration'); + return res.status(400).json({ + error: 'Fonctionnalité in_progress non disponible', + message: 'Veuillez exécuter la migration: node scripts/migrate_003.js' + }); + } + throw inProgressError; + } + } catch (error) { + logger.error('Erreur lors du toggle du statut en cours:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + /** * DELETE /api/todos/:id * Supprimer un todo global diff --git a/scripts/migrate_003.js b/scripts/migrate_003.js new file mode 100644 index 0000000..401f204 --- /dev/null +++ b/scripts/migrate_003.js @@ -0,0 +1,32 @@ +const { pool } = require('../config/database'); +const logger = require('../config/logger'); + +async function migrate() { + const client = await pool.connect(); + try { + logger.info('Starting migration: Add in_progress column to global_todos'); + + // Check if column exists + const checkRes = await client.query(` + SELECT column_name + FROM information_schema.columns + WHERE table_name='global_todos' AND column_name='in_progress' + `); + + if (checkRes.rows.length === 0) { + await client.query('ALTER TABLE global_todos ADD COLUMN in_progress INTEGER DEFAULT 0'); + logger.info('✓ Column in_progress added successfully'); + } else { + logger.info('⚠ Column in_progress already exists'); + } + + } catch (error) { + logger.error('Migration failed:', error); + process.exit(1); + } finally { + client.release(); + await pool.end(); + } +} + +migrate(); diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 24ce9ff..62aa13e 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -572,6 +572,15 @@ const Index = () => { } }; + const handleToggleTodoInProgress = async (todoId: number) => { + try { + await TodosService.toggleInProgress(todoId); + setTodos(todos.map(t => t.id === todoId ? { ...t, in_progress: !t.in_progress } : t)); + } catch (error) { + showError("Erreur lors de la mise à jour du statut"); + } + }; + const confirmAddRssFeed = async (url: string) => { try { const newFeed = await RssService.addFeed(url); @@ -1617,7 +1626,15 @@ const Index = () => { {todo.text} - +