diff --git a/.gitea/workflows/docker.yaml b/.gitea/workflows/docker.yaml
index 32acfe0..832a48d 100644
--- a/.gitea/workflows/docker.yaml
+++ b/.gitea/workflows/docker.yaml
@@ -14,7 +14,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: '20'
+ node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
@@ -28,7 +28,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: '20'
+ node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
diff --git a/.gitea/workflows/test-branches.yaml b/.gitea/workflows/test-branches.yaml
index 2abc517..a82d007 100644
--- a/.gitea/workflows/test-branches.yaml
+++ b/.gitea/workflows/test-branches.yaml
@@ -3,43 +3,43 @@ name: Run Tests on Branches
on:
push:
branches-ignore:
- - 'main'
+ - 'main'
jobs:
test-backend:
name: Backend Tests
runs-on: ubuntu-latest
steps:
- - name: Checkout Code
- uses: actions/checkout@v4
+ - name: Checkout Code
+ uses: actions/checkout@v4
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: 'npm'
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '24'
+ cache: 'npm'
- - name: Install Dependencies
- run: npm ci
+ - name: Install Dependencies
+ run: npm ci
- - name: Run Backend Tests
- run: npm test --workspace=farma-clic-backend -- --ci
+ - name: Run Backend Tests
+ run: npm test --workspace=farma-clic-backend -- --ci
test-frontend:
name: Frontend Tests
runs-on: ubuntu-latest
steps:
- - name: Checkout Code
- uses: actions/checkout@v4
+ - name: Checkout Code
+ uses: actions/checkout@v4
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: 'npm'
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '24'
+ cache: 'npm'
- - name: Install Dependencies
- run: npm ci
+ - name: Install Dependencies
+ run: npm ci
- - name: Run Frontend Tests
- run: npm test --workspace=farma-clic-frontend -- --run --reporter=basic
+ - name: Run Frontend Tests
+ run: npm test --workspace=farma-clic-frontend -- --run --reporter=basic
diff --git a/docs/superpowers/plans/2026-07-06-profile-redesign.md b/docs/superpowers/plans/2026-07-06-profile-redesign.md
new file mode 100644
index 0000000..064bba8
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-06-profile-redesign.md
@@ -0,0 +1,1135 @@
+# Profile Redesign Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Redesign the Profile section to be more user-friendly: editable name/last name, avatar upload, search history as addresses, and remove irrelevant sections.
+
+**Architecture:** Add new fields to users table (first_name, last_name, avatar_url), create search_history table for location searches, update frontend/mobile profile views to be editable and modern.
+
+**Tech Stack:** React (frontend), React Native/Expo (mobile), SQLite/PostgreSQL (backend), Node.js/Express
+
+---
+
+## File Structure
+
+### Backend
+- Modify: `apps/backend/server.js` - Add new fields, search history API
+- Create: `apps/backend/migrations/` - Database migration scripts
+
+### Frontend (Web)
+- Modify: `apps/frontend/src/views/ProfileView.jsx` - Redesign profile view
+- Modify: `apps/frontend/src/views/ProfileView.css` - Update styles
+
+### Mobile
+- Modify: `apps/frontend-mobile/app/(tabs)/profile.tsx` - Redesign mobile profile
+
+---
+
+## Task 1: Backend - Add new user fields and search history table
+
+**Files:**
+- Modify: `apps/backend/server.js`
+
+- [ ] **Step 1: Add migration for new user fields**
+
+Add to `initDatabase()` function, after existing user table creation:
+
+```javascript
+// Add new fields for profile redesign
+if (pgPool) {
+ await pgPool.query(`
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name TEXT;
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name TEXT;
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT;
+ `);
+} else {
+ try { await dbRun('ALTER TABLE users ADD COLUMN first_name TEXT'); } catch {}
+ try { await dbRun('ALTER TABLE users ADD COLUMN last_name TEXT'); } catch {}
+ try { await dbRun('ALTER TABLE users ADD COLUMN avatar_url TEXT'); } catch {}
+}
+```
+
+- [ ] **Step 2: Create search_history table**
+
+Add after user alerts table creation:
+
+```javascript
+// Search history for "find pharmacies near me"
+if (pgPool) {
+ await pgPool.query(`
+ CREATE TABLE IF NOT EXISTS search_history (
+ id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(id),
+ address TEXT NOT NULL,
+ latitude DOUBLE PRECISION,
+ longitude DOUBLE PRECISION,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+ )
+ `);
+} else {
+ await dbRun(`
+ CREATE TABLE IF NOT EXISTS search_history (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ address TEXT NOT NULL,
+ latitude REAL,
+ longitude REAL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ )
+ `);
+}
+```
+
+- [ ] **Step 3: Update GET /api/users/me to include new fields**
+
+Change the SELECT query in `app.get('/api/users/me', ...)`:
+
+```javascript
+const user = await userDbGet(
+ 'SELECT id, username, first_name, last_name, avatar_url, is_admin, address, latitude, longitude FROM users WHERE id = ?',
+ [req.session.userId]
+);
+```
+
+And update the response:
+
+```javascript
+res.json({
+ id: user.id,
+ username: user.username,
+ first_name: user.first_name || null,
+ last_name: user.last_name || null,
+ avatar_url: user.avatar_url || null,
+ is_admin: Boolean(user.is_admin),
+ address: user.address || null,
+ latitude: user.latitude,
+ longitude: user.longitude,
+});
+```
+
+- [ ] **Step 4: Update PUT /api/users/me to handle new fields**
+
+Change the PUT handler to accept and save new fields:
+
+```javascript
+app.put('/api/users/me', requireAuth, async (req, res) => {
+ try {
+ const { first_name, last_name, avatar_url, address, latitude, longitude } = req.body || {};
+
+ const fName = first_name == null ? null : String(first_name).trim().slice(0, 100);
+ const lName = last_name == null ? null : String(last_name).trim().slice(0, 100);
+ const avatar = avatar_url == null ? null : String(avatar_url).slice(0, 500);
+
+ // ... existing address/latitude/longitude handling ...
+
+ await userDbRun(
+ 'UPDATE users SET first_name = ?, last_name = ?, avatar_url = ?, address = ?, latitude = ?, longitude = ? WHERE id = ?',
+ [fName, lName, avatar, addr, lat, lon, req.session.userId]
+ );
+
+ // ... rest of response ...
+ }
+});
+```
+
+- [ ] **Step 5: Add search history API endpoints**
+
+Add after user alerts API:
+
+```javascript
+// Get search history for current user
+app.get('/api/search-history', requireAuth, async (req, res) => {
+ try {
+ const rows = await userDbAll(
+ 'SELECT id, address, latitude, longitude, created_at FROM search_history WHERE user_id = ? ORDER BY created_at DESC LIMIT 20',
+ [req.session.userId]
+ );
+ res.json(rows);
+ } catch (error) {
+ console.error('Error fetching search history:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// Add new search to history
+app.post('/api/search-history', requireAuth, async (req, res) => {
+ try {
+ const { address, latitude, longitude } = req.body || {};
+ if (!address) {
+ return res.status(400).json({ error: 'Address is required' });
+ }
+
+ const result = await userDbRun(
+ 'INSERT INTO search_history (user_id, address, latitude, longitude) VALUES (?, ?, ?, ?)',
+ [req.session.userId, address, latitude || null, longitude || null]
+ );
+
+ res.json({ id: result.lastID, address, latitude, longitude });
+ } catch (error) {
+ console.error('Error adding search history:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// Delete search history item
+app.delete('/api/search-history/:id', requireAuth, async (req, res) => {
+ try {
+ await userDbRun(
+ 'DELETE FROM search_history WHERE id = ? AND user_id = ?',
+ [req.params.id, req.session.userId]
+ );
+ res.json({ ok: true });
+ } catch (error) {
+ console.error('Error deleting search history:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+```
+
+- [ ] **Step 6: Commit backend changes**
+
+```bash
+git add apps/backend/server.js
+git commit -m "feat: add profile fields and search history API"
+```
+
+---
+
+## Task 2: Frontend - Redesign ProfileView
+
+**Files:**
+- Modify: `apps/frontend/src/views/ProfileView.jsx`
+- Modify: `apps/frontend/src/views/ProfileView.css`
+
+- [ ] **Step 1: Update ProfileView.jsx with editable fields**
+
+Replace the entire ProfileView.jsx with:
+
+```jsx
+import React, { useEffect, useState, useRef } from 'react';
+import './ProfileView.css';
+
+function ProfileView({ currentUser, onProfileSaved, onShowSaved, onLogout, onAdminClick }) {
+ const [firstName, setFirstName] = useState(currentUser?.first_name || '');
+ const [lastName, setLastName] = useState(currentUser?.last_name || '');
+ const [avatarUrl, setAvatarUrl] = useState(currentUser?.avatar_url || '');
+ const [saving, setSaving] = useState(false);
+ const [feedback, setFeedback] = useState(null);
+ const [searchHistory, setSearchHistory] = useState([]);
+ const [uploading, setUploading] = useState(false);
+ const fileInputRef = useRef(null);
+
+ useEffect(() => {
+ setFirstName(currentUser?.first_name || '');
+ setLastName(currentUser?.last_name || '');
+ setAvatarUrl(currentUser?.avatar_url || '');
+ setFeedback(null);
+ loadSearchHistory();
+ }, [currentUser?.id]);
+
+ async function loadSearchHistory() {
+ try {
+ const res = await fetch('/api/search-history', { credentials: 'include' });
+ if (res.ok) {
+ const data = await res.json();
+ setSearchHistory(data);
+ }
+ } catch (err) {
+ console.error('Error loading search history:', err);
+ }
+ }
+
+ async function handleSave(e) {
+ e?.preventDefault();
+ setSaving(true);
+ setFeedback(null);
+ try {
+ const res = await fetch('/api/users/me', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({
+ first_name: firstName.trim() || null,
+ last_name: lastName.trim() || null,
+ avatar_url: avatarUrl || null,
+ }),
+ });
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({}));
+ throw new Error(err.error || `Error al guardar (HTTP ${res.status})`);
+ }
+ const updated = await res.json();
+ onProfileSaved?.(updated);
+ setFeedback({ type: 'ok', text: 'Perfil guardado.' });
+ } catch (err) {
+ setFeedback({ type: 'err', text: err.message || 'Error al guardar' });
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ async function handleAvatarUpload(e) {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ setUploading(true);
+ setFeedback(null);
+ try {
+ // Convert to base64 for simplicity (in production, use a file upload service)
+ const reader = new FileReader();
+ reader.onload = async () => {
+ const base64 = reader.result;
+ setAvatarUrl(base64);
+ // Auto-save after upload
+ try {
+ const res = await fetch('/api/users/me', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ avatar_url: base64 }),
+ });
+ if (res.ok) {
+ const updated = await res.json();
+ onProfileSaved?.(updated);
+ setFeedback({ type: 'ok', text: 'Foto de perfil actualizada.' });
+ }
+ } catch (err) {
+ setFeedback({ type: 'err', text: 'Error al guardar la foto' });
+ }
+ setUploading(false);
+ };
+ reader.readAsDataURL(file);
+ } catch (err) {
+ setFeedback({ type: 'err', text: 'Error al procesar la imagen' });
+ setUploading(false);
+ }
+ }
+
+ async function handleDeleteSearch(id) {
+ try {
+ const res = await fetch(`/api/search-history/${id}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ });
+ if (res.ok) {
+ setSearchHistory(prev => prev.filter(item => item.id !== id));
+ }
+ } catch (err) {
+ console.error('Error deleting search:', err);
+ }
+ }
+
+ const displayName = [firstName, lastName].filter(Boolean).join(' ') || currentUser?.username || 'Usuario';
+
+ return (
+
+ {/* Avatar Section */}
+
+
fileInputRef.current?.click()}
+ >
+ {avatarUrl ? (
+

+ ) : (
+
+ )}
+
+
+
+ {uploading &&
Subiendo foto...
}
+
+
+ {/* Editable Name Section */}
+
+
+ {/* Search History Section */}
+
+
+
+ {searchHistory.length > 0 && (
+
+
Tus búsquedas recientes:
+ {searchHistory.map((item) => (
+
+
{item.address}
+
+
+ ))}
+
+ )}
+
+ {currentUser?.is_admin && (
+
+ )}
+
+
+
+
+ );
+}
+
+export default ProfileView;
+```
+
+- [ ] **Step 2: Update ProfileView.css with new styles**
+
+Add these styles to the end of the CSS file:
+
+```css
+/* Profile Avatar Editable */
+.profile-avatar-editable {
+ cursor: pointer;
+ position: relative;
+}
+
+.profile-avatar-editable:hover .profile-avatar-overlay {
+ opacity: 1;
+}
+
+.profile-avatar-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: var(--radius-full);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity 0.2s;
+ color: white;
+}
+
+.profile-avatar-image {
+ width: 100%;
+ height: 100%;
+ border-radius: var(--radius-full);
+ object-fit: cover;
+}
+
+/* Editable Info Cards */
+.info-card-input {
+ width: 100%;
+ padding: 0.75rem;
+ border: 2px solid var(--outline-variant);
+ border-radius: var(--radius);
+ background: var(--surface);
+ color: var(--on-surface);
+ font-size: 1rem;
+ font-family: inherit;
+ margin-top: 0.5rem;
+}
+
+.info-card-input:focus {
+ outline: none;
+ border-color: var(--primary);
+}
+
+.info-card-input:disabled {
+ opacity: 0.6;
+}
+
+/* Search History */
+.profile-search-history {
+ background: var(--surface-container-lowest);
+ border: 1px solid var(--outline-variant);
+ border-radius: var(--radius-md);
+ padding: 1rem;
+ margin-top: 0.5rem;
+}
+
+.profile-search-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 0;
+ border-bottom: 1px solid var(--outline-variant);
+}
+
+.profile-search-item:last-child {
+ border-bottom: none;
+}
+
+.profile-search-address {
+ flex: 1;
+ font-size: 0.9rem;
+ color: var(--on-surface);
+}
+
+.profile-search-delete {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--on-surface-variant);
+ padding: 0.25rem;
+ border-radius: var(--radius);
+ transition: background 0.15s;
+}
+
+.profile-search-delete:hover {
+ background: rgba(186, 26, 26, 0.1);
+ color: var(--error);
+}
+```
+
+- [ ] **Step 3: Commit frontend changes**
+
+```bash
+git add apps/frontend/src/views/ProfileView.jsx apps/frontend/src/views/ProfileView.css
+git commit -m "feat: redesign profile view with editable fields and search history"
+```
+
+---
+
+## Task 3: Mobile - Redesign ProfileScreen
+
+**Files:**
+- Modify: `apps/frontend-mobile/app/(tabs)/profile.tsx`
+
+- [ ] **Step 1: Update ProfileScreen.tsx with new functionality**
+
+Replace the entire profile.tsx with:
+
+```tsx
+import React, { useState, useEffect } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator } from 'react-native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import * as ImagePicker from 'expo-image-picker';
+import { useAuth } from '../../hooks/useAuth';
+import { colors, spacing, borderRadius } from '../../constants/theme';
+
+interface SearchHistoryItem {
+ id: number;
+ address: string;
+ latitude: number | null;
+ longitude: number | null;
+ created_at: string;
+}
+
+export default function ProfileScreen() {
+ const router = useRouter();
+ const { user, isAuthenticated, isLoading, logout, isAdmin } = useAuth();
+
+ const [firstName, setFirstName] = useState(user?.first_name || '');
+ const [lastName, setLastName] = useState(user?.last_name || '');
+ const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url || '');
+ const [searchHistory, setSearchHistory] = useState([]);
+ const [saving, setSaving] = useState(false);
+ const [feedback, setFeedback] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ loadSearchHistory();
+ }
+ }, [isAuthenticated]);
+
+ useEffect(() => {
+ setFirstName(user?.first_name || '');
+ setLastName(user?.last_name || '');
+ setAvatarUrl(user?.avatar_url || '');
+ }, [user]);
+
+ async function loadSearchHistory() {
+ try {
+ const res = await fetch('/api/search-history', { credentials: 'include' });
+ if (res.ok) {
+ const data = await res.json();
+ setSearchHistory(data);
+ }
+ } catch (err) {
+ console.error('Error loading search history:', err);
+ }
+ }
+
+ async function handleSave() {
+ setSaving(true);
+ setFeedback(null);
+ try {
+ const res = await fetch('/api/users/me', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({
+ first_name: firstName.trim() || null,
+ last_name: lastName.trim() || null,
+ avatar_url: avatarUrl || null,
+ }),
+ });
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({}));
+ throw new Error(err.error || 'Error al guardar');
+ }
+ setFeedback({ type: 'ok', text: 'Perfil guardado.' });
+ } catch (err: any) {
+ setFeedback({ type: 'err', text: err.message || 'Error al guardar' });
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ async function handlePickImage() {
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
+ allowsEditing: true,
+ aspect: [1, 1],
+ quality: 0.8,
+ });
+
+ if (!result.canceled && result.assets[0]) {
+ setAvatarUrl(result.assets[0].uri);
+ // Auto-save
+ try {
+ const res = await fetch('/api/users/me', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ avatar_url: result.assets[0].uri }),
+ });
+ if (res.ok) {
+ setFeedback({ type: 'ok', text: 'Foto de perfil actualizada.' });
+ }
+ } catch (err) {
+ setFeedback({ type: 'err', text: 'Error al guardar la foto' });
+ }
+ }
+ }
+
+ async function handleDeleteSearch(id: number) {
+ try {
+ const res = await fetch(`/api/search-history/${id}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ });
+ if (res.ok) {
+ setSearchHistory(prev => prev.filter(item => item.id !== id));
+ }
+ } catch (err) {
+ console.error('Error deleting search:', err);
+ }
+ }
+
+ const handleLogout = async () => {
+ Alert.alert(
+ 'Cerrar Sesión',
+ '¿Estás seguro que deseas cerrar sesión?',
+ [
+ { text: 'Cancelar', style: 'cancel' },
+ {
+ text: 'Cerrar Sesión',
+ style: 'destructive',
+ onPress: async () => {
+ await logout();
+ router.replace('/auth/login');
+ }
+ },
+ ]
+ );
+ };
+
+ if (isLoading) {
+ return (
+
+ Cargando...
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return (
+
+
+
+ Inicia Sesión
+
+ Inicia sesión para acceder a todas las funcionalidades
+
+ router.push('/auth/login')}
+ >
+ Iniciar Sesión
+
+ router.push('/auth/register')}
+ >
+ Crear cuenta nueva
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Avatar Section */}
+
+
+ {avatarUrl ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ Toca para cambiar foto
+
+
+ {/* Editable Fields */}
+
+
+ Nombre
+
+
+
+ Apellidos
+
+
+
+ {feedback && (
+
+ {feedback.text}
+
+ )}
+
+
+ {saving ? (
+
+ ) : (
+ Guardar Cambios
+ )}
+
+
+
+ {/* Menu Section */}
+
+ router.push('/saved')}>
+
+ Mis Direcciones
+
+
+
+ {searchHistory.length > 0 && (
+
+ Tus búsquedas recientes:
+ {searchHistory.map((item) => (
+
+ {item.address}
+ handleDeleteSearch(item.id)}
+ style={styles.searchDelete}
+ >
+
+
+
+ ))}
+
+ )}
+
+ {isAdmin && (
+
+
+ Panel Admin
+
+
+ )}
+
+
+ {/* Logout Button */}
+
+
+ Cerrar Sesión
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ loadingText: {
+ textAlign: 'center',
+ marginTop: spacing.xxl,
+ color: colors.textSecondary,
+ },
+ authPrompt: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: spacing.xl,
+ },
+ authTitle: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: colors.text,
+ marginTop: spacing.lg,
+ },
+ authSubtitle: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ textAlign: 'center',
+ marginTop: spacing.sm,
+ marginBottom: spacing.xl,
+ },
+ avatarSection: {
+ alignItems: 'center',
+ padding: spacing.xl,
+ backgroundColor: colors.card,
+ },
+ avatarContainer: {
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ backgroundColor: colors.background,
+ justifyContent: 'center',
+ alignItems: 'center',
+ overflow: 'hidden',
+ },
+ avatarImage: {
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ },
+ avatarOverlay: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ opacity: 0.8,
+ },
+ editAvatarText: {
+ marginTop: spacing.sm,
+ color: colors.textSecondary,
+ fontSize: 14,
+ },
+ formSection: {
+ padding: spacing.xl,
+ backgroundColor: colors.card,
+ marginTop: spacing.md,
+ },
+ inputGroup: {
+ marginBottom: spacing.md,
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: colors.textSecondary,
+ marginBottom: spacing.xs,
+ },
+ input: {
+ borderWidth: 1,
+ borderColor: colors.border || '#E0E0E0',
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ fontSize: 16,
+ color: colors.text,
+ },
+ feedback: {
+ padding: spacing.md,
+ borderRadius: borderRadius.md,
+ marginBottom: spacing.md,
+ },
+ feedbackOk: {
+ backgroundColor: 'rgba(0, 69, 13, 0.1)',
+ borderWidth: 1,
+ borderColor: 'rgba(0, 69, 13, 0.2)',
+ },
+ feedbackErr: {
+ backgroundColor: 'rgba(186, 26, 26, 0.1)',
+ borderWidth: 1,
+ borderColor: 'rgba(186, 26, 26, 0.2)',
+ },
+ feedbackText: {
+ fontSize: 14,
+ },
+ saveButton: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ alignItems: 'center',
+ },
+ saveButtonDisabled: {
+ opacity: 0.6,
+ },
+ saveButtonText: {
+ color: colors.textInverse,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ menuSection: {
+ marginTop: spacing.md,
+ backgroundColor: colors.card,
+ },
+ menuItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: spacing.md,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.separator,
+ },
+ menuText: {
+ flex: 1,
+ marginLeft: spacing.md,
+ fontSize: 16,
+ color: colors.text,
+ },
+ searchHistorySection: {
+ padding: spacing.md,
+ backgroundColor: colors.background,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.separator,
+ },
+ searchHistoryTitle: {
+ fontSize: 14,
+ color: colors.textSecondary,
+ marginBottom: spacing.sm,
+ },
+ searchItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: spacing.sm,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.separator,
+ },
+ searchAddress: {
+ flex: 1,
+ fontSize: 14,
+ color: colors.text,
+ },
+ searchDelete: {
+ padding: spacing.xs,
+ },
+ button: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.md,
+ paddingVertical: spacing.md,
+ paddingHorizontal: spacing.xl,
+ },
+ buttonText: {
+ color: colors.textInverse,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ linkButton: {
+ marginTop: spacing.md,
+ },
+ linkText: {
+ color: colors.primary,
+ fontSize: 14,
+ },
+ logoutButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ margin: spacing.xl,
+ padding: spacing.md,
+ backgroundColor: colors.card,
+ borderRadius: borderRadius.md,
+ borderWidth: 1,
+ borderColor: colors.danger,
+ },
+ logoutText: {
+ marginLeft: spacing.sm,
+ color: colors.danger,
+ fontSize: 16,
+ fontWeight: '500',
+ },
+});
+```
+
+- [ ] **Step 2: Add expo-image-picker dependency**
+
+```bash
+cd apps/frontend-mobile && npx expo install expo-image-picker
+```
+
+- [ ] **Step 3: Commit mobile changes**
+
+```bash
+git add apps/frontend-mobile/app/\(tabs\)/profile.tsx
+git commit -m "feat: redesign mobile profile with editable fields and avatar upload"
+```
+
+---
+
+## Task 4: Integration - Save search history when user searches nearby
+
+**Files:**
+- Modify: `apps/frontend-mobile/app/(tabs)/index.tsx` or wherever the "find pharmacies near me" search happens
+
+- [ ] **Step 1: Find the nearby search function**
+
+Search for the component that handles "buscar farmacias cerca de mí" or similar nearby search.
+
+- [ ] **Step 2: Add search history saving**
+
+After a successful nearby search, add:
+
+```javascript
+// Save to search history
+try {
+ await fetch('/api/search-history', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({
+ address: userAddress, // The address used in the search
+ latitude: userLatitude,
+ longitude: userLongitude,
+ }),
+ });
+} catch (err) {
+ console.error('Error saving search history:', err);
+}
+```
+
+- [ ] **Step 3: Commit integration changes**
+
+```bash
+git add
+git commit -m "feat: save search history when searching nearby pharmacies"
+```
+
+---
+
+## Task 5: Final verification and cleanup
+
+- [ ] **Step 1: Run tests**
+
+```bash
+npm test
+```
+
+- [ ] **Step 2: Build and verify**
+
+```bash
+npm run build
+```
+
+- [ ] **Step 3: Final commit**
+
+```bash
+git add -A
+git commit -m "chore: complete profile redesign implementation"
+```
+
+---
+
+## Summary of Changes
+
+1. **Backend**: Added `first_name`, `last_name`, `avatar_url` fields to users table; created `search_history` table; added API endpoints for search history CRUD
+2. **Frontend (Web)**: Redesigned ProfileView with editable name fields, clickable avatar for photo upload, search history display, removed emergency contacts and text configuration sections
+3. **Mobile**: Redesigned ProfileScreen with same features using React Native components and expo-image-picker
+4. **Integration**: Added search history saving when user performs nearby pharmacy searches
diff --git a/package-lock.json b/package-lock.json
index 6bc3d29..c8e181c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,9 @@
"turbo": "^2.5.0"
}
},
- "apps/API": {},
+ "apps/API": {
+ "name": "farma-clic-api-sources"
+ },
"apps/backend": {
"name": "farma-clic-backend",
"version": "1.0.0",
@@ -40,6 +42,7 @@
"@opentelemetry/sdk-trace-base": "^1.28.0",
"@opentelemetry/semantic-conventions": "^1.28.0",
"axios": "^1.6.0",
+ "barcode-detector": "^3.2.0",
"bcrypt": "^5.1.1",
"connect-pg-simple": "^10.0.0",
"connect-sqlite3": "^0.9.16",
@@ -103,6 +106,7 @@
"expo-constants": "~57.0.3",
"expo-dev-client": "~57.0.5",
"expo-device": "~7.0.2",
+ "expo-image-picker": "~57.0.2",
"expo-linking": "~57.0.1",
"expo-local-authentication": "~57.0.0",
"expo-notifications": "~57.0.3",
@@ -724,6 +728,25 @@
"react-native": "*"
}
},
+ "apps/frontend-mobile/node_modules/expo-image-loader": {
+ "version": "57.0.0",
+ "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-57.0.0.tgz",
+ "integrity": "sha512-EhwnoPC4T/EMdB7Nsg7qITzhO/qB0hUnmVghmtAKQwwDVaLWWYCGE4NLkes3zk9Ub451SmS/swgPp4PCPzOlnw==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "apps/frontend-mobile/node_modules/expo-image-picker": {
+ "version": "57.0.2",
+ "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-57.0.2.tgz",
+ "integrity": "sha512-XW+C5weIthkOLCJZzExeRJf9pcWfaXNHSDm76gmZVhDUVIHdi9IS3eihAIF0WA0fBo9ogIfQs/iep/c6CzIMKg==",
+ "dependencies": {
+ "expo-image-loader": "~57.0.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"apps/frontend-mobile/node_modules/expo-keep-awake": {
"version": "57.0.0",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.0.tgz",