Mantente al día con tus medicamentos.
diff --git a/backend/server.js b/backend/server.js index 1f52395..a321018 100644 --- a/backend/server.js +++ b/backend/server.js @@ -65,7 +65,8 @@ const sessionConfig = { resave: false, saveUninitialized: false, cookie: { - secure: process.env.NODE_ENV === 'production', + secure: process.env.COOKIE_SECURE === 'true', + sameSite: 'lax', httpOnly: true, maxAge: 24 * 60 * 60 * 1000 // 24 hours } @@ -225,6 +226,22 @@ async function initDatabase() { `); } + if (pgPool) { + await pgPool.query(` + CREATE TABLE IF NOT EXISTS user_alerts ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id), + type TEXT NOT NULL CHECK (type IN ('reminder', 'availability', 'schedule')), + medicine_nregistro TEXT, + title TEXT, + detail TEXT, + schedule TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() + ) + `); + } + await dbRun(` CREATE TABLE IF NOT EXISTS push_subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -254,7 +271,7 @@ async function initDatabase() { `); await dbRun(`CREATE INDEX IF NOT EXISTS idx_push_subs_pharm ON push_subscriptions_pharmacy(medicine_nregistro, pharmacy_id)`); - if (!pgPool) { +if (!pgPool) { // SQLite-only migrations for existing deployments try { await dbRun('ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0'); } catch {} try { await dbRun('ALTER TABLE users ADD COLUMN address TEXT'); } catch {} @@ -262,13 +279,32 @@ async function initDatabase() { try { await dbRun('ALTER TABLE users ADD COLUMN longitude REAL'); } catch {} } - // Add user_id to push tables (SQLite — no cross-DB FK) +// Add user_id to push tables (SQLite — no cross-DB FK) try { await dbRun('ALTER TABLE push_subscriptions ADD COLUMN user_id INTEGER'); } catch {} try { await dbRun('ALTER TABLE push_subscriptions_pharmacy ADD COLUMN user_id INTEGER'); } catch {} - console.log('Database initialized successfully'); - } catch (error) { - console.error('Error initializing database:', error); + // Create alerts table for persistent Avisos data + try { + await dbRun(` + CREATE TABLE IF NOT EXISTS user_alerts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + type TEXT NOT NULL CHECK (type IN ('reminder', 'availability', 'schedule')), + medicine_nregistro TEXT, + title TEXT, + detail TEXT, + schedule TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + `); + } catch (e) { + if (!/table already exists/i.test(e.message)) throw e; + } + } catch (err) { + console.error('initDatabase failed:', err); + throw err; } } @@ -630,6 +666,92 @@ app.put('/api/users/me', requireAuth, async (req, res) => { } }); +// ========== USER ALERTS API ========== + +// Get all alerts for the current user +app.get('/api/alerts', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const rows = await userDbAll( + 'SELECT id, type, medicine_nregistro, title, detail, schedule, created_at, updated_at FROM user_alerts WHERE user_id = ? ORDER BY created_at DESC', + [userId] + ); + res.json(rows); + } catch (error) { + console.error('Error fetching alerts:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Create a new alert +app.post('/api/alerts', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const { type, medicine_nregistro, title, detail, schedule } = req.body || {}; + + if (!type || !['reminder', 'availability', 'schedule'].includes(type)) { + return res.status(400).json({ error: 'Valid type is required (reminder, availability, schedule)' }); + } + + const result = await userDbRun( + 'INSERT INTO user_alerts (user_id, type, medicine_nregistro, title, detail, schedule) VALUES (?, ?, ?, ?, ?, ?)', + [userId, type, medicine_nregistro || null, title || null, detail || null, schedule || null] + ); + + const newAlert = await userDbGet('SELECT * FROM user_alerts WHERE id = ?', [result.lastID]); + res.status(201).json(newAlert); + } catch (error) { + console.error('Error creating alert:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Update an existing alert +app.put('/api/alerts/:id', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const alertId = parseInt(req.params.id); + const { medicine_nregistro, title, detail, schedule } = req.body || {}; + + const result = await userDbRun( + 'UPDATE user_alerts SET medicine_nregistro = ?, title = ?, detail = ?, schedule = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?', + [medicine_nregistro || null, title || null, detail || null, schedule || null, alertId, userId] + ); + + if (result.changes === 0) { + return res.status(404).json({ error: 'Alert not found' }); + } + + const updatedAlert = await userDbGet('SELECT * FROM user_alerts WHERE id = ?', [alertId]); + res.json(updatedAlert); + } catch (error) { + console.error('Error updating alert:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Delete an alert +app.delete('/api/alerts/:id', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const alertId = parseInt(req.params.id); + + const result = await userDbRun( + 'DELETE FROM user_alerts WHERE id = ? AND user_id = ?', + [alertId, userId] + ); + + if (result.changes === 0) { + return res.status(404).json({ error: 'Alert not found' }); + } + + res.status(204).end(); + } catch (error) { + console.error('Error deleting alert:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + // Geocode for any authenticated user (rate-limited). Same impl as /api/admin/geocode. app.get('/api/geocode', requireAuth, geocodeLimiter, async (req, res) => { const q = (req.query.q || '').trim(); diff --git a/docs/superpowers/specs/2026-07-02-admin-access-design.md b/docs/superpowers/specs/2026-07-02-admin-access-design.md new file mode 100644 index 0000000..b0d6f8a --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-admin-access-design.md @@ -0,0 +1,73 @@ +# Diseño: Acceso al Panel de Administración + +## Resumen +Añadir un botón de acceso al panel de administración en la página de perfil (pestaña "Usuario") que solo sea visible para usuarios con permisos de administrador. + +## Objetivo +Proporcionar una forma intuitiva y consistente para que los usuarios administradores accedan al panel de administración desde la interfaz principal. + +## Requisitos +1. El botón solo debe aparecer cuando el usuario tiene `is_admin: true` +2. El botón debe seguir el mismo estilo que los otros elementos del menú del perfil +3. Al hacer clic, debe navegar a la vista de administración existente +4. No debe afectar la experiencia de usuarios no administradores + +## Diseño Detallado + +### Ubicación +- **Página:** ProfileView (pestaña "Usuario" en BottomNav) +- **Sección:** Dentro de `.profile-menu`, después de "Configuración de Texto" +- **Posición:** Antes del botón "Cerrar Sesión" + +### Componente +- **Tipo:** Botón con ícono, texto y chevron (igual que otros items del menú) +- **Ícono:** Engranaje (⚙️) con fondo de color primario +- **Texto:** "Panel de Administración" +- **Estilo:** Idéntico a `.profile-menu-item` existente + +### Comportamiento +1. **Renderizado condicional:** + ```jsx + {currentUser?.is_admin && ( + + )} + ``` + +2. **Navegación:** + - Recibir función `onAdminClick` como prop + - En `App.jsx`, implementar `handleAdminClick` que cambie `screen` a `'admin'` + +### Cambios en Archivos + +#### 1. `frontend/src/views/ProfileView.jsx` +- Añadir prop `onAdminClick` +- Renderizar botón condicionalmente basado en `currentUser?.is_admin` + +#### 2. `frontend/src/App.jsx` +- Añadir función `handleAdminClick` que establezca `screen` en `'admin'` +- Pasar `onAdminClick={handleAdminClick}` a `ProfileView` + +### Estilos +No se requieren cambios en CSS - se reutilizan las clases existentes: +- `.profile-menu-item` +- `.menu-item-icon--primary` +- `.menu-item-label` +- `.menu-item-chevron` + +## Criterios de Aceptación +1. ✅ Botón visible solo para usuarios admin +2. ✅ Botón no visible para usuarios normales +3. ✅ Estilo consistente con otros elementos del menú +4. ✅ Al hacer clic, navega a la vista de administración +5. ✅ No se muestra en dispositivos móviles si el usuario no es admin + +## Impacto +- **Archivos modificados:** 2 (`ProfileView.jsx`, `App.jsx`) +- **Complejidad:** Baja +- **Riesgo:** Mínimo - solo añade un elemento condicional \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index f3a88e2..4d57bd9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@
- +
- )}
- Mantente al día con tus medicamentos.
Cargando avisos...
} + {!loading && error &&{error}
} + + {!loading && !error && ( + <> + {/* Availability Section */} +No tienes suscripciones de disponibilidad.
+ ) : ( + availability.map((alert) => { + const cardColor = alert.scope === 'pharmacy' ? 'primary' : 'tertiary'; + const cardIcon = alert.scope === 'pharmacy' ? 'store' : 'bell'; + const key = `${alert.scope}:${alert.id}`; + return ( ++ 📍 {alert.pharmacy_address} +
+ )} +