Refactor frontend-mobile
Run Tests on Branches / Frontend Tests (push) Has been cancelled
Run Tests on Branches / Backend Tests (push) Has been cancelled

This commit is contained in:
Antoni Nuñez Romeu
2026-07-07 18:08:44 +02:00
parent 69d66729aa
commit 9949b85001
14 changed files with 771 additions and 100 deletions
+86 -6
View File
@@ -1,14 +1,40 @@
import { Tabs } from 'expo-router'; import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { View, StyleSheet, Platform } from 'react-native';
import { colors, shadows } from '../../constants/theme';
function ScanIcon({ color, size }: { color: string; size: number }) {
return (
<View style={styles.fabContainer}>
<View style={[styles.fab, shadows.scanButton]}>
<Ionicons name="scan" size={size} color="#ffffff" />
</View>
</View>
);
}
export default function TabLayout() { export default function TabLayout() {
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
tabBarActiveTintColor: '#007AFF', tabBarActiveTintColor: colors.primary,
tabBarInactiveTintColor: '#8E8E93', tabBarInactiveTintColor: colors.textSecondary,
tabBarStyle: styles.tabBar,
tabBarLabelStyle: styles.tabBarLabel,
headerStyle: styles.header,
headerTintColor: colors.primary,
headerTitleStyle: styles.headerTitle,
}} }}
> >
<Tabs.Screen
name="home"
options={{
title: 'Inicio',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen <Tabs.Screen
name="index" name="index"
options={{ options={{
@@ -18,13 +44,27 @@ export default function TabLayout() {
), ),
}} }}
/> />
<Tabs.Screen
name="scan"
options={{
title: 'Escanear',
tabBarIcon: ({ color, size }) => <ScanIcon color={color} size={size} />,
tabBarLabel: () => null,
}}
/>
<Tabs.Screen
name="alerts"
options={{
title: 'Avisos',
tabBarIcon: ({ color, size }) => (
<Ionicons name="notifications" size={size} color={color} />
),
}}
/>
<Tabs.Screen <Tabs.Screen
name="map" name="map"
options={{ options={{
title: 'Mapa', href: null,
tabBarIcon: ({ color, size }) => (
<Ionicons name="map" size={size} color={color} />
),
}} }}
/> />
<Tabs.Screen <Tabs.Screen
@@ -39,3 +79,43 @@ export default function TabLayout() {
</Tabs> </Tabs>
); );
} }
const styles = StyleSheet.create({
tabBar: {
backgroundColor: colors.card,
borderTopColor: colors.border,
height: Platform.OS === 'ios' ? 88 : 64,
paddingBottom: Platform.OS === 'ios' ? 24 : 8,
paddingTop: 8,
},
tabBarLabel: {
fontSize: 11,
fontWeight: '500',
},
header: {
backgroundColor: colors.card,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 1,
},
headerTitle: {
color: colors.text,
fontWeight: '600',
fontSize: 17,
},
fabContainer: {
position: 'absolute',
top: -16,
alignItems: 'center',
},
fab: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: colors.scanButton,
alignItems: 'center',
justifyContent: 'center',
},
});
+284
View File
@@ -0,0 +1,284 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Alert } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, shadows } from '../../constants/theme';
import { LoadingSpinner } from '../../components/LoadingSpinner';
interface NotificationItem {
scope: string;
id: number;
medicine_name?: string;
medicine_nregistro?: string;
pharmacy_name?: string;
pharmacy_id?: number;
pharmacy_address?: string;
created_at?: string;
}
export default function AlertsScreen() {
const [items, setItems] = useState<NotificationItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
useEffect(() => {
loadNotifications();
}, []);
async function loadNotifications() {
setIsLoading(true);
setError(null);
try {
const res = await fetch('/api/notifications/mine', { credentials: 'include' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const merged = [
...(data.pharmacy || []),
...(data.global || []),
].sort((a: NotificationItem, b: NotificationItem) =>
(b.created_at || '').localeCompare(a.created_at || '')
);
setItems(merged);
} catch (err: any) {
setError(err.message || 'No se pudieron cargar las notificaciones');
} finally {
setIsLoading(false);
}
}
async function handleDelete(item: NotificationItem) {
const key = `${item.scope}:${item.id}`;
Alert.alert(
'Eliminar notificación',
`¿Eliminar la notificación de ${item.medicine_name || item.medicine_nregistro}?`,
[
{ text: 'Cancelar', style: 'cancel' },
{
text: 'Eliminar',
style: 'destructive',
onPress: async () => {
setDeletingId(key);
try {
const res = await fetch('/api/notifications/mine', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ scope: item.scope, id: item.id }),
});
if (!res.ok && res.status !== 204) throw new Error(`HTTP ${res.status}`);
setItems(prev => prev.filter(i => !(i.scope === item.scope && i.id === item.id)));
} catch (err: any) {
Alert.alert('Error', err.message || 'No se pudo eliminar');
} finally {
setDeletingId(null);
}
},
},
]
);
}
function renderItem({ item }: { item: NotificationItem }) {
const key = `${item.scope}:${item.id}`;
return (
<View style={styles.item}>
<View style={styles.itemContent}>
<Text style={styles.itemName} numberOfLines={2}>
{item.medicine_name || item.medicine_nregistro}
</Text>
<View style={styles.itemMeta}>
<View style={styles.chip}>
<Ionicons
name={item.scope === 'pharmacy' ? 'medical' : 'globe'}
size={12}
color={colors.primary}
/>
<Text style={styles.chipText}>
{item.scope === 'pharmacy'
? item.pharmacy_name || `Farmacia #${item.pharmacy_id}`
: 'Cualquier farmacia'}
</Text>
</View>
{item.pharmacy_address && (
<Text style={styles.itemAddress} numberOfLines={1}>
{item.pharmacy_address}
</Text>
)}
</View>
</View>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => handleDelete(item)}
disabled={deletingId === key}
>
{deletingId === key ? (
<Ionicons name="hourglass" size={18} color={colors.danger} />
) : (
<Ionicons name="trash-outline" size={18} color={colors.danger} />
)}
</TouchableOpacity>
</View>
);
}
if (isLoading) {
return <LoadingSpinner message="Cargando notificaciones..." />;
}
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Notificaciones Guardadas</Text>
<Text style={styles.subtitle}>
Recibe avisos cuando medicamentos sin stock se repongan
</Text>
</View>
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{error}</Text>
<TouchableOpacity onPress={loadNotifications} style={styles.retryButton}>
<Text style={styles.retryText}>Reintentar</Text>
</TouchableOpacity>
</View>
)}
{!error && items.length === 0 && (
<View style={styles.emptyContainer}>
<Ionicons name="notifications-off-outline" size={64} color={colors.border} />
<Text style={styles.emptyTitle}>Sin notificaciones</Text>
<Text style={styles.emptyText}>
Toca la campana en una farmacia sin stock para recibir notificaciones cuando se reponga.
</Text>
</View>
)}
{!error && items.length > 0 && (
<FlatList
data={items}
keyExtractor={(item) => `${item.scope}:${item.id}`}
renderItem={renderItem}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
header: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.lg,
paddingBottom: spacing.md,
},
title: {
fontSize: 22,
fontWeight: 'bold',
color: colors.text,
},
subtitle: {
fontSize: 14,
color: colors.textSecondary,
marginTop: spacing.xs,
lineHeight: 20,
},
list: {
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl,
gap: spacing.sm,
},
item: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.card,
borderRadius: borderRadius.lg,
padding: spacing.md,
...shadows.card,
},
itemContent: {
flex: 1,
gap: spacing.xs,
},
itemName: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
lineHeight: 22,
},
itemMeta: {
gap: spacing.xs,
},
chip: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
backgroundColor: colors.primaryContainer,
borderRadius: borderRadius.full,
paddingHorizontal: spacing.sm,
paddingVertical: 3,
alignSelf: 'flex-start',
},
chipText: {
fontSize: 12,
fontWeight: '600',
color: colors.onPrimaryContainer,
},
itemAddress: {
fontSize: 12,
color: colors.textSecondary,
},
deleteButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.dangerContainer,
alignItems: 'center',
justifyContent: 'center',
marginLeft: spacing.sm,
},
errorContainer: {
margin: spacing.lg,
padding: spacing.md,
backgroundColor: colors.dangerContainer,
borderRadius: borderRadius.lg,
alignItems: 'center',
gap: spacing.sm,
},
errorText: {
fontSize: 14,
color: colors.danger,
textAlign: 'center',
},
retryButton: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
},
retryText: {
fontSize: 14,
fontWeight: '600',
color: colors.primary,
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.xl,
gap: spacing.md,
},
emptyTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.text,
},
emptyText: {
fontSize: 14,
color: colors.textSecondary,
textAlign: 'center',
lineHeight: 20,
},
});
+137
View File
@@ -0,0 +1,137 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { colors, spacing, borderRadius, shadows } from '../../constants/theme';
export default function HomeScreen() {
const router = useRouter();
return (
<View style={styles.container}>
<View style={styles.hero}>
<Image
source={require('../../assets/farmaclic_logo.png')}
style={styles.logo}
resizeMode="contain"
/>
<Text style={styles.brandName}>FarmaClic</Text>
<Text style={styles.description}>
Encuentra tus medicamentos en farmacias cercanas
</Text>
</View>
<View style={styles.cards}>
<TouchableOpacity
style={styles.card}
activeOpacity={0.85}
onPress={() => router.push('/(tabs)')}
>
<View style={[styles.cardIcon, styles.cardIconSearch]}>
<Ionicons name="search" size={24} color={colors.onPrimaryContainer} />
</View>
<View style={styles.cardContent}>
<Text style={styles.cardLabel}>Buscar Medicamento</Text>
<Ionicons name="chevron-forward" size={20} color={colors.onPrimaryContainer} style={{ opacity: 0.7 }} />
</View>
</TouchableOpacity>
<TouchableOpacity
style={[styles.card, styles.cardScan]}
activeOpacity={0.85}
onPress={() => router.push('/scanner')}
>
<View style={[styles.cardIcon, styles.cardIconScan]}>
<Ionicons name="scan" size={24} color="#ffffff" />
</View>
<View style={styles.cardContent}>
<Text style={[styles.cardLabel, styles.cardLabelScan]}>Escanear TSI</Text>
<Ionicons name="chevron-forward" size={20} color="#ffffff" style={{ opacity: 0.7 }} />
</View>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.lg,
gap: spacing.lg,
},
hero: {
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.md,
},
logo: {
width: 120,
height: 120,
marginBottom: spacing.xs,
},
brandName: {
fontSize: 28,
fontWeight: 'bold',
color: colors.text,
letterSpacing: -0.5,
},
description: {
fontSize: 16,
color: colors.textSecondary,
textAlign: 'center',
maxWidth: 260,
lineHeight: 24,
},
cards: {
width: '100%',
maxWidth: 320,
gap: spacing.md,
},
card: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.md,
backgroundColor: colors.primaryContainer,
borderRadius: borderRadius.lg,
padding: spacing.md,
minHeight: 64,
...shadows.card,
},
cardScan: {
backgroundColor: colors.scanButton,
},
cardIcon: {
width: 48,
height: 48,
borderRadius: borderRadius.md,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
},
cardIconSearch: {
backgroundColor: 'rgba(255, 255, 255, 0.3)',
},
cardIconScan: {
backgroundColor: 'rgba(255, 255, 255, 0.2)',
},
cardContent: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
cardLabel: {
fontSize: 18,
fontWeight: '700',
color: colors.onPrimaryContainer,
lineHeight: 24,
},
cardLabelScan: {
color: '#ffffff',
},
});
+18 -9
View File
@@ -59,7 +59,7 @@ export default function HomeScreen() {
style={styles.scannerButton} style={styles.scannerButton}
onPress={() => router.push('/scanner')} onPress={() => router.push('/scanner')}
> >
<Ionicons name="scan" size={24} color={colors.primary} /> <Ionicons name="scan" size={22} color="#ffffff" />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -96,25 +96,34 @@ const styles = StyleSheet.create({
searchContainer: { searchContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingRight: spacing.lg,
}, },
scannerButton: { scannerButton: {
marginRight: spacing.md, width: 44,
padding: spacing.sm, height: 44,
backgroundColor: colors.card, borderRadius: 22,
borderRadius: borderRadius.md, backgroundColor: colors.scanButton,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#2b5bb5',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 12,
elevation: 6,
}, },
list: { list: {
paddingBottom: spacing.xl, paddingBottom: spacing.xl,
}, },
errorContainer: { errorContainer: {
padding: spacing.md, padding: spacing.md,
marginHorizontal: spacing.md, marginHorizontal: spacing.lg,
backgroundColor: '#F8D7DA', backgroundColor: colors.dangerContainer,
borderRadius: 8, borderRadius: borderRadius.lg,
}, },
errorText: { errorText: {
color: '#721C24', color: colors.danger,
textAlign: 'center', textAlign: 'center',
fontSize: 14,
}, },
emptyContainer: { emptyContainer: {
padding: spacing.xl, padding: spacing.xl,
+6 -6
View File
@@ -92,14 +92,14 @@ const styles = StyleSheet.create({
left: spacing.md, left: spacing.md,
right: spacing.md, right: spacing.md,
backgroundColor: colors.card, backgroundColor: colors.card,
borderRadius: 8, borderRadius: borderRadius.lg,
padding: spacing.sm, padding: spacing.sm + 4,
alignItems: 'center', alignItems: 'center',
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25, shadowOpacity: 0.08,
shadowRadius: 4, shadowRadius: 20,
elevation: 5, elevation: 4,
}, },
legendText: { legendText: {
fontSize: 14, fontSize: 14,
+36 -36
View File
@@ -830,7 +830,7 @@ const styles = StyleSheet.create({
width: 100, width: 100,
height: 100, height: 100,
borderRadius: 50, borderRadius: 50,
backgroundColor: colors.background, backgroundColor: colors.primaryContainer,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
overflow: 'hidden', overflow: 'hidden',
@@ -846,7 +846,7 @@ const styles = StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)', backgroundColor: 'rgba(0, 0, 0, 0.4)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
opacity: 0.8, opacity: 0.8,
@@ -857,17 +857,17 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
}, },
infoSection: { infoSection: {
padding: spacing.xl, padding: spacing.lg,
backgroundColor: colors.card, backgroundColor: colors.card,
marginTop: spacing.md, marginTop: spacing.sm,
gap: spacing.md, gap: spacing.md,
}, },
infoCard: { infoCard: {
backgroundColor: colors.background, backgroundColor: colors.surfaceLow,
padding: spacing.md, padding: spacing.md,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
borderWidth: 1, borderWidth: 1,
borderColor: colors.border || '#E0E0E0', borderColor: colors.border,
}, },
infoLabel: { infoLabel: {
fontSize: 12, fontSize: 12,
@@ -883,7 +883,7 @@ const styles = StyleSheet.create({
color: colors.text, color: colors.text,
}, },
menuSection: { menuSection: {
marginTop: spacing.md, marginTop: spacing.sm,
backgroundColor: colors.card, backgroundColor: colors.card,
}, },
menuItem: { menuItem: {
@@ -891,7 +891,7 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
padding: spacing.md, padding: spacing.md,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.separator, borderBottomColor: colors.border,
}, },
menuText: { menuText: {
flex: 1, flex: 1,
@@ -901,9 +901,9 @@ const styles = StyleSheet.create({
}, },
searchHistorySection: { searchHistorySection: {
padding: spacing.md, padding: spacing.md,
backgroundColor: colors.background, backgroundColor: colors.surfaceLow,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.separator, borderBottomColor: colors.border,
}, },
searchHistoryTitle: { searchHistoryTitle: {
fontSize: 14, fontSize: 14,
@@ -916,7 +916,7 @@ const styles = StyleSheet.create({
justifyContent: 'space-between', justifyContent: 'space-between',
paddingVertical: spacing.sm, paddingVertical: spacing.sm,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.separator, borderBottomColor: colors.border,
}, },
searchAddress: { searchAddress: {
flex: 1, flex: 1,
@@ -928,7 +928,7 @@ const styles = StyleSheet.create({
}, },
button: { button: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
paddingVertical: spacing.md, paddingVertical: spacing.md,
paddingHorizontal: spacing.xl, paddingHorizontal: spacing.xl,
}, },
@@ -951,7 +951,7 @@ const styles = StyleSheet.create({
margin: spacing.xl, margin: spacing.xl,
padding: spacing.md, padding: spacing.md,
backgroundColor: colors.card, backgroundColor: colors.card,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
borderWidth: 1, borderWidth: 1,
borderColor: colors.danger, borderColor: colors.danger,
}, },
@@ -979,7 +979,7 @@ const styles = StyleSheet.create({
justifyContent: 'space-between', justifyContent: 'space-between',
padding: spacing.lg, padding: spacing.lg,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.separator, borderBottomColor: colors.border,
}, },
modalTitle: { modalTitle: {
fontSize: 18, fontSize: 18,
@@ -1000,26 +1000,26 @@ const styles = StyleSheet.create({
}, },
modalInput: { modalInput: {
borderWidth: 1, borderWidth: 1,
borderColor: colors.border || '#E0E0E0', borderColor: colors.border,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
padding: spacing.md, padding: spacing.md,
fontSize: 16, fontSize: 16,
color: colors.text, color: colors.text,
}, },
modalFeedback: { modalFeedback: {
padding: spacing.md, padding: spacing.md,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
marginBottom: spacing.md, marginBottom: spacing.md,
}, },
modalFeedbackOk: { modalFeedbackOk: {
backgroundColor: 'rgba(0, 69, 13, 0.1)', backgroundColor: '#eaf7ec',
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(0, 69, 13, 0.2)', borderColor: '#cfead0',
}, },
modalFeedbackErr: { modalFeedbackErr: {
backgroundColor: 'rgba(186, 26, 26, 0.1)', backgroundColor: colors.dangerContainer,
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(186, 26, 26, 0.2)', borderColor: '#fecaca',
}, },
modalFeedbackText: { modalFeedbackText: {
fontSize: 14, fontSize: 14,
@@ -1034,9 +1034,9 @@ const styles = StyleSheet.create({
modalCancelBtn: { modalCancelBtn: {
paddingVertical: spacing.md, paddingVertical: spacing.md,
paddingHorizontal: spacing.lg, paddingHorizontal: spacing.lg,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
borderWidth: 1, borderWidth: 1,
borderColor: colors.border || '#E0E0E0', borderColor: colors.border,
}, },
modalCancelText: { modalCancelText: {
fontSize: 16, fontSize: 16,
@@ -1046,7 +1046,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.primary, backgroundColor: colors.primary,
paddingVertical: spacing.md, paddingVertical: spacing.md,
paddingHorizontal: spacing.xl, paddingHorizontal: spacing.xl,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
minWidth: 100, minWidth: 100,
alignItems: 'center', alignItems: 'center',
}, },
@@ -1068,7 +1068,7 @@ const styles = StyleSheet.create({
tabContainer: { tabContainer: {
flexDirection: 'row', flexDirection: 'row',
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.separator, borderBottomColor: colors.border,
}, },
tabButton: { tabButton: {
flex: 1, flex: 1,
@@ -1115,10 +1115,10 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: spacing.md, padding: spacing.md,
backgroundColor: colors.background, backgroundColor: colors.surfaceLow,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
borderWidth: 1, borderWidth: 1,
borderColor: colors.border || '#E5E5EA', borderColor: colors.border,
}, },
uploadIconContainer: { uploadIconContainer: {
width: 56, width: 56,
@@ -1155,14 +1155,14 @@ const styles = StyleSheet.create({
alignItems: 'flex-start', alignItems: 'flex-start',
justifyContent: 'space-between', justifyContent: 'space-between',
padding: spacing.md, padding: spacing.md,
backgroundColor: colors.background, backgroundColor: colors.surfaceLow,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
borderWidth: 1, borderWidth: 1,
borderColor: colors.border || '#E0E0E0', borderColor: colors.border,
}, },
addressItemDefault: { addressItemDefault: {
borderColor: colors.primary, borderColor: colors.primary,
backgroundColor: 'rgba(0, 69, 13, 0.03)', backgroundColor: '#eaf7ec',
}, },
addressItemInfo: { addressItemInfo: {
flex: 1, flex: 1,
@@ -1181,7 +1181,7 @@ const styles = StyleSheet.create({
addressBadge: { addressBadge: {
alignSelf: 'flex-start', alignSelf: 'flex-start',
backgroundColor: colors.primary, backgroundColor: colors.primary,
borderRadius: 12, borderRadius: borderRadius.full,
paddingHorizontal: spacing.sm, paddingHorizontal: spacing.sm,
paddingVertical: 2, paddingVertical: 2,
marginTop: 4, marginTop: 4,
@@ -1219,9 +1219,9 @@ const styles = StyleSheet.create({
padding: spacing.md, padding: spacing.md,
marginTop: spacing.md, marginTop: spacing.md,
borderWidth: 2, borderWidth: 2,
borderColor: colors.border || '#E0E0E0', borderColor: colors.border,
borderStyle: 'dashed', borderStyle: 'dashed',
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
}, },
addressAddBtnText: { addressAddBtnText: {
fontSize: 15, fontSize: 15,
+81
View File
@@ -0,0 +1,81 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { colors, spacing, borderRadius, shadows } from '../../constants/theme';
export default function ScanTabScreen() {
const router = useRouter();
return (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.iconContainer}>
<Ionicons name="scan" size={64} color={colors.scanButton} />
</View>
<Text style={styles.title}>Escanear TSI</Text>
<Text style={styles.description}>
Escanea el código de barras de tu tarjeta sanitaria para encontrar tus medicamentos
</Text>
<TouchableOpacity
style={[styles.button, shadows.scanButton]}
activeOpacity={0.85}
onPress={() => router.push('/scanner')}
>
<Ionicons name="scan" size={24} color="#ffffff" />
<Text style={styles.buttonText}>Iniciar escaneo</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
alignItems: 'center',
justifyContent: 'center',
},
content: {
alignItems: 'center',
paddingHorizontal: spacing.xl,
gap: spacing.md,
},
iconContainer: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: colors.primaryContainer,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.sm,
},
title: {
fontSize: 22,
fontWeight: 'bold',
color: colors.text,
},
description: {
fontSize: 15,
color: colors.textSecondary,
textAlign: 'center',
lineHeight: 22,
maxWidth: 280,
},
button: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
backgroundColor: colors.scanButton,
borderRadius: borderRadius.lg,
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
marginTop: spacing.md,
},
buttonText: {
fontSize: 17,
fontWeight: '600',
color: '#ffffff',
},
});
+15 -7
View File
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { registerForPushNotifications, addNotificationListener, addNotificationResponseListener } from '../services/notifications'; import { registerForPushNotifications, addNotificationListener, addNotificationResponseListener } from '../services/notifications';
import { colors } from '../constants/theme';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -35,20 +36,27 @@ export default function RootLayout() {
return ( return (
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Stack> <Stack
screenOptions={{
headerStyle: { backgroundColor: colors.card },
headerTintColor: colors.primary,
headerTitleStyle: { color: colors.text, fontWeight: '600' },
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen <Stack.Screen
name="medicine/[id]" name="medicine/[id]"
options={{ options={{ title: 'Medicamento' }}
title: 'Medicamento',
headerTintColor: '#007AFF',
}}
/> />
<Stack.Screen <Stack.Screen
name="pharmacy/[id]" name="pharmacy/[id]"
options={{ title: 'Farmacia' }}
/>
<Stack.Screen
name="scanner"
options={{ options={{
title: 'Farmacia', title: 'Escanear',
headerTintColor: '#007AFF', headerShown: false,
}} }}
/> />
<Stack.Screen <Stack.Screen
+11 -6
View File
@@ -129,7 +129,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'flex-start',
padding: spacing.md, padding: spacing.lg,
backgroundColor: colors.card, backgroundColor: colors.card,
}, },
name: { name: {
@@ -142,14 +142,14 @@ const styles = StyleSheet.create({
infoSection: { infoSection: {
backgroundColor: colors.card, backgroundColor: colors.card,
marginTop: spacing.sm, marginTop: spacing.sm,
padding: spacing.md, padding: spacing.lg,
}, },
infoRow: { infoRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingVertical: spacing.sm, paddingVertical: spacing.sm,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.separator, borderBottomColor: colors.border,
}, },
infoLabel: { infoLabel: {
fontSize: 14, fontSize: 14,
@@ -162,7 +162,7 @@ const styles = StyleSheet.create({
}, },
pharmaciesSection: { pharmaciesSection: {
marginTop: spacing.sm, marginTop: spacing.sm,
padding: spacing.md, padding: spacing.lg,
}, },
sectionTitle: { sectionTitle: {
fontSize: 18, fontSize: 18,
@@ -177,9 +177,14 @@ const styles = StyleSheet.create({
}, },
pharmacyCard: { pharmacyCard: {
backgroundColor: colors.card, backgroundColor: colors.card,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
marginBottom: spacing.sm, marginBottom: spacing.sm,
overflow: 'hidden', overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
}, },
pharmacyCardContent: { pharmacyCardContent: {
flexDirection: 'row', flexDirection: 'row',
@@ -219,7 +224,7 @@ const styles = StyleSheet.create({
gap: spacing.xs, gap: spacing.xs,
paddingVertical: spacing.sm, paddingVertical: spacing.sm,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: colors.separator, borderTopColor: colors.border,
}, },
directionsText: { directionsText: {
color: colors.primary, color: colors.primary,
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

@@ -52,14 +52,14 @@ export function MedicineCard({ medicine }: MedicineCardProps) {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
backgroundColor: colors.card, backgroundColor: colors.card,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
padding: spacing.md, padding: spacing.md,
marginHorizontal: spacing.md, marginHorizontal: spacing.lg,
marginVertical: spacing.xs, marginVertical: spacing.xs,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1, shadowOpacity: 0.06,
shadowRadius: 4, shadowRadius: 8,
elevation: 2, elevation: 2,
}, },
header: { header: {
@@ -92,7 +92,7 @@ const styles = StyleSheet.create({
price: { price: {
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
color: colors.text, color: colors.primary,
marginLeft: spacing.xs, marginLeft: spacing.xs,
}, },
labContainer: { labContainer: {
@@ -60,11 +60,16 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.card, backgroundColor: colors.card,
borderRadius: borderRadius.md, borderRadius: borderRadius.lg,
paddingHorizontal: spacing.md, paddingHorizontal: spacing.md,
paddingVertical: spacing.sm, paddingVertical: spacing.sm + 2,
marginHorizontal: spacing.md, marginHorizontal: spacing.lg,
marginVertical: spacing.sm, marginVertical: spacing.sm,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
}, },
icon: { icon: {
marginRight: spacing.sm, marginRight: spacing.sm,
@@ -33,13 +33,13 @@ const styles = StyleSheet.create({
borderRadius: borderRadius.sm, borderRadius: borderRadius.sm,
}, },
success: { success: {
backgroundColor: '#D4EDDA', backgroundColor: '#eaf7ec',
}, },
warning: { warning: {
backgroundColor: '#FFF3CD', backgroundColor: '#fff3cd',
}, },
danger: { danger: {
backgroundColor: '#F8D7DA', backgroundColor: '#feecec',
}, },
text: { text: {
fontSize: 12, fontSize: 12,
+80 -18
View File
@@ -1,25 +1,47 @@
export const colors = { export const colors = {
// Primary // Primary - pastel green pharmacy palette (matching PWA)
primary: '#007AFF', primary: '#7fbf8f',
primaryLight: '#4DA3FF', primaryLight: '#cfead0',
primaryDark: '#0056CC', primaryDark: '#09310a',
onPrimary: '#09310a',
primaryContainer: '#cfead0',
onPrimaryContainer: '#0d2b12',
// Secondary - soft blue
secondary: '#a3b8ff',
secondaryContainer: '#dbe7ff',
// Tertiary - soft lavender
tertiary: '#c9b3ff',
tertiaryContainer: '#efe7ff',
// Scan button - vivid blue (intentionally prominent)
scanButton: '#2b5bb5',
scanButtonShadow: 'rgba(43, 91, 181, 0.32)',
// Semantic // Semantic
success: '#34C759', success: '#34C759',
danger: '#FF3B30', danger: '#b91c1c',
dangerContainer: '#feecec',
warning: '#FF9500', warning: '#FF9500',
// Neutral // Surface
background: '#F2F2F7', background: '#fbfbfb',
card: '#FFFFFF', card: '#ffffff',
border: '#E5E5EA', surfaceLow: '#f2f4f5',
separator: '#C6C6C8', surface: '#eceeef',
surfaceHigh: '#e6e8e9',
border: '#c0c9bb',
separator: '#c0c9bb',
// Text // Text
text: '#1C1C1E', text: '#111417',
textSecondary: '#8E8E93', textSecondary: '#41493e',
textInverse: '#FFFFFF', textInverse: '#ffffff',
textLink: '#007AFF', textLink: '#7fbf8f',
// Accent
accentWarm: '#f5a97a',
}; };
export const spacing = { export const spacing = {
@@ -32,27 +54,67 @@ export const spacing = {
}; };
export const borderRadius = { export const borderRadius = {
sm: 8, sm: 4,
md: 12, md: 8,
lg: 16, lg: 12,
xl: 24, xl: 16,
full: 9999,
}; };
export const typography = { export const typography = {
title: { title: {
fontSize: 28, fontSize: 28,
fontWeight: 'bold' as const, fontWeight: 'bold' as const,
color: '#111417',
}, },
heading: { heading: {
fontSize: 20, fontSize: 20,
fontWeight: '600' as const, fontWeight: '600' as const,
color: '#111417',
}, },
body: { body: {
fontSize: 16, fontSize: 16,
fontWeight: 'normal' as const, fontWeight: 'normal' as const,
color: '#111417',
},
bodyLarge: {
fontSize: 18,
fontWeight: '400' as const,
color: '#41493e',
lineHeight: 27,
}, },
caption: { caption: {
fontSize: 12, fontSize: 12,
fontWeight: 'normal' as const, fontWeight: 'normal' as const,
color: '#41493e',
},
label: {
fontSize: 14,
fontWeight: '500' as const,
color: '#41493e',
},
};
export const shadows = {
soft: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 20,
elevation: 3,
},
card: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
},
scanButton: {
shadowColor: '#2b5bb5',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.32,
shadowRadius: 26,
elevation: 8,
}, },
}; };