254 lines
8.5 KiB
JavaScript
254 lines
8.5 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
||
// 1. Elements
|
||
const video = document.getElementById('alp-video');
|
||
const canvas = document.getElementById('alp-canvas');
|
||
const overlayText = document.getElementById('alp-status-text');
|
||
const stepScan = document.getElementById('alp-step-scan');
|
||
const stepAction = document.getElementById('alp-step-action');
|
||
|
||
// Product Data Elements
|
||
const productNameEl = document.getElementById('alp-product-name');
|
||
const photoListEl = document.getElementById('alp-photo-list');
|
||
|
||
// Buttons
|
||
const manualForm = document.getElementById('alp-manual-form');
|
||
const btnExpiry = document.getElementById('btn-snap-expiry');
|
||
const btnPkg = document.getElementById('btn-snap-packaging');
|
||
const btnReset = document.getElementById('btn-reset');
|
||
|
||
// State
|
||
let currentStream = null;
|
||
let barcodeDetector = null;
|
||
let isScanning = false;
|
||
let currentProductId = null;
|
||
|
||
// 2. Initialize
|
||
initBarcodeDetector();
|
||
startCamera();
|
||
|
||
// 3. Event Listeners
|
||
manualForm.addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
const val = document.getElementById('alp-manual-input').value.trim();
|
||
if(val) fetchProduct(val);
|
||
});
|
||
|
||
btnReset.addEventListener('click', resetApp);
|
||
|
||
btnExpiry.addEventListener('click', () => takePhoto('expiry'));
|
||
btnPkg.addEventListener('click', () => takePhoto('packaging'));
|
||
|
||
// --- Core Functions ---
|
||
|
||
async function initBarcodeDetector() {
|
||
if ('BarcodeDetector' in window) {
|
||
// Check supported formats
|
||
const formats = await BarcodeDetector.getSupportedFormats();
|
||
if (formats.includes('ean_13')) {
|
||
barcodeDetector = new BarcodeDetector({ formats: ['ean_13'] });
|
||
console.log('BarcodeDetector ready');
|
||
} else {
|
||
overlayText.textContent = "EAN13 not supported by device";
|
||
}
|
||
} else {
|
||
console.warn('BarcodeDetector API not supported in this browser');
|
||
overlayText.textContent = "Auto-scan not supported. Use manual input.";
|
||
}
|
||
}
|
||
|
||
async function startCamera() {
|
||
try {
|
||
const constraints = {
|
||
video: {
|
||
facingMode: 'environment', // Rear camera
|
||
width: { ideal: 1280 },
|
||
height: { ideal: 720 }
|
||
}
|
||
};
|
||
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||
video.srcObject = currentStream;
|
||
|
||
// Wait for video to be ready
|
||
video.onloadedmetadata = () => {
|
||
video.play();
|
||
if(barcodeDetector) {
|
||
isScanning = true;
|
||
overlayText.textContent = "Scan Barcode...";
|
||
scanLoop();
|
||
} else {
|
||
overlayText.textContent = "Camera Ready (Manual Mode)";
|
||
}
|
||
};
|
||
} catch (err) {
|
||
console.error(err);
|
||
overlayText.textContent = "Camera Access Denied or Error";
|
||
}
|
||
}
|
||
|
||
async function scanLoop() {
|
||
if (!isScanning || !barcodeDetector || currentProductId) return;
|
||
|
||
try {
|
||
const barcodes = await barcodeDetector.detect(video);
|
||
if (barcodes.length > 0) {
|
||
const code = barcodes[0].rawValue;
|
||
isScanning = false; // Stop scanning
|
||
fetchProduct(code);
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
// Detection error (common in loop)
|
||
}
|
||
|
||
// Scan every 200ms to save battery
|
||
setTimeout(() => requestAnimationFrame(scanLoop), 200);
|
||
}
|
||
|
||
function fetchProduct(identifier) {
|
||
overlayText.textContent = "Searching...";
|
||
isScanning = false;
|
||
|
||
const fd = new FormData();
|
||
fd.append('action', 'searchProduct');
|
||
fd.append('identifier', identifier);
|
||
|
||
fetch(window.alpAjaxUrl, { method: 'POST', body: fd })
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
loadProductView(data.data);
|
||
} else {
|
||
alert(data.message || 'Product not found');
|
||
resetApp(); // Go back to scanning
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error(err);
|
||
alert('Network Error');
|
||
resetApp();
|
||
});
|
||
}
|
||
|
||
function loadProductView(productData) {
|
||
currentProductId = productData.id_product;
|
||
productNameEl.textContent = `[${productData.reference}] ${productData.name}`;
|
||
|
||
renderPhotos(productData.existing_photos);
|
||
|
||
// Switch View
|
||
stepScan.style.display = 'none';
|
||
stepAction.style.display = 'block';
|
||
}
|
||
|
||
function takePhoto(type) {
|
||
if (!currentProductId) return;
|
||
|
||
// Capture frame
|
||
const w = video.videoWidth;
|
||
const h = video.videoHeight;
|
||
|
||
// Crop to square (center)
|
||
const size = Math.min(w, h);
|
||
const x = (w - size) / 2;
|
||
const y = (h - size) / 2;
|
||
|
||
canvas.width = 800;
|
||
canvas.height = 800;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(video, x, y, size, size, 0, 0, 800, 800);
|
||
|
||
const dataUrl = canvas.toDataURL('image/webp', 0.8);
|
||
|
||
// Upload
|
||
const fd = new FormData();
|
||
fd.append('action', 'uploadImage');
|
||
fd.append('id_product', currentProductId);
|
||
fd.append('image_type', type);
|
||
fd.append('imageData', dataUrl);
|
||
|
||
// Visual feedback
|
||
const btn = (type === 'expiry') ? btnExpiry : btnPkg;
|
||
const originalText = btn.innerHTML;
|
||
btn.innerHTML = 'Uploading...';
|
||
btn.disabled = true;
|
||
|
||
fetch(window.alpAjaxUrl, { method: 'POST', body: fd })
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Add new photo to list without reload
|
||
addPhotoToDom(data.photo);
|
||
// Flash success message
|
||
alert(`Saved as ${type}!`);
|
||
} else {
|
||
alert('Error: ' + data.message);
|
||
}
|
||
})
|
||
.catch(err => alert('Upload failed'))
|
||
.finally(() => {
|
||
btn.innerHTML = originalText;
|
||
btn.disabled = false;
|
||
});
|
||
}
|
||
|
||
function renderPhotos(photos) {
|
||
photoListEl.innerHTML = '';
|
||
if(photos && photos.length) {
|
||
photos.forEach(addPhotoToDom);
|
||
} else {
|
||
photoListEl.innerHTML = '<p class="text-muted">No photos yet.</p>';
|
||
}
|
||
}
|
||
|
||
function addPhotoToDom(photo) {
|
||
// Remove "No photos" msg if exists
|
||
if (photoListEl.querySelector('p')) photoListEl.innerHTML = '';
|
||
|
||
const div = document.createElement('div');
|
||
div.className = 'alp-thumb';
|
||
|
||
const badgeClass = (photo.type === 'expiry') ? 'badge-success' : 'badge-info';
|
||
|
||
div.innerHTML = `
|
||
<img src="${photo.url}" target="_blank">
|
||
<span class="badge ${badgeClass}">${photo.type}</span>
|
||
<button class="btn-delete" onclick="deletePhoto(${currentProductId}, '${photo.name}', this)">×</button>
|
||
`;
|
||
photoListEl.prepend(div);
|
||
}
|
||
|
||
function resetApp() {
|
||
currentProductId = null;
|
||
document.getElementById('alp-manual-input').value = '';
|
||
stepAction.style.display = 'none';
|
||
stepScan.style.display = 'block';
|
||
|
||
if(barcodeDetector) {
|
||
isScanning = true;
|
||
overlayText.textContent = "Scan Barcode...";
|
||
scanLoop();
|
||
} else {
|
||
overlayText.textContent = "Camera Ready (Manual Mode)";
|
||
}
|
||
}
|
||
|
||
// Expose delete function globally so onclick in HTML works
|
||
window.deletePhoto = function(idProduct, imgName, btnEl) {
|
||
if(!confirm('Delete this photo?')) return;
|
||
|
||
const fd = new FormData();
|
||
fd.append('action', 'deleteImage');
|
||
fd.append('id_product', idProduct);
|
||
fd.append('image_name', imgName);
|
||
|
||
fetch(window.alpAjaxUrl, { method: 'POST', body: fd })
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
if(data.success) {
|
||
btnEl.closest('.alp-thumb').remove();
|
||
} else {
|
||
alert('Error deleting');
|
||
}
|
||
});
|
||
};
|
||
}); |