import React, { useState, useCallback, useRef } from 'react'; import { BarcodeScanner, BarcodeFormat } from '@capacitor-mlkit/barcode-scanning'; import { Capacitor } from '@capacitor/core'; import { BrowserMultiFormatReader } from '@zxing/browser'; import { BarcodeFormat as ZxingFormat, DecodeHintType } from '@zxing/library'; import './ScannerView.css'; const CIP_REGEX = /^[A-Z0-9]{8,30}$/i; function playBeep() { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(1046, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(1318, ctx.currentTime + 0.08); gain.gain.setValueAtTime(0.28, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35); osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.38); } catch (_) { } } function ScannerView({ onClose, onSelectMedicine }) { const videoRef = useRef(null); const streamRef = useRef(null); const rafRef = useRef(null); const detectorRef = useRef(null); // Log de diagnóstico al montar React.useEffect(() => { console.log('[Scanner] BarcodeDetector global:', typeof BarcodeDetector); console.log('[Scanner] Navegador:', navigator.userAgent); }, []); const [phase, setPhase] = useState('idle'); const [manualCip, setManualCip] = useState(''); const [prescriptions, setPrescriptions] = useState([]); const [scannedCip, setScannedCip] = useState(null); const [loadingPrescriptions, setLoadingPrescriptions] = useState(false); const [errorMsg, setErrorMsg] = useState(''); // Photo upload state const [previewUrl, setPreviewUrl] = useState(null); const [ocrLoading, setOcrLoading] = useState(false); const cameraInputRef = useRef(null); const galleryInputRef = useRef(null); const isNative = Capacitor.isNativePlatform(); const fetchPrescriptions = useCallback(async (cip) => { setLoadingPrescriptions(true); try { const res = await fetch(`/api/tsi/${encodeURIComponent(cip)}/prescriptions`); const data = await res.json(); setPrescriptions(data); } catch { setPrescriptions([]); } finally { setLoadingPrescriptions(false); } }, []); function stopCamera() { streamRef.current?.getTracks().forEach(t => t.stop()); streamRef.current = null; if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } } async function handleNativeScan() { setErrorMsg(''); try { const supported = await BarcodeScanner.isSupported(); if (!supported) { setErrorMsg('El escáner no está disponible en este dispositivo.'); setPhase('error'); return; } const permission = await BarcodeScanner.checkPermissions(); if (permission.camera !== 'granted') { const request = await BarcodeScanner.requestPermissions(); if (request.camera !== 'granted') { setErrorMsg('Permiso de cámara denegado. Actívalo en ajustes o introduce el código CIP manualmente.'); setPhase('error'); return; } } setPhase('scanning'); const result = await BarcodeScanner.scan({ formats: [BarcodeFormat.PDF417, BarcodeFormat.Code128, BarcodeFormat.QrCode], autoZoom: true, }); const barcode = result.barcodes[0]; const rawValue = barcode?.rawValue; if (!rawValue || !CIP_REGEX.test(rawValue)) { setErrorMsg('Código de barras inválido. Intenta de nuevo o introduce el CIP manualmente.'); setPhase('error'); return; } playBeep(); setScannedCip(rawValue); setPhase('prescriptions'); fetchPrescriptions(rawValue); } catch (err) { if (err?.message?.includes('cancel') || err?.message?.includes('User')) { setPhase('idle'); return; } setErrorMsg(`Error al escanear: ${err.message || 'Error desconocido'}`); setPhase('error'); } } async function handleWebScan() { setErrorMsg(''); console.log('[Scanner] Iniciando escaneo web...'); try { if (!navigator.mediaDevices?.getUserMedia) { setErrorMsg('Cámara no disponible en este navegador.'); setPhase('error'); return; } const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } }, }); console.log('[Scanner] Stream de cámara obtenido'); streamRef.current = stream; setPhase('scanning'); await new Promise((resolve) => { const waitForVideo = () => { if (videoRef.current) { videoRef.current.srcObject = stream; videoRef.current.play().then(() => { console.log('[Scanner] Video reproduciéndose'); console.log('[Scanner] Video dims:', videoRef.current.videoWidth, 'x', videoRef.current.videoHeight); resolve(); }).catch((e) => { console.error('[Scanner] Error al reproducir video:', e); resolve(); }); } else { requestAnimationFrame(waitForVideo); } }; requestAnimationFrame(waitForVideo); }); console.log('[Scanner] Usando zxing local (@zxing/browser)...'); const hints = new Map(); hints.set(DecodeHintType.POSSIBLE_FORMATS, [ ZxingFormat.CODE_128, ZxingFormat.CODE_39, ZxingFormat.CODE_93, ZxingFormat.PDF_417, ZxingFormat.QR_CODE, ZxingFormat.EAN_13, ZxingFormat.EAN_8, ZxingFormat.UPC_A, ZxingFormat.UPC_E, ZxingFormat.ITF, ]); hints.set(DecodeHintType.TRY_HARDER, true); const zxingReader = new BrowserMultiFormatReader(hints); detectorRef.current = zxingReader; const formatNames = { 0:'AZTEC', 1:'CODABAR', 2:'CODE_39', 3:'CODE_93', 4:'CODE_128', 5:'DATA_MATRIX', 6:'EAN_8', 7:'EAN_13', 8:'ITF', 10:'PDF_417', 11:'QR_CODE', 12:'RSS_14', 14:'UPC_A', 15:'UPC_E' }; console.log('[Scanner] Iniciando decodeFromVideoElement...'); const controls = await zxingReader.decodeFromVideoElement(videoRef.current, (result, error) => { if (result) { const rawValue = result.getText(); const cleaned = rawValue.replace(/[^A-Z0-9]/gi, ''); const format = result.getBarcodeFormat(); console.log('[Scanner] zxing detectó! raw:', rawValue, '| limpio:', cleaned, '| formato:', formatNames[format] || format); if (cleaned && CIP_REGEX.test(cleaned)) { console.log('[Scanner] CIP válido ✓'); controls.stop(); stopCamera(); playBeep(); setScannedCip(cleaned); setPhase('prescriptions'); fetchPrescriptions(cleaned); } else if (rawValue) { console.log('[Scanner] Código no cumple regex:', cleaned); } } }); console.log('[Scanner] Escaneando...'); } catch (err) { console.error('[Scanner] Error general:', err); stopCamera(); if (err.name === 'NotAllowedError') { setErrorMsg('Permiso de cámara denegado. Permite el acceso e intenta de nuevo.'); } else if (err.name === 'NotFoundError') { setErrorMsg('No se detectó ninguna cámara. Introduce el código CIP manualmente.'); } else { setErrorMsg(`Error de cámara: ${err.message || 'Error desconocido'}`); } setPhase('error'); } } function handleStartScan() { if (isNative) { handleNativeScan(); } else { handleWebScan(); } } function handleManualSubmit(e) { e.preventDefault(); const cip = manualCip.trim(); if (!cip) { setErrorMsg('Introduce un código CIP.'); setPhase('error'); return; } if (!CIP_REGEX.test(cip)) { setErrorMsg('Formato CIP inválido. Debe tener 16 caracteres alfanuméricos.'); setPhase('error'); return; } playBeep(); setScannedCip(cip); setPhase('prescriptions'); fetchPrescriptions(cip); } // Photo upload handlers function handlePhotoSelect(e) { const file = e.target.files?.[0]; if (!file) return; setErrorMsg(''); setPreviewUrl(URL.createObjectURL(file)); setPhase('photo-preview'); // Reset input so selecting the same file again triggers onChange e.target.value = ''; } async function handlePhotoOcr() { if (!previewUrl) return; setOcrLoading(true); setErrorMsg(''); try { const res = await fetch(previewUrl); const blob = await res.blob(); const formData = new FormData(); formData.append('photo', blob, 'tsi.jpg'); const ocrRes = await fetch('/api/tsi/ocr', { method: 'POST', body: formData }); const data = await ocrRes.json(); if (!ocrRes.ok) { setErrorMsg(data.error || 'No se pudo leer la imagen. Intenta con otra foto.'); setPhase('error'); return; } const cip = data.cip; if (!CIP_REGEX.test(cip)) { setErrorMsg(`CIP detectado "${cip}" no tiene formato válido. Introduce el código manualmente.`); setPhase('error'); return; } playBeep(); setScannedCip(cip); setPreviewUrl(null); setPhase('prescriptions'); fetchPrescriptions(cip); } catch (err) { setErrorMsg(`Error al procesar la imagen: ${err.message || 'Error desconocido'}`); setPhase('error'); } finally { setOcrLoading(false); } } function handlePhotoDiscard() { setPreviewUrl(null); setPhase('idle'); } function handlePickPrescription(rx) { stopCamera(); onSelectMedicine(rx.name); } function handleBack() { stopCamera(); if (phase === 'prescriptions') { setPhase('idle'); setPrescriptions([]); setScannedCip(null); } else if (phase === 'photo-preview') { setPreviewUrl(null); setPhase('idle'); } else { onClose(); } } return (

Escanear TSI

Escanea el código de barras de tu tarjeta sanitaria para ver tus recetas activas

{phase !== 'prescriptions' && ( <> {phase === 'error' && (

{errorMsg}

)} {phase === 'idle' && ( <>
)} {phase === 'scanning' && !isNative && (