diff --git a/database.js b/database.js index d2a34f8..afd172d 100644 --- a/database.js +++ b/database.js @@ -78,6 +78,11 @@ db.all("PRAGMA table_info(tasks)", (err, rows) => { if (!columnNames.includes("labels")) { db.run("ALTER TABLE tasks ADD COLUMN labels TEXT DEFAULT '[]'"); } + + // Revision stage + if (!columnNames.includes("revision_stage")) { + db.run("ALTER TABLE tasks ADD COLUMN revision_stage INTEGER DEFAULT 0"); + } }); // Pre-populate some subjects if empty diff --git a/index.html b/index.html index b2d766d..a94c06c 100644 --- a/index.html +++ b/index.html @@ -716,5 +716,16 @@

New task

+ \ No newline at end of file diff --git a/js/app.js b/js/app.js index 5f50a0b..c62f47e 100644 --- a/js/app.js +++ b/js/app.js @@ -1168,7 +1168,12 @@ function renderTasks() { labelsHtml = t.labels.map(l => `${l}`).join(' '); } - html += ` + let revisionHtml = ''; + if (t.revision_stage > 0) { + revisionHtml = `🔁 Stage ${t.revision_stage}`; + } + + html += `
${sub.short_code} + + ${revisionHtml} ${labelsHtml}
@@ -2280,3 +2287,57 @@ if (calendarDownloadBtn) { downloadCalendar(); }); } + + +// ================= SPACED REPETITION (Issue 1178) ================= +window.showRevisionModal = function(task) { + const modal = document.getElementById('revision-modal'); + if (!modal) return; + modal.style.display = 'flex'; + + const btnEasy = document.getElementById('rev-btn-easy'); + const btnMedium = document.getElementById('rev-btn-medium'); + const btnHard = document.getElementById('rev-btn-hard'); + + // Clone to remove old event listeners + const newEasy = btnEasy.cloneNode(true); + const newMedium = btnMedium.cloneNode(true); + const newHard = btnHard.cloneNode(true); + btnEasy.parentNode.replaceChild(newEasy, btnEasy); + btnMedium.parentNode.replaceChild(newMedium, btnMedium); + btnHard.parentNode.replaceChild(newHard, btnHard); + + const handleFeedback = (difficulty) => { + modal.style.display = 'none'; + let nextStage = task.revision_stage || 1; + if (difficulty === 'easy') nextStage += 1; + else if (difficulty === 'hard') nextStage = Math.max(1, nextStage - 1); + + let daysToAdd = 1; + if (nextStage === 2) daysToAdd = 3; + else if (nextStage === 3) daysToAdd = 7; + else if (nextStage >= 4) daysToAdd = 14; + + const revDate = new Date(); + revDate.setDate(revDate.getDate() + daysToAdd); + + let baseTitle = task.title; + if (baseTitle.startsWith('Revision: ')) { + baseTitle = baseTitle.replace('Revision: ', ''); + } + + store.addTasks([{ + title: `Revision: ${baseTitle}`, + subject_id: task.subject_id, + due_at: revDate.toISOString(), + status: 'Not Started', + priority: task.priority, + revision_stage: nextStage + }]); + }; + + newEasy.addEventListener('click', () => handleFeedback('easy')); + newMedium.addEventListener('click', () => handleFeedback('medium')); + newHard.addEventListener('click', () => handleFeedback('hard')); +}; + diff --git a/js/store.js b/js/store.js index 4ec0feb..74cc1b5 100644 --- a/js/store.js +++ b/js/store.js @@ -214,6 +214,27 @@ export const store = { if (newStatus === 'Done') { triggerConfetti(); + + // SPACED REPETITION LOGIC (Issue 1178) + if (!task.revision_stage || task.revision_stage === 0) { + // Schedule first revision 1 day later + const revDate = new Date(); + revDate.setDate(revDate.getDate() + 1); + + this.addTasks([{ + title: `Revision: ${task.title}`, + subject_id: task.subject_id, + due_at: revDate.toISOString(), + status: 'Not Started', + priority: task.priority, + revision_stage: 1 + }]); + } else { + // It's already a revision task, ask for feedback + if (window.showRevisionModal) { + window.showRevisionModal(task); + } + } } try { diff --git a/server.js b/server.js index 3863485..0fbe037 100644 --- a/server.js +++ b/server.js @@ -377,8 +377,8 @@ app.post('/api/tasks', (req, res) => { let errors = []; const stmt = db.prepare(`INSERT INTO tasks - (id, subject_id, title, due_at, status, priority, confidence_score, notes, estimated_duration, is_estimated_duration_min, labels) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); + (id, subject_id, title, due_at, status, priority, confidence_score, notes, estimated_duration, is_estimated_duration_min, labels, revision_stage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); let pending = tasks.length; @@ -439,6 +439,7 @@ app.post('/api/tasks', (req, res) => { Number.isFinite(Number(t.estimated_duration)) ? Number(t.estimated_duration) : null, t.is_estimated_duration_min === 0 ? 0 : 1, typeof t.labels === 'string' ? t.labels : JSON.stringify(t.labels || []), + t.revision_stage || 0, function (insertErr) { if (insertErr) { errors.push({ task: t, error: insertErr.message }); @@ -481,7 +482,7 @@ app.post('/api/tasks', (req, res) => { // ================= UPDATE ================= app.put('/api/tasks/:id', (req, res) => { - const { status, archived, title, subject_id, due_at, notes, priority, estimated_duration, is_estimated_duration_min,labels } = req.body; + const { status, archived, title, subject_id, due_at, notes, priority, estimated_duration, is_estimated_duration_min,labels, revision_stage } = req.body; let query = 'UPDATE tasks SET '; @@ -498,6 +499,7 @@ app.put('/api/tasks/:id', (req, res) => { if (estimated_duration !== undefined) { updates.push('estimated_duration = ?'); params.push(Number.isFinite(Number(estimated_duration)) ? Number(estimated_duration) : null); } if (is_estimated_duration_min !== undefined) { updates.push('is_estimated_duration_min = ?'); params.push(is_estimated_duration_min === 0 ? 0 : 1); } if (labels !== undefined) { updates.push('labels = ?'); params.push(typeof labels === 'string' ? labels : JSON.stringify(labels)); } + if (revision_stage !== undefined) { updates.push('revision_stage = ?'); params.push(revision_stage); } if (updates.length === 0) { return res.status(400).json({ error: 'No fields to update' });