From d22f1b5646c67c9dbdde8302ab363303f2546a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Mon, 6 Jul 2026 11:33:09 +0200 Subject: [PATCH] feat: add barcode scanner for medicine lookup --- frontend-mobile/app/(tabs)/index.tsx | 35 ++- frontend-mobile/app/scanner.tsx | 46 ++++ frontend-mobile/components/BarcodeScanner.tsx | 207 ++++++++++++++++++ 3 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 frontend-mobile/app/scanner.tsx create mode 100644 frontend-mobile/components/BarcodeScanner.tsx diff --git a/frontend-mobile/app/(tabs)/index.tsx b/frontend-mobile/app/(tabs)/index.tsx index b79f59e..0db711b 100644 --- a/frontend-mobile/app/(tabs)/index.tsx +++ b/frontend-mobile/app/(tabs)/index.tsx @@ -1,15 +1,18 @@ import React, { useState, useEffect } from 'react'; -import { View, FlatList, StyleSheet, Text } from 'react-native'; +import { View, FlatList, StyleSheet, Text, TouchableOpacity } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useRouter } from 'expo-router'; import { SearchBar } from '../../components/SearchBar'; import { MedicineCard } from '../../components/MedicineCard'; import { LoadingSpinner } from '../../components/LoadingSpinner'; import { useDebounce } from '../../hooks/useDebounce'; import { searchMedicines } from '../../services/medicines'; -import { colors, spacing } from '../../constants/theme'; +import { colors, spacing, borderRadius } from '../../constants/theme'; import { Medicine } from '../../types'; import { config } from '../../constants/config'; export default function HomeScreen() { + const router = useRouter(); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -46,11 +49,19 @@ export default function HomeScreen() { return ( - + + + router.push('/scanner')} + > + + + {isLoading && } @@ -82,6 +93,16 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: colors.background, }, + searchContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + scannerButton: { + marginRight: spacing.md, + padding: spacing.sm, + backgroundColor: colors.card, + borderRadius: borderRadius.md, + }, list: { paddingBottom: spacing.xl, }, diff --git a/frontend-mobile/app/scanner.tsx b/frontend-mobile/app/scanner.tsx new file mode 100644 index 0000000..02a786b --- /dev/null +++ b/frontend-mobile/app/scanner.tsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { useRouter } from 'expo-router'; +import { BarcodeScanner } from '../components/BarcodeScanner'; +import { searchMedicines } from '../services/medicines'; + +export default function ScannerScreen() { + const router = useRouter(); + const [isSearching, setIsSearching] = useState(false); + + const handleBarcodeScanned = async (barcode: string) => { + setIsSearching(true); + try { + const results = await searchMedicines(barcode); + if (results.length > 0) { + router.push(`/medicine/${results[0].nregistro}`); + } else { + router.push(`/medicine/${barcode}`); + } + } catch (error) { + console.error('Error searching medicine:', error); + router.push(`/medicine/${barcode}`); + } finally { + setIsSearching(false); + } + }; + + const handleClose = () => { + router.back(); + }; + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); diff --git a/frontend-mobile/components/BarcodeScanner.tsx b/frontend-mobile/components/BarcodeScanner.tsx new file mode 100644 index 0000000..2515d52 --- /dev/null +++ b/frontend-mobile/components/BarcodeScanner.tsx @@ -0,0 +1,207 @@ +import React, { useState } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { CameraView, useCameraPermissions } from 'expo-camera'; +import { Ionicons } from '@expo/vector-icons'; +import { colors, spacing, borderRadius } from '../constants/theme'; + +interface BarcodeScannerProps { + onBarcodeScanned: (barcode: string) => void; + onClose: () => void; +} + +export function BarcodeScanner({ onBarcodeScanned, onClose }: BarcodeScannerProps) { + const [permission, requestPermission] = useCameraPermissions(); + const [scanned, setScanned] = useState(false); + + if (!permission) { + return ; + } + + if (!permission.granted) { + return ( + + + Permiso de cámara requerido + + Necesitamos acceso a la cámara para escanear códigos de barras + + + Conceder permiso + + + Cancelar + + + ); + } + + const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => { + if (scanned) return; + setScanned(true); + onBarcodeScanned(data); + }; + + return ( + + + + + + + + + + + + + Apunta la cámara al código de barras del medicamento + + + + + + + + {scanned && ( + + setScanned(false)} + > + Escanear de nuevo + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000', + }, + permissionContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.background, + padding: spacing.xl, + }, + permissionTitle: { + fontSize: 20, + fontWeight: 'bold', + color: colors.text, + marginTop: spacing.lg, + }, + permissionText: { + fontSize: 16, + color: colors.textSecondary, + textAlign: 'center', + marginTop: spacing.sm, + marginBottom: spacing.xl, + }, + permissionButton: { + backgroundColor: colors.primary, + borderRadius: borderRadius.md, + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + }, + permissionButtonText: { + color: colors.textInverse, + fontSize: 16, + fontWeight: '600', + }, + cancelButton: { + marginTop: spacing.md, + }, + cancelButtonText: { + color: colors.textSecondary, + fontSize: 14, + }, + overlay: { + flex: 1, + backgroundColor: 'transparent', + justifyContent: 'center', + alignItems: 'center', + }, + scannerFrame: { + width: 250, + height: 250, + borderWidth: 2, + borderColor: 'transparent', + position: 'relative', + }, + corner: { + position: 'absolute', + width: 30, + height: 30, + borderColor: colors.primary, + }, + topLeft: { + top: 0, + left: 0, + borderTopWidth: 3, + borderLeftWidth: 3, + }, + topRight: { + top: 0, + right: 0, + borderTopWidth: 3, + borderRightWidth: 3, + }, + bottomLeft: { + bottom: 0, + left: 0, + borderBottomWidth: 3, + borderLeftWidth: 3, + }, + bottomRight: { + bottom: 0, + right: 0, + borderBottomWidth: 3, + borderRightWidth: 3, + }, + instruction: { + color: colors.textInverse, + fontSize: 14, + textAlign: 'center', + marginTop: spacing.xl, + backgroundColor: 'rgba(0,0,0,0.6)', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + borderRadius: borderRadius.sm, + }, + closeButton: { + position: 'absolute', + top: spacing.xl, + right: spacing.xl, + backgroundColor: 'rgba(0,0,0,0.6)', + borderRadius: 20, + padding: spacing.sm, + }, + scannedOverlay: { + position: 'absolute', + bottom: spacing.xxl, + left: spacing.xl, + right: spacing.xl, + }, + scanAgainButton: { + backgroundColor: colors.primary, + borderRadius: borderRadius.md, + padding: spacing.md, + alignItems: 'center', + }, + scanAgainText: { + color: colors.textInverse, + fontSize: 16, + fontWeight: '600', + }, +});