Logos & scanner changes
This commit is contained in:
@@ -43,6 +43,12 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
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) => {
|
||||
@@ -237,6 +243,61 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
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);
|
||||
@@ -248,6 +309,9 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
setPhase('idle');
|
||||
setPrescriptions([]);
|
||||
setScannedCip(null);
|
||||
} else if (phase === 'photo-preview') {
|
||||
setPreviewUrl(null);
|
||||
setPhase('idle');
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
@@ -285,13 +349,51 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
)}
|
||||
|
||||
{phase === 'idle' && (
|
||||
<button className="scan-btn scan-btn--primary scan-btn--start" onClick={handleStartScan}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
||||
<circle cx="12" cy="13" r="4" />
|
||||
</svg>
|
||||
Abrir cámara
|
||||
</button>
|
||||
<>
|
||||
<button className="scan-btn scan-btn--primary scan-btn--start" onClick={handleStartScan}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
||||
<circle cx="12" cy="13" r="4" />
|
||||
</svg>
|
||||
Abrir cámara
|
||||
</button>
|
||||
|
||||
<div className="upload-section">
|
||||
<label className="upload-label">O sube una foto de tu TSI</label>
|
||||
<div className="upload-row">
|
||||
<button className="upload-option" onClick={() => cameraInputRef.current?.click()}>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
||||
<circle cx="12" cy="13" r="4" />
|
||||
</svg>
|
||||
<span>Hacer foto</span>
|
||||
</button>
|
||||
<button className="upload-option" onClick={() => galleryInputRef.current?.click()}>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<polyline points="21 15 16 10 5 21" />
|
||||
</svg>
|
||||
<span>Subir de galería</span>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
ref={cameraInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
onChange={handlePhotoSelect}
|
||||
className="upload-hidden-input"
|
||||
/>
|
||||
<input
|
||||
ref={galleryInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handlePhotoSelect}
|
||||
className="upload-hidden-input"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{phase === 'scanning' && !isNative && (
|
||||
@@ -314,6 +416,38 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === 'photo-preview' && (
|
||||
<div className="photo-preview-panel">
|
||||
<div className="photo-preview-container">
|
||||
<img src={previewUrl} alt="TSI capturada" className="photo-preview-img" />
|
||||
</div>
|
||||
{ocrLoading ? (
|
||||
<div className="scanning-active">
|
||||
<div className="scanner-spinner" />
|
||||
<p>Procesando imagen…</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="photo-preview-actions">
|
||||
<button className="scan-btn scan-btn--primary scan-btn--start" onClick={handlePhotoOcr}>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="4 7 4 4 20 4 20 7" />
|
||||
<line x1="9" y1="20" x2="15" y2="20" />
|
||||
<line x1="12" y1="4" x2="12" y2="20" />
|
||||
</svg>
|
||||
Escanear imagen
|
||||
</button>
|
||||
<button className="scan-btn scan-btn--ghost" onClick={handlePhotoDiscard}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
Descartar
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="cip-form" onSubmit={handleManualSubmit}>
|
||||
<label className="cip-label" htmlFor="cip-input">O introduce el código CIP manualmente</label>
|
||||
<div className="cip-row">
|
||||
|
||||
Reference in New Issue
Block a user