const express = require('express'); const db = require('../db'); const { broadcast } = require('../lib/broadcast'); const PDFDocument = require('pdfkit'); const router = express.Router(); // GET /api/progress — all topics with pct calculation router.get('/', (req, res) => { try { const rows = db.prepare('SELECT * FROM progress ORDER BY topic').all(); const result = rows.map(r => { const pct = r.exercises_done > 0 ? Math.round((r.exercises_correct / r.exercises_done) * 100) : 0; return { topic: r.topic, exercises_done: r.exercises_done, exercises_correct: r.exercises_correct, percentage: pct, last_session: r.last_session, notes: r.notes, }; }); res.json(result); } catch (err) { console.error('[progress] list error:', err.message); res.status(500).json({ error: err.message }); } }); // PUT /api/progress/:topic — update exercises (body: { correct: bool }) router.put('/:topic', (req, res) => { const topic = req.params.topic; const { correct } = req.body; if (correct === undefined) { return res.status(400).json({ error: 'correct is required' }); } try { const existing = db.prepare('SELECT * FROM progress WHERE topic = ?').get(topic); if (existing) { db.prepare(` UPDATE progress SET exercises_done = exercises_done + 1, exercises_correct = exercises_correct + ?, last_session = datetime('now') WHERE topic = ? `).run(correct === true ? 1 : 0, topic); } else { db.prepare(` INSERT INTO progress (topic, exercises_done, exercises_correct, last_session, notes) VALUES (?, 1, ?, datetime('now'), '[]') `).run(topic, correct === true ? 1 : 0); } const row = db.prepare('SELECT * FROM progress WHERE topic = ?').get(topic); const pct = row.exercises_done > 0 ? Math.round((row.exercises_correct / row.exercises_done) * 100) : 0; const result = { ...row, percentage: pct }; broadcast({ type: 'progress_update', data: result }); res.json(result); } catch (err) { console.error('[progress] update error:', err.message); res.status(500).json({ error: err.message }); } }); // DELETE /api/progress/:topic — reset router.delete('/:topic', (req, res) => { const topic = req.params.topic; try { const info = db.prepare('DELETE FROM progress WHERE topic = ?').run(topic); if (info.changes === 0) { return res.status(404).json({ error: 'Topic not found' }); } res.json({ deleted: true }); } catch (err) { console.error('[progress] delete error:', err.message); res.status(500).json({ error: err.message }); } }); // POST /api/progress/sessions — UPSERT daily minutes router.post('/sessions', (req, res) => { const { date, minutes, topic } = req.body; if (!date || minutes === undefined) { return res.status(400).json({ error: 'date and minutes are required' }); } try { // NULL != NULL in SQLite UNIQUE constraint — explicit NULL handling keeps things working const existing = db.prepare('SELECT * FROM study_sessions WHERE session_date = ? AND (topic = ? OR (topic IS NULL AND ? IS NULL))').get(date, topic || null, topic || null); if (existing) { db.prepare('UPDATE study_sessions SET minutes = minutes + ? WHERE id = ?').run(minutes, existing.id); } else { db.prepare('INSERT INTO study_sessions (session_date, minutes, topic) VALUES (?, ?, ?)').run(date, minutes, topic || null); } const row = db.prepare('SELECT * FROM study_sessions WHERE session_date = ? AND (topic = ? OR (topic IS NULL AND ? IS NULL))').get(date, topic || null, topic || null); res.json(row); } catch (err) { console.error('[progress] session error:', err.message); res.status(500).json({ error: err.message }); } }); // GET /api/progress/heatmap — aggregated study minutes per day router.get('/heatmap', (req, res) => { try { const days = parseInt(req.query.days, 10) || 365; const since = new Date(); since.setDate(since.getDate() - days); const sinceStr = since.toISOString().split('T')[0]; const rows = db.prepare( 'SELECT session_date as date, SUM(minutes) as minutes FROM study_sessions WHERE session_date >= ? GROUP BY session_date ORDER BY session_date' ).all(sinceStr); res.json(rows); } catch (err) { console.error('[progress] heatmap error:', err.message); res.status(500).json({ error: err.message }); } }); // GET /api/progress/exam/pdf — generate simulated exam PDF router.get('/exam/pdf', (req, res) => { try { const rows = db.prepare( 'SELECT * FROM progress ORDER BY last_session DESC LIMIT 20' ).all(); if (!rows.length) { return res.status(404).json({ error: 'No progress data' }); } const doc = new PDFDocument({ margin: 50 }); const filename = `examen-simulado-${new Date().toISOString().slice(0, 10)}.pdf`; res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); doc.pipe(res); res.on('error', () => { try { doc.end(); } catch {} }); // Header doc.fontSize(22).text('Examen Simulado', 50, 50); doc.fontSize(12).text(`Generado: ${new Date().toLocaleDateString('es-ES')}`, 50, 80); doc.moveDown(2); // Topics table doc.fontSize(14).text('Temas evaluados', 50, doc.y); doc.moveDown(0.5); doc.fontSize(10); rows.forEach((row, idx) => { const pct = row.exercises_done > 0 ? Math.round((row.exercises_correct / row.exercises_done) * 100) : 0; doc.text(`${idx + 1}. ${row.topic} — ${pct}% (${row.exercises_correct}/${row.exercises_done})`, 60, doc.y); }); if (rows.length === 20) { doc.moveDown(1); doc.fontSize(9).fillColor('gray').text('Nota: se muestran los 20 temas más recientes.', 50, doc.y); doc.fillColor('black'); } doc.moveDown(2); doc.fontSize(14).text('Ejercicios de muestra', 50, doc.y); doc.moveDown(0.5); doc.fontSize(11); rows.slice(0, 10).forEach((row, idx) => { doc.text(`${idx + 1}. Describe el tema "${row.topic}" con sus puntos clave.`, 60, doc.y); doc.moveDown(0.3); doc.text(' Respuesta: _______________________________________________', 60, doc.y); doc.moveDown(1); }); doc.end(); } catch (err) { console.error('[progress] exam/pdf error:', err.message); res.status(500).json({ error: err.message }); } }); module.exports = router;