Merge pull request 'added camera input for andriod conversion' (#154) from temp/ocr-andriod-camera into main
Reviewed-on: Pozomind/pozo-retail-app#154
This commit is contained in:
commit
be3b62f79c
|
|
@ -2,16 +2,9 @@ import { useState, useRef, useEffect } from 'react';
|
|||
import {
|
||||
Upload,
|
||||
Button,
|
||||
Spin,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Divider,
|
||||
Row,
|
||||
Col,
|
||||
Table,
|
||||
Image,
|
||||
DatePicker,
|
||||
} from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
|
|
@ -21,7 +14,6 @@ import {
|
|||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
DeleteFilled,
|
||||
CameraOutlined,
|
||||
EditOutlined,
|
||||
|
|
@ -37,218 +29,8 @@ import { InputField } from '../../Components/Forms/InputField.jsx';
|
|||
import TextAreaInput from '../../Components/Forms/TextArea.jsx';
|
||||
import { uploadImage } from '../../Features/upload/upload.js';
|
||||
import { getSession } from '../../Services/Others.js';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Column } = Table;
|
||||
|
||||
const CameraModal = ({
|
||||
visible,
|
||||
onClose,
|
||||
onCapture,
|
||||
setCameraModalVisible = () => { },
|
||||
}) => {
|
||||
const videoRef = useRef(null);
|
||||
const canvasRef = useRef(null);
|
||||
const [stream, setStream] = useState(null);
|
||||
const [image, setImage] = useState(null);
|
||||
const [cameraOn, setCameraOn] = useState(false);
|
||||
|
||||
const startCamera = async () => {
|
||||
console.log('Starting camera function called...');
|
||||
|
||||
// First test - just set camera on without stream
|
||||
setCameraOn(true);
|
||||
console.log('Camera state forced to ON for testing');
|
||||
|
||||
try {
|
||||
console.log('Checking getUserMedia support...');
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
console.error('getUserMedia not supported');
|
||||
message.error('Camera not supported on this device');
|
||||
setCameraModalVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Requesting camera access...');
|
||||
let mediaStream;
|
||||
try {
|
||||
// Try high quality first
|
||||
mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
width: { ideal: 1920 },
|
||||
height: { ideal: 1080 },
|
||||
},
|
||||
});
|
||||
} catch (highResError) {
|
||||
console.log(
|
||||
'High resolution failed, trying basic constraints:',
|
||||
highResError.message
|
||||
);
|
||||
// Fallback to basic constraints
|
||||
mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment' },
|
||||
});
|
||||
}
|
||||
console.log('Got media stream:', mediaStream);
|
||||
|
||||
if (videoRef.current) {
|
||||
console.log('Setting video source...');
|
||||
videoRef.current.srcObject = mediaStream;
|
||||
setStream(mediaStream);
|
||||
console.log('Stream set successfully');
|
||||
} else {
|
||||
console.error('Video ref is null');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Camera error:', err.name, err.message);
|
||||
message.error(`Camera error: ${err.message}`);
|
||||
setCameraModalVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stopCamera = () => {
|
||||
console.log('Stopping camera...');
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((track) => {
|
||||
console.log('Stopping track:', track);
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
setStream(null);
|
||||
setCameraOn(false);
|
||||
console.log('Camera stopped');
|
||||
};
|
||||
|
||||
const captureImage = () => {
|
||||
const video = videoRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(video, 0, 0);
|
||||
const imageData = canvas.toDataURL('image/jpeg', 0.95);
|
||||
setImage(imageData);
|
||||
stopCamera();
|
||||
};
|
||||
|
||||
const retake = () => {
|
||||
setImage(null);
|
||||
startCamera();
|
||||
};
|
||||
|
||||
const handleUse = () => {
|
||||
canvasRef.current.toBlob(
|
||||
(blob) => {
|
||||
const file = new File([blob], 'camera-capture.jpg', {
|
||||
type: 'image/jpeg',
|
||||
});
|
||||
onCapture(file, true); // Pass true to indicate direct submit
|
||||
handleClose();
|
||||
},
|
||||
'image/jpeg',
|
||||
0.95
|
||||
);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
stopCamera();
|
||||
setImage(null);
|
||||
setCameraOn(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
startCamera(); // Auto-start camera when modal opens
|
||||
return () => stopCamera();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<DefaultModal
|
||||
title="Camera"
|
||||
open={visible}
|
||||
handleCancel={handleClose}
|
||||
footer={false}
|
||||
width={400}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
{/* <div style={{ marginBottom: 10, padding: 10, backgroundColor: '#f0f0f0' }}>
|
||||
<p>Debug: Camera={cameraOn ? 'ON' : 'OFF'}, Stream={stream ? 'Active' : 'None'}, Image={image ? 'Yes' : 'No'}</p>
|
||||
</div> */}
|
||||
|
||||
{!cameraOn && !image && (
|
||||
<div>
|
||||
<p style={{ marginBottom: 16, color: '#666' }}>
|
||||
Starting camera...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cameraOn && (
|
||||
<div>
|
||||
<p>Camera is ON - Video should appear below:</p>
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted
|
||||
width="300"
|
||||
height="400"
|
||||
style={{ border: '2px solid red', marginBottom: 10 }}
|
||||
onCanPlay={() => console.log('Video can play')}
|
||||
onError={(e) => console.error('Video error:', e)}
|
||||
onLoadedMetadata={() => console.log('Video metadata loaded')}
|
||||
/>
|
||||
<br />
|
||||
<Button onClick={captureImage} type="primary">
|
||||
Capture
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setCameraOn(false)}
|
||||
style={{ marginLeft: 10 }}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{image && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
<Button onClick={retake}>Retake</Button>
|
||||
<Button onClick={handleUse} type="primary">
|
||||
Use Photo
|
||||
</Button>
|
||||
</div>
|
||||
<img
|
||||
src={image}
|
||||
alt="Captured"
|
||||
style={{
|
||||
width: '80vw',
|
||||
borderRadius: 8,
|
||||
marginBottom: 10,
|
||||
height: '60vh',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<canvas ref={canvasRef} style={{ display: 'none' }} />
|
||||
</div>
|
||||
</DefaultModal>
|
||||
);
|
||||
};
|
||||
import ResizableCropper from '../../Components/PedalOCR/CropperArea.jsx';
|
||||
import ThumbnailsList from '../../Components/PedalOCR/ThumbnailsList.jsx';
|
||||
|
||||
const ScanModal = ({
|
||||
visible = false,
|
||||
|
|
@ -258,42 +40,77 @@ const ScanModal = ({
|
|||
processingStep = null,
|
||||
ocrLoading = false,
|
||||
handleClearImage = () => { },
|
||||
uploadedImage = null,
|
||||
selectedFile = null,
|
||||
images = [],
|
||||
setImages = () => { },
|
||||
finalImages = [],
|
||||
setCurrentIndex = () => { },
|
||||
setFinalImages = () => { },
|
||||
currentIndex = null,
|
||||
}) => {
|
||||
|
||||
const cameraRef = useRef(null);
|
||||
const isMobile =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
) ||
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0;
|
||||
const [cameraModalVisible, setCameraModalVisible] = useState(false);
|
||||
|
||||
const [buttonDisable, setButtonDisable] = useState(false);
|
||||
|
||||
const handleCameraCapture = (file, directSubmit = false) => {
|
||||
console.log(
|
||||
'Camera capture received:',
|
||||
file,
|
||||
'Direct submit:',
|
||||
directSubmit
|
||||
);
|
||||
const handleCapture = (file) => {
|
||||
console.log('Handle capture called with file:', file?.name);
|
||||
|
||||
handleUpload(file);
|
||||
|
||||
if (directSubmit) {
|
||||
setButtonDisable(true);
|
||||
setTimeout(() => {
|
||||
try {
|
||||
onSubmit(file, true);
|
||||
console.log('Triggering submit after camera capture');
|
||||
} catch (err) {
|
||||
console.error('Submit Error:', err?.message);
|
||||
} finally {
|
||||
setButtonDisable(false);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const newImage = {
|
||||
id: Date.now(),
|
||||
src: e.target.result,
|
||||
file: file,
|
||||
};
|
||||
setImages([newImage]);
|
||||
setCurrentIndex(0);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleCropComplete = (croppedImage) => {
|
||||
setFinalImages((prev) => {
|
||||
const newImages = [...prev];
|
||||
newImages[currentIndex] = croppedImage;
|
||||
return newImages;
|
||||
});
|
||||
fetch(croppedImage)
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const file = new File([blob], 'camera-capture.jpg', {
|
||||
type: 'image/jpeg',
|
||||
});
|
||||
handleUpload(file);
|
||||
});
|
||||
}
|
||||
|
||||
const handleRemoveImage = (index) => {
|
||||
setFinalImages((prev) => prev.filter((_, i) => i !== index));
|
||||
handleClearImage();
|
||||
};
|
||||
|
||||
const handleRecropImage = (index) => {
|
||||
const imageToRecrop = finalImages[index];
|
||||
setFinalImages((prev) => prev.filter((_, i) => i !== index));
|
||||
// Add back to images for re-cropping
|
||||
const newImage = { id: Date.now(), src: imageToRecrop };
|
||||
setImages((prev) => [...prev, newImage]);
|
||||
setCurrentIndex(images.length);
|
||||
};
|
||||
|
||||
const resetAll = () => {
|
||||
setImages([]);
|
||||
setFinalImages([]);
|
||||
setCurrentIndex(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<DefaultModal
|
||||
title={
|
||||
|
|
@ -368,12 +185,52 @@ const ScanModal = ({
|
|||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
ref={cameraRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
multiple
|
||||
hidden
|
||||
onChange={(e) => {
|
||||
const rawFile = e?.target?.files?.[0];
|
||||
if (!rawFile) return;
|
||||
handleCapture(rawFile);
|
||||
}}
|
||||
/>
|
||||
{currentIndex !== null && images[currentIndex] && (
|
||||
<ResizableCropper
|
||||
imageSrc={images[currentIndex].src}
|
||||
currentIndex={currentIndex}
|
||||
total={images.length}
|
||||
onCropComplete={handleCropComplete}
|
||||
onCancel={resetAll}
|
||||
onNext={() => {
|
||||
if (currentIndex + 1 < images.length) {
|
||||
setCurrentIndex(currentIndex + 1);
|
||||
} else {
|
||||
setCurrentIndex(null);
|
||||
setImages([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{finalImages.length > 0 && currentIndex === null && (
|
||||
<>
|
||||
<h4>Uploaded Image :</h4>
|
||||
<ThumbnailsList
|
||||
images={finalImages}
|
||||
onRemove={handleRemoveImage}
|
||||
onRecrop={handleRecropImage}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isMobile && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<CameraOutlined />}
|
||||
onClick={() => setCameraModalVisible(true)}
|
||||
onClick={() => cameraRef.current.click()}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
|
|
@ -399,14 +256,14 @@ const ScanModal = ({
|
|||
)}
|
||||
<Upload
|
||||
accept="image/*"
|
||||
beforeUpload={handleUpload}
|
||||
beforeUpload={handleCapture}
|
||||
showUploadList={false}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
margin: '0 auto',
|
||||
border: '2px dashed #999',
|
||||
padding: 30,
|
||||
padding: isMobile ? '12px 30px' : 30,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
|
|
@ -414,48 +271,11 @@ const ScanModal = ({
|
|||
style={{ fontSize: 40, color: '#1292ee' }}
|
||||
/>
|
||||
<p style={{ marginTop: '4px' }}>
|
||||
Click or drag image to upload
|
||||
{`${isMobile ? 'Upload' : 'Click or drag image to upload'}`}
|
||||
</p>
|
||||
</div>
|
||||
</Upload>
|
||||
</div>
|
||||
|
||||
{uploadedImage && (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<div className="preview-header">
|
||||
<div style={{ fontFamily: 'Poppins', fontWeight: '500' }}>
|
||||
Preview:
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleClearImage}
|
||||
icon={
|
||||
<CloseOutlined
|
||||
style={{ height: '15px', width: '15px' }}
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
<Image
|
||||
src={uploadedImage}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: 300,
|
||||
border: '1px solid #d9d9d9',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<CameraModal
|
||||
visible={cameraModalVisible}
|
||||
onClose={() => setCameraModalVisible(false)}
|
||||
onCapture={handleCameraCapture}
|
||||
setCameraModalVisible={setCameraModalVisible}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -498,6 +318,9 @@ const InvoiceImageExtractorModal = ({
|
|||
|
||||
const extractedTableRef = useRef(null);
|
||||
const [form] = Form.useForm();
|
||||
const [images, setImages] = useState([]);
|
||||
const [finalImages, setFinalImages] = useState([]);
|
||||
const [currentIndex, setCurrentIndex] = useState(null);
|
||||
const [ocrLoading, setOcrLoading] = useState(false);
|
||||
const [uploadedImage, setUploadedImage] = useState(null);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
|
|
@ -661,6 +484,9 @@ const InvoiceImageExtractorModal = ({
|
|||
cleanupUploadedURL();
|
||||
setProcessingStep(null);
|
||||
setSelectedFile(null);
|
||||
setImages([]);
|
||||
setCurrentIndex(null);
|
||||
setFinalImages([]);
|
||||
if (visible || submit) {
|
||||
form.resetFields();
|
||||
setExtractedText('');
|
||||
|
|
@ -728,6 +554,7 @@ const InvoiceImageExtractorModal = ({
|
|||
CompId,
|
||||
BranchId
|
||||
}
|
||||
console.log('Dispatching OCR request with data:', data);
|
||||
const response = await dispatch(
|
||||
getInvoiceImageData(data)
|
||||
).unwrap();
|
||||
|
|
@ -823,6 +650,9 @@ const InvoiceImageExtractorModal = ({
|
|||
} else if (uploadMoreVisible) {
|
||||
setUploadMoreVisible(false);
|
||||
}
|
||||
setImages([]);
|
||||
setCurrentIndex(null);
|
||||
setFinalImages([]);
|
||||
message.success('Invoice processed successfully!');
|
||||
} else {
|
||||
if (response?.statusCode === 1 && response?.data?.length === 0) {
|
||||
|
|
@ -832,7 +662,7 @@ const InvoiceImageExtractorModal = ({
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('API OCR Error:', err);
|
||||
console.error('API OCR Error:', err?.message);
|
||||
message.error('Failed to process invoice.');
|
||||
} finally {
|
||||
setOcrLoading(false);
|
||||
|
|
@ -954,8 +784,13 @@ const InvoiceImageExtractorModal = ({
|
|||
processingStep={processingStep}
|
||||
ocrLoading={ocrLoading}
|
||||
handleClearImage={handleClearImage}
|
||||
uploadedImage={uploadedImage}
|
||||
selectedFile={selectedFile}
|
||||
images={images}
|
||||
setImages={setImages}
|
||||
finalImages={finalImages}
|
||||
setFinalImages={setFinalImages}
|
||||
currentIndex={currentIndex}
|
||||
setCurrentIndex={setCurrentIndex}
|
||||
/>
|
||||
|
||||
{/* PREVIEW MODAL */}
|
||||
|
|
@ -1213,8 +1048,13 @@ const InvoiceImageExtractorModal = ({
|
|||
processingStep={processingStep}
|
||||
ocrLoading={ocrLoading}
|
||||
handleClearImage={handleClearImage}
|
||||
uploadedImage={uploadedImage}
|
||||
selectedFile={selectedFile}
|
||||
images={images}
|
||||
setImages={setImages}
|
||||
finalImages={finalImages}
|
||||
setFinalImages={setFinalImages}
|
||||
currentIndex={currentIndex}
|
||||
setCurrentIndex={setCurrentIndex}
|
||||
/>
|
||||
</DefaultModal>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Reference in New Issue