first commit
This commit is contained in:
309
views/js/admin.js
Normal file
309
views/js/admin.js
Normal file
@@ -0,0 +1,309 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- DOM Element References ---
|
||||
const videoContainer = document.getElementById('alp-video-container');
|
||||
const video = document.getElementById('alp-video');
|
||||
const canvas = document.getElementById('alp-canvas');
|
||||
const overlay = document.getElementById('alp-viewfinder-overlay');
|
||||
const overlayText = document.getElementById('alp-overlay-text');
|
||||
const cameraSelector = document.getElementById('alp-camera-selector');
|
||||
const manualInputForm = document.getElementById('alp-manual-form');
|
||||
const productInfoSection = document.getElementById('alp-product-info');
|
||||
const productNameEl = document.getElementById('alp-product-name');
|
||||
const productPricesEl = document.getElementById('alp-product-prices');
|
||||
const existingPhotosSection = document.getElementById('alp-existing-photos');
|
||||
const existingPhotosContainer = document.getElementById('alp-photos-container');
|
||||
const messageArea = document.getElementById('alp-message-area');
|
||||
|
||||
// --- State Management ---
|
||||
const AppState = {
|
||||
IDLE: 'idle', // Camera off, welcome message
|
||||
READY_TO_SCAN: 'ready_to_scan', // Camera on, waiting for tap to scan
|
||||
SCANNING: 'scanning', // Actively looking for barcode
|
||||
PRODUCT_FOUND: 'product_found', // Product found, waiting for tap to take photo
|
||||
UPLOADING: 'uploading' // Photo is being sent to server
|
||||
};
|
||||
let currentState = AppState.IDLE;
|
||||
let currentStream = null;
|
||||
let barcodeDetector = null;
|
||||
let currentProductId = null;
|
||||
const ajaxUrl = window.addLivePhotoAjaxUrl || '';
|
||||
|
||||
// --- Initialization ---
|
||||
if (!('BarcodeDetector' in window)) {
|
||||
showMessage('Barcode Detector API is not supported. Please use manual input.', true);
|
||||
} else {
|
||||
barcodeDetector = new BarcodeDetector({ formats: ['ean_13'] });
|
||||
}
|
||||
if (!navigator.mediaDevices) {
|
||||
showMessage('Camera access is not supported in this browser.', true);
|
||||
} else {
|
||||
populateCameraSelector();
|
||||
}
|
||||
|
||||
updateUIForState(AppState.IDLE); // Set initial UI state
|
||||
|
||||
// --- Event Listeners ---
|
||||
videoContainer.addEventListener('click', handleViewfinderTap);
|
||||
cameraSelector.addEventListener('change', handleCameraChange);
|
||||
manualInputForm.addEventListener('submit', handleManualSubmit);
|
||||
existingPhotosContainer.addEventListener('click', handleDeleteClick);
|
||||
|
||||
// --- Core Logic ---
|
||||
function handleViewfinderTap() {
|
||||
switch (currentState) {
|
||||
case AppState.IDLE:
|
||||
startCamera();
|
||||
break;
|
||||
case AppState.READY_TO_SCAN:
|
||||
detectBarcode();
|
||||
break;
|
||||
case AppState.PRODUCT_FOUND:
|
||||
takePhoto();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUIForState(newState, customText = null) {
|
||||
currentState = newState;
|
||||
let textContent = '';
|
||||
overlay.style.display = 'flex';
|
||||
|
||||
switch (newState) {
|
||||
case AppState.IDLE:
|
||||
textContent = "Tap to Start Camera";
|
||||
break;
|
||||
case AppState.READY_TO_SCAN:
|
||||
textContent = "Tap to Scan Barcode";
|
||||
break;
|
||||
case AppState.SCANNING:
|
||||
textContent = `<div class="spinner"></div>`;
|
||||
break;
|
||||
case AppState.PRODUCT_FOUND:
|
||||
textContent = "Tap to Take Picture";
|
||||
break;
|
||||
case AppState.UPLOADING:
|
||||
textContent = "Uploading...";
|
||||
break;
|
||||
}
|
||||
overlayText.innerHTML = customText || textContent;
|
||||
}
|
||||
|
||||
async function startCamera() {
|
||||
if (currentStream) return;
|
||||
const constraints = { video: { deviceId: cameraSelector.value ? { exact: cameraSelector.value } : undefined, facingMode: 'environment' } };
|
||||
try {
|
||||
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
video.srcObject = currentStream;
|
||||
await video.play();
|
||||
updateUIForState(AppState.READY_TO_SCAN);
|
||||
} catch (err) {
|
||||
console.error('Error accessing camera:', err);
|
||||
stopCamera(); // Ensure everything is reset
|
||||
updateUIForState(AppState.IDLE, 'Camera Error. Tap to retry.');
|
||||
}
|
||||
}
|
||||
|
||||
function stopCamera() {
|
||||
if (currentStream) {
|
||||
currentStream.getTracks().forEach(track => track.stop());
|
||||
currentStream = null;
|
||||
}
|
||||
video.srcObject = null;
|
||||
updateUIForState(AppState.IDLE);
|
||||
}
|
||||
|
||||
async function detectBarcode() {
|
||||
if (!barcodeDetector || currentState !== AppState.READY_TO_SCAN) return;
|
||||
updateUIForState(AppState.SCANNING);
|
||||
try {
|
||||
const barcodes = await barcodeDetector.detect(video);
|
||||
if (barcodes.length > 0) {
|
||||
searchProduct(barcodes[0].rawValue);
|
||||
} else {
|
||||
showMessage('No barcode found. Please try again.', true);
|
||||
updateUIForState(AppState.READY_TO_SCAN);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Barcode detection error:', err);
|
||||
showMessage('Error during barcode detection.', true);
|
||||
updateUIForState(AppState.READY_TO_SCAN);
|
||||
}
|
||||
}
|
||||
|
||||
function takePhoto() {
|
||||
if (!currentStream || !currentProductId || currentState !== AppState.PRODUCT_FOUND) return;
|
||||
updateUIForState(AppState.UPLOADING);
|
||||
|
||||
const targetWidth = 800, targetHeight = 800;
|
||||
canvas.width = targetWidth; canvas.height = targetHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const videoWidth = video.videoWidth, videoHeight = video.videoHeight;
|
||||
const size = Math.min(videoWidth, videoHeight);
|
||||
const x = (videoWidth - size) / 2, y = (videoHeight - size) / 2;
|
||||
ctx.drawImage(video, x, y, size, size, 0, 0, targetWidth, targetHeight);
|
||||
const imageData = canvas.toDataURL('image/webp', 0.8);
|
||||
|
||||
uploadImage(imageData);
|
||||
}
|
||||
|
||||
function resetForNextProduct() {
|
||||
currentProductId = null;
|
||||
productInfoSection.style.display = 'none';
|
||||
existingPhotosSection.style.display = 'none';
|
||||
existingPhotosContainer.innerHTML = '';
|
||||
updateUIForState(AppState.READY_TO_SCAN);
|
||||
}
|
||||
|
||||
// --- AJAX and Helper Functions ---
|
||||
async function searchProduct(identifier) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'searchProduct'); formData.append('identifier', identifier);
|
||||
try {
|
||||
const response = await fetch(ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const product = result.data;
|
||||
currentProductId = product.id_product;
|
||||
displayProductInfo(product);
|
||||
updateUIForState(AppState.PRODUCT_FOUND);
|
||||
} else {
|
||||
showMessage(result.message, true);
|
||||
updateUIForState(AppState.READY_TO_SCAN);
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage('Network error searching for product.', true);
|
||||
updateUIForState(AppState.READY_TO_SCAN);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadImage(imageData) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'uploadImage'); formData.append('id_product', currentProductId); formData.append('imageData', imageData);
|
||||
try {
|
||||
const response = await fetch(ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showMessage(result.message, false);
|
||||
appendNewPhoto(result.data.new_photo);
|
||||
setTimeout(resetForNextProduct, 1500); // Pause to show success, then reset
|
||||
} else {
|
||||
showMessage(result.message, true);
|
||||
updateUIForState(AppState.PRODUCT_FOUND); // Allow user to try photo again
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage('Network error uploading photo.', true);
|
||||
updateUIForState(AppState.PRODUCT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
async function populateCameraSelector() { /* (This function can remain from previous versions) */ }
|
||||
function handleCameraChange() { /* (This function can remain from previous versions) */ }
|
||||
function handleManualSubmit(e) { /* (This function can remain from previous versions) */ }
|
||||
function handleDeleteClick(e) { /* (This function can remain from previous versions) */ }
|
||||
function displayProductInfo(product) { /* (This function can remain from previous versions) */ }
|
||||
function appendNewPhoto(photo) { /* (This function can remain from previous versions) */ }
|
||||
function showMessage(text, isError = false) { /* (This function can remain from previous versions) */ }
|
||||
|
||||
// --- Re-pasting the helper functions for completeness ---
|
||||
|
||||
async function populateCameraSelector() {
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
||||
cameraSelector.innerHTML = '';
|
||||
videoDevices.forEach((device, index) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = device.deviceId;
|
||||
option.textContent = device.label || `Camera ${index + 1}`;
|
||||
cameraSelector.appendChild(option);
|
||||
});
|
||||
const preferredCameraId = localStorage.getItem('addLivePhoto_preferredCameraId');
|
||||
if (preferredCameraId && cameraSelector.querySelector(`option[value="${preferredCameraId}"]`)) {
|
||||
cameraSelector.value = preferredCameraId;
|
||||
}
|
||||
} catch (err) { console.error('Error enumerating devices:', err); }
|
||||
}
|
||||
|
||||
function handleCameraChange() {
|
||||
localStorage.setItem('addLivePhoto_preferredCameraId', cameraSelector.value);
|
||||
if (currentStream) { // If camera is active, restart it with the new selection
|
||||
stopCamera();
|
||||
startCamera();
|
||||
}
|
||||
}
|
||||
|
||||
function handleManualSubmit(e) {
|
||||
e.preventDefault();
|
||||
const identifier = document.getElementById('alp-manual-identifier').value.trim();
|
||||
if (identifier) {
|
||||
showMessage(`Searching for: ${identifier}...`);
|
||||
searchProduct(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteClick(e) {
|
||||
if (e.target && e.target.classList.contains('delete-photo-btn')) {
|
||||
const button = e.target;
|
||||
const imageName = button.dataset.imageName;
|
||||
const productId = button.dataset.productId;
|
||||
if (confirm(`Are you sure you want to delete this photo?`)) {
|
||||
// Simplified delete without a dedicated function
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'deleteImage');
|
||||
formData.append('id_product', productId);
|
||||
formData.append('image_name', imageName);
|
||||
fetch(ajaxUrl, { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
showMessage(result.message, false);
|
||||
button.closest('.photo-thumb').remove();
|
||||
} else {
|
||||
showMessage(result.message, true);
|
||||
}
|
||||
}).catch(err => showMessage('Network error deleting photo.', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function displayProductInfo(product) {
|
||||
productNameEl.textContent = `[ID: ${product.id_product}] ${product.name}`;
|
||||
let pricesHtml = `Wholesale: ${product.wholesale_price} | Sale: ${product.retail_price}`;
|
||||
if (product.discounted_price) {
|
||||
pricesHtml += ` | <strong class="text-danger">Discounted: ${product.discounted_price}</strong>`;
|
||||
}
|
||||
productPricesEl.innerHTML = pricesHtml;
|
||||
renderExistingPhotos(product.existing_photos, product.id_product);
|
||||
productInfoSection.style.display = 'block';
|
||||
}
|
||||
|
||||
function renderExistingPhotos(photos, productId) {
|
||||
existingPhotosContainer.innerHTML = '';
|
||||
if (photos && photos.length > 0) {
|
||||
existingPhotosSection.style.display = 'block';
|
||||
photos.forEach(photo => appendNewPhoto(photo, productId));
|
||||
} else {
|
||||
existingPhotosSection.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function appendNewPhoto(photo, productId = currentProductId) {
|
||||
const thumbDiv = document.createElement('div');
|
||||
thumbDiv.className = 'photo-thumb';
|
||||
thumbDiv.innerHTML = `
|
||||
<a href="${photo.url}" target="_blank">
|
||||
<img src="${photo.url}" alt="Live photo" loading="lazy" />
|
||||
</a>
|
||||
<button class="btn btn-sm btn-danger delete-photo-btn" data-product-id="${productId}" data-image-name="${photo.name}">X</button>
|
||||
`;
|
||||
existingPhotosContainer.prepend(thumbDiv);
|
||||
existingPhotosSection.style.display = 'block';
|
||||
}
|
||||
|
||||
function showMessage(text, isError = false) {
|
||||
messageArea.textContent = text;
|
||||
messageArea.className = isError ? 'alert alert-danger' : 'alert alert-info';
|
||||
messageArea.style.display = 'block';
|
||||
setTimeout(() => { messageArea.style.display = 'none'; }, 4000); // Message disappears after 4s
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user