diff --git a/backend/server.js b/backend/server.js index a321018..6bd53f8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -139,6 +139,14 @@ async function userDbRun(sql, params = []) { return dbRun(sql.replace(/\s+RETURNING\s+\w+\s*$/i, ''), params); } +async function userDbAll(sql, params = []) { + if (pgPool) { + const res = await pgPool.query(toPositional(sql), params); + return res.rows; + } + return dbAll(sql, params); +} + // Accept opening_hours as either an object or a JSON string from the client // and return a TEXT value (or null) safe to persist. function serializeOpeningHours(value) { @@ -376,6 +384,75 @@ app.get('/api/medicines/:medicineId', async (req, res) => { } }); +// ========== AUTHENTICATION MIDDLEWARE ========== + +// Middleware to check if user is authenticated +const requireAuth = (req, res, next) => { + if (req.session && req.session.userId) { + return next(); + } + return res.status(401).json({ error: 'Authentication required' }); +}; + +// Middleware to check if user is an admin +const requireAdmin = (req, res, next) => { + if (req.session && req.session.userId && req.session.isAdmin) { + return next(); + } + if (req.session && req.session.userId) { + return res.status(403).json({ error: 'Admin access required' }); + } + return res.status(401).json({ error: 'Authentication required' }); +}; + +// ========== RECENT SEARCHES (session-based) ========== + +const MAX_RECENT = 10; + +// Get recent searches for the logged-in user +app.get('/api/search/recent', requireAuth, (req, res) => { + res.json(req.session.recentSearches || []); +}); + +// Save a medicine to recent searches +app.post('/api/search/recent', requireAuth, (req, res) => { + try { + const { medicine } = req.body; + if (!medicine || !medicine.id) { + return res.status(400).json({ error: 'Medicine data required' }); + } + + if (!req.session.recentSearches) { + req.session.recentSearches = []; + } + + // Remove duplicate if exists (by id) + req.session.recentSearches = req.session.recentSearches.filter( + (m) => m.id !== medicine.id + ); + + // Add to front with timestamp + req.session.recentSearches.unshift({ + id: medicine.id, + name: medicine.name, + active_ingredient: medicine.active_ingredient, + dosage: medicine.dosage, + form: medicine.form, + timestamp: Date.now(), + }); + + // Keep only last MAX_RECENT + if (req.session.recentSearches.length > MAX_RECENT) { + req.session.recentSearches = req.session.recentSearches.slice(0, MAX_RECENT); + } + + res.json({ ok: true }); + } catch (error) { + console.error('Error saving recent search:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + // Get all pharmacies (for admin/debugging) app.get('/api/pharmacies', async (req, res) => { try { @@ -438,27 +515,6 @@ app.get('/api/tsi/:cip/prescriptions', async (req, res) => { } }); -// ========== AUTHENTICATION MIDDLEWARE ========== - -// Middleware to check if user is authenticated -const requireAuth = (req, res, next) => { - if (req.session && req.session.userId) { - return next(); - } - return res.status(401).json({ error: 'Authentication required' }); -}; - -// Middleware to check if user is an admin -const requireAdmin = (req, res, next) => { - if (req.session && req.session.userId && req.session.isAdmin) { - return next(); - } - if (req.session && req.session.userId) { - return res.status(403).json({ error: 'Admin access required' }); - } - return res.status(401).json({ error: 'Authentication required' }); -}; - // ========== AUTHENTICATION ROUTES ========== // Login endpoint diff --git a/docker-compose.yml b/docker-compose.yml index bc5e24e..0249e81 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,18 +7,20 @@ services: image: postgres:16-alpine restart: unless-stopped environment: - POSTGRES_DB: farmaclic - POSTGRES_USER: farmaclic + POSTGRES_DB: farmafinder + POSTGRES_USER: farmafinder POSTGRES_PASSWORD: ${PG_PASSWORD:-change-me-in-production} volumes: - postgres_data:/var/lib/postgresql/data backend: - image: git.hacecalor.net/ichitux/farmaclic-backend:latest + image: git.hacecalor.net/ichitux/farmafinder-backend:latest build: context: . dockerfile: backend/Dockerfile restart: unless-stopped + env_file: + - ./backend/.env ports: - "3001:3001" environment: @@ -31,13 +33,13 @@ services: REDIS_PASSWORD: ${REDIS_PASSWORD:-} DATABASE_PATH: /app/data/database.sqlite FARMACIAS_WEBHOOK_URL: ${FARMACIAS_WEBHOOK_URL:-} - PG_URL: postgresql://farmaclic:${PG_PASSWORD:-change-me-in-production}@postgres:5432/farmaclic + PG_URL: postgresql://farmafinder:${PG_PASSWORD:-change-me-in-production}@postgres:5432/farmafinder # OpenTelemetry — exported via OTLP gRPC to the shared Alloy collector - OTEL_SERVICE_NAME: farmaclic-backend + OTEL_SERVICE_NAME: farmafinder-backend OTEL_EXPORTER_OTLP_ENDPOINT: http://host.docker.internal:4317 OTEL_TRACES_EXPORTER: otlp OTEL_LOGS_EXPORTER: otlp - OTEL_RESOURCE_ATTRIBUTES: service.namespace=farmaclic + OTEL_RESOURCE_ATTRIBUTES: service.namespace=farmafinder volumes: - backend_data:/app/data depends_on: @@ -45,12 +47,12 @@ services: - postgres frontend: - image: git.hacecalor.net/ichitux/farmaclic-frontend:latest + image: git.hacecalor.net/ichitux/farmafinder-frontend:latest build: context: ./frontend args: VITE_FARO_ENDPOINT: ${VITE_FARO_ENDPOINT:-http://localhost:4318} - VITE_FARO_APP_NAME: ${VITE_FARO_APP_NAME:-farmaclic-frontend} + VITE_FARO_APP_NAME: ${VITE_FARO_APP_NAME:-farmafinder-frontend} VITE_FARO_ENV: ${VITE_FARO_ENV:-production} VITE_FARO_APP_VERSION: ${VITE_FARO_APP_VERSION:-1.0.0} restart: unless-stopped diff --git a/frontend/src/views/SearchView.jsx b/frontend/src/views/SearchView.jsx index 63f2c7c..ff05c77 100644 --- a/frontend/src/views/SearchView.jsx +++ b/frontend/src/views/SearchView.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import SearchBar from '../components/SearchBar'; import MedicineResults from '../components/MedicineResults'; import PharmacyList from '../components/PharmacyList'; @@ -13,11 +13,6 @@ const suggestions = [ { name: 'Omeprazol', icon: 'emergency_home', color: 'tint' }, ]; -const recentResults = [ - { name: 'Amoxicilina', detail: '500mg • 24 Cápsulas', tag: 'Antibiótico', distance: '300m - Farmacia Central' }, - { name: 'Losartán', detail: '50mg • 30 Comprimidos', tag: 'Hipertensión', distance: '1.2km - Farmacia del Sol' }, -]; - function SearchView({ currentUser, onLoginRequest }) { const [searchQuery, setSearchQuery] = useState(''); const [medicines, setMedicines] = useState([]); @@ -29,6 +24,44 @@ function SearchView({ currentUser, onLoginRequest }) { const [sortByDistance, setSortByDistance] = useState(false); const [locating, setLocating] = useState(false); const [locationError, setLocationError] = useState(''); + const [recentSearches, setRecentSearches] = useState([]); + + // Fetch recent searches when user is logged in + useEffect(() => { + if (!currentUser) { + setRecentSearches([]); + return; + } + fetch('/api/search/recent', { credentials: 'include' }) + .then((r) => r.json()) + .then((data) => setRecentSearches(Array.isArray(data) ? data : [])) + .catch(() => setRecentSearches([])); + }, [currentUser]); + + const saveToRecent = useCallback((medicine) => { + if (!currentUser) return; + fetch('/api/search/recent', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ medicine }), + }).catch(() => {}); + // Update local state immediately + setRecentSearches((prev) => { + const filtered = prev.filter((m) => m.id !== medicine.id); + return [ + { + id: medicine.id, + name: medicine.name, + active_ingredient: medicine.active_ingredient, + dosage: medicine.dosage, + form: medicine.form, + timestamp: Date.now(), + }, + ...filtered, + ].slice(0, 10); + }); + }, [currentUser]); useEffect(() => { const searchMedicines = async () => { @@ -157,43 +190,58 @@ function SearchView({ currentUser, onLoginRequest }) { -
-

Resultados Recientes

-
- {recentResults.map((r, i) => ( -
-
-
-

{r.name}

-

{r.detail}

+ {currentUser && recentSearches.length > 0 && ( +
+

Resultados Recientes

+
+ {recentSearches.map((r) => ( +
setSelectedMedicine(r)} + style={{ cursor: 'pointer' }} + > +
+
+

{r.name}

+

+ {r.dosage && `${r.dosage}`} + {r.dosage && r.form && ' \u2022 '} + {r.form} +

+
+ {r.active_ingredient && ( + {r.active_ingredient} + )}
- {r.tag} +
-
- - - - - {r.distance} -
- -
- ))} -
-
+ ))} + + + )} )} {searchQuery && !selectedMedicine && ( { + saveToRecent(m); + setSelectedMedicine(m); + }} query={searchQuery} currentUser={currentUser} onLoginRequest={onLoginRequest}