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}
-
+